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 message) {
if (debugLogging) print('${DateTime.now()}: $message');
}
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 138. See
// https://github.com/flutter/devtools/issues/9357.
'--web-browser-flag=--headless=new',
'--web-browser-flag=--no-sandbox',
],
for (final arg in dartDefineArgs) '--dart-define=$arg',
];
debugLog('> flutter ${flutterDriveArgs.join(' ')}');
final process = await Process.start(
Platform.isWindows ? 'flutter.bat' : '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;
await process.exitCode.timeout(const Duration(minutes: 8), onTimeout: () {
testTimedOut = true;
// TODO(srawlins): Refactor the retry situation to catch a
// TimeoutException, and not recursively call `runTest`.
return -1;
});
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.',
);
attemptNumber++;
debugLog('running the test (attempt $attemptNumber)');
await runTest(attemptNumber: attemptNumber);
}
}
if (exceptionBuffer.isNotEmpty) {
throw Exception(exceptionBuffer.toString());
}
}
}
debugLog('running the test (attempt 1)');
await runTest(attemptNumber: 0);
}