Auto-format Framework (#160545)

This auto-formats all *.dart files in the repository outside of the
`engine` subdirectory and enforces that these files stay formatted with
a presubmit check.

**Reviewers:** Please carefully review all the commits except for the
one titled "formatted". The "formatted" commit was auto-generated by
running `dev/tools/format.sh -a -f`. The other commits were hand-crafted
to prepare the repo for the formatting change. I recommend reviewing the
commits one-by-one via the "Commits" tab and avoiding Github's "Files
changed" tab as it will likely slow down your browser because of the
size of this PR.

---------

Co-authored-by: Kate Lovett <katelovett@google.com>
Co-authored-by: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com>
This commit is contained in:
Michael Goderbauer
2024-12-19 12:06:21 -08:00
committed by GitHub
parent 8e0993eda8
commit 5491c8c146
4411 changed files with 455108 additions and 415991 deletions

View File

@@ -8,17 +8,24 @@ import '../framework/runner.dart';
class TestCommand extends Command<void> {
TestCommand() {
argParser.addOption('task',
abbr: 't',
help: 'The name of a task listed under bin/tasks.\n'
' Example: complex_layout__start_up.\n');
argParser.addMultiOption('task-args',
help: 'List of arguments to pass to the task.\n'
'For example, "--task-args build" is passed as "bin/task/task.dart --build"');
argParser.addOption(
'task',
abbr: 't',
help:
'The name of a task listed under bin/tasks.\n'
' Example: complex_layout__start_up.\n',
);
argParser.addMultiOption(
'task-args',
help:
'List of arguments to pass to the task.\n'
'For example, "--task-args build" is passed as "bin/task/task.dart --build"',
);
argParser.addOption(
'device-id',
abbr: 'd',
help: 'Target device id (prefixes are allowed, names are not supported).\n'
help:
'Target device id (prefixes are allowed, names are not supported).\n'
'The option will be ignored if the test target does not run on a\n'
'mobile device. This still respects the device operating system\n'
'settings in the test case, and will results in error if no device\n'
@@ -26,17 +33,20 @@ class TestCommand extends Command<void> {
);
argParser.addFlag(
'exit',
help: 'Exit on the first test failure. Currently flakes are intentionally (though '
'incorrectly) not considered to be failures.',
help:
'Exit on the first test failure. Currently flakes are intentionally (though '
'incorrectly) not considered to be failures.',
);
argParser.addOption(
'git-branch',
help: '[Flutter infrastructure] Git branch of the current commit. LUCI\n'
help:
'[Flutter infrastructure] Git branch of the current commit. LUCI\n'
'checkouts run in detached HEAD state, so the branch must be passed.',
);
argParser.addOption(
'local-engine',
help: 'Name of a build output within the engine out directory, if you\n'
help:
'Name of a build output within the engine out directory, if you\n'
'are building Flutter locally. Use this to select a specific\n'
'version of the engine if you have built multiple engine targets.\n'
'This path is relative to --local-engine-src-path/out. This option\n'
@@ -44,7 +54,8 @@ class TestCommand extends Command<void> {
);
argParser.addOption(
'local-engine-host',
help: 'Name of a build output within the engine out directory, if you\n'
help:
'Name of a build output within the engine out directory, if you\n'
'are building Flutter locally. Use this to select a specific\n'
'version of the engine to use as the host platform if you have built '
'multiple engine targets.\n'
@@ -53,22 +64,26 @@ class TestCommand extends Command<void> {
);
argParser.addOption(
'local-engine-src-path',
help: 'Path to your engine src directory, if you are building Flutter\n'
help:
'Path to your engine src directory, if you are building Flutter\n'
'locally. Defaults to \$FLUTTER_ENGINE if set, or tries to guess at\n'
'the location based on the value of the --flutter-root option.',
);
argParser.addOption('luci-builder', help: '[Flutter infrastructure] Name of the LUCI builder being run on.');
argParser.addOption('results-file',
help: '[Flutter infrastructure] File path for test results. If passed with\n'
'task, will write test results to the file.');
argParser.addOption(
'luci-builder',
help: '[Flutter infrastructure] Name of the LUCI builder being run on.',
);
argParser.addOption(
'results-file',
help:
'[Flutter infrastructure] File path for test results. If passed with\n'
'task, will write test results to the file.',
);
argParser.addFlag(
'silent',
help: 'Suppresses standard output and only print standard error output.',
);
argParser.addFlag(
'use-emulator',
help: 'Use an emulator instead of a device to run tests.'
);
argParser.addFlag('use-emulator', help: 'Use an emulator instead of a device to run tests.');
}
@override

View File

@@ -14,18 +14,34 @@ class UploadResultsCommand extends Command<void> {
'service-account-token-file',
help: 'Authentication token for uploading results.',
);
argParser.addOption('test-flaky', help: 'Flag to show whether the test is flaky: "True" or "False"');
argParser.addOption(
'test-flaky',
help: 'Flag to show whether the test is flaky: "True" or "False"',
);
argParser.addOption(
'git-branch',
help: '[Flutter infrastructure] Git branch of the current commit. LUCI\n'
help:
'[Flutter infrastructure] Git branch of the current commit. LUCI\n'
'checkouts run in detached HEAD state, so the branch must be passed.',
);
argParser.addOption('luci-builder', help: '[Flutter infrastructure] Name of the LUCI builder being run on.');
argParser.addOption('task-name', help: '[Flutter infrastructure] Name of the task being run on.');
argParser.addOption('benchmark-tags', help: '[Flutter infrastructure] Benchmark tags to surface on Skia Perf');
argParser.addOption(
'luci-builder',
help: '[Flutter infrastructure] Name of the LUCI builder being run on.',
);
argParser.addOption(
'task-name',
help: '[Flutter infrastructure] Name of the task being run on.',
);
argParser.addOption(
'benchmark-tags',
help: '[Flutter infrastructure] Benchmark tags to surface on Skia Perf',
);
argParser.addOption('test-status', help: 'Test status: Succeeded|Failed');
argParser.addOption('commit-time', help: 'Commit time in UNIX timestamp');
argParser.addOption('builder-bucket', help: '[Flutter infrastructure] Luci builder bucket the test is running in.');
argParser.addOption(
'builder-bucket',
help: '[Flutter infrastructure] Luci builder bucket the test is running in.',
);
}
@override

View File

@@ -26,18 +26,18 @@ enum FieldJustification { LEFT, RIGHT, CENTER }
/// See [printSummary] for more.
class ABTest {
ABTest({required this.localEngine, required this.localEngineHost, required this.taskName})
: runStart = DateTime.now(),
_aResults = <String, List<double>>{},
_bResults = <String, List<double>>{};
: runStart = DateTime.now(),
_aResults = <String, List<double>>{},
_bResults = <String, List<double>>{};
ABTest.fromJsonMap(Map<String, dynamic> jsonResults)
: localEngine = jsonResults[kLocalEngineKeyName] as String,
localEngineHost = jsonResults[kLocalEngineHostKeyName] as String?,
taskName = jsonResults[kTaskNameKeyName] as String,
runStart = DateTime.parse(jsonResults[kRunStartKeyName] as String),
_runEnd = DateTime.parse(jsonResults[kRunEndKeyName] as String),
_aResults = _convertFrom(jsonResults[kAResultsKeyName] as Map<String, dynamic>),
_bResults = _convertFrom(jsonResults[kBResultsKeyName] as Map<String, dynamic>);
: localEngine = jsonResults[kLocalEngineKeyName] as String,
localEngineHost = jsonResults[kLocalEngineHostKeyName] as String?,
taskName = jsonResults[kTaskNameKeyName] as String,
runStart = DateTime.parse(jsonResults[kRunStartKeyName] as String),
_runEnd = DateTime.parse(jsonResults[kRunEndKeyName] as String),
_aResults = _convertFrom(jsonResults[kAResultsKeyName] as Map<String, dynamic>),
_bResults = _convertFrom(jsonResults[kBResultsKeyName] as Map<String, dynamic>);
final String localEngine;
final String? localEngineHost;
@@ -51,7 +51,7 @@ class ABTest {
static Map<String, List<double>> _convertFrom(dynamic results) {
final Map<String, dynamic> resultMap = results as Map<String, dynamic>;
return <String, List<double>> {
return <String, List<double>>{
for (final String key in resultMap.keys)
key: (resultMap[key] as List<dynamic>).cast<double>(),
};
@@ -86,16 +86,15 @@ class ABTest {
}
Map<String, dynamic> get jsonMap => <String, dynamic>{
kBenchmarkTypeKeyName: kBenchmarkResultsType,
kBenchmarkVersionKeyName: kBenchmarkABVersion,
kLocalEngineKeyName: localEngine,
if (localEngineHost != null)
kLocalEngineHostKeyName: localEngineHost,
kTaskNameKeyName: taskName,
kRunStartKeyName: runStart.toIso8601String(),
kRunEndKeyName: runEnd!.toIso8601String(),
kAResultsKeyName: _aResults,
kBResultsKeyName: _bResults,
kBenchmarkTypeKeyName: kBenchmarkResultsType,
kBenchmarkVersionKeyName: kBenchmarkABVersion,
kLocalEngineKeyName: localEngine,
if (localEngineHost != null) kLocalEngineHostKeyName: localEngineHost,
kTaskNameKeyName: taskName,
kRunStartKeyName: runStart.toIso8601String(),
kRunEndKeyName: runEnd!.toIso8601String(),
kAResultsKeyName: _aResults,
kBResultsKeyName: _bResults,
};
static void updateColumnLengths(List<int> lengths, List<String?> results) {
@@ -106,10 +105,12 @@ class ABTest {
}
}
static void formatResult(StringBuffer buffer,
List<int> lengths,
List<FieldJustification> aligns,
List<String?> values) {
static void formatResult(
StringBuffer buffer,
List<int> lengths,
List<FieldJustification> aligns,
List<String?> values,
) {
for (int column = 0; column < lengths.length; column++) {
final int len = lengths[column];
String? value = values[column];
@@ -117,13 +118,13 @@ class ABTest {
value = ''.padRight(len);
} else {
value = switch (aligns[column]) {
FieldJustification.LEFT => value.padRight(len),
FieldJustification.RIGHT => value.padLeft(len),
FieldJustification.LEFT => value.padRight(len),
FieldJustification.RIGHT => value.padLeft(len),
FieldJustification.CENTER => value.padLeft((len + value.length) ~/ 2).padRight(len),
};
}
if (column > 0) {
value = value.padLeft(len+1);
value = value.padLeft(len + 1);
}
buffer.write(value);
}
@@ -141,22 +142,28 @@ class ABTest {
for (final String scoreKey in <String>{...summariesA.keys, ...summariesB.keys})
<String?>[
scoreKey,
summariesA[scoreKey]?.averageString, summariesA[scoreKey]?.noiseString,
summariesB[scoreKey]?.averageString, summariesB[scoreKey]?.noiseString,
summariesA[scoreKey]?.averageString,
summariesA[scoreKey]?.noiseString,
summariesB[scoreKey]?.averageString,
summariesB[scoreKey]?.noiseString,
summariesA[scoreKey]?.improvementOver(summariesB[scoreKey]),
],
];
final List<String> titles = <String>[
'Score',
'Average A', '(noise)',
'Average B', '(noise)',
'Average A',
'(noise)',
'Average B',
'(noise)',
'Speed-up',
];
final List<FieldJustification> alignments = <FieldJustification>[
FieldJustification.LEFT,
FieldJustification.RIGHT, FieldJustification.LEFT,
FieldJustification.RIGHT, FieldJustification.LEFT,
FieldJustification.RIGHT,
FieldJustification.LEFT,
FieldJustification.RIGHT,
FieldJustification.LEFT,
FieldJustification.CENTER,
];
@@ -167,11 +174,10 @@ class ABTest {
}
final StringBuffer buffer = StringBuffer();
formatResult(buffer, lengths,
<FieldJustification>[
FieldJustification.CENTER,
...alignments.skip(1),
], titles);
formatResult(buffer, lengths, <FieldJustification>[
FieldJustification.CENTER,
...alignments.skip(1),
], titles);
for (final List<String?> row in tableRows) {
formatResult(buffer, lengths, alignments, row);
}
@@ -208,10 +214,7 @@ class ABTest {
return buffer.toString();
}
Set<String> get _allScoreKeys => <String>{
..._aResults.keys,
..._bResults.keys,
};
Set<String> get _allScoreKeys => <String>{..._aResults.keys, ..._bResults.keys};
/// Returns the summary as a tab-separated spreadsheet.
///
@@ -253,10 +256,7 @@ class ABTest {
}
class _ScoreSummary {
_ScoreSummary({
required this.average,
required this.noise,
});
_ScoreSummary({required this.average, required this.noise});
/// Average (arithmetic mean) of a series of values collected by a benchmark.
final double average;
@@ -283,13 +283,14 @@ void _addResult(TaskResult result, Map<String, List<double>> results) {
Map<String, _ScoreSummary> _summarize(Map<String, List<double>> results) {
return results.map<String, _ScoreSummary>((String scoreKey, List<double> values) {
final double average = _computeAverage(values);
return MapEntry<String, _ScoreSummary>(scoreKey, _ScoreSummary(
average: average,
// If the average is zero, the benchmark got the perfect score with no noise.
noise: average > 0
? _computeStandardDeviationForPopulation(values) / average
: 0.0,
));
return MapEntry<String, _ScoreSummary>(
scoreKey,
_ScoreSummary(
average: average,
// If the average is zero, the benchmark got the perfect score with no noise.
noise: average > 0 ? _computeStandardDeviationForPopulation(values) / average : 0.0,
),
);
});
}

View File

@@ -22,14 +22,13 @@ final List<String> debugAssets = <String>[
'assets/flutter_assets/vm_snapshot_data',
];
final List<String> baseApkFiles = <String> [
'classes.dex',
'AndroidManifest.xml',
];
final List<String> baseApkFiles = <String>['classes.dex', 'AndroidManifest.xml'];
/// Runs the given [testFunction] on a freshly generated Flutter project.
Future<void> runProjectTest(Future<void> Function(FlutterProject project) testFunction) async {
final Directory tempDir = Directory.systemTemp.createTempSync('flutter_devicelab_gradle_plugin_test.');
final Directory tempDir = Directory.systemTemp.createTempSync(
'flutter_devicelab_gradle_plugin_test.',
);
final FlutterProject project = await FlutterProject.create(tempDir, 'hello');
try {
@@ -40,8 +39,12 @@ Future<void> runProjectTest(Future<void> Function(FlutterProject project) testFu
}
/// Runs the given [testFunction] on a freshly generated Flutter plugin project.
Future<void> runPluginProjectTest(Future<void> Function(FlutterPluginProject pluginProject) testFunction) async {
final Directory tempDir = Directory.systemTemp.createTempSync('flutter_devicelab_gradle_plugin_test.');
Future<void> runPluginProjectTest(
Future<void> Function(FlutterPluginProject pluginProject) testFunction,
) async {
final Directory tempDir = Directory.systemTemp.createTempSync(
'flutter_devicelab_gradle_plugin_test.',
);
final FlutterPluginProject pluginProject = await FlutterPluginProject.create(tempDir, 'aaa');
try {
@@ -52,9 +55,16 @@ Future<void> runPluginProjectTest(Future<void> Function(FlutterPluginProject plu
}
/// Runs the given [testFunction] on a freshly generated Flutter module project.
Future<void> runModuleProjectTest(Future<void> Function(FlutterModuleProject moduleProject) testFunction) async {
final Directory tempDir = Directory.systemTemp.createTempSync('flutter_devicelab_gradle_module_test.');
final FlutterModuleProject moduleProject = await FlutterModuleProject.create(tempDir, 'hello_module');
Future<void> runModuleProjectTest(
Future<void> Function(FlutterModuleProject moduleProject) testFunction,
) async {
final Directory tempDir = Directory.systemTemp.createTempSync(
'flutter_devicelab_gradle_module_test.',
);
final FlutterModuleProject moduleProject = await FlutterModuleProject.create(
tempDir,
'hello_module',
);
try {
await testFunction(moduleProject);
@@ -66,18 +76,12 @@ Future<void> runModuleProjectTest(Future<void> Function(FlutterModuleProject mod
/// Returns the list of files inside an Android Package Kit.
Future<Iterable<String>> getFilesInApk(String apk) async {
if (!File(apk).existsSync()) {
throw TaskResult.failure(
'Gradle did not produce an output artifact file at: $apk');
throw TaskResult.failure('Gradle did not produce an output artifact file at: $apk');
}
final String files = await _evalApkAnalyzer(
<String>[
'files',
'list',
apk,
]
);
final String files = await _evalApkAnalyzer(<String>['files', 'list', apk]);
return files.split('\n').map((String file) => file.substring(1).trim());
}
/// Returns the list of files inside an Android App Bundle.
Future<Iterable<String>> getFilesInAppBundle(String bundle) {
return getFilesInApk(bundle);
@@ -102,8 +106,8 @@ bool hasMultipleOccurrences(String text, Pattern pattern) {
/// The Android home directory.
String get _androidHome {
final String? androidHome = Platform.environment['ANDROID_HOME'] ??
Platform.environment['ANDROID_SDK_ROOT'];
final String? androidHome =
Platform.environment['ANDROID_HOME'] ?? Platform.environment['ANDROID_SDK_ROOT'];
if (androidHome == null || androidHome.isEmpty) {
throw Exception('Environment variable `ANDROID_HOME` is not set.');
}
@@ -120,19 +124,22 @@ Future<String> _evalApkAnalyzer(
if (javaHome == null || javaHome.isEmpty) {
throw Exception('No JAVA_HOME set.');
}
final String apkAnalyzer = path
.join(_androidHome, 'cmdline-tools', 'latest', 'bin', Platform.isWindows ? 'apkanalyzer.bat' : 'apkanalyzer');
if (canRun(apkAnalyzer)) {
return eval(
apkAnalyzer,
args,
printStdout: printStdout,
workingDirectory: workingDirectory,
environment: <String, String>{
'JAVA_HOME': javaHome,
},
);
}
final String apkAnalyzer = path.join(
_androidHome,
'cmdline-tools',
'latest',
'bin',
Platform.isWindows ? 'apkanalyzer.bat' : 'apkanalyzer',
);
if (canRun(apkAnalyzer)) {
return eval(
apkAnalyzer,
args,
printStdout: printStdout,
workingDirectory: workingDirectory,
environment: <String, String>{'JAVA_HOME': javaHome},
);
}
final String javaBinary = path.join(javaHome, 'bin', 'java');
assert(canRun(javaBinary));
@@ -140,7 +147,7 @@ Future<String> _evalApkAnalyzer(
final String libs = path.join(androidTools, 'lib');
assert(Directory(libs).existsSync());
final String classSeparator = Platform.isWindows ? ';' : ':';
final String classSeparator = Platform.isWindows ? ';' : ':';
return eval(
javaBinary,
<String>[
@@ -171,22 +178,18 @@ class ApkExtractor {
if (_extracted) {
return;
}
final String packages = await _evalApkAnalyzer(
<String>[
'dex',
'packages',
apkFile.path,
],
);
final String packages = await _evalApkAnalyzer(<String>['dex', 'packages', apkFile.path]);
final List<String> lines = packages.split('\n');
_classes = Set<String>.from(
lines.where((String line) => line.startsWith('C'))
.map<String>((String line) => line.split('\t').last),
lines
.where((String line) => line.startsWith('C'))
.map<String>((String line) => line.split('\t').last),
);
assert(_classes.isNotEmpty);
_methods = Set<String>.from(
lines.where((String line) => line.startsWith('M'))
.map<String>((String line) => line.split('\t').last)
lines
.where((String line) => line.startsWith('M'))
.map<String>((String line) => line.split('\t').last),
);
assert(_methods.isNotEmpty);
_extracted = true;
@@ -219,14 +222,7 @@ class ApkExtractor {
/// Gets the content of the `AndroidManifest.xml`.
Future<String> getAndroidManifest(String apk) async {
return _evalApkAnalyzer(
<String>[
'manifest',
'print',
apk,
],
workingDirectory: _androidHome,
);
return _evalApkAnalyzer(<String>['manifest', 'print', apk], workingDirectory: _androidHome);
}
/// Checks that the [apk] includes any classes from a particularly library with
@@ -325,14 +321,16 @@ android {
Future<void> addProductFlavors(Iterable<String> flavors) async {
final File buildScript = appBuildFile;
final String flavorConfig = flavors.map((String name) {
return '''
final String flavorConfig = flavors
.map((String name) {
return '''
create("$name") {
applicationIdSuffix = ".$name"
versionNameSuffix = "-$name"
}
''';
}).join('\n');
})
.join('\n');
buildScript.openWrite(mode: FileMode.append).write('''
android {
@@ -346,21 +344,24 @@ android {
Future<void> introduceError() async {
final File buildScript = appBuildFile;
await buildScript.writeAsString((await buildScript.readAsString()).replaceAll('buildTypes', 'builTypes'));
await buildScript.writeAsString(
(await buildScript.readAsString()).replaceAll('buildTypes', 'builTypes'),
);
}
Future<void> introducePubspecError() async {
final File pubspec = File(
path.join(parent.path, 'hello', 'pubspec.yaml')
);
final File pubspec = File(path.join(parent.path, 'hello', 'pubspec.yaml'));
final String contents = pubspec.readAsStringSync();
final String newContents = contents.replaceFirst('${Platform.lineTerminator}flutter:${Platform.lineTerminator}', '''
final String newContents = contents.replaceFirst(
'${Platform.lineTerminator}flutter:${Platform.lineTerminator}',
'''
flutter:
assets:
- lib/gallery/example_code.dart
''');
''',
);
pubspec.writeAsStringSync(newContents);
}
@@ -387,7 +388,11 @@ class FlutterPluginProject {
final Directory parent;
final String name;
static Future<FlutterPluginProject> create(Directory directory, String name, {List<String> options = const <String>['--platforms=ios,android']}) async {
static Future<FlutterPluginProject> create(
Directory directory,
String name, {
List<String> options = const <String>['--platforms=ios,android'],
}) async {
await inDirectory(directory, () async {
await flutter('create', options: <String>['--template=plugin', ...options, name]);
});
@@ -397,11 +402,22 @@ class FlutterPluginProject {
String get rootPath => path.join(parent.path, name);
String get examplePath => path.join(rootPath, 'example');
String get exampleAndroidPath => path.join(examplePath, 'android');
String get debugApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'flutter-apk', 'app-debug.apk');
String get releaseApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'flutter-apk', 'app-release.apk');
String get releaseArmApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'flutter-apk','app-armeabi-v7a-release.apk');
String get releaseArm64ApkPath => path.join(examplePath, 'build', 'app', 'outputs', 'flutter-apk', 'app-arm64-v8a-release.apk');
String get releaseBundlePath => path.join(examplePath, 'build', 'app', 'outputs', 'bundle', 'release', 'app.aab');
String get debugApkPath =>
path.join(examplePath, 'build', 'app', 'outputs', 'flutter-apk', 'app-debug.apk');
String get releaseApkPath =>
path.join(examplePath, 'build', 'app', 'outputs', 'flutter-apk', 'app-release.apk');
String get releaseArmApkPath => path.join(
examplePath,
'build',
'app',
'outputs',
'flutter-apk',
'app-armeabi-v7a-release.apk',
);
String get releaseArm64ApkPath =>
path.join(examplePath, 'build', 'app', 'outputs', 'flutter-apk', 'app-arm64-v8a-release.apk');
String get releaseBundlePath =>
path.join(examplePath, 'build', 'app', 'outputs', 'bundle', 'release', 'app.aab');
}
class FlutterModuleProject {
@@ -426,9 +442,10 @@ Future<void> _runGradleTask({
List<String>? options,
}) async {
final ProcessResult result = await _resultOfGradleTask(
workingDirectory: workingDirectory,
task: task,
options: options);
workingDirectory: workingDirectory,
task: task,
options: options,
);
if (result.exitCode != 0) {
print('stdout:');
print(result.stdout);
@@ -454,44 +471,61 @@ Future<ProcessResult> _resultOfGradleTask({
print('\nUsing JAVA_HOME=$javaHome');
final List<String> args = <String>[
'app:$task',
...?options,
];
final String gradle = path.join(workingDirectory, Platform.isWindows ? 'gradlew.bat' : './gradlew');
final List<String> args = <String>['app:$task', ...?options];
final String gradle = path.join(
workingDirectory,
Platform.isWindows ? 'gradlew.bat' : './gradlew',
);
print('┌── $gradle');
print(File(path.join(workingDirectory, gradle)).readAsLinesSync().map((String line) => '| $line').join('\n'));
print(
File(
path.join(workingDirectory, gradle),
).readAsLinesSync().map((String line) => '| $line').join('\n'),
);
print('└─────────────────────────────────────────────────────────────────────────────────────');
print(
'Running Gradle:\n'
' Executable: $gradle\n'
' Arguments: ${args.join(' ')}\n'
' Working directory: $workingDirectory\n'
' JAVA_HOME: $javaHome\n'
' JAVA_HOME: $javaHome\n',
);
return Process.run(
gradle,
args,
workingDirectory: workingDirectory,
environment: <String, String>{ 'JAVA_HOME': javaHome },
environment: <String, String>{'JAVA_HOME': javaHome},
);
}
/// Returns [null] if target matches [expectedTarget], otherwise returns an error message.
String? validateSnapshotDependency(FlutterProject project, String expectedTarget) {
final File snapshotBlob = File(
path.join(project.rootPath, 'build', 'app', 'intermediates',
'flutter', 'debug', 'flutter_build.d'));
path.join(
project.rootPath,
'build',
'app',
'intermediates',
'flutter',
'debug',
'flutter_build.d',
),
);
assert(snapshotBlob.existsSync());
final String contentSnapshot = snapshotBlob.readAsStringSync();
return contentSnapshot.contains('$expectedTarget ')
? null : 'Dependency file should have $expectedTarget as target. Instead found $contentSnapshot';
? null
: 'Dependency file should have $expectedTarget as target. Instead found $contentSnapshot';
}
File getAndroidBuildFile(String androidAppPath, {bool settings = false}) {
final File groovyFile = File(path.join(androidAppPath, settings ? 'settings.gradle' : 'build.gradle'));
final File kotlinFile = File(path.join(androidAppPath, settings ? 'settings.gradle.kts' : 'build.gradle.kts'));
final File groovyFile = File(
path.join(androidAppPath, settings ? 'settings.gradle' : 'build.gradle'),
);
final File kotlinFile = File(
path.join(androidAppPath, settings ? 'settings.gradle.kts' : 'build.gradle.kts'),
);
if (groovyFile.existsSync()) {
return groovyFile;

View File

@@ -79,30 +79,32 @@ class Chrome {
/// The [onError] callback is called with an error message when the Chrome
/// process encounters an error. In particular, [onError] is called when the
/// Chrome process exits prematurely, i.e. before [stop] is called.
static Future<Chrome> launch(ChromeOptions options, { String? workingDirectory, required ChromeErrorCallback onError }) async {
static Future<Chrome> launch(
ChromeOptions options, {
String? workingDirectory,
required ChromeErrorCallback onError,
}) async {
if (!io.Platform.isWindows) {
final io.ProcessResult versionResult = io.Process.runSync(_findSystemChromeExecutable(), const <String>['--version']);
final io.ProcessResult versionResult = io.Process.runSync(
_findSystemChromeExecutable(),
const <String>['--version'],
);
print('Launching ${versionResult.stdout}');
} else {
print('Launching Chrome...');
}
final String jsFlags = options.enableWasmGC ? <String>[
'--experimental-wasm-gc',
'--experimental-wasm-type-reflection',
].join(' ') : '';
final String jsFlags =
options.enableWasmGC
? <String>['--experimental-wasm-gc', '--experimental-wasm-type-reflection'].join(' ')
: '';
final bool withDebugging = options.debugPort != null;
final List<String> args = <String>[
if (options.userDataDirectory != null)
'--user-data-dir=${options.userDataDirectory}',
if (options.url != null)
options.url!,
if (io.Platform.environment['CHROME_NO_SANDBOX'] == 'true')
'--no-sandbox',
if (options.headless ?? false)
'--headless',
if (withDebugging)
'--remote-debugging-port=${options.debugPort}',
if (options.userDataDirectory != null) '--user-data-dir=${options.userDataDirectory}',
if (options.url != null) options.url!,
if (io.Platform.environment['CHROME_NO_SANDBOX'] == 'true') '--no-sandbox',
if (options.headless ?? false) '--headless',
if (withDebugging) '--remote-debugging-port=${options.debugPort}',
'--window-size=${options.windowWidth},${options.windowHeight}',
'--disable-extensions',
'--disable-popup-blocking',
@@ -134,7 +136,7 @@ class Chrome {
final WipConnection? _debugConnection;
bool _isStopped = false;
Completer<void> ?_tracingCompleter;
Completer<void>? _tracingCompleter;
StreamSubscription<WipEvent>? _tracingSubscription;
List<Map<String, dynamic>>? _tracingData;
@@ -148,7 +150,7 @@ class Chrome {
if (_tracingCompleter != null) {
throw StateError(
'Cannot start a new performance trace. A tracing session labeled '
'"$label" is already in progress.'
'"$label" is already in progress.',
);
}
_tracingCompleter = Completer<void>();
@@ -167,10 +169,14 @@ class Chrome {
} else if (event.method == 'Tracing.dataCollected') {
final dynamic value = event.params?['value'];
if (value is! List) {
throw FormatException('"Tracing.dataCollected" returned malformed data. '
'Expected a List but got: ${value.runtimeType}');
throw FormatException(
'"Tracing.dataCollected" returned malformed data. '
'Expected a List but got: ${value.runtimeType}',
);
}
_tracingData?.addAll((event.params?['value'] as List<dynamic>).cast<Map<String, dynamic>>());
_tracingData?.addAll(
(event.params?['value'] as List<dynamic>).cast<Map<String, dynamic>>(),
);
}
});
await _debugConnection?.sendCommand('Tracing.start', <String, dynamic>{
@@ -226,8 +232,7 @@ String _findSystemChromeExecutable() {
}
if (io.Platform.isLinux) {
final io.ProcessResult which =
io.Process.runSync('which', <String>['google-chrome']);
final io.ProcessResult which = io.Process.runSync('which', <String>['google-chrome']);
if (which.exitCode != 0) {
throw Exception('Failed to locate system Chrome installation.');
@@ -238,11 +243,12 @@ String _findSystemChromeExecutable() {
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
} else if (io.Platform.isWindows) {
const String kWindowsExecutable = r'Google\Chrome\Application\chrome.exe';
final List<String> kWindowsPrefixes = <String?>[
io.Platform.environment['LOCALAPPDATA'],
io.Platform.environment['PROGRAMFILES'],
io.Platform.environment['PROGRAMFILES(X86)'],
].whereType<String>().toList();
final List<String> kWindowsPrefixes =
<String?>[
io.Platform.environment['LOCALAPPDATA'],
io.Platform.environment['PROGRAMFILES'],
io.Platform.environment['PROGRAMFILES(X86)'],
].whereType<String>().toList();
final String windowsPrefix = kWindowsPrefixes.firstWhere((String prefix) {
final String expectedPath = path.join(prefix, kWindowsExecutable);
return io.File(expectedPath).existsSync();
@@ -272,7 +278,8 @@ Future<Uri> _getRemoteDebuggerUrl(Uri base) async {
final io.HttpClient client = io.HttpClient();
final io.HttpClientRequest request = await client.getUrl(base.resolve('/json/list'));
final io.HttpClientResponse response = await request.close();
final List<dynamic>? jsonObject = await json.fuse(utf8).decoder.bind(response).single as List<dynamic>?;
final List<dynamic>? jsonObject =
await json.fuse(utf8).decoder.bind(response).single as List<dynamic>?;
if (jsonObject == null || jsonObject.isEmpty) {
return base;
}
@@ -289,10 +296,9 @@ class BlinkTraceSummary {
static BlinkTraceSummary? fromJson(List<Map<String, dynamic>> traceJson) {
try {
// Convert raw JSON data to BlinkTraceEvent objects sorted by timestamp.
List<BlinkTraceEvent> events = traceJson
.map<BlinkTraceEvent>(BlinkTraceEvent.fromJson)
.toList()
..sort((BlinkTraceEvent a, BlinkTraceEvent b) => a.ts! - b.ts!);
List<BlinkTraceEvent> events =
traceJson.map<BlinkTraceEvent>(BlinkTraceEvent.fromJson).toList()
..sort((BlinkTraceEvent a, BlinkTraceEvent b) => a.ts! - b.ts!);
Exception noMeasuredFramesFound() => Exception(
'No measured frames found in benchmark tracing data. This likely '
@@ -349,12 +355,21 @@ class BlinkTraceSummary {
// Compute averages and summarize.
return BlinkTraceSummary._(
averageBeginFrameTime: _computeAverageDuration(frames.map((BlinkFrame frame) => frame.beginFrame).whereType<BlinkTraceEvent>().toList()),
averageUpdateLifecyclePhasesTime: _computeAverageDuration(frames.map((BlinkFrame frame) => frame.updateAllLifecyclePhases).whereType<BlinkTraceEvent>().toList()),
averageBeginFrameTime: _computeAverageDuration(
frames.map((BlinkFrame frame) => frame.beginFrame).whereType<BlinkTraceEvent>().toList(),
),
averageUpdateLifecyclePhasesTime: _computeAverageDuration(
frames
.map((BlinkFrame frame) => frame.updateAllLifecyclePhases)
.whereType<BlinkTraceEvent>()
.toList(),
),
);
} catch (_) {
final io.File traceFile = io.File('./chrome-trace.json');
io.stderr.writeln('Failed to interpret the Chrome trace contents. The trace was saved in ${traceFile.path}');
io.stderr.writeln(
'Failed to interpret the Chrome trace contents. The trace was saved in ${traceFile.path}',
);
traceFile.writeAsStringSync(const JsonEncoder.withIndent(' ').convert(traceJson));
rethrow;
}
@@ -381,9 +396,10 @@ class BlinkTraceSummary {
final Duration averageTotalUIFrameTime;
@override
String toString() => '$BlinkTraceSummary('
'averageBeginFrameTime: ${averageBeginFrameTime.inMicroseconds / 1000}ms, '
'averageUpdateLifecyclePhasesTime: ${averageUpdateLifecyclePhasesTime.inMicroseconds / 1000}ms)';
String toString() =>
'$BlinkTraceSummary('
'averageBeginFrameTime: ${averageBeginFrameTime.inMicroseconds / 1000}ms, '
'averageUpdateLifecyclePhasesTime: ${averageUpdateLifecyclePhasesTime.inMicroseconds / 1000}ms)';
}
/// Contains events pertaining to a single frame in the Blink trace data.
@@ -405,14 +421,15 @@ class BlinkFrame {
/// their average as a [Duration] value.
Duration _computeAverageDuration(List<BlinkTraceEvent> events) {
// Compute the sum of "tdur" fields of the last _kMeasuredSampleCount events.
final double sum = events
.skip(math.max(events.length - _kMeasuredSampleCount, 0))
.fold(0.0, (double previousValue, BlinkTraceEvent event) {
if (event.tdur == null) {
throw FormatException('Trace event lacks "tdur" field: $event');
}
return previousValue + event.tdur!;
});
final double sum = events.skip(math.max(events.length - _kMeasuredSampleCount, 0)).fold(0.0, (
double previousValue,
BlinkTraceEvent event,
) {
if (event.tdur == null) {
throw FormatException('Trace event lacks "tdur" field: $event');
}
return previousValue + event.tdur!;
});
final int sampleCount = math.min(events.length, _kMeasuredSampleCount);
return Duration(microseconds: sum ~/ sampleCount);
}
@@ -447,15 +464,15 @@ class BlinkTraceEvent {
///
/// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
BlinkTraceEvent.fromJson(Map<String, dynamic> json)
: args = json['args'] as Map<String, dynamic>,
cat = json['cat'] as String,
name = json['name'] as String,
ph = json['ph'] as String,
pid = _readInt(json, 'pid'),
tid = _readInt(json, 'tid'),
ts = _readInt(json, 'ts'),
tts = _readInt(json, 'tts'),
tdur = _readInt(json, 'tdur');
: args = json['args'] as Map<String, dynamic>,
cat = json['cat'] as String,
name = json['name'] as String,
ph = json['ph'] as String,
pid = _readInt(json, 'pid'),
tid = _readInt(json, 'tid'),
ts = _readInt(json, 'ts'),
tts = _readInt(json, 'tts'),
tdur = _readInt(json, 'tdur');
/// Event-specific data.
final Map<String, dynamic> args;
@@ -496,11 +513,10 @@ class BlinkTraceEvent {
///
/// This event is a duration event that has its `tdur` populated.
bool get isBeginFrame {
return ph == 'X' && (
name == 'WebViewImpl::beginFrame' ||
name == 'WebFrameWidgetBase::BeginMainFrame' ||
name == 'WebFrameWidgetImpl::BeginMainFrame'
);
return ph == 'X' &&
(name == 'WebViewImpl::beginFrame' ||
name == 'WebFrameWidgetBase::BeginMainFrame' ||
name == 'WebFrameWidgetImpl::BeginMainFrame');
}
/// An "update all lifecycle phases" event contains UI thread computations
@@ -514,10 +530,9 @@ class BlinkTraceEvent {
///
/// This event is a duration event that has its `tdur` populated.
bool get isUpdateAllLifecyclePhases {
return ph == 'X' && (
name == 'WebViewImpl::updateAllLifecyclePhases' ||
name == 'WebFrameWidgetImpl::UpdateLifecycle'
);
return ph == 'X' &&
(name == 'WebViewImpl::updateAllLifecyclePhases' ||
name == 'WebFrameWidgetImpl::UpdateLifecycle');
}
/// Whether this is the beginning of a "measured_frame" event.
@@ -537,16 +552,17 @@ class BlinkTraceEvent {
bool get isEndMeasuredFrame => ph == 'e' && name == 'measured_frame';
@override
String toString() => '$BlinkTraceEvent('
'args: ${json.encode(args)}, '
'cat: $cat, '
'name: $name, '
'ph: $ph, '
'pid: $pid, '
'tid: $tid, '
'ts: $ts, '
'tts: $tts, '
'tdur: $tdur)';
String toString() =>
'$BlinkTraceEvent('
'args: ${json.encode(args)}, '
'cat: $cat, '
'name: $name, '
'ph: $ph, '
'pid: $pid, '
'tid: $tid, '
'ts: $ts, '
'tts: $tts, '
'tdur: $tdur)';
}
/// Read an integer out of [json] stored under [key].
@@ -572,48 +588,56 @@ int? _readInt(Map<String, dynamic> json, String key) {
/// Inconsistency detected by ld.so: ../elf/dl-tls.c: 493: _dl_allocate_tls_init: Assertion `listp->slotinfo[cnt].gen <= GL(dl_tls_generation)' failed!
const String _kGlibcError = 'Inconsistency detected by ld.so';
Future<io.Process> _spawnChromiumProcess(String executable, List<String> args, { String? workingDirectory }) async {
Future<io.Process> _spawnChromiumProcess(
String executable,
List<String> args, {
String? workingDirectory,
}) async {
// Keep attempting to launch the browser until one of:
// - Chrome launched successfully, in which case we just return from the loop.
// - The tool detected an unretryable Chrome error, in which case we throw ToolExit.
while (true) {
final io.Process process = await io.Process.start(executable, args, workingDirectory: workingDirectory);
final io.Process process = await io.Process.start(
executable,
args,
workingDirectory: workingDirectory,
);
process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((String line) {
print('[CHROME STDOUT]: $line');
});
process.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((String line) {
print('[CHROME STDOUT]: $line');
});
// Wait until the DevTools are listening before trying to connect. This is
// only required for flutter_test --platform=chrome and not flutter run.
bool hitGlibcBug = false;
await process.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.map((String line) {
print('[CHROME STDERR]:$line');
if (line.contains(_kGlibcError)) {
hitGlibcBug = true;
}
return line;
})
.firstWhere((String line) => line.startsWith('DevTools listening'), orElse: () {
if (hitGlibcBug) {
print(
'Encountered glibc bug https://sourceware.org/bugzilla/show_bug.cgi?id=19329. '
'Will try launching browser again.',
);
return '';
}
print('Failed to launch browser. Command used to launch it: ${args.join(' ')}');
throw Exception(
'Failed to launch browser. Make sure you are using an up-to-date '
'Chrome or Edge. Otherwise, consider using -d web-server instead '
'and filing an issue at https://github.com/flutter/flutter/issues.',
.transform(utf8.decoder)
.transform(const LineSplitter())
.map((String line) {
print('[CHROME STDERR]:$line');
if (line.contains(_kGlibcError)) {
hitGlibcBug = true;
}
return line;
})
.firstWhere(
(String line) => line.startsWith('DevTools listening'),
orElse: () {
if (hitGlibcBug) {
print(
'Encountered glibc bug https://sourceware.org/bugzilla/show_bug.cgi?id=19329. '
'Will try launching browser again.',
);
return '';
}
print('Failed to launch browser. Command used to launch it: ${args.join(' ')}');
throw Exception(
'Failed to launch browser. Make sure you are using an up-to-date '
'Chrome or Edge. Otherwise, consider using -d web-server instead '
'and filing an issue at https://github.com/flutter/flutter/issues.',
);
},
);
});
if (!hitGlibcBug) {
return process;
@@ -622,9 +646,14 @@ Future<io.Process> _spawnChromiumProcess(String executable, List<String> args, {
// A precaution that avoids accumulating browser processes, in case the
// glibc bug doesn't cause the browser to quit and we keep looping and
// launching more processes.
unawaited(process.exitCode.timeout(const Duration(seconds: 1), onTimeout: () {
process.kill();
return 0;
}));
unawaited(
process.exitCode.timeout(
const Duration(seconds: 1),
onTimeout: () {
process.kill();
return 0;
},
),
);
}
}

View File

@@ -15,16 +15,17 @@ import 'package:meta/meta.dart';
import 'task_result.dart';
import 'utils.dart';
typedef ProcessRunSync = ProcessResult Function(
String,
List<String>, {
Map<String, String>? environment,
bool includeParentEnvironment,
bool runInShell,
Encoding? stderrEncoding,
Encoding? stdoutEncoding,
String? workingDirectory,
});
typedef ProcessRunSync =
ProcessResult Function(
String,
List<String>, {
Map<String, String>? environment,
bool includeParentEnvironment,
bool runInShell,
Encoding? stderrEncoding,
Encoding? stdoutEncoding,
String? workingDirectory,
});
/// Class for test runner to interact with Flutter's infrastructure service, Cocoon.
///
@@ -38,7 +39,11 @@ class Cocoon {
@visibleForTesting this.processRunSync = Process.runSync,
@visibleForTesting this.requestRetryLimit = 5,
@visibleForTesting this.requestTimeoutLimit = 30,
}) : _httpClient = AuthenticatedCocoonClient(serviceAccountTokenPath, httpClient: httpClient, filesystem: fs);
}) : _httpClient = AuthenticatedCocoonClient(
serviceAccountTokenPath,
httpClient: httpClient,
filesystem: fs,
);
/// Client to make http requests to Cocoon.
final AuthenticatedCocoonClient _httpClient;
@@ -105,8 +110,10 @@ class Cocoon {
resultsJson['TestFlaky'] = isTestFlaky ?? false;
if (_shouldUpdateCocoon(resultsJson, builderBucket ?? 'prod')) {
await retry(
() async => _sendUpdateTaskRequest(resultsJson).timeout(Duration(seconds: requestTimeoutLimit)),
retryIf: (Exception e) => e is SocketException || e is TimeoutException || e is ClientException,
() async =>
_sendUpdateTaskRequest(resultsJson).timeout(Duration(seconds: requestTimeoutLimit)),
retryIf:
(Exception e) => e is SocketException || e is TimeoutException || e is ClientException,
maxAttempts: requestRetryLimit,
);
}
@@ -191,7 +198,8 @@ class Cocoon {
/// as version changes to the backend, datastore issues, or latency issues.
final Response response = await retry(
() => _httpClient.post(url, body: json.encode(jsonData)),
retryIf: (Exception e) => e is SocketException || e is TimeoutException || e is ClientException,
retryIf:
(Exception e) => e is SocketException || e is TimeoutException || e is ClientException,
maxAttempts: requestRetryLimit,
);
return json.decode(response.body) as Map<String, dynamic>;
@@ -204,8 +212,8 @@ class AuthenticatedCocoonClient extends BaseClient {
this._serviceAccountTokenPath, {
@visibleForTesting Client? httpClient,
@visibleForTesting FileSystem? filesystem,
}) : _delegate = httpClient ?? Client(),
_fs = filesystem ?? const LocalFileSystem();
}) : _delegate = httpClient ?? Client(),
_fs = filesystem ?? const LocalFileSystem();
/// Authentication token to have the ability to upload and record test results.
///
@@ -234,12 +242,13 @@ class AuthenticatedCocoonClient extends BaseClient {
if (response.statusCode != 200) {
throw ClientException(
'AuthenticatedClientError:\n'
' URI: ${request.url}\n'
' HTTP Status: ${response.statusCode}\n'
' Response body:\n'
'${(await Response.fromStream(response)).body}',
request.url);
'AuthenticatedClientError:\n'
' URI: ${request.url}\n'
' HTTP Status: ${response.statusCode}\n'
' Response body:\n'
'${(await Response.fromStream(response)).body}',
request.url,
);
}
return response;
}

View File

@@ -64,7 +64,6 @@ final RegExp flutterCompileSdkString = RegExp(r'flutter\.compileSdkVersion|flutt
/// A simple class containing a Kotlin, Gradle, and AGP version.
class VersionTuple {
VersionTuple({
required this.agpVersion,
required this.gradleVersion,
@@ -94,7 +93,8 @@ class VersionTuple {
Future<TaskResult> buildFlutterApkWithSpecifiedDependencyVersions({
required List<VersionTuple> versionTuples,
required Directory tempDir,
required LocalFileSystem localFileSystem,}) async {
required LocalFileSystem localFileSystem,
}) async {
for (final VersionTuple versions in versionTuples) {
final Directory innerTempDir = tempDir.createTempSync(versions.gradleVersion);
try {
@@ -102,25 +102,33 @@ Future<TaskResult> buildFlutterApkWithSpecifiedDependencyVersions({
section('Create new app with dependency versions: $versions');
await flutter(
'create',
options: <String>[
'dependency_checker_app',
'--platforms=android',
],
options: <String>['dependency_checker_app', '--platforms=android'],
workingDirectory: innerTempDir.path,
);
final String appPath = '${innerTempDir.absolute.path}/dependency_checker_app';
if (versions.compileSdkVersion != null) {
final File appGradleBuild = getAndroidBuildFile(localFileSystem.path.join(appPath, 'android', 'app'));
final String appBuildContent = appGradleBuild.readAsStringSync()
.replaceFirst(flutterCompileSdkString, versions.compileSdkVersion!);
final File appGradleBuild = getAndroidBuildFile(
localFileSystem.path.join(appPath, 'android', 'app'),
);
final String appBuildContent = appGradleBuild.readAsStringSync().replaceFirst(
flutterCompileSdkString,
versions.compileSdkVersion!,
);
appGradleBuild.writeAsStringSync(appBuildContent);
}
// Modify gradle version to passed in version.
final File gradleWrapperProperties = localFileSystem.file(localFileSystem.path.join(
appPath, 'android', 'gradle', 'wrapper', 'gradle-wrapper.properties'));
final File gradleWrapperProperties = localFileSystem.file(
localFileSystem.path.join(
appPath,
'android',
'gradle',
'wrapper',
'gradle-wrapper.properties',
),
);
final String propertyContent = gradleWrapperPropertiesFileContent.replaceFirst(
gradleReplacementString,
versions.gradleVersion,
@@ -136,20 +144,16 @@ Future<TaskResult> buildFlutterApkWithSpecifiedDependencyVersions({
.replaceFirst(kgpReplacementString, versions.kotlinVersion);
await gradleSettingsFile.writeAsString(settingsContent, flush: true);
// Ensure that gradle files exists from templates.
section("Ensure 'flutter build apk' succeeds with Gradle ${versions.gradleVersion}, AGP ${versions.agpVersion}, and Kotlin ${versions.kotlinVersion}");
await flutter(
'build',
options: <String>[
'apk',
'--debug',
],
workingDirectory: appPath,
section(
"Ensure 'flutter build apk' succeeds with Gradle ${versions.gradleVersion}, AGP ${versions.agpVersion}, and Kotlin ${versions.kotlinVersion}",
);
await flutter('build', options: <String>['apk', '--debug'], workingDirectory: appPath);
} catch (e) {
tempDir.deleteSync(recursive: true);
return TaskResult.failure('Failed to build app with Gradle ${versions.gradleVersion}, AGP ${versions.agpVersion}, and Kotlin ${versions.kotlinVersion}, error was:\n$e');
return TaskResult.failure(
'Failed to build app with Gradle ${versions.gradleVersion}, AGP ${versions.agpVersion}, and Kotlin ${versions.kotlinVersion}, error was:\n$e',
);
}
}
tempDir.deleteSync(recursive: true);

View File

@@ -25,12 +25,7 @@ class DeviceException implements Exception {
/// Gets the artifact path relative to the current directory.
String getArtifactPath() {
return path.normalize(
path.join(
path.current,
'../../bin/cache/artifacts',
)
);
return path.normalize(path.join(path.current, '../../bin/cache/artifacts'));
}
/// Return the item is in idList if find a match, otherwise return null
@@ -201,10 +196,7 @@ abstract class Device {
Future<void> awaitDevice();
Future<void> uninstallApp() async {
await flutter('install', options: <String>[
'--uninstall-only',
'-d',
deviceId]);
await flutter('install', options: <String>['--uninstall-only', '-d', deviceId]);
await Future<void>.delayed(const Duration(seconds: 2));
@@ -217,10 +209,7 @@ abstract class Device {
}
}
enum AndroidCPU {
arm,
arm64,
}
enum AndroidCPU { arm, arm64 }
class AndroidDeviceDiscovery implements DeviceDiscovery {
factory AndroidDeviceDiscovery({AndroidCPU? cpu}) {
@@ -258,7 +247,7 @@ class AndroidDeviceDiscovery implements DeviceDiscovery {
return switch (cpu) {
null => Future<bool>.value(true),
AndroidCPU.arm64 => device.isArm64(),
AndroidCPU.arm => device.isArm(),
AndroidCPU.arm => device.isArm(),
};
}
@@ -266,9 +255,10 @@ class AndroidDeviceDiscovery implements DeviceDiscovery {
/// [workingDevice].
@override
Future<void> chooseWorkingDevice() async {
final List<AndroidDevice> allDevices = (await discoverDevices())
.map<AndroidDevice>((String id) => AndroidDevice(deviceId: id))
.toList();
final List<AndroidDevice> allDevices =
(await discoverDevices())
.map<AndroidDevice>((String id) => AndroidDevice(deviceId: id))
.toList();
if (allDevices.isEmpty) {
throw const DeviceException('No Android devices detected');
@@ -281,7 +271,6 @@ class AndroidDeviceDiscovery implements DeviceDiscovery {
break;
}
}
} else {
// TODO(yjbanov): filter out and warn about those with low battery level
_workingDevice = allDevices[math.Random().nextInt(allDevices.length)];
@@ -301,7 +290,9 @@ class AndroidDeviceDiscovery implements DeviceDiscovery {
_workingDevice = AndroidDevice(deviceId: matchedId);
if (cpu != null) {
if (!await _matchesCPURequirement(_workingDevice!)) {
throw DeviceException('The selected device $matchedId does not match the cpu requirement');
throw DeviceException(
'The selected device $matchedId does not match the cpu requirement',
);
}
}
print('Choose device by ID: $matchedId');
@@ -309,14 +300,13 @@ class AndroidDeviceDiscovery implements DeviceDiscovery {
}
throw DeviceException(
'Device with ID $deviceId is not found for operating system: '
'$deviceOperatingSystem'
);
'$deviceOperatingSystem',
);
}
@override
Future<List<String>> discoverDevices() async {
final List<String> output = (await eval(adbPath, <String>['devices', '-l']))
.trim().split('\n');
final List<String> output = (await eval(adbPath, <String>['devices', '-l'])).trim().split('\n');
final List<String> results = <String>[];
for (final String line in output) {
// Skip lines like: * daemon started successfully *
@@ -391,10 +381,10 @@ class LinuxDeviceDiscovery implements DeviceDiscovery {
}
@override
Future<void> chooseWorkingDevice() async { }
Future<void> chooseWorkingDevice() async {}
@override
Future<void> chooseWorkingDeviceById(String deviceId) async { }
Future<void> chooseWorkingDeviceById(String deviceId) async {}
@override
Future<List<String>> discoverDevices() async {
@@ -402,10 +392,10 @@ class LinuxDeviceDiscovery implements DeviceDiscovery {
}
@override
Future<void> performPreflightTasks() async { }
Future<void> performPreflightTasks() async {}
@override
Future<Device> get workingDevice async => _device;
Future<Device> get workingDevice async => _device;
}
class MacosDeviceDiscovery implements DeviceDiscovery {
@@ -425,10 +415,10 @@ class MacosDeviceDiscovery implements DeviceDiscovery {
}
@override
Future<void> chooseWorkingDevice() async { }
Future<void> chooseWorkingDevice() async {}
@override
Future<void> chooseWorkingDeviceById(String deviceId) async { }
Future<void> chooseWorkingDeviceById(String deviceId) async {}
@override
Future<List<String>> discoverDevices() async {
@@ -436,10 +426,10 @@ class MacosDeviceDiscovery implements DeviceDiscovery {
}
@override
Future<void> performPreflightTasks() async { }
Future<void> performPreflightTasks() async {}
@override
Future<Device> get workingDevice async => _device;
Future<Device> get workingDevice async => _device;
}
class WindowsDeviceDiscovery implements DeviceDiscovery {
@@ -459,10 +449,10 @@ class WindowsDeviceDiscovery implements DeviceDiscovery {
}
@override
Future<void> chooseWorkingDevice() async { }
Future<void> chooseWorkingDevice() async {}
@override
Future<void> chooseWorkingDeviceById(String deviceId) async { }
Future<void> chooseWorkingDeviceById(String deviceId) async {}
@override
Future<List<String>> discoverDevices() async {
@@ -470,10 +460,10 @@ class WindowsDeviceDiscovery implements DeviceDiscovery {
}
@override
Future<void> performPreflightTasks() async { }
Future<void> performPreflightTasks() async {}
@override
Future<Device> get workingDevice async => _device;
Future<Device> get workingDevice async => _device;
}
class FuchsiaDeviceDiscovery implements DeviceDiscovery {
@@ -488,7 +478,7 @@ class FuchsiaDeviceDiscovery implements DeviceDiscovery {
FuchsiaDevice? _workingDevice;
String get _ffx {
final String ffx = path.join(getArtifactPath(), 'fuchsia', 'tools','x64', 'ffx');
final String ffx = path.join(getArtifactPath(), 'fuchsia', 'tools', 'x64', 'ffx');
if (!File(ffx).existsSync()) {
throw FileSystemException("Couldn't find ffx at location $ffx");
}
@@ -511,9 +501,10 @@ class FuchsiaDeviceDiscovery implements DeviceDiscovery {
/// Picks the first connected Fuchsia device.
@override
Future<void> chooseWorkingDevice() async {
final List<FuchsiaDevice> allDevices = (await discoverDevices())
.map<FuchsiaDevice>((String id) => FuchsiaDevice(deviceId: id))
.toList();
final List<FuchsiaDevice> allDevices =
(await discoverDevices())
.map<FuchsiaDevice>((String id) => FuchsiaDevice(deviceId: id))
.toList();
if (allDevices.isEmpty) {
throw const DeviceException('No Fuchsia devices detected');
@@ -532,15 +523,18 @@ class FuchsiaDeviceDiscovery implements DeviceDiscovery {
}
throw DeviceException(
'Device with ID $deviceId is not found for operating system: '
'$deviceOperatingSystem'
);
'$deviceOperatingSystem',
);
}
@override
Future<List<String>> discoverDevices() async {
final List<String> output = (await eval(_ffx, <String>['target', 'list', '-f', 's']))
.trim()
.split('\n');
final List<String> output = (await eval(_ffx, <String>[
'target',
'list',
'-f',
's',
])).trim().split('\n');
final List<String> devices = <String>[];
for (final String line in output) {
@@ -556,20 +550,13 @@ class FuchsiaDeviceDiscovery implements DeviceDiscovery {
final Map<String, HealthCheckResult> results = <String, HealthCheckResult>{};
for (final String deviceId in await discoverDevices()) {
try {
final int resolveResult = await exec(
_ffx,
<String>[
'target',
'list',
'-f',
'a',
deviceId,
]
);
final int resolveResult = await exec(_ffx, <String>['target', 'list', '-f', 'a', deviceId]);
if (resolveResult == 0) {
results['fuchsia-device-$deviceId'] = HealthCheckResult.success();
} else {
results['fuchsia-device-$deviceId'] = HealthCheckResult.failure('Cannot resolve device $deviceId');
results['fuchsia-device-$deviceId'] = HealthCheckResult.failure(
'Cannot resolve device $deviceId',
);
}
} on Exception catch (error, stacktrace) {
results['fuchsia-device-$deviceId'] = HealthCheckResult.error(error, stacktrace);
@@ -594,7 +581,11 @@ class AndroidDevice extends Device {
@override
Future<void> toggleFixedPerformanceMode(bool enable) async {
await shellExec('cmd', <String>['power', 'set-fixed-performance-mode-enabled', if (enable) 'true' else 'false']);
await shellExec('cmd', <String>[
'power',
'set-fixed-performance-mode-enabled',
if (enable) 'true' else 'false',
]);
}
/// Whether the device is awake.
@@ -677,15 +668,15 @@ class AndroidDevice extends Device {
Future<void> _updateDeviceInfo() async {
String info;
try {
info = await shellEval(
info = await shellEval('getprop', <String>[
'ro.bootimage.build.fingerprint',
';',
'getprop',
<String>[
'ro.bootimage.build.fingerprint', ';',
'getprop', 'ro.build.version.release', ';',
'getprop', 'ro.build.version.sdk',
],
silent: true,
);
'ro.build.version.release',
';',
'getprop',
'ro.build.version.sdk',
], silent: true);
} on IOException {
info = '';
}
@@ -700,22 +691,32 @@ class AndroidDevice extends Device {
}
/// Executes [command] on `adb shell`.
Future<void> shellExec(String command, List<String> arguments, { Map<String, String>? environment, bool silent = false }) async {
Future<void> shellExec(
String command,
List<String> arguments, {
Map<String, String>? environment,
bool silent = false,
}) async {
await adb(<String>['shell', command, ...arguments], environment: environment, silent: silent);
}
/// Executes [command] on `adb shell` and returns its standard output as a [String].
Future<String> shellEval(String command, List<String> arguments, { Map<String, String>? environment, bool silent = false }) {
Future<String> shellEval(
String command,
List<String> arguments, {
Map<String, String>? environment,
bool silent = false,
}) {
return adb(<String>['shell', command, ...arguments], environment: environment, silent: silent);
}
/// Runs `adb` with the given [arguments], selecting this device.
Future<String> adb(
List<String> arguments, {
Map<String, String>? environment,
bool silent = false,
bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
}) {
List<String> arguments, {
Map<String, String>? environment,
bool silent = false,
bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
}) {
return eval(
adbPath,
<String>['-s', deviceId, ...arguments],
@@ -731,9 +732,7 @@ class AndroidDevice extends Device {
final String meminfo = await shellEval('dumpsys', <String>['meminfo', packageName]);
final Match? match = RegExp(r'TOTAL\s+(\d+)').firstMatch(meminfo);
assert(match != null, 'could not parse dumpsys meminfo output');
return <String, dynamic>{
'total_kb': int.parse(match!.group(1)!),
};
return <String, dynamic>{'total_kb': int.parse(match!.group(1)!)};
}
@override
@@ -752,21 +751,23 @@ class AndroidDevice extends Device {
// Catch the whole log.
<String>['-s', deviceId, 'logcat'],
);
_loggingProcess!.stdout
.transform<String>(const Utf8Decoder(allowMalformed: true))
.listen((String line) {
sink.write(line);
});
_loggingProcess!.stderr
.transform<String>(const Utf8Decoder(allowMalformed: true))
.listen((String line) {
sink.write(line);
});
unawaited(_loggingProcess!.exitCode.then<void>((int exitCode) {
if (!_abortedLogging) {
sink.writeln('adb logcat failed with exit code $exitCode.\n');
}
}));
_loggingProcess!.stdout.transform<String>(const Utf8Decoder(allowMalformed: true)).listen((
String line,
) {
sink.write(line);
});
_loggingProcess!.stderr.transform<String>(const Utf8Decoder(allowMalformed: true)).listen((
String line,
) {
sink.write(line);
});
unawaited(
_loggingProcess!.exitCode.then<void>((int exitCode) {
if (!_abortedLogging) {
sink.writeln('adb logcat failed with exit code $exitCode.\n');
}
}),
);
}
@override
@@ -805,27 +806,39 @@ class AndroidDevice extends Device {
<String>['-s', deviceId, 'logcat', 'ActivityManager:I', 'flutter:V', '*:F'],
);
process.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('adb logcat: $line');
if (!stream.isClosed) {
stream.sink.add(line);
}
}, onDone: () { stdoutDone.complete(); });
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen(
(String line) {
print('adb logcat: $line');
if (!stream.isClosed) {
stream.sink.add(line);
}
},
onDone: () {
stdoutDone.complete();
},
);
process.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('adb logcat stderr: $line');
}, onDone: () { stderrDone.complete(); });
unawaited(process.exitCode.then<void>((int exitCode) {
print('adb logcat process terminated with exit code $exitCode');
if (!aborted) {
stream.addError(BuildFailedError('adb logcat failed with exit code $exitCode.\n'));
processDone.complete();
}
}));
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen(
(String line) {
print('adb logcat stderr: $line');
},
onDone: () {
stderrDone.complete();
},
);
unawaited(
process.exitCode.then<void>((int exitCode) {
print('adb logcat process terminated with exit code $exitCode');
if (!aborted) {
stream.addError(BuildFailedError('adb logcat failed with exit code $exitCode.\n'));
processDone.complete();
}
}),
);
await Future.any<dynamic>(<Future<dynamic>>[
Future.wait<void>(<Future<void>>[
stdoutDone.future,
@@ -871,7 +884,11 @@ class AndroidDevice extends Device {
print('Waiting for device.');
final String waitOut = await adb(<String>['wait-for-device']);
print(waitOut);
const RetryOptions retryOptions = RetryOptions(delayFactor: Duration(seconds: 1), maxAttempts: 10, maxDelay: Duration(minutes: 1));
const RetryOptions retryOptions = RetryOptions(
delayFactor: Duration(seconds: 1),
maxAttempts: 10,
maxDelay: Duration(minutes: 1),
);
await retryOptions.retry(() async {
final String adbShellOut = await adb(<String>['shell', 'getprop sys.boot_completed']);
if (adbShellOut != '1') {
@@ -913,9 +930,8 @@ class IosDeviceDiscovery implements DeviceDiscovery {
/// [workingDevice].
@override
Future<void> chooseWorkingDevice() async {
final List<IosDevice> allDevices = (await discoverDevices())
.map<IosDevice>((String id) => IosDevice(deviceId: id))
.toList();
final List<IosDevice> allDevices =
(await discoverDevices()).map<IosDevice>((String id) => IosDevice(deviceId: id)).toList();
if (allDevices.isEmpty) {
throw const DeviceException('No iOS devices detected');
@@ -936,16 +952,23 @@ class IosDeviceDiscovery implements DeviceDiscovery {
}
throw DeviceException(
'Device with ID $deviceId is not found for operating system: '
'$deviceOperatingSystem'
);
'$deviceOperatingSystem',
);
}
@override
Future<List<String>> discoverDevices() async {
final List<dynamic> results = json.decode(await eval(
path.join(flutterDirectory.path, 'bin', 'flutter'),
<String>['devices', '--machine', '--suppress-analytics', '--device-timeout', '5'],
)) as List<dynamic>;
final List<dynamic> results =
json.decode(
await eval(path.join(flutterDirectory.path, 'bin', 'flutter'), <String>[
'devices',
'--machine',
'--suppress-analytics',
'--device-timeout',
'5',
]),
)
as List<dynamic>;
// [
// {
@@ -1003,13 +1026,20 @@ class IosDeviceDiscovery implements DeviceDiscovery {
/// iOS device.
class IosDevice extends Device {
IosDevice({ required this.deviceId });
IosDevice({required this.deviceId});
@override
final String deviceId;
String get idevicesyslogPath {
return path.join(flutterDirectory.path, 'bin', 'cache', 'artifacts', 'libimobiledevice', 'idevicesyslog');
return path.join(
flutterDirectory.path,
'bin',
'cache',
'artifacts',
'libimobiledevice',
'idevicesyslog',
);
}
String get dyldLibraryPath {
@@ -1034,25 +1064,25 @@ class IosDevice extends Device {
_loggingProcess = await startProcess(
idevicesyslogPath,
<String>['-u', deviceId, '--quiet'],
environment: <String, String>{
'DYLD_LIBRARY_PATH': dyldLibraryPath,
},
environment: <String, String>{'DYLD_LIBRARY_PATH': dyldLibraryPath},
);
_loggingProcess!.stdout.transform<String>(const Utf8Decoder(allowMalformed: true)).listen((
String line,
) {
sink.write(line);
});
_loggingProcess!.stderr.transform<String>(const Utf8Decoder(allowMalformed: true)).listen((
String line,
) {
sink.write(line);
});
unawaited(
_loggingProcess!.exitCode.then<void>((int exitCode) {
if (!_abortedLogging) {
sink.writeln('idevicesyslog failed with exit code $exitCode.\n');
}
}),
);
_loggingProcess!.stdout
.transform<String>(const Utf8Decoder(allowMalformed: true))
.listen((String line) {
sink.write(line);
});
_loggingProcess!.stderr
.transform<String>(const Utf8Decoder(allowMalformed: true))
.listen((String line) {
sink.write(line);
});
unawaited(_loggingProcess!.exitCode.then<void>((int exitCode) {
if (!_abortedLogging) {
sink.writeln('idevicesyslog failed with exit code $exitCode.\n');
}
}));
}
@override
@@ -1132,7 +1162,7 @@ class LinuxDevice extends Device {
}
@override
Future<void> home() async { }
Future<void> home() async {}
@override
Future<bool> isAsleep() async {
@@ -1151,25 +1181,25 @@ class LinuxDevice extends Device {
Future<void> clearLogs() async {}
@override
Future<void> reboot() async { }
Future<void> reboot() async {}
@override
Future<void> sendToSleep() async { }
Future<void> sendToSleep() async {}
@override
Future<void> stop(String packageName) async { }
Future<void> stop(String packageName) async {}
@override
Future<void> tap(int x, int y) async { }
Future<void> tap(int x, int y) async {}
@override
Future<void> togglePower() async { }
Future<void> togglePower() async {}
@override
Future<void> unlock() async { }
Future<void> unlock() async {}
@override
Future<void> wakeUp() async { }
Future<void> wakeUp() async {}
@override
Future<void> awaitDevice() async {}
@@ -1187,7 +1217,7 @@ class MacosDevice extends Device {
}
@override
Future<void> home() async { }
Future<void> home() async {}
@override
Future<bool> isAsleep() async {
@@ -1206,25 +1236,25 @@ class MacosDevice extends Device {
Future<void> clearLogs() async {}
@override
Future<void> reboot() async { }
Future<void> reboot() async {}
@override
Future<void> sendToSleep() async { }
Future<void> sendToSleep() async {}
@override
Future<void> stop(String packageName) async { }
Future<void> stop(String packageName) async {}
@override
Future<void> tap(int x, int y) async { }
Future<void> tap(int x, int y) async {}
@override
Future<void> togglePower() async { }
Future<void> togglePower() async {}
@override
Future<void> unlock() async { }
Future<void> unlock() async {}
@override
Future<void> wakeUp() async { }
Future<void> wakeUp() async {}
@override
Future<void> awaitDevice() async {}
@@ -1242,7 +1272,7 @@ class WindowsDevice extends Device {
}
@override
Future<void> home() async { }
Future<void> home() async {}
@override
Future<bool> isAsleep() async {
@@ -1261,25 +1291,25 @@ class WindowsDevice extends Device {
Future<void> clearLogs() async {}
@override
Future<void> reboot() async { }
Future<void> reboot() async {}
@override
Future<void> sendToSleep() async { }
Future<void> sendToSleep() async {}
@override
Future<void> stop(String packageName) async { }
Future<void> stop(String packageName) async {}
@override
Future<void> tap(int x, int y) async { }
Future<void> tap(int x, int y) async {}
@override
Future<void> togglePower() async { }
Future<void> togglePower() async {}
@override
Future<void> unlock() async { }
Future<void> unlock() async {}
@override
Future<void> wakeUp() async { }
Future<void> wakeUp() async {}
@override
Future<void> awaitDevice() async {}
@@ -1287,7 +1317,7 @@ class WindowsDevice extends Device {
/// Fuchsia device.
class FuchsiaDevice extends Device {
const FuchsiaDevice({ required this.deviceId });
const FuchsiaDevice({required this.deviceId});
@override
final String deviceId;
@@ -1344,13 +1374,14 @@ class FuchsiaDevice extends Device {
/// Path to the `adb` executable.
String get adbPath {
final String? androidHome = Platform.environment['ANDROID_HOME'] ?? Platform.environment['ANDROID_SDK_ROOT'];
final String? androidHome =
Platform.environment['ANDROID_HOME'] ?? Platform.environment['ANDROID_SDK_ROOT'];
if (androidHome == null) {
throw const DeviceException(
'The ANDROID_HOME environment variable is '
'missing. The variable must point to the Android '
'SDK directory containing platform-tools.'
'SDK directory containing platform-tools.',
);
}
@@ -1364,7 +1395,7 @@ String get adbPath {
}
class FakeDevice extends Device {
const FakeDevice({ required this.deviceId });
const FakeDevice({required this.deviceId});
@override
final String deviceId;
@@ -1461,8 +1492,8 @@ class FakeDeviceDiscovery implements DeviceDiscovery {
}
throw DeviceException(
'Device with ID $deviceId is not found for operating system: '
'$deviceOperatingSystem'
);
'$deviceOperatingSystem',
);
}
@override
@@ -1480,5 +1511,5 @@ class FakeDeviceDiscovery implements DeviceDiscovery {
}
@override
Future<void> performPreflightTasks() async { }
Future<void> performPreflightTasks() async {}
}

View File

@@ -45,7 +45,7 @@ bool _isTaskRegistered = false;
///
/// If no `processManager` is provided, a default [LocalProcessManager] is created
/// for the task.
Future<TaskResult> task(TaskFunction task, { ProcessManager? processManager }) async {
Future<TaskResult> task(TaskFunction task, {ProcessManager? processManager}) async {
if (_isTaskRegistered) {
throw StateError('A task is already registered');
}
@@ -66,18 +66,16 @@ Future<TaskResult> task(TaskFunction task, { ProcessManager? processManager }) a
class _TaskRunner {
_TaskRunner(this.task, this.processManager) {
final String successResponse = json.encode(
const <String, String>{
'result': 'success',
},
);
final String successResponse = json.encode(const <String, String>{'result': 'success'});
registerExtension('ext.cocoonRunTask',
(String method, Map<String, String> parameters) async {
final Duration? taskTimeout = parameters.containsKey('timeoutInMinutes')
? Duration(minutes: int.parse(parameters['timeoutInMinutes']!))
: null;
final bool runFlutterConfig = parameters['runFlutterConfig'] != 'false'; // used by tests to avoid changing the configuration
registerExtension('ext.cocoonRunTask', (String method, Map<String, String> parameters) async {
final Duration? taskTimeout =
parameters.containsKey('timeoutInMinutes')
? Duration(minutes: int.parse(parameters['timeoutInMinutes']!))
: null;
final bool runFlutterConfig =
parameters['runFlutterConfig'] !=
'false'; // used by tests to avoid changing the configuration
final bool runProcessCleanup = parameters['runProcessCleanup'] != 'false';
final String? localEngine = parameters['localEngine'];
final String? localEngineHost = parameters['localEngineHost'];
@@ -89,22 +87,25 @@ class _TaskRunner {
localEngineHost: localEngineHost,
);
const Duration taskResultReceivedTimeout = Duration(seconds: 30);
_taskResultReceivedTimeout = Timer(
taskResultReceivedTimeout,
() {
logger.severe('Task runner did not acknowledge task results in $taskResultReceivedTimeout.');
_closeKeepAlivePort();
exitCode = 1;
}
);
_taskResultReceivedTimeout = Timer(taskResultReceivedTimeout, () {
logger.severe(
'Task runner did not acknowledge task results in $taskResultReceivedTimeout.',
);
_closeKeepAlivePort();
exitCode = 1;
});
return ServiceExtensionResponse.result(json.encode(result.toJson()));
});
registerExtension('ext.cocoonRunnerReady',
(String method, Map<String, String> parameters) async {
registerExtension('ext.cocoonRunnerReady', (
String method,
Map<String, String> parameters,
) async {
return ServiceExtensionResponse.result(successResponse);
});
registerExtension('ext.cocoonTaskResultReceived',
(String method, Map<String, String> parameters) async {
registerExtension('ext.cocoonTaskResultReceived', (
String method,
Map<String, String> parameters,
) async {
_closeKeepAlivePort();
return ServiceExtensionResponse.result(successResponse);
});
@@ -134,7 +135,8 @@ class _TaskRunner {
/// Signals that this task runner finished running the task.
Future<TaskResult> get whenDone => _completer.future;
Future<TaskResult> run(Duration? taskTimeout, {
Future<TaskResult> run(
Duration? taskTimeout, {
bool runFlutterConfig = true,
bool runProcessCleanup = true,
required String? localEngine,
@@ -151,7 +153,9 @@ class _TaskRunner {
processName: 'dart$exe',
processManager: processManager,
);
final Set<RunningProcessInfo> allProcesses = await getRunningProcesses(processManager: processManager);
final Set<RunningProcessInfo> allProcesses = await getRunningProcesses(
processManager: processManager,
);
beforeRunningDartInstances.forEach(print);
for (final RunningProcessInfo info in allProcesses) {
if (info.commandLine.contains('iproxy')) {
@@ -162,14 +166,18 @@ class _TaskRunner {
if (runFlutterConfig) {
print('Enabling configs for macOS and Linux...');
final int configResult = await exec(path.join(flutterDirectory.path, 'bin', 'flutter'), <String>[
'config',
'-v',
'--enable-macos-desktop',
'--enable-linux-desktop',
if (localEngine != null) ...<String>['--local-engine', localEngine],
if (localEngineHost != null) ...<String>['--local-engine-host', localEngineHost],
], canFail: true);
final int configResult = await exec(
path.join(flutterDirectory.path, 'bin', 'flutter'),
<String>[
'config',
'-v',
'--enable-macos-desktop',
'--enable-linux-desktop',
if (localEngine != null) ...<String>['--local-engine', localEngine],
if (localEngineHost != null) ...<String>['--local-engine-host', localEngineHost],
],
canFail: true,
);
if (configResult != 0) {
print('Failed to enable configuration, tasks may not run.');
}
@@ -184,7 +192,8 @@ class _TaskRunner {
IOSink? sink;
try {
if (device != null && device.canStreamLogs && hostAgent.dumpDirectory != null) {
sink = File(path.join(hostAgent.dumpDirectory!.path, '${device.deviceId}.log')).openWrite();
sink =
File(path.join(hostAgent.dumpDirectory!.path, '${device.deviceId}.log')).openWrite();
await device.startLoggingToSink(sink);
}
@@ -295,23 +304,26 @@ class _TaskRunner {
Future<TaskResult> _performTask() {
final Completer<TaskResult> completer = Completer<TaskResult>();
Chain.capture(() async {
completer.complete(await task());
}, onError: (dynamic taskError, Chain taskErrorStack) {
final String message = 'Task failed: $taskError';
stderr
..writeln(message)
..writeln('\nStack trace:')
..writeln(taskErrorStack.terse);
// IMPORTANT: We're completing the future _successfully_ but with a value
// that indicates a task failure. This is intentional. At this point we
// are catching errors coming from arbitrary (and untrustworthy) task
// code. Our goal is to convert the failure into a readable message.
// Propagating it further is not useful.
if (!completer.isCompleted) {
completer.complete(TaskResult.failure(message));
}
});
Chain.capture(
() async {
completer.complete(await task());
},
onError: (dynamic taskError, Chain taskErrorStack) {
final String message = 'Task failed: $taskError';
stderr
..writeln(message)
..writeln('\nStack trace:')
..writeln(taskErrorStack.terse);
// IMPORTANT: We're completing the future _successfully_ but with a value
// that indicates a task failure. This is intentional. At this point we
// are catching errors coming from arbitrary (and untrustworthy) task
// code. Our goal is to convert the failure into a readable message.
// Propagating it further is not useful.
if (!completer.isCompleted) {
completer.complete(TaskResult.failure(message));
}
},
);
return completer.future;
}
}

View File

@@ -8,13 +8,14 @@ import 'package:meta/meta.dart';
import 'package:platform/platform.dart';
/// The current host machine running the tests.
HostAgent get hostAgent => HostAgent(platform: const LocalPlatform(), fileSystem: const LocalFileSystem());
HostAgent get hostAgent =>
HostAgent(platform: const LocalPlatform(), fileSystem: const LocalFileSystem());
/// Host machine running the tests.
class HostAgent {
HostAgent({required Platform platform, required FileSystem fileSystem})
: _platform = platform,
_fileSystem = fileSystem;
: _platform = platform,
_fileSystem = fileSystem;
final Platform _platform;
final FileSystem _fileSystem;

View File

@@ -17,12 +17,7 @@ Future<String> fileType(String pathToBinary) {
}
Future<String?> minPhoneOSVersion(String pathToBinary) async {
final String loadCommands = await eval('otool', <String>[
'-l',
'-arch',
'arm64',
pathToBinary,
]);
final String loadCommands = await eval('otool', <String>['-l', '-arch', 'arm64', pathToBinary]);
if (!loadCommands.contains('LC_VERSION_MIN_IPHONEOS')) {
return null;
}
@@ -37,9 +32,7 @@ Future<String?> minPhoneOSVersion(String pathToBinary) async {
final List<String> lines = LineSplitter.split(loadCommands).toList();
lines.asMap().forEach((int index, String line) {
if (line.contains('LC_VERSION_MIN_IPHONEOS') && lines.length - index - 1 > 3) {
final String versionLine = lines
.skip(index - 1)
.take(4).last;
final String versionLine = lines.skip(index - 1).take(4).last;
final RegExp versionRegex = RegExp(r'\s*version\s*(\S*)');
minVersion = versionRegex.firstMatch(versionLine)?.group(1);
}
@@ -56,35 +49,27 @@ Future<void> testWithNewIOSSimulator(
SimulatorFunction testFunction, {
String deviceTypeId = 'com.apple.CoreSimulator.SimDeviceType.iPhone-11',
}) async {
final String availableRuntimes = await eval(
'xcrun',
<String>[
'simctl',
'list',
'runtimes',
],
workingDirectory: flutterDirectory.path,
);
final String availableRuntimes = await eval('xcrun', <String>[
'simctl',
'list',
'runtimes',
], workingDirectory: flutterDirectory.path);
final String runtimesForSelectedXcode = await eval(
'xcrun',
<String>[
'simctl',
'runtime',
'match',
'list',
'--json',
],
workingDirectory: flutterDirectory.path,
);
final String runtimesForSelectedXcode = await eval('xcrun', <String>[
'simctl',
'runtime',
'match',
'list',
'--json',
], workingDirectory: flutterDirectory.path);
// Get the preferred runtime build for the selected Xcode version. Preferred
// means the runtime was either bundled with Xcode, exactly matched your SDK
// version, or it's indicated a better match for your SDK.
final Map<String, Object?> decodeResult = json.decode(runtimesForSelectedXcode) as Map<String, Object?>;
final String? iosKey = decodeResult.keys
.where((String key) => key.contains('iphoneos'))
.firstOrNull;
final Map<String, Object?> decodeResult =
json.decode(runtimesForSelectedXcode) as Map<String, Object?>;
final String? iosKey =
decodeResult.keys.where((String key) => key.contains('iphoneos')).firstOrNull;
final String? runtimeBuildForSelectedXcode = switch (decodeResult[iosKey]) {
{'preferredBuild': final String build} => build,
_ => null,
@@ -100,8 +85,7 @@ Future<void> testWithNewIOSSimulator(
// For example, iOS 17 (released with Xcode 15) may be available even if the
// selected Xcode version is 14.
for (final String runtime in LineSplitter.split(availableRuntimes)) {
if (runtimeBuildForSelectedXcode != null &&
!runtime.contains(runtimeBuildForSelectedXcode)) {
if (runtimeBuildForSelectedXcode != null && !runtime.contains(runtimeBuildForSelectedXcode)) {
continue;
}
// These seem to be in order, so allow matching multiple lines so it grabs
@@ -120,26 +104,18 @@ Future<void> testWithNewIOSSimulator(
}
}
final String deviceId = await eval(
'xcrun',
<String>[
'simctl',
'create',
deviceName,
deviceTypeId,
iOSSimRuntime,
],
workingDirectory: flutterDirectory.path,
);
await eval(
'xcrun',
<String>[
'simctl',
'boot',
deviceId,
],
workingDirectory: flutterDirectory.path,
);
final String deviceId = await eval('xcrun', <String>[
'simctl',
'create',
deviceName,
deviceTypeId,
iOSSimRuntime,
], workingDirectory: flutterDirectory.path);
await eval('xcrun', <String>[
'simctl',
'boot',
deviceId,
], workingDirectory: flutterDirectory.path);
await testFunction(deviceId);
}
@@ -149,21 +125,13 @@ Future<void> removeIOSSimulator(String? deviceId) async {
if (deviceId != null && deviceId != '') {
await eval(
'xcrun',
<String>[
'simctl',
'shutdown',
deviceId,
],
<String>['simctl', 'shutdown', deviceId],
canFail: true,
workingDirectory: flutterDirectory.path,
);
await eval(
'xcrun',
<String>[
'simctl',
'delete',
deviceId,
],
<String>['simctl', 'delete', deviceId],
canFail: true,
workingDirectory: flutterDirectory.path,
);
@@ -211,12 +179,9 @@ Future<bool> runXcodeTests({
resultBundlePath,
'test',
'COMPILER_INDEX_STORE_ENABLE=NO',
if (developmentTeam != null)
'DEVELOPMENT_TEAM=$developmentTeam',
if (codeSignStyle != null)
'CODE_SIGN_STYLE=$codeSignStyle',
if (provisioningProfile != null)
'PROVISIONING_PROFILE_SPECIFIER=$provisioningProfile',
if (developmentTeam != null) 'DEVELOPMENT_TEAM=$developmentTeam',
if (codeSignStyle != null) 'CODE_SIGN_STYLE=$codeSignStyle',
if (provisioningProfile != null) 'PROVISIONING_PROFILE_SPECIFIER=$provisioningProfile',
if (disabledSandboxEntitlementFile != null)
'CODE_SIGN_ENTITLEMENTS=${disabledSandboxEntitlementFile.path}',
],
@@ -230,17 +195,13 @@ Future<bool> runXcodeTests({
if (dumpDirectory != null) {
if (xcresultBundle.existsSync()) {
// Zip the test results to the artifacts directory for upload.
final String zipPath = path.join(dumpDirectory.path,
'$testName-${DateTime.now().toLocal().toIso8601String()}.zip');
final String zipPath = path.join(
dumpDirectory.path,
'$testName-${DateTime.now().toLocal().toIso8601String()}.zip',
);
await exec(
'zip',
<String>[
'-r',
'-9',
'-q',
zipPath,
path.basename(xcresultBundle.path),
],
<String>['-r', '-9', '-q', zipPath, path.basename(xcresultBundle.path)],
workingDirectory: resultBundleTemp,
canFail: true, // Best effort to get the logs.
);
@@ -260,10 +221,7 @@ Future<bool> runXcodeTests({
/// access to the app. To workaround this in CI, we create and use a entitlements
/// file with sandboxing disabled. See
/// https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox.
File? _createDisabledSandboxEntitlementFile(
String platformDirectory,
String configuration,
) {
File? _createDisabledSandboxEntitlementFile(String platformDirectory, String configuration) {
String entitlementDefaultFileName;
if (configuration == 'Release') {
entitlementDefaultFileName = 'Release';
@@ -283,15 +241,15 @@ File? _createDisabledSandboxEntitlementFile(
return null;
}
final String originalEntitlementFileContents =
entitlementFile.readAsStringSync();
final String tempEntitlementPath = Directory.systemTemp
.createTempSync('flutter_disable_sandbox_entitlement.')
.path;
final File disabledSandboxEntitlementFile = File(path.join(
tempEntitlementPath,
'${entitlementDefaultFileName}WithDisabledSandboxing.entitlements',
));
final String originalEntitlementFileContents = entitlementFile.readAsStringSync();
final String tempEntitlementPath =
Directory.systemTemp.createTempSync('flutter_disable_sandbox_entitlement.').path;
final File disabledSandboxEntitlementFile = File(
path.join(
tempEntitlementPath,
'${entitlementDefaultFileName}WithDisabledSandboxing.entitlements',
),
);
disabledSandboxEntitlementFile.createSync(recursive: true);
disabledSandboxEntitlementFile.writeAsStringSync(
originalEntitlementFileContents.replaceAll(
@@ -307,14 +265,5 @@ File? _createDisabledSandboxEntitlementFile(
/// Returns global (external) symbol table entries, delimited by new lines.
Future<String> dumpSymbolTable(String filePath) {
return eval(
'nm',
<String>[
'--extern-only',
'--just-symbol-name',
filePath,
'-arch',
'arm64',
],
);
return eval('nm', <String>['--extern-only', '--just-symbol-name', filePath, '-arch', 'arm64']);
}

View File

@@ -52,7 +52,11 @@ Future<FlutterDestination> connectFlutterDestination() async {
/// "host_type": "linux",
/// "host_version": "debian-10.11"
/// }
List<MetricPoint> parse(Map<String, dynamic> resultsJson, Map<String, dynamic> benchmarkTags, String taskName) {
List<MetricPoint> parse(
Map<String, dynamic> resultsJson,
Map<String, dynamic> benchmarkTags,
String taskName,
) {
print('Results to upload to skia perf: $resultsJson');
print('Benchmark tags to upload to skia perf: $benchmarkTags');
final List<String> scoreKeys =
@@ -72,13 +76,12 @@ List<MetricPoint> parse(Map<String, dynamic> resultsJson, Map<String, dynamic> b
};
// Append additional benchmark tags, which will surface in Skia Perf dashboards.
tags = mergeMaps<String, String>(
tags, benchmarkTags.map((String key, dynamic value) => MapEntry<String, String>(key, value.toString())));
metricPoints.add(
MetricPoint(
(resultData[scoreKey] as num).toDouble(),
tags,
tags,
benchmarkTags.map(
(String key, dynamic value) => MapEntry<String, String>(key, value.toString()),
),
);
metricPoints.add(MetricPoint((resultData[scoreKey] as num).toDouble(), tags));
}
return metricPoints;
}
@@ -100,10 +103,7 @@ Future<void> upload(
) async {
await metricsDestination.update(
metricPoints,
DateTime.fromMillisecondsSinceEpoch(
commitTimeSinceEpoch,
isUtc: true,
),
DateTime.fromMillisecondsSinceEpoch(commitTimeSinceEpoch, isUtc: true),
taskName,
);
}
@@ -114,7 +114,12 @@ Future<void> upload(
/// 1. Run DeviceLab test, writing results to a known path
/// 2. Request service account token from luci auth (valid for at least 3 minutes)
/// 3. Upload results from (1) to skia perf.
Future<void> uploadToSkiaPerf(String? resultsPath, String? commitTime, String? taskName, String? benchmarkTags) async {
Future<void> uploadToSkiaPerf(
String? resultsPath,
String? commitTime,
String? taskName,
String? benchmarkTags,
) async {
int commitTimeSinceEpoch;
if (resultsPath == null) {
return;
@@ -125,7 +130,8 @@ Future<void> uploadToSkiaPerf(String? resultsPath, String? commitTime, String? t
commitTimeSinceEpoch = DateTime.now().millisecondsSinceEpoch;
}
taskName = taskName ?? 'default';
final Map<String, dynamic> benchmarkTagsMap = jsonDecode(benchmarkTags ?? '{}') as Map<String, dynamic>;
final Map<String, dynamic> benchmarkTagsMap =
jsonDecode(benchmarkTags ?? '{}') as Map<String, dynamic>;
final File resultFile = File(resultsPath);
Map<String, dynamic> resultsJson = <String, dynamic>{};
resultsJson = json.decode(await resultFile.readAsString()) as Map<String, dynamic>;
@@ -150,10 +156,7 @@ Future<void> uploadToSkiaPerf(String? resultsPath, String? commitTime, String? t
/// For example:
/// Old file name: `backdrop_filter_perf__timeline_summary`
/// New file name: `backdrop_filter_perf__timeline_summary_intel_linux_motoG4`
String metricFileName(
String taskName,
Map<String, dynamic> benchmarkTagsMap,
) {
String metricFileName(String taskName, Map<String, dynamic> benchmarkTagsMap) {
final StringBuffer fileName = StringBuffer(taskName);
if (benchmarkTagsMap.containsKey('arch')) {
fileName

View File

@@ -74,7 +74,9 @@ Future<void> runTasks(
} else {
section('Flaky status for "$taskName"');
if (failureCount > 0) {
print('Total ${failureCount+1} executions: $failureCount failures and 1 false positive.');
print(
'Total ${failureCount + 1} executions: $failureCount failures and 1 false positive.',
);
print('flaky: true');
// TODO(ianh): stop ignoring this failure. We should set exitCode=1, and quit
// if exitOnFirstTestFailure is true.
@@ -197,17 +199,16 @@ Future<TaskResult> runTask(
taskExecutable,
...?taskArgs,
],
environment: <String, String>{
if (deviceId != null)
DeviceIdEnvName: deviceId,
},
environment: <String, String>{if (deviceId != null) DeviceIdEnvName: deviceId},
);
bool runnerFinished = false;
unawaited(runner.exitCode.whenComplete(() {
runnerFinished = true;
}));
unawaited(
runner.exitCode.whenComplete(() {
runnerFinished = true;
}),
);
final Completer<Uri> uri = Completer<Uri>();
@@ -215,42 +216,44 @@ Future<TaskResult> runTask(
.transform<String>(const Utf8Decoder())
.transform<String>(const LineSplitter())
.listen((String line) {
if (!uri.isCompleted) {
final Uri? serviceUri = parseServiceUri(line, prefix: RegExp('The Dart VM service is listening on '));
if (serviceUri != null) {
uri.complete(serviceUri);
}
}
if (!silent) {
stdout.writeln('[${DateTime.now()}] [STDOUT] $line');
}
});
if (!uri.isCompleted) {
final Uri? serviceUri = parseServiceUri(
line,
prefix: RegExp('The Dart VM service is listening on '),
);
if (serviceUri != null) {
uri.complete(serviceUri);
}
}
if (!silent) {
stdout.writeln('[${DateTime.now()}] [STDOUT] $line');
}
});
final StreamSubscription<String> stderrSub = runner.stderr
.transform<String>(const Utf8Decoder())
.transform<String>(const LineSplitter())
.listen((String line) {
stderr.writeln('[${DateTime.now()}] [STDERR] $line');
});
stderr.writeln('[${DateTime.now()}] [STDERR] $line');
});
try {
final ConnectionResult result = await _connectToRunnerIsolate(await uri.future);
print('[$taskName] Connected to VM server.');
isolateParams = isolateParams == null ? <String, String>{} : Map<String, String>.of(isolateParams);
isolateParams =
isolateParams == null ? <String, String>{} : Map<String, String>.of(isolateParams);
isolateParams['runProcessCleanup'] = terminateStrayDartProcesses.toString();
final VmService service = result.vmService;
final String isolateId = result.isolate.id!;
final Map<String, dynamic> taskResultJson = (await service.callServiceExtension(
'ext.cocoonRunTask',
args: isolateParams,
isolateId: isolateId,
)).json!;
final Map<String, dynamic> taskResultJson =
(await service.callServiceExtension(
'ext.cocoonRunTask',
args: isolateParams,
isolateId: isolateId,
)).json!;
// Notify the task process that the task result has been received and it
// can proceed to shutdown.
await _acknowledgeTaskResultReceived(
service: service,
isolateId: isolateId,
);
await _acknowledgeTaskResultReceived(service: service, isolateId: isolateId);
final TaskResult taskResult = TaskResult.fromJson(taskResultJson);
final int exitCode = await runner.exitCode;
print('[$taskName] Process terminated with exit code $exitCode.');
@@ -288,7 +291,10 @@ Future<ConnectionResult> _connectToRunnerIsolate(Uri vmServiceUri) async {
}
final IsolateRef isolate = vm.isolates!.first;
// Sanity check to ensure we're talking with the main isolate.
final Response response = await client.callServiceExtension('ext.cocoonRunnerReady', isolateId: isolate.id);
final Response response = await client.callServiceExtension(
'ext.cocoonRunnerReady',
isolateId: isolate.id,
);
if (response.json!['result'] != 'success') {
throw 'not ready yet';
}
@@ -309,14 +315,11 @@ Future<ConnectionResult> _connectToRunnerIsolate(Uri vmServiceUri) async {
}
Future<void> _acknowledgeTaskResultReceived({
required VmService service,
required String isolateId,
}) async {
required VmService service,
required String isolateId,
}) async {
try {
await service.callServiceExtension(
'ext.cocoonTaskResultReceived',
isolateId: isolateId,
);
await service.callServiceExtension('ext.cocoonTaskResultReceived', isolateId: isolateId);
} on RPCError {
// The target VM may shutdown before the response is received.
}

View File

@@ -17,10 +17,10 @@ class RunningProcessInfo {
@override
bool operator ==(Object other) {
return other is RunningProcessInfo
&& other.pid == pid
&& other.commandLine == commandLine
&& other.creationDate == creationDate;
return other is RunningProcessInfo &&
other.pid == pid &&
other.commandLine == commandLine &&
other.creationDate == creationDate;
}
Future<bool> terminate({required ProcessManager processManager}) async {
@@ -30,7 +30,7 @@ class RunningProcessInfo {
// TODO(ianh): Move Windows to killPid once we can.
// - killPid on Windows has not-useful return code: https://github.com/dart-lang/sdk/issues/47675
final ProcessResult result = await processManager.run(<String>[
'taskkill.exe',
'taskkill.exe',
'/pid',
'$pid',
'/f',
@@ -66,15 +66,13 @@ Future<Set<RunningProcessInfo>> windowsRunningProcesses(
) async {
// PowerShell script to get the command line arguments and create time of a process.
// See: https://docs.microsoft.com/en-us/windows/desktop/cimwin32prov/win32-process
final String script = processName != null
? '"Get-CimInstance Win32_Process -Filter \\"name=\'$processName\'\\" | Select-Object ProcessId,CreationDate,CommandLine | Format-Table -AutoSize | Out-String -Width 4096"'
: '"Get-CimInstance Win32_Process | Select-Object ProcessId,CreationDate,CommandLine | Format-Table -AutoSize | Out-String -Width 4096"';
final String script =
processName != null
? '"Get-CimInstance Win32_Process -Filter \\"name=\'$processName\'\\" | Select-Object ProcessId,CreationDate,CommandLine | Format-Table -AutoSize | Out-String -Width 4096"'
: '"Get-CimInstance Win32_Process | Select-Object ProcessId,CreationDate,CommandLine | Format-Table -AutoSize | Out-String -Width 4096"';
// TODO(ianh): Unfortunately, there doesn't seem to be a good way to get
// ProcessManager to run this.
final ProcessResult result = await Process.run(
'powershell -command $script',
<String>[],
);
final ProcessResult result = await Process.run('powershell -command $script', <String>[]);
if (result.exitCode != 0) {
print('Could not list processes!');
print(result.stderr);
@@ -115,10 +113,7 @@ Iterable<RunningProcessInfo> processPowershellOutput(String output) sync* {
// 3/11/2019 11:01:54 AM
// 12/11/2019 11:01:54 AM
String rawTime = line.substring(
creationDateHeaderStart,
creationDateHeaderEnd,
).trim();
String rawTime = line.substring(creationDateHeaderStart, creationDateHeaderEnd).trim();
if (rawTime[1] == '/') {
rawTime = '0$rawTime';
@@ -177,10 +172,7 @@ Future<Set<RunningProcessInfo>> posixRunningProcesses(
/// Sat Mar 9 20:12:47 2019 1 /sbin/launchd
/// Sat Mar 9 20:13:00 2019 49 /usr/sbin/syslogd
@visibleForTesting
Iterable<RunningProcessInfo> processPsOutput(
String output,
String? processName,
) sync* {
Iterable<RunningProcessInfo> processPsOutput(String output, String? processName) sync* {
bool inTableBody = false;
for (String line in output.split('\n')) {
if (line.trim().startsWith('STARTED')) {

View File

@@ -8,7 +8,8 @@ import 'package:path/path.dart' as path;
import 'package:pub_semver/pub_semver.dart';
String adbPath() {
final String? androidHome = io.Platform.environment['ANDROID_HOME'] ?? io.Platform.environment['ANDROID_SDK_ROOT'];
final String? androidHome =
io.Platform.environment['ANDROID_HOME'] ?? io.Platform.environment['ANDROID_SDK_ROOT'];
if (androidHome == null) {
return 'adb';
} else {
@@ -24,7 +25,9 @@ Future<Version> getTalkbackVersion() async {
'com.google.android.marvin.talkback',
]);
if (result.exitCode != 0) {
throw Exception('Failed to get TalkBack version: ${result.stdout as String}\n${result.stderr as String}');
throw Exception(
'Failed to get TalkBack version: ${result.stdout as String}\n${result.stderr as String}',
);
}
final List<String> lines = (result.stdout as String).split('\n');
String? version;
@@ -39,7 +42,9 @@ Future<Version> getTalkbackVersion() async {
}
// Android doesn't quite use semver, so convert the version string to semver form.
final RegExp startVersion = RegExp(r'(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(\.(?<build>\d+))?');
final RegExp startVersion = RegExp(
r'(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(\.(?<build>\d+))?',
);
final RegExpMatch? match = startVersion.firstMatch(version);
if (match == null) {
return Version(0, 0, 0);

View File

@@ -8,19 +8,19 @@ import 'dart:io';
/// A result of running a single task.
class TaskResult {
TaskResult.buildOnly()
: succeeded = true,
data = null,
detailFiles = null,
benchmarkScoreKeys = null,
message = 'No tests run';
: succeeded = true,
data = null,
detailFiles = null,
benchmarkScoreKeys = null,
message = 'No tests run';
/// Constructs a successful result.
TaskResult.success(this.data, {
TaskResult.success(
this.data, {
this.benchmarkScoreKeys = const <String>[],
this.detailFiles = const <String>[],
this.message = 'success',
})
: succeeded = true {
}) : succeeded = true {
const JsonEncoder prettyJson = JsonEncoder.withIndent(' ');
if (benchmarkScoreKeys != null) {
for (final String key in benchmarkScoreKeys!) {
@@ -36,7 +36,8 @@ class TaskResult {
}
/// Constructs a successful result using JSON data stored in a file.
factory TaskResult.successFromFile(File file, {
factory TaskResult.successFromFile(
File file, {
List<String> benchmarkScoreKeys = const <String>[],
List<String> detailFiles = const <String>[],
}) {
@@ -51,9 +52,12 @@ class TaskResult {
factory TaskResult.fromJson(Map<String, dynamic> json) {
final bool success = json['success'] as bool;
if (success) {
final List<String> benchmarkScoreKeys = (json['benchmarkScoreKeys'] as List<dynamic>? ?? <String>[]).cast<String>();
final List<String> detailFiles = (json['detailFiles'] as List<dynamic>? ?? <String>[]).cast<String>();
return TaskResult.success(json['data'] as Map<String, dynamic>?,
final List<String> benchmarkScoreKeys =
(json['benchmarkScoreKeys'] as List<dynamic>? ?? <String>[]).cast<String>();
final List<String> detailFiles =
(json['detailFiles'] as List<dynamic>? ?? <String>[]).cast<String>();
return TaskResult.success(
json['data'] as Map<String, dynamic>?,
benchmarkScoreKeys: benchmarkScoreKeys,
detailFiles: detailFiles,
message: json['reason'] as String?,
@@ -65,10 +69,10 @@ class TaskResult {
/// Constructs an unsuccessful result.
TaskResult.failure(this.message)
: succeeded = false,
data = null,
detailFiles = null,
benchmarkScoreKeys = null;
: succeeded = false,
data = null,
detailFiles = null,
benchmarkScoreKeys = null;
/// Whether the task succeeded.
final bool succeeded;
@@ -106,9 +110,7 @@ class TaskResult {
/// "reason": failure reason string valid only for unsuccessful results
/// }
Map<String, dynamic> toJson() {
final Map<String, dynamic> json = <String, dynamic>{
'success': succeeded,
};
final Map<String, dynamic> json = <String, dynamic>{'success': succeeded};
if (succeeded) {
json['data'] = data;

View File

@@ -67,8 +67,7 @@ class ProcessInfo {
command: $command
started: $startTime
pid : ${process.pid}
'''
.trim();
'''.trim();
}
}
@@ -77,8 +76,8 @@ class HealthCheckResult {
HealthCheckResult.success([this.details]) : succeeded = true;
HealthCheckResult.failure(this.details) : succeeded = false;
HealthCheckResult.error(dynamic error, dynamic stackTrace)
: succeeded = false,
details = 'ERROR: $error${stackTrace != null ? '\n$stackTrace' : ''}';
: succeeded = false,
details = 'ERROR: $error${stackTrace != null ? '\n$stackTrace' : ''}';
final bool succeeded;
final String? details;
@@ -111,7 +110,7 @@ void fail(String message) {
}
// Remove the given file or directory.
void rm(FileSystemEntity entity, { bool recursive = false}) {
void rm(FileSystemEntity entity, {bool recursive = false}) {
if (entity.existsSync()) {
// This should not be necessary, but it turns out that
// on Windows it's common for deletions to fail due to
@@ -136,8 +135,7 @@ Directory dir(String path) => Directory(path);
File file(String path) => File(path);
void copy(File sourceFile, Directory targetDirectory, {String? name}) {
final File target = file(
path.join(targetDirectory.path, name ?? path.basename(sourceFile.path)));
final File target = file(path.join(targetDirectory.path, name ?? path.basename(sourceFile.path)));
target.writeAsBytesSync(sourceFile.readAsBytesSync());
}
@@ -162,10 +160,8 @@ void recursiveCopy(Directory source, Directory target) {
}
}
FileSystemEntity move(FileSystemEntity whatToMove,
{required Directory to, String? name}) {
return whatToMove
.renameSync(path.join(to.path, name ?? path.basename(whatToMove.path)));
FileSystemEntity move(FileSystemEntity whatToMove, {required Directory to, String? name}) {
return whatToMove.renameSync(path.join(to.path, name ?? path.basename(whatToMove.path)));
}
/// Equivalent of `chmod a+x file`
@@ -174,11 +170,7 @@ void makeExecutable(File file) {
if (Platform.isWindows) {
return;
}
final ProcessResult result = _processManager.runSync(<String>[
'chmod',
'a+x',
file.path,
]);
final ProcessResult result = _processManager.runSync(<String>['chmod', 'a+x', file.path]);
if (result.exitCode != 0) {
throw FileSystemException(
@@ -250,12 +242,7 @@ Future<String?> getCurrentFlutterRepoCommit() {
Future<DateTime> getFlutterRepoCommitTimestamp(String commit) {
// git show -s --format=%at 4b546df7f0b3858aaaa56c4079e5be1ba91fbb65
return inDirectory<DateTime>(flutterDirectory, () async {
final String unixTimestamp = await eval('git', <String>[
'show',
'-s',
'--format=%at',
commit,
]);
final String unixTimestamp = await eval('git', <String>['show', '-s', '--format=%at', commit]);
final int secondsSinceEpoch = int.parse(unixTimestamp);
return DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000);
});
@@ -289,12 +276,15 @@ Future<Process> startProcess(
String executable,
List<String>? arguments, {
Map<String, String>? environment,
bool isBot = true, // set to false to pretend not to be on a bot (e.g. to test user-facing outputs)
bool isBot =
true, // set to false to pretend not to be on a bot (e.g. to test user-facing outputs)
String? workingDirectory,
}) async {
final String command = '$executable ${arguments?.join(" ") ?? ""}';
final String finalWorkingDirectory = workingDirectory ?? cwd;
final Map<String, String> newEnvironment = Map<String, String>.from(environment ?? <String, String>{});
final Map<String, String> newEnvironment = Map<String, String>.from(
environment ?? <String, String>{},
);
newEnvironment['BOT'] = isBot ? 'true' : 'false';
newEnvironment['LANG'] = 'en_US.UTF-8';
print('Executing "$command" in "$finalWorkingDirectory" with environment $newEnvironment');
@@ -307,9 +297,11 @@ Future<Process> startProcess(
final ProcessInfo processInfo = ProcessInfo(command, process);
_runningProcesses.add(processInfo);
unawaited(process.exitCode.then<void>((int exitCode) {
_runningProcesses.remove(processInfo);
}));
unawaited(
process.exitCode.then<void>((int exitCode) {
_runningProcesses.remove(processInfo);
}),
);
return process;
}
@@ -346,7 +338,7 @@ Future<int> exec(
executable,
arguments,
environment: environment,
canFail : canFail,
canFail: canFail,
workingDirectory: workingDirectory,
output: output,
stderr: stderr,
@@ -397,32 +389,39 @@ Future<void> forwardStandardStreams(
StringBuffer? stderr,
bool printStdout = true,
bool printStderr = true,
}) {
}) {
final Completer<void> stdoutDone = Completer<void>();
final Completer<void> stderrDone = Completer<void>();
process.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
if (printStdout) {
print('stdout: $line');
}
output?.writeln(line);
}, onDone: () { stdoutDone.complete(); });
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen(
(String line) {
if (printStdout) {
print('stdout: $line');
}
output?.writeln(line);
},
onDone: () {
stdoutDone.complete();
},
);
process.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
if (printStderr) {
print('stderr: $line');
}
stderr?.writeln(line);
}, onDone: () { stderrDone.complete(); });
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen(
(String line) {
if (printStderr) {
print('stderr: $line');
}
stderr?.writeln(line);
},
onDone: () {
stderrDone.complete();
},
);
return Future.wait<void>(<Future<void>>[
stdoutDone.future,
stderrDone.future,
]);
return Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]);
}
/// Executes a command and returns its standard output as a String.
@@ -476,11 +475,8 @@ List<String> _flutterCommandArgs(
final bool pubOrPackagesCommand = command.startsWith('packages') || command.startsWith('pub');
return <String>[
command,
if (deviceOperatingSystem == DeviceOperatingSystem.ios && supportedDeviceTimeoutCommands.contains(command))
...<String>[
'--device-timeout',
'5',
],
if (deviceOperatingSystem == DeviceOperatingSystem.ios &&
supportedDeviceTimeoutCommands.contains(command)) ...<String>['--device-timeout', '5'],
// DDS should generally be disabled for flutter drive in CI.
// See https://github.com/flutter/flutter/issues/152684.
@@ -500,34 +496,33 @@ List<String> _flutterCommandArgs(
// the same allowed args.
if (!pubOrPackagesCommand) '--ci',
if (!pubOrPackagesCommand && hostAgent.dumpDirectory != null)
'--debug-logs-dir=${hostAgent.dumpDirectory!.path}'
'--debug-logs-dir=${hostAgent.dumpDirectory!.path}',
];
}
/// Runs the flutter `command`, and returns the exit code.
/// If `canFail` is `false`, the future completes with an error.
Future<int> flutter(String command, {
Future<int> flutter(
String command, {
List<String> options = const <String>[],
bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
bool driveWithDds = false, // `flutter drive` tests should generally have dds disabled.
// The exception is tests that also exercise DevTools, such as
// DevToolsMemoryTest in perf_tests.dart.
bool driveWithDds = false, // `flutter drive` tests should generally have dds disabled.
// The exception is tests that also exercise DevTools, such as
// DevToolsMemoryTest in perf_tests.dart.
Map<String, String>? environment,
String? workingDirectory,
StringBuffer? output, // if not null, the stdout will be written here
StringBuffer? stderr, // if not null, the stderr will be written here
}) async {
final List<String> args = _flutterCommandArgs(
command, options, driveWithDds: driveWithDds,
);
final List<String> args = _flutterCommandArgs(command, options, driveWithDds: driveWithDds);
final int exitCode = await exec(
path.join(flutterDirectory.path, 'bin', 'flutter'),
args,
canFail: canFail,
environment: environment,
workingDirectory: workingDirectory,
output: output,
stderr: stderr,
path.join(flutterDirectory.path, 'bin', 'flutter'),
args,
canFail: canFail,
environment: environment,
workingDirectory: workingDirectory,
output: output,
stderr: stderr,
);
if (exitCode != 0 && !canFail) {
@@ -558,10 +553,12 @@ Future<int> flutter(String command, {
///
/// The actual process executes asynchronously. A handle to the subprocess is
/// returned in the form of a [Future] that completes to a [Process] object.
Future<Process> startFlutter(String command, {
Future<Process> startFlutter(
String command, {
List<String> options = const <String>[],
Map<String, String> environment = const <String, String>{},
bool isBot = true, // set to false to pretend not to be on a bot (e.g. to test user-facing outputs)
bool isBot =
true, // set to false to pretend not to be on a bot (e.g. to test user-facing outputs)
String? workingDirectory,
}) async {
final List<String> args = _flutterCommandArgs(command, options);
@@ -573,16 +570,19 @@ Future<Process> startFlutter(String command, {
workingDirectory: workingDirectory,
);
unawaited(process.exitCode.then<void>((int exitCode) async {
if (exitCode != 0) {
await _flutterScreenshot(workingDirectory: workingDirectory);
}
}));
unawaited(
process.exitCode.then<void>((int exitCode) async {
if (exitCode != 0) {
await _flutterScreenshot(workingDirectory: workingDirectory);
}
}),
);
return process;
}
/// Runs a `flutter` command and returns the standard output as a string.
Future<String> evalFlutter(String command, {
Future<String> evalFlutter(
String command, {
List<String> options = const <String>[],
bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
Map<String, String>? environment,
@@ -590,19 +590,26 @@ Future<String> evalFlutter(String command, {
String? workingDirectory,
}) {
final List<String> args = _flutterCommandArgs(command, options);
return eval(path.join(flutterDirectory.path, 'bin', 'flutter'), args,
canFail: canFail, environment: environment, stderr: stderr, workingDirectory: workingDirectory);
return eval(
path.join(flutterDirectory.path, 'bin', 'flutter'),
args,
canFail: canFail,
environment: environment,
stderr: stderr,
workingDirectory: workingDirectory,
);
}
Future<ProcessResult> executeFlutter(String command, {
Future<ProcessResult> executeFlutter(
String command, {
List<String> options = const <String>[],
bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
}) async {
final List<String> args = _flutterCommandArgs(command, options);
final ProcessResult processResult = await _processManager.run(
<String>[path.join(flutterDirectory.path, 'bin', 'flutter'), ...args],
workingDirectory: cwd,
);
final ProcessResult processResult = await _processManager.run(<String>[
path.join(flutterDirectory.path, 'bin', 'flutter'),
...args,
], workingDirectory: cwd);
if (processResult.exitCode != 0 && !canFail) {
await _flutterScreenshot();
@@ -610,7 +617,7 @@ Future<ProcessResult> executeFlutter(String command, {
return processResult;
}
Future<void> _flutterScreenshot({ String? workingDirectory }) async {
Future<void> _flutterScreenshot({String? workingDirectory}) async {
try {
final Directory? dumpDirectory = hostAgent.dumpDirectory;
if (dumpDirectory == null) {
@@ -624,18 +631,16 @@ Future<void> _flutterScreenshot({ String? workingDirectory }) async {
final String deviceId = (await devices.workingDevice).deviceId;
print('Taking screenshot of working device $deviceId at $screenshotPath');
final List<String> args = _flutterCommandArgs(
'screenshot',
<String>[
'--out',
screenshotPath,
'-d', deviceId,
],
);
final ProcessResult screenshot = await _processManager.run(
<String>[path.join(flutterDirectory.path, 'bin', 'flutter'), ...args],
workingDirectory: workingDirectory ?? cwd,
);
final List<String> args = _flutterCommandArgs('screenshot', <String>[
'--out',
screenshotPath,
'-d',
deviceId,
]);
final ProcessResult screenshot = await _processManager.run(<String>[
path.join(flutterDirectory.path, 'bin', 'flutter'),
...args,
], workingDirectory: workingDirectory ?? cwd);
if (screenshot.exitCode != 0) {
print('Failed to take screenshot. Continuing.');
@@ -645,11 +650,9 @@ Future<void> _flutterScreenshot({ String? workingDirectory }) async {
}
}
String get dartBin =>
path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'dart');
String get dartBin => path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'dart');
String get pubBin =>
path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'pub');
String get pubBin => path.join(flutterDirectory.path, 'bin', 'cache', 'dart-sdk', 'bin', 'pub');
Future<int> dart(List<String> args) => exec(dartBin, args);
@@ -664,14 +667,13 @@ Future<String?> findJavaHome() async {
if (hits.isEmpty) {
return null;
}
final String javaBinary = hits.first
.split(': ')
.last;
final String javaBinary = hits.first.split(': ').last;
// javaBinary == /some/path/to/java/home/bin/java
_javaHome = path.dirname(path.dirname(javaBinary));
}
return _javaHome;
}
String? _javaHome;
Future<T> inDirectory<T>(dynamic directory, Future<T> Function() action) async {
@@ -693,7 +695,10 @@ void cd(dynamic directory) {
cwd = directory.path;
d = directory;
} else {
throw FileSystemException('Unsupported directory type ${directory.runtimeType}', directory.toString());
throw FileSystemException(
'Unsupported directory type ${directory.runtimeType}',
directory.toString(),
);
}
if (!d.existsSync()) {
@@ -758,8 +763,7 @@ Future<void> runAndCaptureAsyncStacks(Future<void> Function() callback) {
bool canRun(String path) => _processManager.canRun(path);
final RegExp _obsRegExp =
RegExp('A Dart VM Service .* is available at: ');
final RegExp _obsRegExp = RegExp('A Dart VM Service .* is available at: ');
final RegExp _obsPortRegExp = RegExp(r'(\S+:(\d+)/\S*)$');
final RegExp _obsUriRegExp = RegExp(r'((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)');
@@ -767,17 +771,14 @@ final RegExp _obsUriRegExp = RegExp(r'((http|//)[a-zA-Z0-9:/=_\-\.\[\]]+)');
///
/// The `prefix`, if specified, is a regular expression pattern and must not contain groups.
/// `prefix` defaults to the RegExp: `A Dart VM Service .* is available at: `.
int? parseServicePort(String line, {
Pattern? prefix,
}) {
int? parseServicePort(String line, {Pattern? prefix}) {
prefix ??= _obsRegExp;
final Iterable<Match> matchesIter = prefix.allMatches(line);
if (matchesIter.isEmpty) {
return null;
}
final Match prefixMatch = matchesIter.first;
final List<Match> matches =
_obsPortRegExp.allMatches(line, prefixMatch.end).toList();
final List<Match> matches = _obsPortRegExp.allMatches(line, prefixMatch.end).toList();
return matches.isEmpty ? null : int.parse(matches[0].group(2)!);
}
@@ -785,17 +786,14 @@ int? parseServicePort(String line, {
///
/// The `prefix`, if specified, is a regular expression pattern and must not contain groups.
/// `prefix` defaults to the RegExp: `A Dart VM Service .* is available at: `.
Uri? parseServiceUri(String line, {
Pattern? prefix,
}) {
Uri? parseServiceUri(String line, {Pattern? prefix}) {
prefix ??= _obsRegExp;
final Iterable<Match> matchesIter = prefix.allMatches(line);
if (matchesIter.isEmpty) {
return null;
}
final Match prefixMatch = matchesIter.first;
final List<Match> matches =
_obsUriRegExp.allMatches(line, prefixMatch.end).toList();
final List<Match> matches = _obsUriRegExp.allMatches(line, prefixMatch.end).toList();
return matches.isEmpty ? null : Uri.parse(matches[0].group(0)!);
}
@@ -860,7 +858,7 @@ void checkFileContains(List<Pattern> patterns, String filePath) {
if (!fileContent.contains(pattern)) {
throw TaskResult.failure(
'Expected to find `$pattern` in `$filePath` '
'instead it found:\n$fileContent'
'instead it found:\n$fileContent',
);
}
}
@@ -875,10 +873,7 @@ Future<int> gitClone({required String path, required String repo}) async {
await Directory(path).create(recursive: true);
return inDirectory<int>(
path,
() => exec('git', <String>['clone', repo]),
);
return inDirectory<int>(path, () => exec('git', <String>['clone', repo]));
}
/// Call [fn] retrying so long as [retryIf] return `true` for the exception
@@ -901,8 +896,7 @@ Future<T> retry<T>(
try {
return await fn();
} on Exception catch (e) {
if (attempt >= maxAttempts ||
(retryIf != null && !(await retryIf(e)))) {
if (attempt >= maxAttempts || (retryIf != null && !(await retryIf(e)))) {
rethrow;
}
}
@@ -924,12 +918,8 @@ Future<void> createFfiPackage(String name, Directory parent) async {
name,
],
);
await _pinDependencies(
File(path.join(parent.path, name, 'pubspec.yaml')),
);
await _pinDependencies(
File(path.join(parent.path, name, 'example', 'pubspec.yaml')),
);
await _pinDependencies(File(path.join(parent.path, name, 'pubspec.yaml')));
await _pinDependencies(File(path.join(parent.path, name, 'example', 'pubspec.yaml')));
});
}

View File

@@ -37,57 +37,53 @@ Future<Map<String, double>> readJsonResults(Process process) {
.transform<String>(const Utf8Decoder())
.transform<String>(const LineSplitter())
.listen((String line) async {
print('[STDOUT] $line');
print('[STDOUT] $line');
if (line.contains(jsonStart)) {
jsonStarted = true;
return;
}
if (line.contains(jsonStart)) {
jsonStarted = true;
return;
}
if (line.contains(testComplete)) {
processWasKilledIntentionally = true;
// Sending a SIGINT/SIGTERM to the process here isn't reliable because [process] is
// the shell (flutter is a shell script) and doesn't pass the signal on.
// Sending a `q` is an instruction to quit using the console runner.
// See https://github.com/flutter/flutter/issues/19208
process.stdin.write('q');
await process.stdin.flush();
// Give the process a couple of seconds to exit and run shutdown hooks
// before sending kill signal.
// TODO(fujino): https://github.com/flutter/flutter/issues/134566
await Future<void>.delayed(const Duration(seconds: 2));
// Also send a kill signal in case the `q` above didn't work.
process.kill(ProcessSignal.sigint);
try {
final Map<String, double> results =
Map<String, double>.from(<String, dynamic>{
for (final String data in collectedJson)
...json.decode(data) as Map<String, dynamic>
});
completer.complete(results);
} catch (ex) {
completer.completeError(
'Decoding JSON failed ($ex). JSON strings where: $collectedJson');
}
return;
}
if (line.contains(testComplete)) {
processWasKilledIntentionally = true;
// Sending a SIGINT/SIGTERM to the process here isn't reliable because [process] is
// the shell (flutter is a shell script) and doesn't pass the signal on.
// Sending a `q` is an instruction to quit using the console runner.
// See https://github.com/flutter/flutter/issues/19208
process.stdin.write('q');
await process.stdin.flush();
// Give the process a couple of seconds to exit and run shutdown hooks
// before sending kill signal.
// TODO(fujino): https://github.com/flutter/flutter/issues/134566
await Future<void>.delayed(const Duration(seconds: 2));
// Also send a kill signal in case the `q` above didn't work.
process.kill(ProcessSignal.sigint);
try {
final Map<String, double> results = Map<String, double>.from(<String, dynamic>{
for (final String data in collectedJson) ...json.decode(data) as Map<String, dynamic>,
});
completer.complete(results);
} catch (ex) {
completer.completeError(
'Decoding JSON failed ($ex). JSON strings where: $collectedJson',
);
}
return;
}
if (jsonStarted && line.contains(jsonEnd)) {
collectedJson.add(jsonBuf.toString().trim());
jsonBuf.clear();
jsonStarted = false;
}
if (jsonStarted && line.contains(jsonEnd)) {
collectedJson.add(jsonBuf.toString().trim());
jsonBuf.clear();
jsonStarted = false;
}
if (jsonStarted && line.contains(jsonPrefix)) {
jsonBuf.writeln(line.substring(line.indexOf(jsonPrefix) + jsonPrefix.length));
}
});
if (jsonStarted && line.contains(jsonPrefix)) {
jsonBuf.writeln(line.substring(line.indexOf(jsonPrefix) + jsonPrefix.length));
}
});
process.exitCode.then<void>((int code) async {
await Future.wait<void>(<Future<void>>[
stdoutSub.cancel(),
stderrSub.cancel(),
]);
await Future.wait<void>(<Future<void>>[stdoutSub.cancel(), stderrSub.cancel()]);
if (!processWasKilledIntentionally && code != 0) {
completer.completeError('flutter run failed: exit code=$code');
}

View File

@@ -65,10 +65,7 @@ abstract class _Benchmark {
Directory get directory;
List<String> get options => <String>[
'--benchmark',
if (watch) '--watch',
];
List<String> get options => <String>['--benchmark', if (watch) '--watch'];
Future<double> execute(int iteration, int targetIterations) async {
section('Analyze $title ${watch ? 'with watcher' : ''} - ${iteration + 1} / $targetIterations');

View File

@@ -22,32 +22,22 @@ const List<String> kSentinelStr = <String>[
/// This test fails if the application hangs during this period.
/// https://ui.perfetto.dev/#!/?s=da6628c3a92456ae8fa3f345d0186e781da77e90fc8a64d073e9fee11d1e65
/// Regression test for https://github.com/flutter/flutter/issues/98973
TaskFunction androidChoreographerDoFrameTest({
Map<String, String>? environment,
}) {
final Directory tempDir = Directory.systemTemp
.createTempSync('flutter_devicelab_android_surface_recreation.');
TaskFunction androidChoreographerDoFrameTest({Map<String, String>? environment}) {
final Directory tempDir = Directory.systemTemp.createTempSync(
'flutter_devicelab_android_surface_recreation.',
);
return () async {
try {
section('Create app');
await inDirectory(tempDir, () async {
await flutter(
'create',
options: <String>[
'--platforms',
'android',
'app',
],
options: <String>['--platforms', 'android', 'app'],
environment: environment,
);
});
final File mainDart = File(path.join(
tempDir.absolute.path,
'app',
'lib',
'main.dart',
));
final File mainDart = File(path.join(tempDir.absolute.path, 'app', 'lib', 'main.dart'));
if (!mainDart.existsSync()) {
return TaskResult.failure('${mainDart.path} does not exist');
}
@@ -88,59 +78,51 @@ Future<void> main() async {
section('Flutter run (mode: $mode)');
late Process run;
await inDirectory(path.join(tempDir.path, 'app'), () async {
run = await startFlutter(
'run',
options: <String>['--$mode', '--verbose'],
);
run = await startFlutter('run', options: <String>['--$mode', '--verbose']);
});
int currSentinelIdx = 0;
final StreamSubscription<void> stdout = run.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
if (currSentinelIdx < sentinelCompleters.keys.length &&
line.contains(sentinelCompleters.keys.elementAt(currSentinelIdx))) {
sentinelCompleters.values.elementAt(currSentinelIdx).complete();
currSentinelIdx++;
print('stdout(MATCHED): $line');
} else {
print('stdout: $line');
}
});
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
if (currSentinelIdx < sentinelCompleters.keys.length &&
line.contains(sentinelCompleters.keys.elementAt(currSentinelIdx))) {
sentinelCompleters.values.elementAt(currSentinelIdx).complete();
currSentinelIdx++;
print('stdout(MATCHED): $line');
} else {
print('stdout: $line');
}
});
final StreamSubscription<void> stderr = run.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('stderr: $line');
});
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('stderr: $line');
});
final Completer<void> exitCompleter = Completer<void>();
unawaited(run.exitCode.then((int exitCode) {
exitCompleter.complete();
}));
unawaited(
run.exitCode.then((int exitCode) {
exitCompleter.complete();
}),
);
section('Wait for sentinels (mode: $mode)');
for (final Completer<void> completer in sentinelCompleters.values) {
if (nextCompleterIdx == 0) {
// Don't time out because we don't know how long it would take to get the first log.
await Future.any<dynamic>(
<Future<dynamic>>[
completer.future,
exitCompleter.future,
],
);
await Future.any<dynamic>(<Future<dynamic>>[completer.future, exitCompleter.future]);
} else {
try {
// Time out since this should not take 1s after the first log was received.
await Future.any<dynamic>(
<Future<dynamic>>[
completer.future.timeout(const Duration(seconds: 1)),
exitCompleter.future,
],
);
await Future.any<dynamic>(<Future<dynamic>>[
completer.future.timeout(const Duration(seconds: 1)),
exitCompleter.future,
]);
} on TimeoutException {
break;
}
@@ -179,7 +161,7 @@ Future<void> main() async {
}
final TaskResult releaseResult = await runTestFor('release');
if (releaseResult.failed) {
if (releaseResult.failed) {
return releaseResult;
}

View File

@@ -19,34 +19,22 @@ final RegExp _lifecycleSentinelRegExp = RegExp(r'==== lifecycle\: (.+) ====');
/// Tests the following Android lifecycles: Activity#onStop(), Activity#onResume(), Activity#onPause(),
/// and Activity#onDestroy() from Dart perspective in debug, profile, and release modes.
TaskFunction androidLifecyclesTest({
Map<String, String>? environment,
}) {
final Directory tempDir = Directory.systemTemp
.createTempSync('flutter_devicelab_activity_destroy.');
TaskFunction androidLifecyclesTest({Map<String, String>? environment}) {
final Directory tempDir = Directory.systemTemp.createTempSync(
'flutter_devicelab_activity_destroy.',
);
return () async {
try {
section('Create app');
await inDirectory(tempDir, () async {
await flutter(
'create',
options: <String>[
'--platforms',
'android',
'--org',
_kOrgName,
'app',
],
options: <String>['--platforms', 'android', '--org', _kOrgName, 'app'],
environment: environment,
);
});
final File mainDart = File(path.join(
tempDir.absolute.path,
'app',
'lib',
'main.dart',
));
final File mainDart = File(path.join(tempDir.absolute.path, 'app', 'lib', 'main.dart'));
if (!mainDart.existsSync()) {
return TaskResult.failure('${mainDart.path} does not exist');
}
@@ -77,20 +65,17 @@ void main() {
late Process run;
await inDirectory(path.join(tempDir.path, 'app'), () async {
run = await startFlutter(
'run',
options: <String>['--$mode'],
);
run = await startFlutter('run', options: <String>['--$mode']);
});
final StreamController<String> lifecycles = StreamController<String>();
final StreamIterator<String> lifecycleItr = StreamIterator<String>(lifecycles.stream);
final StreamSubscription<void> stdout = run.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String log) {
final RegExpMatch? match = _lifecycleSentinelRegExp.firstMatch(log);
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String log) {
final RegExpMatch? match = _lifecycleSentinelRegExp.firstMatch(log);
print('stdout: $log');
if (match == null) {
return;
@@ -98,14 +83,14 @@ void main() {
final String lifecycle = match[1]!;
print('stdout: Found app lifecycle: $lifecycle');
lifecycles.add(lifecycle);
});
});
final StreamSubscription<void> stderr = run.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String log) {
print('stderr: $log');
});
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String log) {
print('stderr: $log');
});
Future<void> expectedLifecycle(String expected) async {
section('Wait for lifecycle: $expected (mode: $mode)');
@@ -122,7 +107,8 @@ void main() {
await device.shellExec('input', <String>['keyevent', 'KEYCODE_APP_SWITCH']);
await expectedLifecycle('AppLifecycleState.inactive');
if (device.apiLevel == 28) { // Device lab currently runs 28.
if (device.apiLevel == 28) {
// Device lab currently runs 28.
await expectedLifecycle('AppLifecycleState.paused');
await expectedLifecycle('AppLifecycleState.detached');
}
@@ -136,7 +122,8 @@ void main() {
await device.shellExec('am', <String>['start', '-a', 'android.settings.SETTINGS']);
await expectedLifecycle('AppLifecycleState.inactive');
if (device.apiLevel == 28) { // Device lab currently runs 28.
if (device.apiLevel == 28) {
// Device lab currently runs 28.
await expectedLifecycle('AppLifecycleState.paused');
await expectedLifecycle('AppLifecycleState.detached');
}
@@ -168,7 +155,7 @@ void main() {
}
final TaskResult releaseResult = await runTestFor('release');
if (releaseResult.failed) {
if (releaseResult.failed) {
return releaseResult;
}

View File

@@ -19,11 +19,10 @@ class DriverTest {
DriverTest(
this.testDirectory,
this.testTarget, {
this.extraOptions = const <String>[],
this.deviceIdOverride,
this.environment,
}
);
this.extraOptions = const <String>[],
this.deviceIdOverride,
this.environment,
});
final String testDirectory;
final String testTarget;

View File

@@ -6,51 +6,46 @@ import '../framework/framework.dart';
import '../framework/task_result.dart';
import '../framework/utils.dart';
/// Tests the following Android lifecycles: Activity#onStop(), Activity#onResume(), Activity#onPause(),
/// and Activity#onDestroy() from Dart perspective in debug, profile, and release modes.
TaskFunction androidViewsTest({
Map<String, String>? environment,
}){
return () async {
section('Build APK');
await flutter(
'build',
options: <String>[
'apk',
'--config-only',
],
environment: environment,
workingDirectory: '${flutterDirectory.path}/dev/integration_tests/android_views'
);
TaskFunction androidViewsTest({Map<String, String>? environment}) {
return () async {
section('Build APK');
await flutter(
'build',
options: <String>['apk', '--config-only'],
environment: environment,
workingDirectory: '${flutterDirectory.path}/dev/integration_tests/android_views',
);
/// Any gradle command downloads gradle if not already present in the cache.
/// ./gradlew dependencies downloads any gradle defined dependencies to the cache.
/// https://docs.gradle.org/current/userguide/viewing_debugging_dependencies.html
/// Downloading gradle and downloading dependencies are a common source of flakes
/// and moving those to an infra step that can be retried shifts the blame
/// individual tests to the infra itself.
section('Download android dependencies');
final int exitCode = await exec(
'./gradlew',
<String>['-q', 'dependencies'],
workingDirectory:
'${flutterDirectory.path}/dev/integration_tests/android_views/android'
);
if (exitCode != 0) {
return TaskResult.failure('Failed to download gradle dependencies');
}
section('Run flutter drive on android views');
await flutter(
'drive',
options: <String>[
'--browser-name=android-chrome',
'--android-emulator', '--no-start-paused',
'--purge-persistent-cache', '--device-timeout=30',
],
environment: environment,
workingDirectory: '${flutterDirectory.path}/dev/integration_tests/android_views'
);
return TaskResult.success(null);
};
/// Any gradle command downloads gradle if not already present in the cache.
/// ./gradlew dependencies downloads any gradle defined dependencies to the cache.
/// https://docs.gradle.org/current/userguide/viewing_debugging_dependencies.html
/// Downloading gradle and downloading dependencies are a common source of flakes
/// and moving those to an infra step that can be retried shifts the blame
/// individual tests to the infra itself.
section('Download android dependencies');
final int exitCode = await exec(
'./gradlew',
<String>['-q', 'dependencies'],
workingDirectory: '${flutterDirectory.path}/dev/integration_tests/android_views/android',
);
if (exitCode != 0) {
return TaskResult.failure('Failed to download gradle dependencies');
}
section('Run flutter drive on android views');
await flutter(
'drive',
options: <String>[
'--browser-name=android-chrome',
'--android-emulator',
'--no-start-paused',
'--purge-persistent-cache',
'--device-timeout=30',
],
environment: environment,
workingDirectory: '${flutterDirectory.path}/dev/integration_tests/android_views',
);
return TaskResult.success(null);
};
}

View File

@@ -14,7 +14,7 @@ import '../framework/utils.dart';
///
/// Using this [Task] allows DeviceLab capacity to only be spent on the [test].
abstract class BuildTestTask {
BuildTestTask(this.args, {this.workingDirectory, this.runFlutterClean = true,}) {
BuildTestTask(this.args, {this.workingDirectory, this.runFlutterClean = true}) {
final ArgResults argResults = argParser.parse(args);
applicationBinaryPath = argResults[kApplicationBinaryPathOption] as String?;
buildOnly = argResults[kBuildOnlyFlag] as bool;
@@ -25,10 +25,11 @@ abstract class BuildTestTask {
static const String kBuildOnlyFlag = 'build';
static const String kTestOnlyFlag = 'test';
final ArgParser argParser = ArgParser()
..addOption(kApplicationBinaryPathOption)
..addFlag(kBuildOnlyFlag)
..addFlag(kTestOnlyFlag);
final ArgParser argParser =
ArgParser()
..addOption(kApplicationBinaryPathOption)
..addFlag(kBuildOnlyFlag)
..addFlag(kTestOnlyFlag);
/// Args passed from the test runner via "--task-arg".
final List<String> args;
@@ -61,7 +62,6 @@ abstract class BuildTestTask {
await flutter('build', options: getBuildArgs(deviceOperatingSystem));
copyArtifacts();
});
}
/// Run Flutter drive test from [getTestArgs] against the application under test on the device.
@@ -79,10 +79,12 @@ abstract class BuildTestTask {
}
/// Args passed to flutter build to build the application under test.
List<String> getBuildArgs(DeviceOperatingSystem deviceOperatingSystem) => throw UnimplementedError('getBuildArgs is not implemented');
List<String> getBuildArgs(DeviceOperatingSystem deviceOperatingSystem) =>
throw UnimplementedError('getBuildArgs is not implemented');
/// Args passed to flutter drive to test the built application.
List<String> getTestArgs(DeviceOperatingSystem deviceOperatingSystem, String deviceId) => throw UnimplementedError('getTestArgs is not implemented');
List<String> getTestArgs(DeviceOperatingSystem deviceOperatingSystem, String deviceId) =>
throw UnimplementedError('getTestArgs is not implemented');
/// Copy artifacts to [applicationBinaryPath] if specified.
///
@@ -90,7 +92,8 @@ abstract class BuildTestTask {
void copyArtifacts() => throw UnimplementedError('copyArtifacts is not implemented');
/// Logic to construct [TaskResult] from this test's results.
Future<TaskResult> parseTaskResult() => throw UnimplementedError('parseTaskResult is not implemented');
Future<TaskResult> parseTaskResult() =>
throw UnimplementedError('parseTaskResult is not implemented');
/// Path to the built application under test.
///

View File

@@ -12,12 +12,10 @@ import '../framework/framework.dart';
import '../framework/task_result.dart';
import '../framework/utils.dart';
TaskFunction dartPluginRegistryTest({
String? deviceIdOverride,
Map<String, String>? environment,
}) {
final Directory tempDir = Directory.systemTemp
.createTempSync('flutter_devicelab_dart_plugin_test.');
TaskFunction dartPluginRegistryTest({String? deviceIdOverride, Map<String, String>? environment}) {
final Directory tempDir = Directory.systemTemp.createTempSync(
'flutter_devicelab_dart_plugin_test.',
);
return () async {
try {
section('Create implementation plugin');
@@ -36,12 +34,14 @@ TaskFunction dartPluginRegistryTest({
);
});
final File pluginMain = File(path.join(
tempDir.absolute.path,
'aplugin_platform_implementation',
'lib',
'aplugin_platform_implementation.dart',
));
final File pluginMain = File(
path.join(
tempDir.absolute.path,
'aplugin_platform_implementation',
'lib',
'aplugin_platform_implementation.dart',
),
);
if (!pluginMain.existsSync()) {
return TaskResult.failure('${pluginMain.path} does not exist');
}
@@ -56,11 +56,9 @@ class ApluginPlatformInterfaceMacOS {
''', flush: true);
// Patch plugin main pubspec file.
final File pluginImplPubspec = File(path.join(
tempDir.absolute.path,
'aplugin_platform_implementation',
'pubspec.yaml',
));
final File pluginImplPubspec = File(
path.join(tempDir.absolute.path, 'aplugin_platform_implementation', 'pubspec.yaml'),
);
String pluginImplPubspecContent = await pluginImplPubspec.readAsString();
pluginImplPubspecContent = pluginImplPubspecContent.replaceFirst(
' pluginClass: ApluginPlatformImplementationPlugin',
@@ -68,11 +66,11 @@ class ApluginPlatformInterfaceMacOS {
' dartPluginClass: ApluginPlatformInterfaceMacOS\n',
);
pluginImplPubspecContent = pluginImplPubspecContent.replaceFirst(
' platforms:\n',
' implements: aplugin_platform_interface\n'
' platforms:\n');
await pluginImplPubspec.writeAsString(pluginImplPubspecContent,
flush: true);
' platforms:\n',
' implements: aplugin_platform_interface\n'
' platforms:\n',
);
await pluginImplPubspec.writeAsString(pluginImplPubspecContent, flush: true);
section('Create interface plugin');
await inDirectory(tempDir, () async {
@@ -89,25 +87,21 @@ class ApluginPlatformInterfaceMacOS {
environment: environment,
);
});
final File pluginInterfacePubspec = File(path.join(
tempDir.absolute.path,
'aplugin_platform_interface',
'pubspec.yaml',
));
String pluginInterfacePubspecContent =
await pluginInterfacePubspec.readAsString();
pluginInterfacePubspecContent =
pluginInterfacePubspecContent.replaceFirst(
' pluginClass: ApluginPlatformInterfacePlugin',
' default_package: aplugin_platform_implementation\n');
pluginInterfacePubspecContent =
pluginInterfacePubspecContent.replaceFirst(
'dependencies:',
'dependencies:\n'
' aplugin_platform_implementation:\n'
' path: ../aplugin_platform_implementation\n');
await pluginInterfacePubspec.writeAsString(pluginInterfacePubspecContent,
flush: true);
final File pluginInterfacePubspec = File(
path.join(tempDir.absolute.path, 'aplugin_platform_interface', 'pubspec.yaml'),
);
String pluginInterfacePubspecContent = await pluginInterfacePubspec.readAsString();
pluginInterfacePubspecContent = pluginInterfacePubspecContent.replaceFirst(
' pluginClass: ApluginPlatformInterfacePlugin',
' default_package: aplugin_platform_implementation\n',
);
pluginInterfacePubspecContent = pluginInterfacePubspecContent.replaceFirst(
'dependencies:',
'dependencies:\n'
' aplugin_platform_implementation:\n'
' path: ../aplugin_platform_implementation\n',
);
await pluginInterfacePubspec.writeAsString(pluginInterfacePubspecContent, flush: true);
section('Create app');
@@ -126,46 +120,40 @@ class ApluginPlatformInterfaceMacOS {
);
});
final File appPubspec = File(path.join(
tempDir.absolute.path,
'app',
'pubspec.yaml',
));
final File appPubspec = File(path.join(tempDir.absolute.path, 'app', 'pubspec.yaml'));
String appPubspecContent = await appPubspec.readAsString();
appPubspecContent = appPubspecContent.replaceFirst(
'dependencies:',
'dependencies:\n'
' aplugin_platform_interface:\n'
' path: ../aplugin_platform_interface\n');
'dependencies:',
'dependencies:\n'
' aplugin_platform_interface:\n'
' path: ../aplugin_platform_interface\n',
);
await appPubspec.writeAsString(appPubspecContent, flush: true);
section('Flutter run for macos');
late Process run;
await inDirectory(path.join(tempDir.path, 'app'), () async {
run = await startFlutter(
'run',
options: <String>['-d', 'macos', '-v'],
);
run = await startFlutter('run', options: <String>['-d', 'macos', '-v']);
});
Completer<void> registryExecutedCompleter = Completer<void>();
final StreamSubscription<void> stdoutSub = run.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
if (line.contains('ApluginPlatformInterfaceMacOS.registerWith() was called')) {
registryExecutedCompleter.complete();
}
print('stdout: $line');
});
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
if (line.contains('ApluginPlatformInterfaceMacOS.registerWith() was called')) {
registryExecutedCompleter.complete();
}
print('stdout: $line');
});
final StreamSubscription<void> stderrSub = run.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('stderr: $line');
});
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('stderr: $line');
});
final Future<void> stdoutDone = stdoutSub.asFuture<void>();
final Future<void> stderrDone = stderrSub.asFuture<void>();
@@ -175,12 +163,7 @@ class ApluginPlatformInterfaceMacOS {
}
Future<void> waitOrExit(Future<void> future) async {
final dynamic result = await Future.any<dynamic>(
<Future<dynamic>>[
future,
run.exitCode,
],
);
final dynamic result = await Future.any<dynamic>(<Future<dynamic>>[future, run.exitCode]);
if (result is int) {
await waitForStreams();
throw 'process exited with code $result';

View File

@@ -46,11 +46,7 @@ Future<TaskResult> _runWithTempDir(Directory tempDir) async {
const String testDirName = 'entrypoint_dart_registrant';
final String testPath = '${tempDir.path}/$testDirName';
await inDirectory(tempDir, () async {
await flutter('create', options: <String>[
'--platforms',
'android',
testDirName,
]);
await flutter('create', options: <String>['--platforms', 'android', testDirName]);
});
final String mainPath = '${tempDir.path}/$testDirName/lib/main.dart';
print(mainPath);
@@ -65,18 +61,17 @@ Future<TaskResult> _runWithTempDir(Directory tempDir) async {
// (which path_provider has).
await flutter('pub', options: <String>['add', 'path_provider:2.0.9']);
// The problem only manifested on release builds, so we test release.
final Process process =
await startFlutter('run', options: <String>['--release']);
final Process process = await startFlutter('run', options: <String>['--release']);
final Completer<String> completer = Completer<String>();
final StreamSubscription<String> stdoutSub = process.stdout
.transform<String>(const Utf8Decoder())
.transform<String>(const LineSplitter())
.listen((String line) async {
print(line);
if (line.contains(_messagePrefix)) {
completer.complete(line);
}
});
print(line);
if (line.contains(_messagePrefix)) {
completer.complete(line);
}
});
final String entrypoint = await completer.future;
await stdoutSub.cancel();
process.stdin.write('q');
@@ -95,8 +90,7 @@ Future<TaskResult> _runWithTempDir(Directory tempDir) async {
/// registrant.
TaskFunction entrypointDartRegistrant() {
return () async {
final Directory tempDir =
Directory.systemTemp.createTempSync('entrypoint_dart_registrant.');
final Directory tempDir = Directory.systemTemp.createTempSync('entrypoint_dart_registrant.');
try {
return await _runWithTempDir(tempDir);
} finally {

View File

@@ -13,10 +13,10 @@ import '../framework/utils.dart';
const int _kRunsPerBenchmark = 10;
Future<TaskResult> flutterToolStartupBenchmarkTask() async {
final Directory projectParentDirectory =
Directory.systemTemp.createTempSync('flutter_tool_startup_benchmark');
final Directory projectDirectory =
dir(path.join(projectParentDirectory.path, 'benchmark'));
final Directory projectParentDirectory = Directory.systemTemp.createTempSync(
'flutter_tool_startup_benchmark',
);
final Directory projectDirectory = dir(path.join(projectParentDirectory.path, 'benchmark'));
await inDirectory<void>(flutterDirectory, () async {
await flutter('update-packages');
await flutter('create', options: <String>[projectDirectory.path]);
@@ -26,43 +26,37 @@ Future<TaskResult> flutterToolStartupBenchmarkTask() async {
final Map<String, dynamic> data = <String, dynamic>{
// `flutter test` in dir with no `test` folder.
...(await _Benchmark(
projectDirectory,
'test startup',
'test',
).run())
.asMap('flutter_tool_startup_test'),
...(await _Benchmark(projectDirectory, 'test startup', 'test').run()).asMap(
'flutter_tool_startup_test',
),
// `flutter test -d foo_device` in dir with no `test` folder.
...(await _Benchmark(
projectDirectory,
'test startup with specified device',
'test',
options: <String>['-d', 'foo_device'],
).run())
projectDirectory,
'test startup with specified device',
'test',
options: <String>['-d', 'foo_device'],
).run())
.asMap('flutter_tool_startup_test_with_specified_device'),
// `flutter test -v` where no android sdk will be found (at least currently).
...(await _Benchmark(
projectDirectory,
'test startup no android sdk',
'test',
options: <String>['-v'],
environment: <String, String>{
'ANDROID_HOME': 'dummy value',
'ANDROID_SDK_ROOT': 'dummy value',
'PATH': pathWithoutWhereHits(<String>['adb', 'aapt']),
},
).run())
projectDirectory,
'test startup no android sdk',
'test',
options: <String>['-v'],
environment: <String, String>{
'ANDROID_HOME': 'dummy value',
'ANDROID_SDK_ROOT': 'dummy value',
'PATH': pathWithoutWhereHits(<String>['adb', 'aapt']),
},
).run())
.asMap('flutter_tool_startup_test_no_android_sdk'),
// `flutter -h`.
...(await _Benchmark(
projectDirectory,
'help startup',
'-h',
).run())
.asMap('flutter_tool_startup_help'),
...(await _Benchmark(projectDirectory, 'help startup', '-h').run()).asMap(
'flutter_tool_startup_help',
),
};
// Cleanup.
@@ -119,17 +113,18 @@ class _BenchmarkResult {
final int max; // Milliseconds
Map<String, dynamic> asMap(String name) {
return <String, dynamic>{
name: mean,
'${name}_minimum': min,
'${name}_maximum': max,
};
return <String, dynamic>{name: mean, '${name}_minimum': min, '${name}_maximum': max};
}
}
class _Benchmark {
_Benchmark(this.directory, this.title, this.command,
{this.options = const <String>[], this.environment});
_Benchmark(
this.directory,
this.title,
this.command, {
this.options = const <String>[],
this.environment,
});
final Directory directory;
@@ -148,8 +143,7 @@ class _Benchmark {
stopwatch.start();
// canFail is set to true, as e.g. `flutter test` in a dir with no `test`
// directory sets a non-zero return value.
await flutter(command,
options: options, canFail: true, environment: environment);
await flutter(command, options: options, canFail: true, environment: environment);
stopwatch.stop();
});
return stopwatch.elapsedMilliseconds;

View File

@@ -12,7 +12,9 @@ import '../framework/task_result.dart';
import '../framework/utils.dart';
import 'build_test_task.dart';
final Directory galleryDirectory = dir('${flutterDirectory.path}/dev/integration_tests/flutter_gallery');
final Directory galleryDirectory = dir(
'${flutterDirectory.path}/dev/integration_tests/flutter_gallery',
);
/// Temp function during gallery tests transition to build+test model.
///
@@ -42,14 +44,9 @@ TaskFunction createGalleryTransitionE2EBuildTest(
).call;
}
TaskFunction createGalleryTransitionE2ETest({
bool semanticsEnabled = false,
bool? enableImpeller,
}) {
TaskFunction createGalleryTransitionE2ETest({bool semanticsEnabled = false, bool? enableImpeller}) {
return GalleryTransitionTest(
testFile: semanticsEnabled
? 'transitions_perf_e2e_with_semantics'
: 'transitions_perf_e2e',
testFile: semanticsEnabled ? 'transitions_perf_e2e_with_semantics' : 'transitions_perf_e2e',
needFullTimeline: false,
timelineSummaryFile: 'e2e_perf_summary',
transitionDurationFile: null,
@@ -66,21 +63,24 @@ TaskFunction createGalleryTransitionHybridBuildTest(
return GalleryTransitionBuildTest(
args,
semanticsEnabled: semanticsEnabled,
driverFile: semanticsEnabled ? 'transitions_perf_hybrid_with_semantics_test' : 'transitions_perf_hybrid_test',
driverFile:
semanticsEnabled
? 'transitions_perf_hybrid_with_semantics_test'
: 'transitions_perf_hybrid_test',
).call;
}
TaskFunction createGalleryTransitionHybridTest({bool semanticsEnabled = false}) {
return GalleryTransitionTest(
semanticsEnabled: semanticsEnabled,
driverFile: semanticsEnabled
? 'transitions_perf_hybrid_with_semantics_test'
: 'transitions_perf_hybrid_test',
driverFile:
semanticsEnabled
? 'transitions_perf_hybrid_with_semantics_test'
: 'transitions_perf_hybrid_test',
).call;
}
class GalleryTransitionTest {
GalleryTransitionTest({
this.semanticsEnabled = false,
this.testFile = 'transitions_perf',
@@ -109,7 +109,9 @@ class GalleryTransitionTest {
final Device device = await devices.workingDevice;
await device.unlock();
final String deviceId = device.deviceId;
final Directory galleryDirectory = dir('${flutterDirectory.path}/dev/integration_tests/flutter_gallery');
final Directory galleryDirectory = dir(
'${flutterDirectory.path}/dev/integration_tests/flutter_gallery',
);
await inDirectory<void>(galleryDirectory, () async {
String? applicationBinaryPath;
if (deviceOperatingSystem == DeviceOperatingSystem.android) {
@@ -129,41 +131,39 @@ class GalleryTransitionTest {
applicationBinaryPath = 'build/app/outputs/flutter-apk/app-profile.apk';
}
final String testDriver = driverFile ?? (semanticsEnabled
? '${testFile}_with_semantics_test'
: '${testFile}_test');
final String testDriver =
driverFile ?? (semanticsEnabled ? '${testFile}_with_semantics_test' : '${testFile}_test');
section('DRIVE START');
await flutter('drive', options: <String>[
'--profile',
if (enableImpeller != null && enableImpeller!) '--enable-impeller',
if (enableImpeller != null && !enableImpeller!) '--no-enable-impeller',
if (needFullTimeline)
'--trace-startup',
if (applicationBinaryPath != null)
'--use-application-binary=$applicationBinaryPath'
else
...<String>[
'-t',
'test_driver/$testFile.dart',
],
'--driver',
'test_driver/$testDriver.dart',
'-d',
deviceId,
'-v',
'--verbose-system-logs'
]);
await flutter(
'drive',
options: <String>[
'--profile',
if (enableImpeller != null && enableImpeller!) '--enable-impeller',
if (enableImpeller != null && !enableImpeller!) '--no-enable-impeller',
if (needFullTimeline) '--trace-startup',
if (applicationBinaryPath != null)
'--use-application-binary=$applicationBinaryPath'
else ...<String>['-t', 'test_driver/$testFile.dart'],
'--driver',
'test_driver/$testDriver.dart',
'-d',
deviceId,
'-v',
'--verbose-system-logs',
],
);
});
final String testOutputDirectory = Platform.environment['FLUTTER_TEST_OUTPUTS_DIR'] ?? '${galleryDirectory.path}/build';
final Map<String, dynamic> summary = json.decode(
file('$testOutputDirectory/$timelineSummaryFile.json').readAsStringSync(),
) as Map<String, dynamic>;
final String testOutputDirectory =
Platform.environment['FLUTTER_TEST_OUTPUTS_DIR'] ?? '${galleryDirectory.path}/build';
final Map<String, dynamic> summary =
json.decode(file('$testOutputDirectory/$timelineSummaryFile.json').readAsStringSync())
as Map<String, dynamic>;
if (transitionDurationFile != null) {
final Map<String, dynamic> original = json.decode(
file('$testOutputDirectory/$transitionDurationFile.json').readAsStringSync(),
) as Map<String, dynamic>;
final Map<String, dynamic> original =
json.decode(file('$testOutputDirectory/$transitionDurationFile.json').readAsStringSync())
as Map<String, dynamic>;
final Map<String, List<int>> transitions = <String, List<int>>{};
for (final String key in original.keys) {
transitions[key] = List<int>.from(original[key] as List<dynamic>);
@@ -173,16 +173,14 @@ class GalleryTransitionTest {
}
final bool isAndroid = deviceOperatingSystem == DeviceOperatingSystem.android;
return TaskResult.success(summary,
return TaskResult.success(
summary,
detailFiles: <String>[
if (transitionDurationFile != null)
'$testOutputDirectory/$transitionDurationFile.json',
if (timelineTraceFile != null)
'$testOutputDirectory/$timelineTraceFile.json',
if (transitionDurationFile != null) '$testOutputDirectory/$transitionDurationFile.json',
if (timelineTraceFile != null) '$testOutputDirectory/$timelineTraceFile.json',
],
benchmarkScoreKeys: <String>[
if (transitionDurationFile != null)
'missed_transition_count',
if (transitionDurationFile != null) 'missed_transition_count',
'average_frame_build_time_millis',
'worst_frame_build_time_millis',
'90th_percentile_frame_build_time_millis',
@@ -250,7 +248,8 @@ class GalleryTransitionBuildTest extends BuildTestTask {
final String? transitionDurationFile;
final String? driverFile;
final String testOutputDirectory = Platform.environment['FLUTTER_TEST_OUTPUTS_DIR'] ?? '${galleryDirectory.path}/build';
final String testOutputDirectory =
Platform.environment['FLUTTER_TEST_OUTPUTS_DIR'] ?? '${galleryDirectory.path}/build';
@override
void copyArtifacts() {
@@ -283,20 +282,15 @@ class GalleryTransitionBuildTest extends BuildTestTask {
'android-arm,android-arm64',
];
} else if (deviceOperatingSystem == DeviceOperatingSystem.ios) {
return <String>[
'ios',
'--codesign',
'--profile',
'-t',
'test_driver/$testFile.dart',
];
return <String>['ios', '--codesign', '--profile', '-t', 'test_driver/$testFile.dart'];
}
throw Exception('$deviceOperatingSystem has no build configuration');
}
@override
List<String> getTestArgs(DeviceOperatingSystem deviceOperatingSystem, String deviceId) {
final String testDriver = driverFile ?? (semanticsEnabled ? '${testFile}_with_semantics_test' : '${testFile}_test');
final String testDriver =
driverFile ?? (semanticsEnabled ? '${testFile}_with_semantics_test' : '${testFile}_test');
return <String>[
'--no-dds',
'--profile',
@@ -315,14 +309,14 @@ class GalleryTransitionBuildTest extends BuildTestTask {
@override
Future<TaskResult> parseTaskResult() async {
final Map<String, dynamic> summary = json.decode(
file('$testOutputDirectory/$timelineSummaryFile.json').readAsStringSync(),
) as Map<String, dynamic>;
final Map<String, dynamic> summary =
json.decode(file('$testOutputDirectory/$timelineSummaryFile.json').readAsStringSync())
as Map<String, dynamic>;
if (transitionDurationFile != null) {
final Map<String, dynamic> original = json.decode(
file('$testOutputDirectory/$transitionDurationFile.json').readAsStringSync(),
) as Map<String, dynamic>;
final Map<String, dynamic> original =
json.decode(file('$testOutputDirectory/$transitionDurationFile.json').readAsStringSync())
as Map<String, dynamic>;
final Map<String, List<int>> transitions = <String, List<int>>{};
for (final String key in original.keys) {
transitions[key] = List<int>.from(original[key] as List<dynamic>);
@@ -397,7 +391,9 @@ int _countMissedTransitions(Map<String, List<int>> transitions) {
transitions.forEach((String demoName, List<int> durations) {
final int longestDuration = durations.reduce(math.max);
if (longestDuration > kTransitionBudget) {
print('$demoName missed transition time budget ($longestDuration µs > $kTransitionBudget µs)');
print(
'$demoName missed transition time budget ($longestDuration µs > $kTransitionBudget µs)',
);
count++;
}
});

View File

@@ -15,8 +15,12 @@ import '../framework/running_processes.dart';
import '../framework/task_result.dart';
import '../framework/utils.dart';
final Directory _editedFlutterGalleryDir = dir(path.join(Directory.systemTemp.path, 'edited_flutter_gallery'));
final Directory flutterGalleryDir = dir(path.join(flutterDirectory.path, 'dev/integration_tests/flutter_gallery'));
final Directory _editedFlutterGalleryDir = dir(
path.join(Directory.systemTemp.path, 'edited_flutter_gallery'),
);
final Directory flutterGalleryDir = dir(
path.join(flutterDirectory.path, 'dev/integration_tests/flutter_gallery'),
);
const String kSourceLine = 'fontSize: (orientation == Orientation.portrait) ? 32.0 : 24.0';
const String kReplacementLine = 'fontSize: (orientation == Orientation.portrait) ? 34.0 : 24.0';
@@ -26,9 +30,9 @@ TaskFunction createHotModeTest({
List<String>? additionalOptions,
}) {
// This file is modified during the test and needs to be restored at the end.
final File flutterFrameworkSource = file(path.join(
flutterDirectory.path, 'packages/flutter/lib/src/widgets/framework.dart',
));
final File flutterFrameworkSource = file(
path.join(flutterDirectory.path, 'packages/flutter/lib/src/widgets/framework.dart'),
);
final String oldContents = flutterFrameworkSource.readAsStringSync();
return () async {
if (deviceIdOverride == null) {
@@ -56,7 +60,6 @@ TaskFunction createHotModeTest({
late Map<String, dynamic> largeReloadData;
late Map<String, dynamic> freshRestartReloadsData;
await inDirectory<void>(flutterDirectory, () async {
rmTree(_editedFlutterGalleryDir);
mkdirs(_editedFlutterGalleryDir);
@@ -73,14 +76,15 @@ TaskFunction createHotModeTest({
}
if (hotReloadCount == 0) {
// Update a file for 2 library invalidation.
final File appDartSource = file(path.join(
_editedFlutterGalleryDir.path,
'lib/gallery/app.dart',
));
appDartSource.writeAsStringSync(appDartSource.readAsStringSync().replaceFirst(
"'Flutter Gallery'",
"'Updated Flutter Gallery'",
));
final File appDartSource = file(
path.join(_editedFlutterGalleryDir.path, 'lib/gallery/app.dart'),
);
appDartSource.writeAsStringSync(
appDartSource.readAsStringSync().replaceFirst(
"'Flutter Gallery'",
"'Updated Flutter Gallery'",
),
);
process.stdin.writeln('r');
hotReloadCount += 1;
} else {
@@ -98,11 +102,11 @@ TaskFunction createHotModeTest({
}
if (hotReloadCount == 1) {
// Update a file for ~50 library invalidation.
final File appDartSource = file(path.join(
_editedFlutterGalleryDir.path, 'lib/demo/calculator/home.dart',
));
final File appDartSource = file(
path.join(_editedFlutterGalleryDir.path, 'lib/demo/calculator/home.dart'),
);
appDartSource.writeAsStringSync(
appDartSource.readAsStringSync().replaceFirst(kSourceLine, kReplacementLine)
appDartSource.readAsStringSync().replaceFirst(kSourceLine, kReplacementLine),
);
process.stdin.writeln('r');
hotReloadCount += 1;
@@ -122,7 +126,7 @@ TaskFunction createHotModeTest({
if (hotReloadCount == 2) {
// Trigger a framework invalidation (370 libraries) without modifying the source
flutterFrameworkSource.writeAsStringSync(
'${flutterFrameworkSource.readAsStringSync()}\n'
'${flutterFrameworkSource.readAsStringSync()}\n',
);
process.stdin.writeln('r');
hotReloadCount += 1;
@@ -138,34 +142,36 @@ TaskFunction createHotModeTest({
// Start `flutter run` again to make sure it loads from the previous
// state. Frontend loads up from previously generated kernel files.
{
final Process process = await startFlutter(
'run',
options: options,
);
final Process process = await startFlutter('run', options: options);
final Completer<void> stdoutDone = Completer<void>();
final Completer<void> stderrDone = Completer<void>();
process.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
if (line.contains('Reloaded ')) {
process.stdin.writeln('q');
}
print('stdout: $line');
}, onDone: () {
stdoutDone.complete();
});
.listen(
(String line) {
if (line.contains('Reloaded ')) {
process.stdin.writeln('q');
}
print('stdout: $line');
},
onDone: () {
stdoutDone.complete();
},
);
process.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('stderr: $line');
}, onDone: () {
stderrDone.complete();
});
.listen(
(String line) {
print('stderr: $line');
},
onDone: () {
stderrDone.complete();
},
);
await Future.wait<void>(
<Future<void>>[stdoutDone.future, stderrDone.future]);
await Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]);
await process.exitCode;
freshRestartReloadsData =
@@ -274,32 +280,26 @@ Future<Map<String, dynamic>> captureReloadData({
required File benchmarkFile,
required void Function(String, Process) onLine,
}) async {
final Process process = await startFlutter(
'run',
options: options,
);
final Process process = await startFlutter('run', options: options);
final Completer<void> stdoutDone = Completer<void>();
final Completer<void> stderrDone = Completer<void>();
process.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
onLine(line, process);
print('stdout: $line');
}, onDone: stdoutDone.complete);
process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen((
String line,
) {
onLine(line, process);
print('stdout: $line');
}, onDone: stdoutDone.complete);
process.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen(
(String line) => print('stderr: $line'),
onDone: stderrDone.complete,
);
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) => print('stderr: $line'), onDone: stderrDone.complete);
await Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]);
await process.exitCode;
final Map<String, dynamic> result = json.decode(benchmarkFile.readAsStringSync()) as Map<String, dynamic>;
final Map<String, dynamic> result =
json.decode(benchmarkFile.readAsStringSync()) as Map<String, dynamic>;
benchmarkFile.deleteSync();
return result;
}

View File

@@ -40,7 +40,9 @@ TaskFunction createIntegrationTestFlavorsTest({Map<String, String>? environment}
).call;
}
TaskFunction createExternalTexturesFrameRateIntegrationTest({ List<String> extraOptions = const <String>[] }) {
TaskFunction createExternalTexturesFrameRateIntegrationTest({
List<String> extraOptions = const <String>[],
}) {
return DriverTest(
'${flutterDirectory.path}/dev/integration_tests/external_textures',
'lib/frame_rate_main.dart',
@@ -89,9 +91,7 @@ TaskFunction createIOSPlatformViewTests() {
return DriverTest(
'${flutterDirectory.path}/dev/integration_tests/ios_platform_view_tests',
'lib/main.dart',
extraOptions: <String>[
'--dart-define=ENABLE_DRIVER_EXTENSION=true',
],
extraOptions: <String>['--dart-define=ENABLE_DRIVER_EXTENSION=true'],
).call;
}
@@ -135,19 +135,17 @@ TaskFunction createSolidColorTest({required bool enableImpeller}) {
return DriverTest(
'${flutterDirectory.path}/dev/integration_tests/ui',
'lib/solid_color.dart',
extraOptions: <String>[
if (enableImpeller)
'--enable-impeller'
]
extraOptions: <String>[if (enableImpeller) '--enable-impeller'],
).call;
}
TaskFunction dartDefinesTask() {
return DriverTest(
'${flutterDirectory.path}/dev/integration_tests/ui',
'lib/defines.dart', extraOptions: <String>[
'--dart-define=test.valueA=Example,A',
'--dart-define=test.valueB=Value',
'lib/defines.dart',
extraOptions: <String>[
'--dart-define=test.valueA=Example,A',
'--dart-define=test.valueB=Value',
],
).call;
}
@@ -186,11 +184,10 @@ class DriverTest {
DriverTest(
this.testDirectory,
this.testTarget, {
this.extraOptions = const <String>[],
this.deviceIdOverride,
this.environment,
}
);
this.extraOptions = const <String>[],
this.deviceIdOverride,
this.environment,
});
final String testDirectory;
final String testTarget;
@@ -230,12 +227,11 @@ class IntegrationTest {
IntegrationTest(
this.testDirectory,
this.testTarget, {
this.extraOptions = const <String>[],
this.createPlatforms = const <String>[],
this.withTalkBack = false,
this.environment,
}
);
this.extraOptions = const <String>[],
this.createPlatforms = const <String>[],
this.withTalkBack = false,
this.environment,
});
final String testDirectory;
final String testTarget;
@@ -252,28 +248,22 @@ class IntegrationTest {
await flutter('packages', options: <String>['get']);
if (createPlatforms.isNotEmpty) {
await flutter('create', options: <String>[
'--platforms',
createPlatforms.join(','),
'--no-overwrite',
'.'
]);
await flutter(
'create',
options: <String>['--platforms', createPlatforms.join(','), '--no-overwrite', '.'],
);
}
if (withTalkBack) {
if (device is! AndroidDevice) {
return TaskResult.failure('A test that enables TalkBack can only be run on Android devices');
return TaskResult.failure(
'A test that enables TalkBack can only be run on Android devices',
);
}
await enableTalkBack();
}
final List<String> options = <String>[
'-v',
'-d',
deviceId,
testTarget,
...extraOptions,
];
final List<String> options = <String>['-v', '-d', deviceId, testTarget, ...extraOptions];
await flutter('test', options: options, environment: environment);
if (withTalkBack) {

View File

@@ -19,7 +19,6 @@ TaskFunction createMicrobenchmarkTask({
bool? enableImpeller,
Map<String, String> environment = const <String, String>{},
}) {
// Generate a seed for this test stable around the date.
final DateTime seedDate = DateTime.now().toUtc().subtract(const Duration(hours: 7));
final int seed = DateTime(seedDate.year, seedDate.month, seedDate.day).hashCode;
@@ -29,21 +28,14 @@ TaskFunction createMicrobenchmarkTask({
await device.unlock();
await device.clearLogs();
final Directory appDir =
dir(path.join(flutterDirectory.path, 'dev/benchmarks/microbenchmarks'));
final Directory appDir = dir(
path.join(flutterDirectory.path, 'dev/benchmarks/microbenchmarks'),
);
// Hard-uninstall any prior apps.
await inDirectory(appDir, () async {
section('Uninstall previous microbenchmarks app');
await flutter(
'install',
options: <String>[
'-v',
'--uninstall-only',
'-d',
device.deviceId,
],
);
await flutter('install', options: <String>['-v', '--uninstall-only', '-d', device.deviceId]);
});
Future<Map<String, double>> runMicrobench(String benchmarkPath) async {
@@ -63,11 +55,7 @@ TaskFunction createMicrobenchmarkTask({
'--dart-define=seed=$seed',
benchmarkPath,
];
return startFlutter(
'run',
options: options,
environment: environment,
);
return startFlutter('run', options: options, environment: environment);
});
return readJsonResults(flutterProcess);
}
@@ -79,7 +67,6 @@ TaskFunction createMicrobenchmarkTask({
...await runMicrobench('lib/benchmark_collection.dart'),
};
return TaskResult.success(allResults,
benchmarkScoreKeys: allResults.keys.toList());
return TaskResult.success(allResults, benchmarkScoreKeys: allResults.keys.toList());
};
}

View File

@@ -15,11 +15,7 @@ import '../framework/utils.dart';
const String _packageName = 'package_with_native_assets';
const List<String> _buildModes = <String>[
'debug',
'profile',
'release',
];
const List<String> _buildModes = <String>['debug', 'profile', 'release'];
TaskFunction createNativeAssetsTest({
String? deviceIdOverride,
@@ -41,7 +37,9 @@ TaskFunction createNativeAssetsTest({
}
final TaskResult buildModeResult = await inTempDir((Directory tempDirectory) async {
final Directory packageDirectory = await createTestProject(_packageName, tempDirectory);
final Directory exampleDirectory = dir(packageDirectory.uri.resolve('example/').toFilePath());
final Directory exampleDirectory = dir(
packageDirectory.uri.resolve('example/').toFilePath(),
);
final List<String> options = <String>[
'-d',
@@ -128,22 +126,21 @@ Future<int> runFlutter({
required List<String> options,
required void Function(String, Process) onLine,
}) async {
final Process process = await startFlutter(
'run',
options: options,
);
final Process process = await startFlutter('run', options: options);
final Completer<void> stdoutDone = Completer<void>();
final Completer<void> stderrDone = Completer<void>();
process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen((String line) {
process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen((
String line,
) {
onLine(line, process);
print('stdout: $line');
}, onDone: stdoutDone.complete);
process.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(
(String line) => print('stderr: $line'),
onDone: stderrDone.complete,
);
process.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) => print('stderr: $line'), onDone: stderrDone.complete);
await Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]);
final int exitCode = await process.exitCode;
@@ -154,52 +151,29 @@ final String _flutterBin = path.join(flutterDirectory.path, 'bin', 'flutter');
Future<void> enableNativeAssets() async {
print('Enabling configs for native assets...');
final int configResult = await exec(
_flutterBin,
<String>[
'config',
'-v',
'--enable-native-assets',
],
canFail: true);
final int configResult = await exec(_flutterBin, <String>[
'config',
'-v',
'--enable-native-assets',
], canFail: true);
if (configResult != 0) {
print('Failed to enable configuration, tasks may not run.');
}
}
Future<Directory> createTestProject(
String packageName,
Directory tempDirectory,
) async {
await exec(
_flutterBin,
<String>[
'create',
'--no-pub',
'--template=package_ffi',
packageName,
],
workingDirectory: tempDirectory.path,
);
Future<Directory> createTestProject(String packageName, Directory tempDirectory) async {
await exec(_flutterBin, <String>[
'create',
'--no-pub',
'--template=package_ffi',
packageName,
], workingDirectory: tempDirectory.path);
final Directory packageDirectory = Directory(
path.join(tempDirectory.path, packageName),
);
await _pinDependencies(
File(path.join(packageDirectory.path, 'pubspec.yaml')),
);
await _pinDependencies(
File(path.join(packageDirectory.path, 'example', 'pubspec.yaml')),
);
final Directory packageDirectory = Directory(path.join(tempDirectory.path, packageName));
await _pinDependencies(File(path.join(packageDirectory.path, 'pubspec.yaml')));
await _pinDependencies(File(path.join(packageDirectory.path, 'example', 'pubspec.yaml')));
await exec(
_flutterBin,
<String>[
'pub',
'get',
],
workingDirectory: packageDirectory.path,
);
await exec(_flutterBin, <String>['pub', 'get'], workingDirectory: packageDirectory.path);
return packageDirectory;
}
@@ -210,9 +184,10 @@ Future<void> _pinDependencies(File pubspecFile) async {
await pubspecFile.writeAsString(newPubspec);
}
Future<T> inTempDir<T>(Future<T> Function(Directory tempDirectory) fun) async {
final Directory tempDirectory = dir(Directory.systemTemp.createTempSync().resolveSymbolicLinksSync());
final Directory tempDirectory = dir(
Directory.systemTemp.createTempSync().resolveSymbolicLinksSync(),
);
try {
return await fun(tempDirectory);
} finally {

View File

@@ -13,11 +13,11 @@ class NewGalleryPerfTest extends PerfTest {
super.timeoutSeconds,
super.forceOpenGLES,
}) : super(
'${flutterDirectory.path}/dev/integration_tests/new_gallery',
'test_driver/transitions_perf.dart',
timelineFileName,
dartDefine: dartDefine,
createPlatforms: <String>['android', 'ios', 'web'],
enableMergedPlatformThread: true,
);
'${flutterDirectory.path}/dev/integration_tests/new_gallery',
'test_driver/transitions_perf.dart',
timelineFileName,
dartDefine: dartDefine,
createPlatforms: <String>['android', 'ios', 'web'],
enableMergedPlatformThread: true,
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -18,8 +18,9 @@ TaskFunction runTask(adb.DeviceOperatingSystem operatingSystem) {
final adb.Device device = await adb.devices.workingDevice;
await device.unlock();
final Directory appDir = utils.dir(path.join(utils.flutterDirectory.path,
'dev/benchmarks/platform_channels_benchmarks'));
final Directory appDir = utils.dir(
path.join(utils.flutterDirectory.path, 'dev/benchmarks/platform_channels_benchmarks'),
);
final Process flutterProcess = await utils.inDirectory(appDir, () async {
final List<String> createArgs = <String>[
'--platforms',
@@ -39,15 +40,10 @@ TaskFunction runTask(adb.DeviceOperatingSystem operatingSystem) {
'-d',
device.deviceId,
];
return utils.startFlutter(
'run',
options: options,
);
return utils.startFlutter('run', options: options);
});
final Map<String, double> results =
await microbenchmarks.readJsonResults(flutterProcess);
return TaskResult.success(results,
benchmarkScoreKeys: results.keys.toList());
final Map<String, double> results = await microbenchmarks.readJsonResults(flutterProcess);
return TaskResult.success(results, benchmarkScoreKeys: results.keys.toList());
};
}

View File

@@ -49,8 +49,7 @@ class PluginTest {
final bool cocoapodsTransitiveFlutterDependency;
Future<TaskResult> call() async {
final Directory tempDir =
Directory.systemTemp.createTempSync('flutter_devicelab_plugin_test.');
final Directory tempDir = Directory.systemTemp.createTempSync('flutter_devicelab_plugin_test.');
// FFI plugins do not have support for `flutter test`.
// `flutter test` does not do a native build.
// Supporting `flutter test` would require invoking a native build.
@@ -58,8 +57,13 @@ class PluginTest {
try {
section('Create plugin');
final _FlutterProject plugin = await _FlutterProject.create(
tempDir, options, buildTarget,
name: 'plugintest', template: template, environment: pluginCreateEnvironment);
tempDir,
options,
buildTarget,
name: 'plugintest',
template: template,
environment: pluginCreateEnvironment,
);
if (dartOnlyPlugin) {
await plugin.convertDefaultPluginToDartPlugin();
}
@@ -74,8 +78,14 @@ class PluginTest {
}
}
section('Create Flutter app');
final _FlutterProject app = await _FlutterProject.create(tempDir, options, buildTarget,
name: 'plugintestapp', template: 'app', environment: appCreateEnvironment);
final _FlutterProject app = await _FlutterProject.create(
tempDir,
options,
buildTarget,
name: 'plugintestapp',
template: 'app',
environment: appCreateEnvironment,
);
try {
if (cocoapodsTransitiveFlutterDependency) {
section('Disable Swift Package Manager');
@@ -83,8 +93,7 @@ class PluginTest {
}
section('Add plugins');
await app.addPlugin('plugintest',
pluginPath: path.join('..', 'plugintest'));
await app.addPlugin('plugintest', pluginPath: path.join('..', 'plugintest'));
await app.addPlugin('path_provider');
section('Build app');
await app.build(buildTarget, validateNativeBuildProject: !dartOnlyPlugin);
@@ -115,7 +124,10 @@ class PluginTest {
}
}
Future<void> _testLocalEngineConfiguration(_FlutterProject app, String fakeEngineSourcePath) async {
Future<void> _testLocalEngineConfiguration(
_FlutterProject app,
String fakeEngineSourcePath,
) async {
// The tool requires that a directory that looks like an engine build
// actually exists when passing --local-engine, so create a fake skeleton.
final Directory buildDir = Directory(path.join(fakeEngineSourcePath, 'out', 'foo'));
@@ -157,11 +169,11 @@ class _FlutterProject {
String content = await pubspec.readAsString();
content = content.replaceFirst(
'# The following section is specific to Flutter packages.\n'
'flutter:\n',
'flutter:\n',
'# The following section is specific to Flutter packages.\n'
'flutter:\n'
'\n'
' disable-swift-package-manager: true\n'
'flutter:\n'
'\n'
' disable-swift-package-manager: true\n',
);
await pubspec.writeAsString(content, flush: true);
}
@@ -169,12 +181,8 @@ class _FlutterProject {
Future<void> addPlugin(String plugin, {String? pluginPath}) async {
final File pubspec = pubspecFile;
String content = await pubspec.readAsString();
final String dependency =
pluginPath != null ? '$plugin:\n path: $pluginPath' : '$plugin:';
content = content.replaceFirst(
'\ndependencies:\n',
'\ndependencies:\n $dependency\n',
);
final String dependency = pluginPath != null ? '$plugin:\n path: $pluginPath' : '$plugin:';
content = content.replaceFirst('\ndependencies:\n', '\ndependencies:\n $dependency\n');
await pubspec.writeAsString(content, flush: true);
}
@@ -204,13 +212,7 @@ class $dartPluginClass {
await dartCode.writeAsString(content, flush: true);
// Remove any native plugin code.
const List<String> platforms = <String>[
'android',
'ios',
'linux',
'macos',
'windows',
];
const List<String> platforms = <String>['android', 'ios', 'linux', 'macos', 'windows'];
for (final String platform in platforms) {
final Directory platformDir = Directory(path.join(rootPath, platform));
if (platformDir.existsSync()) {
@@ -232,12 +234,12 @@ class $dartPluginClass {
throw TaskResult.failure('Missing expected darwin platform plugin keys');
}
pubspecContent = pubspecContent.replaceAll(
originalIOSKey,
'$originalIOSKey sharedDarwinSource: true\n'
originalIOSKey,
'$originalIOSKey sharedDarwinSource: true\n',
);
pubspecContent = pubspecContent.replaceAll(
originalMacOSKey,
'$originalMacOSKey sharedDarwinSource: true\n'
originalMacOSKey,
'$originalMacOSKey sharedDarwinSource: true\n',
);
await pubspec.writeAsString(pubspecContent, flush: true);
@@ -258,7 +260,10 @@ class $dartPluginClass {
// Remove "s.platform = :ios" to work on all platforms, including macOS.
podspecContent = podspecContent.replaceFirst(RegExp(r'.*s\.platform.*'), '');
podspecContent = podspecContent.replaceFirst("s.dependency 'Flutter'", "s.ios.dependency 'Flutter'\ns.osx.dependency 'FlutterMacOS'");
podspecContent = podspecContent.replaceFirst(
"s.dependency 'Flutter'",
"s.ios.dependency 'Flutter'\ns.osx.dependency 'FlutterMacOS'",
);
await podspec.writeAsString(podspecContent, flush: true);
@@ -315,11 +320,12 @@ public class $pluginClass: NSObject, FlutterPlugin {
switch (buildTarget) {
case 'apk':
if (await exec(
path.join('.', 'gradlew'),
<String>['testDebugUnitTest'],
workingDirectory: path.join(rootPath, 'android'),
canFail: true,
) != 0) {
path.join('.', 'gradlew'),
<String>['testDebugUnitTest'],
workingDirectory: path.join(rootPath, 'android'),
canFail: true,
) !=
0) {
throw TaskResult.failure('Platform unit tests failed');
}
case 'ios':
@@ -342,10 +348,20 @@ public class $pluginClass: NSObject, FlutterPlugin {
}
case 'linux':
if (await exec(
path.join(rootPath, 'build', 'linux', 'x64', 'release', 'plugins', 'plugintest', 'plugintest_test'),
<String>[],
canFail: true,
) != 0) {
path.join(
rootPath,
'build',
'linux',
'x64',
'release',
'plugins',
'plugintest',
'plugintest_test',
),
<String>[],
canFail: true,
) !=
0) {
throw TaskResult.failure('Platform unit tests failed');
}
case 'macos':
@@ -359,26 +375,35 @@ public class $pluginClass: NSObject, FlutterPlugin {
throw TaskResult.failure('Platform unit tests failed');
}
case 'windows':
final String arch = Abi.current() == Abi.windowsX64 ? 'x64': 'arm64';
final String arch = Abi.current() == Abi.windowsX64 ? 'x64' : 'arm64';
if (await exec(
path.join(rootPath, 'build', 'windows', arch, 'plugins', 'plugintest', 'Release', 'plugintest_test.exe'),
<String>[],
canFail: true,
) != 0) {
path.join(
rootPath,
'build',
'windows',
arch,
'plugins',
'plugintest',
'Release',
'plugintest_test.exe',
),
<String>[],
canFail: true,
) !=
0) {
throw TaskResult.failure('Platform unit tests failed');
}
}
}
static Future<_FlutterProject> create(
Directory directory,
List<String> options,
String target,
{
required String name,
required String template,
Map<String, String>? environment,
}) async {
Directory directory,
List<String> options,
String target, {
required String name,
required String template,
Map<String, String>? environment,
}) async {
await inDirectory(directory, () async {
await flutter(
'create',
@@ -405,11 +430,7 @@ public class $pluginClass: NSObject, FlutterPlugin {
Future<void> addCocoapodsTransitiveFlutterDependency() async {
final String iosDirectoryPath = path.join(rootPath, 'ios');
final File nativePod = File(path.join(
iosDirectoryPath,
'NativePod',
'NativePod.podspec',
));
final File nativePod = File(path.join(iosDirectoryPath, 'NativePod', 'NativePod.podspec'));
nativePod.createSync(recursive: true);
nativePod.writeAsStringSync('''
Pod::Spec.new do |s|
@@ -425,12 +446,9 @@ Pod::Spec.new do |s|
end
''');
final File nativePodClass = File(path.join(
iosDirectoryPath,
'NativePod',
'Classes',
'NativePodTest.m',
));
final File nativePodClass = File(
path.join(iosDirectoryPath, 'NativePod', 'Classes', 'NativePodTest.m'),
);
nativePodClass.createSync(recursive: true);
nativePodClass.writeAsStringSync('''
#import <Flutter/Flutter.h>
@@ -446,7 +464,9 @@ end
final File podfileFile = File(path.join(iosDirectoryPath, 'Podfile'));
final List<String> podfileContents = podfileFile.readAsLinesSync();
final int index = podfileContents.indexWhere((String line) => line.contains('flutter_install_all_ios_pods'));
final int index = podfileContents.indexWhere(
(String line) => line.contains('flutter_install_all_ios_pods'),
);
podfileContents.insert(index, "pod 'NativePod', :path => 'NativePod'");
podfileFile.writeAsStringSync(podfileContents.join('\n'));
}
@@ -458,12 +478,13 @@ end
if (!podspec.existsSync()) {
throw TaskResult.failure('podspec file missing at ${podspec.path}');
}
final String versionString = target == 'ios'
? "s.platform = :ios, '12.0'"
: "s.platform = :osx, '10.11'";
final String versionString =
target == 'ios' ? "s.platform = :ios, '12.0'" : "s.platform = :osx, '10.11'";
String podspecContent = podspec.readAsStringSync();
if (!podspecContent.contains(versionString)) {
throw TaskResult.failure('Update this test to match plugin minimum $target deployment version');
throw TaskResult.failure(
'Update this test to match plugin minimum $target deployment version',
);
}
// Add transitive dependency on AppAuth 1.6 targeting iOS 8 and macOS 10.9, which no longer builds in Xcode
// to test the version is forced higher and builds.
@@ -477,7 +498,10 @@ s.platform = :osx, '10.8'
s.dependency 'AppAuth', '1.6.0'
''';
podspecContent = podspecContent.replaceFirst(versionString, target == 'ios' ? iosContent : macosContent);
podspecContent = podspecContent.replaceFirst(
versionString,
target == 'ios' ? iosContent : macosContent,
);
podspec.writeAsStringSync(podspecContent, flush: true);
}
@@ -488,22 +512,23 @@ s.dependency 'AppAuth', '1.6.0'
Directory? localEngine,
}) async {
await inDirectory(Directory(rootPath), () async {
final String buildOutput = await evalFlutter('build', options: <String>[
target,
'-v',
if (target == 'ios')
'--no-codesign',
if (configOnly)
'--config-only',
if (localEngine != null)
final String buildOutput = await evalFlutter(
'build',
options: <String>[
target,
'-v',
if (target == 'ios') '--no-codesign',
if (configOnly) '--config-only',
if (localEngine != null)
// The engine directory is of the form <fake-source-path>/out/<fakename>,
// which has to be broken up into the component flags.
...<String>[
'--local-engine-src-path=${localEngine.parent.parent.path}',
'--local-engine=${path.basename(localEngine.path)}',
'--local-engine-host=${path.basename(localEngine.path)}',
]
]);
],
],
);
if (target == 'ios' || target == 'macos') {
// This warning is confusing and shouldn't be emitted. Plugins often support lower versions than the
@@ -512,25 +537,33 @@ s.dependency 'AppAuth', '1.6.0'
// but the range of supported deployment target versions is 9.0 to 14.0.99.
//
// (or "The macOS deployment target 'MACOSX_DEPLOYMENT_TARGET'"...)
if (buildOutput.contains('is set to 10.0, but the range of supported deployment target versions') ||
buildOutput.contains('is set to 10.8, but the range of supported deployment target versions')) {
if (buildOutput.contains(
'is set to 10.0, but the range of supported deployment target versions',
) ||
buildOutput.contains(
'is set to 10.8, but the range of supported deployment target versions',
)) {
throw TaskResult.failure('Minimum plugin version warning present');
}
if (validateNativeBuildProject) {
final File generatedSwiftManifest = File(path.join(
rootPath,
target,
'Flutter',
'ephemeral',
'Packages',
'FlutterGeneratedPluginSwiftPackage',
'Package.swift'
));
final File generatedSwiftManifest = File(
path.join(
rootPath,
target,
'Flutter',
'ephemeral',
'Packages',
'FlutterGeneratedPluginSwiftPackage',
'Package.swift',
),
);
final bool swiftPackageManagerEnabled = generatedSwiftManifest.existsSync();
if (!swiftPackageManagerEnabled) {
final File podsProject = File(path.join(rootPath, target, 'Pods', 'Pods.xcodeproj', 'project.pbxproj'));
final File podsProject = File(
path.join(rootPath, target, 'Pods', 'Pods.xcodeproj', 'project.pbxproj'),
);
if (!podsProject.existsSync()) {
throw TaskResult.failure('Xcode Pods project file missing at ${podsProject.path}');
}
@@ -541,31 +574,45 @@ s.dependency 'AppAuth', '1.6.0'
// The plugintest plugin target should not have IPHONEOS_DEPLOYMENT_TARGET set since it has been lowered
// in _reduceDarwinPluginMinimumVersion to 10, which is below the target version of 11.
if (podsProjectContent.contains('IPHONEOS_DEPLOYMENT_TARGET = 10')) {
throw TaskResult.failure('Plugin build setting IPHONEOS_DEPLOYMENT_TARGET not removed');
throw TaskResult.failure(
'Plugin build setting IPHONEOS_DEPLOYMENT_TARGET not removed',
);
}
// Transitive dependency AppAuth targeting too-low 8.0 was not fixed.
if (podsProjectContent.contains('IPHONEOS_DEPLOYMENT_TARGET = 8')) {
throw TaskResult.failure('Transitive dependency build setting IPHONEOS_DEPLOYMENT_TARGET=8 not removed');
throw TaskResult.failure(
'Transitive dependency build setting IPHONEOS_DEPLOYMENT_TARGET=8 not removed',
);
}
if (!podsProjectContent.contains(r'"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "$(inherited) i386";')) {
if (!podsProjectContent.contains(
r'"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "$(inherited) i386";',
)) {
throw TaskResult.failure(r'EXCLUDED_ARCHS is not "$(inherited) i386"');
}
} else if (target == 'macos') {
// Same for macOS deployment target, but 10.8.
// The plugintest target should not have MACOSX_DEPLOYMENT_TARGET set.
if (podsProjectContent.contains('MACOSX_DEPLOYMENT_TARGET = 10.8')) {
throw TaskResult.failure('Plugin build setting MACOSX_DEPLOYMENT_TARGET not removed');
throw TaskResult.failure(
'Plugin build setting MACOSX_DEPLOYMENT_TARGET not removed',
);
}
// Transitive dependency AppAuth targeting too-low 10.9 was not fixed.
if (podsProjectContent.contains('MACOSX_DEPLOYMENT_TARGET = 10.9')) {
throw TaskResult.failure('Transitive dependency build setting MACOSX_DEPLOYMENT_TARGET=10.9 not removed');
throw TaskResult.failure(
'Transitive dependency build setting MACOSX_DEPLOYMENT_TARGET=10.9 not removed',
);
}
}
if (localEngine != null) {
final RegExp localEngineSearchPath = RegExp('FRAMEWORK_SEARCH_PATHS\\s*=[^;]*${localEngine.path}');
final RegExp localEngineSearchPath = RegExp(
'FRAMEWORK_SEARCH_PATHS\\s*=[^;]*${localEngine.path}',
);
if (!localEngineSearchPath.hasMatch(podsProjectContent)) {
throw TaskResult.failure('FRAMEWORK_SEARCH_PATHS does not contain the --local-engine path');
throw TaskResult.failure(
'FRAMEWORK_SEARCH_PATHS does not contain the --local-engine path',
);
}
}
}
@@ -578,8 +625,7 @@ s.dependency 'AppAuth', '1.6.0'
if (Platform.isWindows) {
// A running Gradle daemon might prevent us from deleting the project
// folder on Windows.
final String wrapperPath =
path.absolute(path.join(rootPath, 'android', 'gradlew.bat'));
final String wrapperPath = path.absolute(path.join(rootPath, 'android', 'gradlew.bat'));
if (File(wrapperPath).existsSync()) {
await exec(wrapperPath, <String>['--stop'], canFail: true);
}

View File

@@ -71,10 +71,8 @@ TaskFunction createWindowsRunReleaseTest() {
}
class AndroidRunOutputTest extends RunOutputTask {
AndroidRunOutputTest({required super.release}) : super(
'${flutterDirectory.path}/dev/integration_tests/ui',
'lib/main.dart',
);
AndroidRunOutputTest({required super.release})
: super('${flutterDirectory.path}/dev/integration_tests/ui', 'lib/main.dart');
@override
Future<void> prepare(String deviceId) async {
@@ -85,22 +83,20 @@ class AndroidRunOutputTest extends RunOutputTask {
'install',
// TODO(andrewkolos): consider removing -v after
// https://github.com/flutter/flutter/issues/153367 is troubleshot.
options: <String>['--suppress-analytics', '--uninstall-only', '-d', deviceId, '-v'],
options: <String>['--suppress-analytics', '--uninstall-only', '-d', deviceId, '-v'],
isBot: false,
);
uninstall.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
uninstall.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(
(String line) {
print('uninstall:stdout: $line');
});
uninstall.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
},
);
uninstall.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen(
(String line) {
print('uninstall:stderr: $line');
stderr.add(line);
});
},
);
if (await uninstall.exitCode != 0) {
throw 'flutter install --uninstall-only failed.';
}
@@ -122,8 +118,9 @@ class AndroidRunOutputTest extends RunOutputTask {
_findNextMatcherInList(
stdout,
(String line) => line.startsWith('Launching lib/main.dart on ') &&
line.endsWith(' in ${release ? 'release' : 'debug'} mode...'),
(String line) =>
line.startsWith('Launching lib/main.dart on ') &&
line.endsWith(' in ${release ? 'release' : 'debug'} mode...'),
'Launching lib/main.dart on',
);
@@ -136,8 +133,9 @@ class AndroidRunOutputTest extends RunOutputTask {
// Size information is only included in release builds.
_findNextMatcherInList(
stdout,
(String line) => line.contains('Built build/app/outputs/flutter-apk/$apk') &&
(!release || line.contains('MB)')),
(String line) =>
line.contains('Built build/app/outputs/flutter-apk/$apk') &&
(!release || line.contains('MB)')),
'Built build/app/outputs/flutter-apk/$apk',
);
@@ -167,12 +165,11 @@ class WindowsRunOutputTest extends DesktopRunOutputTest {
WindowsRunOutputTest(
super.testDirectory,
super.testTarget, {
required super.release,
super.allowStderr = false,
}
);
required super.release,
super.allowStderr = false,
});
final String arch = Abi.current() == Abi.windowsX64 ? 'x64': 'arm64';
final String arch = Abi.current() == Abi.windowsX64 ? 'x64' : 'arm64';
static final RegExp _buildOutput = RegExp(
r'Building Windows application\.\.\.\s*\d+(\.\d+)?(ms|s)',
@@ -184,24 +181,16 @@ class WindowsRunOutputTest extends DesktopRunOutputTest {
@override
void verifyBuildOutput(List<String> stdout) {
_findNextMatcherInList(
stdout,
_buildOutput.hasMatch,
'Building Windows application...',
);
_findNextMatcherInList(stdout, _buildOutput.hasMatch, 'Building Windows application...');
final String buildMode = release ? 'Release' : 'Debug';
_findNextMatcherInList(
stdout,
(String line) {
if (!_builtOutput.hasMatch(line) || !line.contains(buildMode)) {
return false;
}
_findNextMatcherInList(stdout, (String line) {
if (!_builtOutput.hasMatch(line) || !line.contains(buildMode)) {
return false;
}
return true;
},
'√ Built build\\windows\\$arch\\runner\\$buildMode\\ui.exe',
);
return true;
}, '√ Built build\\windows\\$arch\\runner\\$buildMode\\ui.exe');
}
}
@@ -209,10 +198,9 @@ class DesktopRunOutputTest extends RunOutputTask {
DesktopRunOutputTest(
super.testDirectory,
super.testTarget, {
required super.release,
this.allowStderr = false,
}
);
required super.release,
this.allowStderr = false,
});
/// Whether `flutter run` is expected to produce output on stderr.
final bool allowStderr;
@@ -224,8 +212,9 @@ class DesktopRunOutputTest extends RunOutputTask {
TaskResult verify(List<String> stdout, List<String> stderr) {
_findNextMatcherInList(
stdout,
(String line) => line.startsWith('Launching $testTarget on ') &&
line.endsWith(' in ${release ? 'release' : 'debug'} mode...'),
(String line) =>
line.startsWith('Launching $testTarget on ') &&
line.endsWith(' in ${release ? 'release' : 'debug'} mode...'),
'Launching $testTarget on',
);
@@ -252,21 +241,16 @@ class DesktopRunOutputTest extends RunOutputTask {
/// Test that the output of `flutter run` is expected.
abstract class RunOutputTask {
RunOutputTask(
this.testDirectory,
this.testTarget, {
required this.release,
}
);
RunOutputTask(this.testDirectory, this.testTarget, {required this.release});
static final RegExp _engineLogRegex = RegExp(
r'\[(VERBOSE|INFO|WARNING|ERROR|FATAL):.+\(\d+\)\]',
);
static final RegExp _engineLogRegex = RegExp(r'\[(VERBOSE|INFO|WARNING|ERROR|FATAL):.+\(\d+\)\]');
/// The directory where the app under test is defined.
final String testDirectory;
/// The main entry-point file of the application, as run on the device.
final String testTarget;
/// Whether to run the app in release mode.
final bool release;
@@ -282,40 +266,33 @@ abstract class RunOutputTask {
await prepare(deviceId);
final List<String> options = <String>[
testTarget,
'-d',
deviceId,
if (release) '--release',
];
final List<String> options = <String>[testTarget, '-d', deviceId, if (release) '--release'];
final Process run = await startFlutter(
'run',
options: options,
isBot: false,
);
final Process run = await startFlutter('run', options: options, isBot: false);
int? runExitCode;
run.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('run:stdout: $line');
stdout.add(line);
if (line.contains('Quit (terminate the application on the device).')) {
ready.complete();
}
});
final Stream<String> runStderr = run.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.asBroadcastStream();
run.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen((
String line,
) {
print('run:stdout: $line');
stdout.add(line);
if (line.contains('Quit (terminate the application on the device).')) {
ready.complete();
}
});
final Stream<String> runStderr =
run.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.asBroadcastStream();
runStderr.listen((String line) => print('run:stderr: $line'));
runStderr
.skipWhile(isExpectedStderr)
.listen((String line) => stderr.add(line));
unawaited(run.exitCode.then<void>((int exitCode) { runExitCode = exitCode; }));
await Future.any<dynamic>(<Future<dynamic>>[ ready.future, run.exitCode ]);
runStderr.skipWhile(isExpectedStderr).listen((String line) => stderr.add(line));
unawaited(
run.exitCode.then<void>((int exitCode) {
runExitCode = exitCode;
}),
);
await Future.any<dynamic>(<Future<dynamic>>[ready.future, run.exitCode]);
if (runExitCode != null) {
throw 'Failed to run test app; runner unexpected exited, with exit code $runExitCode.';
}
@@ -327,9 +304,7 @@ abstract class RunOutputTask {
throw 'flutter run ${release ? '--release' : ''} had unexpected output on standard error.';
}
final List<String> engineLogs = List<String>.from(
stdout.where(_engineLogRegex.hasMatch),
);
final List<String> engineLogs = List<String>.from(stdout.where(_engineLogRegex.hasMatch));
if (engineLogs.isNotEmpty) {
throw 'flutter run had unexpected Flutter engine logs $engineLogs';
}
@@ -345,14 +320,15 @@ abstract class RunOutputTask {
bool isExpectedStderr(String line) => false;
/// Verify the output of `flutter run`.
TaskResult verify(List<String> stdout, List<String> stderr) => throw UnimplementedError('verify is not implemented');
TaskResult verify(List<String> stdout, List<String> stderr) =>
throw UnimplementedError('verify is not implemented');
/// Helper that verifies a line in [list] matches [matcher].
/// The [list] is updated to contain the lines remaining after the match.
void _findNextMatcherInList(
List<String> list,
bool Function(String testLine) matcher,
String errorMessageExpectedLine
String errorMessageExpectedLine,
) {
final List<String> copyOfListForErrorMessage = List<String>.from(list);

View File

@@ -20,32 +20,34 @@ import '../framework/utils.dart';
const int benchmarkServerPort = 9999;
const int chromeDebugPort = 10000;
typedef WebBenchmarkOptions = ({
bool useWasm,
bool forceSingleThreadedSkwasm,
});
typedef WebBenchmarkOptions = ({bool useWasm, bool forceSingleThreadedSkwasm});
Future<TaskResult> runWebBenchmark(WebBenchmarkOptions benchmarkOptions) async {
// Reduce logging level. Otherwise, package:webkit_inspection_protocol is way too spammy.
Logger.root.level = Level.INFO;
final String macrobenchmarksDirectory = path.join(flutterDirectory.path, 'dev', 'benchmarks', 'macrobenchmarks');
final String macrobenchmarksDirectory = path.join(
flutterDirectory.path,
'dev',
'benchmarks',
'macrobenchmarks',
);
return inDirectory(macrobenchmarksDirectory, () async {
await flutter('clean');
await evalFlutter('build', options: <String>[
'web',
'--no-tree-shake-icons', // local engine builds are frequently out of sync with the Dart Kernel version
if (benchmarkOptions.useWasm) ...<String>[
'-O4',
'--wasm',
'--no-strip-wasm',
await evalFlutter(
'build',
options: <String>[
'web',
'--no-tree-shake-icons', // local engine builds are frequently out of sync with the Dart Kernel version
if (benchmarkOptions.useWasm) ...<String>['-O4', '--wasm', '--no-strip-wasm'],
'--dart-define=FLUTTER_WEB_ENABLE_PROFILING=true',
'--profile',
'--no-web-resources-cdn',
'-t',
'lib/web_benchmarks.dart',
],
'--dart-define=FLUTTER_WEB_ENABLE_PROFILING=true',
'--profile',
'--no-web-resources-cdn',
'-t',
'lib/web_benchmarks.dart',
]);
final Completer<List<Map<String, dynamic>>> profileData = Completer<List<Map<String, dynamic>>>();
);
final Completer<List<Map<String, dynamic>>> profileData =
Completer<List<Map<String, dynamic>>>();
final List<Map<String, dynamic>> collectedProfiles = <Map<String, dynamic>>[];
List<String>? benchmarks;
late Iterator<String> benchmarkIterator;
@@ -58,82 +60,91 @@ Future<TaskResult> runWebBenchmark(WebBenchmarkOptions benchmarkOptions) async {
late io.HttpServer server;
Cascade cascade = Cascade();
List<Map<String, dynamic>>? latestPerformanceTrace;
cascade = cascade.add((Request request) async {
try {
chrome ??= await whenChromeIsReady;
if (request.requestedUri.path.endsWith('/profile-data')) {
final Map<String, dynamic> profile = json.decode(await request.readAsString()) as Map<String, dynamic>;
final String benchmarkName = profile['name'] as String;
if (benchmarkName != benchmarkIterator.current) {
profileData.completeError(Exception(
'Browser returned benchmark results from a wrong benchmark.\n'
'Requested to run benchmark ${benchmarkIterator.current}, but '
'got results for $benchmarkName.',
));
unawaited(server.close());
}
cascade = cascade
.add((Request request) async {
try {
chrome ??= await whenChromeIsReady;
if (request.requestedUri.path.endsWith('/profile-data')) {
final Map<String, dynamic> profile =
json.decode(await request.readAsString()) as Map<String, dynamic>;
final String benchmarkName = profile['name'] as String;
if (benchmarkName != benchmarkIterator.current) {
profileData.completeError(
Exception(
'Browser returned benchmark results from a wrong benchmark.\n'
'Requested to run benchmark ${benchmarkIterator.current}, but '
'got results for $benchmarkName.',
),
);
unawaited(server.close());
}
// Trace data is null when the benchmark is not frame-based, such as RawRecorder.
if (latestPerformanceTrace != null) {
final BlinkTraceSummary traceSummary = BlinkTraceSummary.fromJson(latestPerformanceTrace!)!;
profile['totalUiFrame.average'] = traceSummary.averageTotalUIFrameTime.inMicroseconds;
profile['scoreKeys'] ??= <dynamic>[]; // using dynamic for consistency with JSON
(profile['scoreKeys'] as List<dynamic>).add('totalUiFrame.average');
latestPerformanceTrace = null;
// Trace data is null when the benchmark is not frame-based, such as RawRecorder.
if (latestPerformanceTrace != null) {
final BlinkTraceSummary traceSummary =
BlinkTraceSummary.fromJson(latestPerformanceTrace!)!;
profile['totalUiFrame.average'] =
traceSummary.averageTotalUIFrameTime.inMicroseconds;
profile['scoreKeys'] ??= <dynamic>[]; // using dynamic for consistency with JSON
(profile['scoreKeys'] as List<dynamic>).add('totalUiFrame.average');
latestPerformanceTrace = null;
}
collectedProfiles.add(profile);
return Response.ok('Profile received');
} else if (request.requestedUri.path.endsWith('/start-performance-tracing')) {
latestPerformanceTrace = null;
await chrome!.beginRecordingPerformance(
request.requestedUri.queryParameters['label']!,
);
return Response.ok('Started performance tracing');
} else if (request.requestedUri.path.endsWith('/stop-performance-tracing')) {
latestPerformanceTrace = await chrome!.endRecordingPerformance();
return Response.ok('Stopped performance tracing');
} else if (request.requestedUri.path.endsWith('/on-error')) {
final Map<String, dynamic> errorDetails =
json.decode(await request.readAsString()) as Map<String, dynamic>;
unawaited(server.close());
// Keep the stack trace as a string. It's thrown in the browser, not this Dart VM.
profileData.completeError('${errorDetails['error']}\n${errorDetails['stackTrace']}');
return Response.ok('');
} else if (request.requestedUri.path.endsWith('/next-benchmark')) {
if (benchmarks == null) {
benchmarks =
(json.decode(await request.readAsString()) as List<dynamic>).cast<String>();
benchmarkIterator = benchmarks!.iterator;
}
if (benchmarkIterator.moveNext()) {
final String nextBenchmark = benchmarkIterator.current;
print('Launching benchmark "$nextBenchmark"');
return Response.ok(nextBenchmark);
} else {
profileData.complete(collectedProfiles);
return Response.notFound('Finished running benchmarks.');
}
} else if (request.requestedUri.path.endsWith('/print-to-console')) {
// A passthrough used by
// `dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart`
// to print information.
final String message = await request.readAsString();
print('[APP] $message');
return Response.ok('Reported.');
} else {
return Response.notFound('This request is not handled by the profile-data handler.');
}
} catch (error, stackTrace) {
profileData.completeError(error, stackTrace);
return Response.internalServerError(body: '$error');
}
collectedProfiles.add(profile);
return Response.ok('Profile received');
} else if (request.requestedUri.path.endsWith('/start-performance-tracing')) {
latestPerformanceTrace = null;
await chrome!.beginRecordingPerformance(request.requestedUri.queryParameters['label']!);
return Response.ok('Started performance tracing');
} else if (request.requestedUri.path.endsWith('/stop-performance-tracing')) {
latestPerformanceTrace = await chrome!.endRecordingPerformance();
return Response.ok('Stopped performance tracing');
} else if (request.requestedUri.path.endsWith('/on-error')) {
final Map<String, dynamic> errorDetails = json.decode(await request.readAsString()) as Map<String, dynamic>;
unawaited(server.close());
// Keep the stack trace as a string. It's thrown in the browser, not this Dart VM.
profileData.completeError('${errorDetails['error']}\n${errorDetails['stackTrace']}');
return Response.ok('');
} else if (request.requestedUri.path.endsWith('/next-benchmark')) {
if (benchmarks == null) {
benchmarks = (json.decode(await request.readAsString()) as List<dynamic>).cast<String>();
benchmarkIterator = benchmarks!.iterator;
}
if (benchmarkIterator.moveNext()) {
final String nextBenchmark = benchmarkIterator.current;
print('Launching benchmark "$nextBenchmark"');
return Response.ok(nextBenchmark);
} else {
profileData.complete(collectedProfiles);
return Response.notFound('Finished running benchmarks.');
}
} else if (request.requestedUri.path.endsWith('/print-to-console')) {
// A passthrough used by
// `dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart`
// to print information.
final String message = await request.readAsString();
print('[APP] $message');
return Response.ok('Reported.');
} else {
return Response.notFound(
'This request is not handled by the profile-data handler.');
}
} catch (error, stackTrace) {
profileData.completeError(error, stackTrace);
return Response.internalServerError(body: '$error');
}
}).add(createBuildDirectoryHandler(
path.join(macrobenchmarksDirectory, 'build', 'web'),
));
})
.add(createBuildDirectoryHandler(path.join(macrobenchmarksDirectory, 'build', 'web')));
server = await io.HttpServer.bind('localhost', benchmarkServerPort);
try {
shelf_io.serveRequests(server, cascade.handler);
final String dartToolDirectory = path.join('$macrobenchmarksDirectory/.dart_tool');
final String userDataDir = io.Directory(dartToolDirectory).createTempSync('flutter_chrome_user_data.').path;
final String userDataDir =
io.Directory(dartToolDirectory).createTempSync('flutter_chrome_user_data.').path;
// TODO(yjbanov): temporarily disables headful Chrome until we get
// devicelab hardware that is able to run it. Our current
@@ -216,10 +227,12 @@ Handler createBuildDirectoryHandler(String buildDirectoryPath) {
// crossOriginIsolated. This will make sure that we get high-resolution
// timers for our benchmark measurements.
if (mimeType == 'text/html' || mimeType == 'text/javascript') {
return response.change(headers: <String, String>{
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
});
return response.change(
headers: <String, String>{
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
);
} else {
return response;
}

View File

@@ -12,12 +12,16 @@ import '../framework/framework.dart';
import '../framework/task_result.dart';
import '../framework/utils.dart';
final Directory _editedFlutterGalleryDir = dir(path.join(Directory.systemTemp.path, 'edited_flutter_gallery'));
final Directory flutterGalleryDir = dir(path.join(flutterDirectory.path, 'dev/integration_tests/flutter_gallery'));
final Directory _editedFlutterGalleryDir = dir(
path.join(Directory.systemTemp.path, 'edited_flutter_gallery'),
);
final Directory flutterGalleryDir = dir(
path.join(flutterDirectory.path, 'dev/integration_tests/flutter_gallery'),
);
const String kInitialStartupTime = 'InitialStartupTime';
const String kFirstRestartTime = 'FistRestartTime';
const String kFirstRecompileTime = 'FirstRecompileTime';
const String kFirstRecompileTime = 'FirstRecompileTime';
const String kSecondStartupTime = 'SecondStartupTime';
const String kSecondRestartTime = 'SecondRestartTime';
@@ -29,12 +33,16 @@ abstract class WebDevice {
TaskFunction createWebDevModeTest(String webDevice, bool enableIncrementalCompiler) {
return () async {
final List<String> options = <String>[
'--hot', '-d', webDevice, '--verbose', '--resident', '--target=lib/main.dart',
'--hot',
'-d',
webDevice,
'--verbose',
'--resident',
'--target=lib/main.dart',
];
int hotRestartCount = 0;
final String expectedMessage = webDevice == WebDevice.webServer
? 'Recompile complete'
: 'Reloaded application';
final String expectedMessage =
webDevice == WebDevice.webServer ? 'Recompile complete' : 'Reloaded application';
final Map<String, int> measurements = <String, int>{};
await inDirectory<void>(flutterDirectory, () async {
rmTree(_editedFlutterGalleryDir);
@@ -42,14 +50,8 @@ TaskFunction createWebDevModeTest(String webDevice, bool enableIncrementalCompil
recursiveCopy(flutterGalleryDir, _editedFlutterGalleryDir);
await inDirectory<void>(_editedFlutterGalleryDir, () async {
{
await flutter(
'packages',
options: <String>['get'],
);
final Process process = await startFlutter(
'run',
options: options,
);
await flutter('packages', options: <String>['get']);
final Process process = await startFlutter('run', options: options);
final Completer<void> stdoutDone = Completer<void>();
final Completer<void> stderrDone = Completer<void>();
@@ -58,124 +60,126 @@ TaskFunction createWebDevModeTest(String webDevice, bool enableIncrementalCompil
process.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
// non-dwds builds do not know when the browser is loaded so keep trying
// until this succeeds.
if (line.contains('Ignoring terminal input')) {
Future<void>.delayed(const Duration(seconds: 1)).then((void _) {
process.stdin.write(restarted ? 'q' : 'r');
});
return;
}
if (line.contains('To hot restart')) {
// measure clean start-up time.
sw.stop();
measurements[kInitialStartupTime] = sw.elapsedMilliseconds;
sw
..reset()
..start();
process.stdin.write('r');
return;
}
if (line.contains(expectedMessage)) {
if (hotRestartCount == 0) {
measurements[kFirstRestartTime] = sw.elapsedMilliseconds;
// Update the file and reload again.
final File appDartSource = file(path.join(
_editedFlutterGalleryDir.path, 'lib/gallery/app.dart',
));
appDartSource.writeAsStringSync(
appDartSource.readAsStringSync().replaceFirst(
"'Flutter Gallery'", "'Updated Flutter Gallery'",
)
);
sw
..reset()
..start();
process.stdin.writeln('r');
++hotRestartCount;
} else {
restarted = true;
measurements[kFirstRecompileTime] = sw.elapsedMilliseconds;
// Quit after second hot restart.
process.stdin.writeln('q');
}
}
print('stdout: $line');
}, onDone: () {
stdoutDone.complete();
});
.listen(
(String line) {
// non-dwds builds do not know when the browser is loaded so keep trying
// until this succeeds.
if (line.contains('Ignoring terminal input')) {
Future<void>.delayed(const Duration(seconds: 1)).then((void _) {
process.stdin.write(restarted ? 'q' : 'r');
});
return;
}
if (line.contains('To hot restart')) {
// measure clean start-up time.
sw.stop();
measurements[kInitialStartupTime] = sw.elapsedMilliseconds;
sw
..reset()
..start();
process.stdin.write('r');
return;
}
if (line.contains(expectedMessage)) {
if (hotRestartCount == 0) {
measurements[kFirstRestartTime] = sw.elapsedMilliseconds;
// Update the file and reload again.
final File appDartSource = file(
path.join(_editedFlutterGalleryDir.path, 'lib/gallery/app.dart'),
);
appDartSource.writeAsStringSync(
appDartSource.readAsStringSync().replaceFirst(
"'Flutter Gallery'",
"'Updated Flutter Gallery'",
),
);
sw
..reset()
..start();
process.stdin.writeln('r');
++hotRestartCount;
} else {
restarted = true;
measurements[kFirstRecompileTime] = sw.elapsedMilliseconds;
// Quit after second hot restart.
process.stdin.writeln('q');
}
}
print('stdout: $line');
},
onDone: () {
stdoutDone.complete();
},
);
process.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('stderr: $line');
}, onDone: () {
stderrDone.complete();
});
.listen(
(String line) {
print('stderr: $line');
},
onDone: () {
stderrDone.complete();
},
);
await Future.wait<void>(<Future<void>>[
stdoutDone.future,
stderrDone.future,
]);
await Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]);
await process.exitCode;
}
// Start `flutter run` again to make sure it loads from the previous
// state. dev compilers loads up from previously compiled JavaScript.
{
final Stopwatch sw = Stopwatch()..start();
final Process process = await startFlutter(
'run',
options: options,
);
final Process process = await startFlutter('run', options: options);
final Completer<void> stdoutDone = Completer<void>();
final Completer<void> stderrDone = Completer<void>();
bool restarted = false;
process.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
// non-dwds builds do not know when the browser is loaded so keep trying
// until this succeeds.
if (line.contains('Ignoring terminal input')) {
Future<void>.delayed(const Duration(seconds: 1)).then((void _) {
process.stdin.write(restarted ? 'q' : 'r');
});
return;
}
if (line.contains('To hot restart')) {
measurements[kSecondStartupTime] = sw.elapsedMilliseconds;
sw
..reset()
..start();
process.stdin.write('r');
return;
}
if (line.contains(expectedMessage)) {
restarted = true;
measurements[kSecondRestartTime] = sw.elapsedMilliseconds;
process.stdin.writeln('q');
}
print('stdout: $line');
}, onDone: () {
stdoutDone.complete();
});
.listen(
(String line) {
// non-dwds builds do not know when the browser is loaded so keep trying
// until this succeeds.
if (line.contains('Ignoring terminal input')) {
Future<void>.delayed(const Duration(seconds: 1)).then((void _) {
process.stdin.write(restarted ? 'q' : 'r');
});
return;
}
if (line.contains('To hot restart')) {
measurements[kSecondStartupTime] = sw.elapsedMilliseconds;
sw
..reset()
..start();
process.stdin.write('r');
return;
}
if (line.contains(expectedMessage)) {
restarted = true;
measurements[kSecondRestartTime] = sw.elapsedMilliseconds;
process.stdin.writeln('q');
}
print('stdout: $line');
},
onDone: () {
stdoutDone.complete();
},
);
process.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('stderr: $line');
}, onDone: () {
stderrDone.complete();
});
.listen(
(String line) {
print('stderr: $line');
},
onDone: () {
stderrDone.complete();
},
);
await Future.wait<void>(<Future<void>>[
stdoutDone.future,
stderrDone.future,
]);
await Future.wait<void>(<Future<void>>[stdoutDone.future, stderrDone.future]);
await process.exitCode;
}
});
@@ -183,12 +187,15 @@ TaskFunction createWebDevModeTest(String webDevice, bool enableIncrementalCompil
if (hotRestartCount != 1) {
return TaskResult.failure(null);
}
return TaskResult.success(measurements, benchmarkScoreKeys: <String>[
kInitialStartupTime,
kFirstRestartTime,
kFirstRecompileTime,
kSecondStartupTime,
kSecondRestartTime,
]);
return TaskResult.success(
measurements,
benchmarkScoreKeys: <String>[
kInitialStartupTime,
kFirstRestartTime,
kFirstRecompileTime,
kSecondStartupTime,
kSecondRestartTime,
],
);
};
}