blob: 26d535b933f1ef5ef4528c9546a83579200f69c8 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: avoid_print, avoid_dynamic_calls
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:args/args.dart';
import 'package:imitation_game/readme_template.dart';
import 'package:mustache_template/mustache.dart';
// ignore_for_file: avoid_as
const int _port = 4040;
Future<String?> _findIpAddress() async {
String? result;
final List<NetworkInterface> interfaces = await NetworkInterface.list();
for (final NetworkInterface interface in interfaces) {
for (final InternetAddress address in interface.addresses) {
if (address.type == InternetAddressType.IPv4) {
// TODO(gaaclarke): Implment having multiple addresses.
assert(result == null);
result = address.address;
}
}
}
return result;
}
Future<String> _getFlutterVersion() async {
String result = '';
final Process flutterVersion =
await Process.start('flutter', <String>['--version']);
flutterVersion.stdout.transform(utf8.decoder).listen((String event) {
result += event;
});
await flutterVersion.exitCode;
return result.trim();
}
typedef FileFilter = bool Function(FileSystemEntity);
Future<List<FileSystemEntity>> findFiles(Directory dir, {FileFilter? where}) {
final List<FileSystemEntity> files = <FileSystemEntity>[];
final Completer<List<FileSystemEntity>> completer =
Completer<List<FileSystemEntity>>();
final Stream<FileSystemEntity> lister = dir.list(recursive: true);
lister.listen((FileSystemEntity file) {
if (where == null || where(file)) {
files.add(file);
}
}, onDone: () => completer.complete(files));
return completer.future;
}
Future<String> _makeMarkdownOutput(Map<String, dynamic> results) async {
// TODO(gaaclarke): Add the Flutter version.
final Template template = Template(readmeTemplate, name: 'README.md');
final Map<String, dynamic> values = Map<String, dynamic>.from(results);
values['date'] = DateTime.now().toUtc();
values['flutterVersion'] = await _getFlutterVersion();
final String output = template.renderString(values);
return output;
}
/// This merges [newResults] into [oldResults] such the union of the keys will
/// have their values from [newResults] and the symmetric difference will have the
/// value from their respective sets.
Map<String, dynamic> _integrate(
{required Map<String, dynamic> oldResults,
required Map<String, dynamic> newResults}) {
final Map<String, dynamic> result = Map<String, dynamic>.from(oldResults);
newResults.forEach((String test, dynamic testValue) {
final Map<String, dynamic> testMap = testValue as Map<String, dynamic>;
testMap.forEach((String platform, dynamic platformValue) {
final Map<String, dynamic> platformMap =
platformValue as Map<String, dynamic>;
platformMap.forEach((String measurement, dynamic measurementValue) {
if (!result.containsKey(test)) {
result[test] = <String, dynamic>{};
}
if (!result[test].containsKey(platform)) {
result[test][platform] = <String, dynamic>{};
}
result[test][platform][measurement] = measurementValue;
});
});
});
return result;
}
class _Script {
_Script({required this.path});
String path;
}
class _ScriptRunner {
_ScriptRunner(this._scriptPaths);
final List<String> _scriptPaths;
Process? _currentProcess;
late StreamSubscription<String> _stdoutSubscription;
late StreamSubscription<String> _stderrSubscription;
Future<_Script?> runNext() async {
if (_currentProcess != null) {
_stdoutSubscription.cancel();
_stderrSubscription.cancel();
_currentProcess!.kill();
_currentProcess = null;
}
if (_scriptPaths.isEmpty) {
return null;
} else {
final String path = _scriptPaths.last;
print('running: $path');
_scriptPaths.removeLast();
_currentProcess = await Process.start('sh', <String>[path]);
// TODO(gaaclarke): Implement a timeout.
_stdoutSubscription =
_currentProcess!.stdout.transform(utf8.decoder).listen((String data) {
print(data);
});
_stderrSubscription =
_currentProcess!.stderr.transform(utf8.decoder).listen((String data) {
print(data);
});
return _Script(path: path);
}
}
}
/// Recursively converts a map of maps to a map of lists of maps.
///
/// For example:
/// _map2List({'a': {'b': 123}}, ['foo', 'bar']) ->
/// {
/// 'foo':[
/// {
/// 'name': 'a',
/// 'bar': [{'name': 'b', 'value': 123}]
/// }
/// ]
/// }
Map<String, dynamic> _map2List(Map<String, dynamic> map, List<String> names) {
final List<Map<String, dynamic>> returnList = <Map<String, dynamic>>[];
final List<String> tail = names.sublist(1);
map.forEach((String key, dynamic value) {
final Map<String, dynamic> testResult = <String, dynamic>{'name': key};
if (tail.isEmpty) {
testResult['value'] = value;
} else {
testResult[tail.first] = _map2List(value, tail)[tail.first];
}
returnList.add(testResult);
});
return <String, dynamic>{names.first: returnList};
}
class _ImitationGame {
final Map<String, dynamic> results = <String, dynamic>{};
late _ScriptRunner _scriptRunner;
_Script? _currentScript;
Future<bool> start(List<String> iosScripts) {
_scriptRunner = _ScriptRunner(iosScripts);
return _runNext();
}
Future<bool> handleResult(Map<String, dynamic> data) {
final String test = data['test'];
final String? platform = data['platform'];
results.putIfAbsent(test, () => <String, dynamic>{});
results[test].putIfAbsent(platform, () => <String, dynamic>{});
data['results'].forEach((String k, dynamic v) {
results[test][platform][k] = v as double;
});
return _runNext();
}
Future<bool> handleTimeout() {
return _runNext();
}
Future<bool> _runNext() async {
_currentScript = await _scriptRunner.runNext();
return _currentScript != null;
}
}
enum _TargetPlatform { ANDROID, IOS }
Future<void> main(List<String> args) async {
final ArgParser parser = ArgParser();
parser.addOption('platform',
allowed: <String>['android', 'ios'], defaultsTo: 'android');
final ArgResults parserResults = parser.parse(args);
final _TargetPlatform targetPlatform = parserResults['platform'] == 'android'
? _TargetPlatform.ANDROID
: _TargetPlatform.IOS;
final HttpServer server = await HttpServer.bind(
InternetAddress.anyIPv4,
_port,
);
final String? ipaddress = await _findIpAddress();
print('Listening on $ipaddress:${server.port}');
for (final FileSystemEntity entity in await findFiles(Directory.current,
where: (FileSystemEntity f) => f.path.endsWith('ip.txt'))) {
final File file = File(entity.path);
file.writeAsStringSync('$ipaddress:${server.port}');
}
final String scriptName = (targetPlatform == _TargetPlatform.ANDROID)
? 'run_android.sh'
: () {
assert(targetPlatform == _TargetPlatform.IOS);
return 'run_ios.sh';
}();
final List<String> scripts = (await findFiles(Directory.current,
where: (FileSystemEntity f) => f.path.endsWith(scriptName)))
.map((FileSystemEntity e) => e.path)
.toList();
if (scripts.isEmpty) {
return;
}
final _ImitationGame game = _ImitationGame();
bool keepRunning = await game.start(scripts);
while (keepRunning) {
try {
final Stream<HttpRequest> timeoutServer = server.timeout(
const Duration(minutes: 5), onTimeout: (EventSink<HttpRequest> sink) {
print('TIMEOUT!');
throw TimeoutException('timeout');
});
await for (final HttpRequest request in timeoutServer) {
print('got request: ${request.method}');
if (request.method == 'POST') {
final String content = await utf8.decoder.bind(request).join();
final Map<String, dynamic> data =
jsonDecode(content) as Map<String, dynamic>;
print('$data');
keepRunning = await game.handleResult(data);
if (!keepRunning) {
break;
}
} else {
request.response.write('use post');
}
await request.response.close();
}
} on TimeoutException catch (_) {
keepRunning = await game.handleTimeout();
}
}
// TODO(gaaclarke): Add a log of what Flutter version generated the numbers.
const String lastResultsFilename = 'last_results.json';
const JsonDecoder decoder = JsonDecoder();
final Map<String, dynamic> lastResults =
decoder.convert(File(lastResultsFilename).readAsStringSync())
as Map<String, dynamic>;
// TODO(aaclarke): Calculate the generation time for each measurement since we
// can't generate everything in one pass (because you are running iOS or Android).
final Map<String, dynamic> totalResults =
_integrate(newResults: game.results, oldResults: lastResults);
const JsonEncoder encoder = JsonEncoder.withIndent(' ');
final String jsonResults = encoder.convert(totalResults);
File(lastResultsFilename).writeAsStringSync(jsonResults);
final Map<String, dynamic> markdownValues =
_map2List(totalResults, <String>['tests', 'platforms', 'measurements']);
File('README.md')
.writeAsStringSync(await _makeMarkdownOutput(markdownValues));
await server.close(force: true);
}