run method
Implementation
Future<void> run(
String testTarget, {
required String testDriver,
bool headless = false,
List<String> dartDefineArgs = const <String>[],
bool debugLogging = false,
}) async {
void debugLog(String log) {
if (debugLogging) print(log);
}
Future<void> runTest({required int attemptNumber}) async {
debugLog('starting attempt #$attemptNumber for $testTarget');
debugLog('starting the flutter drive process');
final flutterDriveArgs = [
'drive',
// Debug outputs from the test will not show up in profile mode. Since
// we rely on debug outputs for detecting errors and exceptions from the
// test, we cannot run this these tests in profile mode until this issue
// is resolved. See https://github.com/flutter/flutter/issues/69070.
// '--profile',
'--driver=$testDriver',
'--target=$testTarget',
'-d',
headless ? 'web-server' : 'chrome',
// --disable-gpu speeds up tests that use ChromeDriver when run on
// GitHub Actions. See https://github.com/flutter/devtools/issues/8301.
'--web-browser-flag=--disable-gpu',
if (headless) ...[
// Flags to avoid breakage with chromedriver 128. See
// https://github.com/flutter/devtools/issues/8301.
'--web-browser-flag=--headless=old',
'--web-browser-flag=--disable-search-engine-choice-screen',
],
for (final arg in dartDefineArgs) '--dart-define=$arg',
];
debugLog('> flutter ${flutterDriveArgs.join(' ')}');
final process = await Process.start('flutter', flutterDriveArgs);
bool stdOutWriteInProgress = false;
bool stdErrWriteInProgress = false;
final exceptionBuffer = StringBuffer();
var testsPassed = false;
listenToProcessOutput(
process,
printTag: 'FlutterDriveProcess',
onStdout: (line) {
if (line.endsWith(_allTestsPassed)) {
testsPassed = true;
}
if (line.startsWith(_IntegrationTestResult.testResultPrefix)) {
final testResultJson = line.substring(line.indexOf('{'));
final testResultMap =
jsonDecode(testResultJson) as Map<String, Object?>;
final result = _IntegrationTestResult.fromJson(testResultMap);
if (!result.result) {
exceptionBuffer
..writeln('$result')
..writeln();
}
}
if (line.contains(_beginExceptionMarker)) {
stdOutWriteInProgress = true;
}
if (stdOutWriteInProgress) {
exceptionBuffer.writeln(line);
// Marks the end of the exception caught by flutter.
if (line.contains(_endExceptionMarker) &&
!line.contains(_beginExceptionMarker)) {
stdOutWriteInProgress = false;
exceptionBuffer.writeln();
}
}
},
onStderr: (line) {
if (line.contains(_errorMarker) ||
line.contains(_unhandledExceptionMarker)) {
stdErrWriteInProgress = true;
}
if (stdErrWriteInProgress) {
exceptionBuffer.writeln(line);
}
},
);
bool testTimedOut = false;
final timeout = Future.delayed(const Duration(minutes: 8)).then((_) {
testTimedOut = true;
});
await Future.any([
process.exitCode,
timeout,
]);
debugLog(
'shutting down processes because '
'${testTimedOut ? 'test timed out' : 'test finished'}',
);
debugLog('attempting to kill the flutter drive process');
process.kill();
debugLog('flutter drive process has exited');
// Ignore exception handling and retries if the tests passed. This is to
// avoid bugs with the test runner where the test can fail after the test
// has passed. See https://github.com/flutter/flutter/issues/129041.
if (!testsPassed) {
if (testTimedOut) {
if (attemptNumber >= _maxRetriesOnTimeout) {
throw Exception(
'Integration test timed out on try #$attemptNumber: $testTarget',
);
} else {
debugLog(
'Integration test timed out on try #$attemptNumber. Retrying '
'$testTarget now.',
);
await runTest(attemptNumber: ++attemptNumber);
}
}
if (exceptionBuffer.isNotEmpty) {
throw Exception(exceptionBuffer.toString());
}
}
}
await runTest(attemptNumber: 0);
}