mirror of
https://github.com/flutter/packages.git
synced 2025-06-18 04:33:52 +08:00
[flutter_plugin_tools] Auto-retry failed FTL tests (#4610)
Currently the flake situation for Firebase Test Lab tests is very bad, and the task running those tests are some of the slowest tasks in the CI. Re-running failed tests is slow, manual work that is signficantly affecting productivity. There are plans to actually address the flake in the short-to-medium term, but in the immediate term this will improve the CI situation, as well as reducing the drag on engineering time that could be spent on the root causes. Part of https://github.com/flutter/flutter/issues/95063
This commit is contained in:
@ -13,6 +13,8 @@
|
|||||||
- Fix `federation-safety-check` handling of plugin deletion, and of top-level
|
- Fix `federation-safety-check` handling of plugin deletion, and of top-level
|
||||||
files in unfederated plugins whose names match federated plugin heuristics
|
files in unfederated plugins whose names match federated plugin heuristics
|
||||||
(e.g., `packages/foo/foo_android.iml`).
|
(e.g., `packages/foo/foo_android.iml`).
|
||||||
|
- Add an auto-retry for failed Firebase Test Lab tests as a short-term patch
|
||||||
|
for flake issues.
|
||||||
|
|
||||||
## 0.7.3
|
## 0.7.3
|
||||||
|
|
||||||
|
@ -176,30 +176,22 @@ class FirebaseTestLabCommand extends PackageLoopingCommand {
|
|||||||
final String testRunId = getStringArg('test-run-id');
|
final String testRunId = getStringArg('test-run-id');
|
||||||
final String resultsDir =
|
final String resultsDir =
|
||||||
'plugins_android_test/${package.displayName}/$buildId/$testRunId/${resultsCounter++}/';
|
'plugins_android_test/${package.displayName}/$buildId/$testRunId/${resultsCounter++}/';
|
||||||
final List<String> args = <String>[
|
|
||||||
'firebase',
|
|
||||||
'test',
|
|
||||||
'android',
|
|
||||||
'run',
|
|
||||||
'--type',
|
|
||||||
'instrumentation',
|
|
||||||
'--app',
|
|
||||||
'build/app/outputs/apk/debug/app-debug.apk',
|
|
||||||
'--test',
|
|
||||||
'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk',
|
|
||||||
'--timeout',
|
|
||||||
'7m',
|
|
||||||
'--results-bucket=${getStringArg('results-bucket')}',
|
|
||||||
'--results-dir=$resultsDir',
|
|
||||||
];
|
|
||||||
for (final String device in getStringListArg('device')) {
|
|
||||||
args.addAll(<String>['--device', device]);
|
|
||||||
}
|
|
||||||
final int exitCode = await processRunner.runAndStream('gcloud', args,
|
|
||||||
workingDir: example.directory);
|
|
||||||
|
|
||||||
if (exitCode != 0) {
|
// Automatically retry failures; there is significant flake with these
|
||||||
printError('Test failure for $testName');
|
// tests whose cause isn't yet understood, and having to re-run the
|
||||||
|
// entire shard for a flake in any one test is extremely slow. This should
|
||||||
|
// be removed once the root cause of the flake is understood.
|
||||||
|
// See https://github.com/flutter/flutter/issues/95063
|
||||||
|
const int maxRetries = 2;
|
||||||
|
bool passing = false;
|
||||||
|
for (int i = 1; i <= maxRetries && !passing; ++i) {
|
||||||
|
if (i > 1) {
|
||||||
|
logWarning('$testName failed on attempt ${i - 1}. Retrying...');
|
||||||
|
}
|
||||||
|
passing = await _runFirebaseTest(example, test, resultsDir: resultsDir);
|
||||||
|
}
|
||||||
|
if (!passing) {
|
||||||
|
printError('Test failure for $testName after $maxRetries attempts');
|
||||||
errors.add('$testName failed tests');
|
errors.add('$testName failed tests');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -238,6 +230,42 @@ class FirebaseTestLabCommand extends PackageLoopingCommand {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs [test] from [example] as a Firebase Test Lab test, returning true if
|
||||||
|
/// the test passed.
|
||||||
|
///
|
||||||
|
/// [resultsDir] should be a unique-to-the-test-run directory to store the
|
||||||
|
/// results on the server.
|
||||||
|
Future<bool> _runFirebaseTest(
|
||||||
|
RepositoryPackage example,
|
||||||
|
File test, {
|
||||||
|
required String resultsDir,
|
||||||
|
}) async {
|
||||||
|
final List<String> args = <String>[
|
||||||
|
'firebase',
|
||||||
|
'test',
|
||||||
|
'android',
|
||||||
|
'run',
|
||||||
|
'--type',
|
||||||
|
'instrumentation',
|
||||||
|
'--app',
|
||||||
|
'build/app/outputs/apk/debug/app-debug.apk',
|
||||||
|
'--test',
|
||||||
|
'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk',
|
||||||
|
'--timeout',
|
||||||
|
'7m',
|
||||||
|
'--results-bucket=${getStringArg('results-bucket')}',
|
||||||
|
'--results-dir=$resultsDir',
|
||||||
|
for (final String device in getStringListArg('device')) ...<String>[
|
||||||
|
'--device',
|
||||||
|
device
|
||||||
|
],
|
||||||
|
];
|
||||||
|
final int exitCode = await processRunner.runAndStream('gcloud', args,
|
||||||
|
workingDir: example.directory);
|
||||||
|
|
||||||
|
return exitCode == 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// Builds [target] using Gradle in the given [project]. Assumes Gradle is
|
/// Builds [target] using Gradle in the given [project]. Assumes Gradle is
|
||||||
/// already configured.
|
/// already configured.
|
||||||
///
|
///
|
||||||
|
@ -271,7 +271,7 @@ public class MainActivityTest {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fails if a test fails', () async {
|
test('fails if a test fails twice', () async {
|
||||||
const String javaTestFileRelativePath =
|
const String javaTestFileRelativePath =
|
||||||
'example/android/app/src/androidTest/MainActivityTest.java';
|
'example/android/app/src/androidTest/MainActivityTest.java';
|
||||||
final Directory pluginDir =
|
final Directory pluginDir =
|
||||||
@ -287,6 +287,7 @@ public class MainActivityTest {
|
|||||||
MockProcess(), // auth
|
MockProcess(), // auth
|
||||||
MockProcess(), // config
|
MockProcess(), // config
|
||||||
MockProcess(exitCode: 1), // integration test #1
|
MockProcess(exitCode: 1), // integration test #1
|
||||||
|
MockProcess(exitCode: 1), // integration test #1 retry
|
||||||
MockProcess(), // integration test #2
|
MockProcess(), // integration test #2
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -315,6 +316,44 @@ public class MainActivityTest {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('passes with warning if a test fails once, then passes on retry',
|
||||||
|
() async {
|
||||||
|
const String javaTestFileRelativePath =
|
||||||
|
'example/android/app/src/androidTest/MainActivityTest.java';
|
||||||
|
final Directory pluginDir =
|
||||||
|
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
|
||||||
|
'example/integration_test/bar_test.dart',
|
||||||
|
'example/integration_test/foo_test.dart',
|
||||||
|
'example/android/gradlew',
|
||||||
|
javaTestFileRelativePath,
|
||||||
|
]);
|
||||||
|
_writeJavaTestFile(pluginDir, javaTestFileRelativePath);
|
||||||
|
|
||||||
|
processRunner.mockProcessesForExecutable['gcloud'] = <Process>[
|
||||||
|
MockProcess(), // auth
|
||||||
|
MockProcess(), // config
|
||||||
|
MockProcess(exitCode: 1), // integration test #1
|
||||||
|
MockProcess(), // integration test #1 retry
|
||||||
|
MockProcess(), // integration test #2
|
||||||
|
];
|
||||||
|
|
||||||
|
final List<String> output = await runCapturingPrint(runner, <String>[
|
||||||
|
'firebase-test-lab',
|
||||||
|
'--device',
|
||||||
|
'model=redfin,version=30',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
output,
|
||||||
|
containsAllInOrder(<Matcher>[
|
||||||
|
contains('Testing example/integration_test/bar_test.dart...'),
|
||||||
|
contains('bar_test.dart failed on attempt 1. Retrying...'),
|
||||||
|
contains('Testing example/integration_test/foo_test.dart...'),
|
||||||
|
contains('Ran for 1 package(s) (1 with warnings)'),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('fails for packages with no androidTest directory', () async {
|
test('fails for packages with no androidTest directory', () async {
|
||||||
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
|
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
|
||||||
'example/integration_test/foo_test.dart',
|
'example/integration_test/foo_test.dart',
|
||||||
|
Reference in New Issue
Block a user