forked from firka/flutter
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:
committed by
GitHub
parent
8e0993eda8
commit
5491c8c146
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user