Skip to content

Commit 4d96a3f

Browse files
authored
Rerun devicelab task from test runner (flutter#86394)
1 parent d056500 commit 4d96a3f

File tree

4 files changed

+138
-21
lines changed

4 files changed

+138
-21
lines changed

dev/devicelab/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ When a device in the lab is free, it will pickup tasks that need to be completed
2727

2828
1. If the task succeeds, the test runner reports the success and uploads its performance metrics to Flutter's infrastructure. Not
2929
all tasks record performance metrics.
30-
2. If the task fails, the test runner reports the failure to Flutter's infrastructure and no performance metrics are collected
30+
2. If task fails, an auto rerun happens. Whenever the last run succeeds, the task will be reported as a success. For this case,
31+
a flake will be flagged and populated to the test result.
32+
3. If the task fails in all reruns, the test runner reports the failure to Flutter's infrastructure and no performance metrics are collected
3133

3234
## Running tests locally
3335

dev/devicelab/lib/framework/cocoon.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ class Cocoon {
4747
/// Url used to send results to.
4848
static const String baseCocoonApiUrl = 'https://flutter-dashboard.appspot.com/api';
4949

50+
/// Threshold to auto retry a failed test.
51+
static const int retryNumber = 2;
52+
5053
/// Underlying [FileSystem] to use.
5154
final FileSystem fs;
5255

dev/devicelab/lib/framework/runner.dart

Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'dart:async';
66
import 'dart:convert';
7+
// import 'dart:core' as core;
78
import 'dart:io';
89

910
import 'package:flutter_devicelab/common.dart';
@@ -15,6 +16,16 @@ import 'devices.dart';
1516
import 'task_result.dart';
1617
import 'utils.dart';
1718

19+
/// Run a list of tasks.
20+
///
21+
/// For each task, an auto rerun will be triggered when task fails.
22+
///
23+
/// If the task succeeds the first time, it will be recorded as successful.
24+
///
25+
/// If the task fails first, but gets passed in the end, the
26+
/// test will be recorded as successful but with a flake flag.
27+
///
28+
/// If the task fails all reruns, it will be recorded as failed.
1829
Future<void> runTasks(
1930
List<String> taskNames, {
2031
bool exitOnFirstTestFailure = false,
@@ -26,33 +37,45 @@ Future<void> runTasks(
2637
String? luciBuilder,
2738
String? resultsPath,
2839
List<String>? taskArgs,
40+
@visibleForTesting Map<String, String>? isolateParams,
41+
@visibleForTesting Function(String) print = print,
42+
@visibleForTesting List<String>? logs,
2943
}) async {
3044
for (final String taskName in taskNames) {
31-
section('Running task "$taskName"');
32-
final TaskResult result = await runTask(
33-
taskName,
34-
deviceId: deviceId,
35-
localEngine: localEngine,
36-
localEngineSrcPath: localEngineSrcPath,
37-
silent: silent,
38-
taskArgs: taskArgs,
39-
);
40-
41-
print('Task result:');
42-
print(const JsonEncoder.withIndent(' ').convert(result));
43-
section('Finished task "$taskName"');
44-
45-
if (resultsPath != null) {
46-
final Cocoon cocoon = Cocoon();
47-
await cocoon.writeTaskResultToFile(
48-
builderName: luciBuilder,
49-
gitBranch: gitBranch,
50-
result: result,
45+
TaskResult result = TaskResult.success(null);
46+
int retry = 0;
47+
while (retry <= Cocoon.retryNumber) {
48+
result = await rerunTask(
49+
taskName,
50+
deviceId: deviceId,
51+
localEngine: localEngine,
52+
localEngineSrcPath: localEngineSrcPath,
53+
silent: silent,
54+
taskArgs: taskArgs,
5155
resultsPath: resultsPath,
56+
gitBranch: gitBranch,
57+
luciBuilder: luciBuilder,
58+
isolateParams: isolateParams,
5259
);
60+
61+
section('Flaky status for "$taskName"');
62+
if (!result.succeeded) {
63+
retry++;
64+
} else {
65+
if (retry > 0) {
66+
print('Total ${retry+1} executions: $retry failures and 1 success');
67+
print('flaky: true');
68+
} else {
69+
print('Total ${retry+1} executions: 1 success');
70+
print('flaky: false');
71+
}
72+
break;
73+
}
5374
}
5475

5576
if (!result.succeeded) {
77+
print('Total $retry executions: 0 success');
78+
print('flaky: false');
5679
exitCode = 1;
5780
if (exitOnFirstTestFailure) {
5881
return;
@@ -61,6 +84,48 @@ Future<void> runTasks(
6184
}
6285
}
6386

87+
/// A rerun wrapper for `runTask`.
88+
///
89+
/// This separates reruns in separate sections.
90+
Future<TaskResult> rerunTask(
91+
String taskName, {
92+
String? deviceId,
93+
String? localEngine,
94+
String? localEngineSrcPath,
95+
bool silent = false,
96+
List<String>? taskArgs,
97+
String? resultsPath,
98+
String? gitBranch,
99+
String? luciBuilder,
100+
@visibleForTesting Map<String, String>? isolateParams,
101+
}) async {
102+
section('Running task "$taskName"');
103+
final TaskResult result = await runTask(
104+
taskName,
105+
deviceId: deviceId,
106+
localEngine: localEngine,
107+
localEngineSrcPath: localEngineSrcPath,
108+
silent: silent,
109+
taskArgs: taskArgs,
110+
isolateParams: isolateParams,
111+
);
112+
113+
print('Task result:');
114+
print(const JsonEncoder.withIndent(' ').convert(result));
115+
section('Finished task "$taskName"');
116+
117+
if (resultsPath != null) {
118+
final Cocoon cocoon = Cocoon();
119+
await cocoon.writeTaskResultToFile(
120+
builderName: luciBuilder,
121+
gitBranch: gitBranch,
122+
result: result,
123+
resultsPath: resultsPath,
124+
);
125+
}
126+
return result;
127+
}
128+
64129
/// Runs a task in a separate Dart VM and collects the result using the VM
65130
/// service protocol.
66131
///

dev/devicelab/test/runner_test.dart

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// @dart = 2.8
6+
7+
import 'package:flutter_devicelab/framework/runner.dart';
8+
9+
import 'common.dart';
10+
11+
void main() {
12+
final Map<String, String> isolateParams = <String, String>{
13+
'runFlutterConfig': 'false',
14+
'runProcessCleanup': 'false',
15+
'timeoutInMinutes': '1',
16+
};
17+
List<String> printLog;
18+
void print(String s) => printLog.add(s);
19+
20+
group('run.dart script', () {
21+
test('Reruns - Test passes the first time.', () async {
22+
printLog = <String>[];
23+
await runTasks(
24+
<String>['smoke_test_success'],
25+
isolateParams: isolateParams,
26+
print: print,
27+
logs: printLog,
28+
);
29+
expect(printLog.length, 2);
30+
expect(printLog[0], 'Total 1 executions: 1 success');
31+
expect(printLog[1], 'flaky: false');
32+
});
33+
34+
test('Reruns - Test fails all reruns.', () async {
35+
printLog = <String>[];
36+
await runTasks(
37+
<String>['smoke_test_failure'],
38+
isolateParams: isolateParams,
39+
print: print,
40+
logs: printLog,
41+
);
42+
expect(printLog.length, 2);
43+
expect(printLog[0], 'Total 3 executions: 0 success');
44+
expect(printLog[1], 'flaky: false');
45+
});
46+
});
47+
}

0 commit comments

Comments
 (0)