Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 0211df9

Browse files
[flutter_tools] provide --timeout option to flutter drive (#114458)
1 parent 3b0f833 commit 0211df9

File tree

5 files changed

+152
-17
lines changed

5 files changed

+152
-17
lines changed

dev/devicelab/bin/tasks/new_gallery__transition_perf.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@ Future<void> main() async {
1717
final Directory galleryDir = Directory(path.join(galleryParentDir.path, 'gallery'));
1818

1919
try {
20-
await task(NewGalleryPerfTest(galleryDir).run);
20+
await task(
21+
NewGalleryPerfTest(
22+
galleryDir,
23+
// time out after 20 minutes allowing the tool to take a screenshot to debug
24+
// https://github.com/flutter/flutter/issues/114025.
25+
timeoutSeconds: 20 * 60,
26+
).run,
27+
);
2128
} finally {
2229
rmTree(galleryParentDir);
2330
}

dev/devicelab/lib/tasks/new_gallery.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class NewGalleryPerfTest extends PerfTest {
1515
String timelineFileName = 'transitions',
1616
String dartDefine = '',
1717
bool enableImpeller = false,
18+
super.timeoutSeconds,
1819
}) : super(
1920
galleryDir.path,
2021
'test_driver/transitions_perf.dart',

dev/devicelab/lib/tasks/perf_tests.dart

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,7 @@ class PerfTest {
914914
this.device,
915915
this.flutterDriveCallback,
916916
this.enableImpeller = false,
917+
this.timeoutSeconds,
917918
}): _resultFilename = resultFilename;
918919

919920
const PerfTest.e2e(
@@ -929,6 +930,7 @@ class PerfTest {
929930
this.device,
930931
this.flutterDriveCallback,
931932
this.enableImpeller = false,
933+
this.timeoutSeconds,
932934
}) : saveTraceFile = false, timelineFileName = null, _resultFilename = resultFilename;
933935

934936
/// The directory where the app under test is defined.
@@ -963,6 +965,9 @@ class PerfTest {
963965
/// Whether the perf test should enable Impeller.
964966
final bool enableImpeller;
965967

968+
/// Number of seconds to time out the test after, allowing debug callbacks to run.
969+
final int? timeoutSeconds;
970+
966971
/// The keys of the values that need to be reported.
967972
///
968973
/// If it's `null`, then report:
@@ -1020,6 +1025,11 @@ class PerfTest {
10201025
'-v',
10211026
'--verbose-system-logs',
10221027
'--profile',
1028+
if (timeoutSeconds != null)
1029+
...<String>[
1030+
'--timeout',
1031+
timeoutSeconds.toString(),
1032+
],
10231033
if (needsFullTimeline)
10241034
'--trace-startup', // Enables "endless" timeline event buffering.
10251035
'-t', testTarget,
@@ -1039,7 +1049,7 @@ class PerfTest {
10391049
if (flutterDriveCallback != null) {
10401050
flutterDriveCallback!(options);
10411051
} else {
1042-
await flutter('drive', options:options);
1052+
await flutter('drive', options: options);
10431053
}
10441054
final Map<String, dynamic> data = json.decode(
10451055
file('${_testOutputDirectory(testDirectory)}/$resultFilename.json').readAsStringSync(),
@@ -1140,7 +1150,7 @@ class PerfTestWithSkSL extends PerfTest {
11401150
);
11411151

11421152
@override
1143-
Future<TaskResult> run() async {
1153+
Future<TaskResult> run({int? timeoutSeconds}) async {
11441154
return inDirectory<TaskResult>(testDirectory, () async {
11451155
// Some initializations
11461156
_device = await devices.workingDevice;

packages/flutter_tools/lib/src/commands/drive.dart

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,12 @@ class DriveCommand extends RunCommandBase {
150150
'Dart VM running The test script.')
151151
..addOption('profile-memory', help: 'Launch devtools and profile application memory, writing '
152152
'The output data to the file path provided to this argument as JSON.',
153-
valueHelp: 'profile_memory.json');
153+
valueHelp: 'profile_memory.json')
154+
..addOption('timeout',
155+
help: 'Timeout the test after the given number of seconds. If the '
156+
'"--screenshot" option is provided, a screenshot will be taken '
157+
'before exiting. Defaults to no timeout.',
158+
valueHelp: '360');
154159
}
155160

156161
final Signals signals;
@@ -173,6 +178,8 @@ class DriveCommand extends RunCommandBase {
173178
final FileSystem _fileSystem;
174179
final Logger _logger;
175180
final FileSystemUtils _fsUtils;
181+
Timer? timeoutTimer;
182+
Map<ProcessSignal, Object>? screenshotTokens;
176183

177184
@override
178185
final String name = 'drive';
@@ -295,13 +302,17 @@ class DriveCommand extends RunCommandBase {
295302
androidEmulator: boolArgDeprecated('android-emulator'),
296303
profileMemory: stringArgDeprecated('profile-memory'),
297304
);
298-
// If the test is sent a signal, take a screenshot before exiting
299-
final Map<ProcessSignal, Object> screenshotTokens = _registerScreenshotCallbacks((ProcessSignal signal) async {
300-
_logger.printError('Caught $signal');
301-
await _takeScreenshot(device);
302-
});
305+
306+
// If the test is sent a signal or times out, take a screenshot
307+
_registerScreenshotCallbacks(device);
308+
303309
final int testResult = await testResultFuture;
304-
_unregisterScreenshotCallbacks(screenshotTokens);
310+
311+
if (timeoutTimer != null) {
312+
timeoutTimer!.cancel();
313+
}
314+
_unregisterScreenshotCallbacks();
315+
305316
if (testResult != 0 && screenshot != null) {
306317
// Take a screenshot while the app is still running.
307318
await _takeScreenshot(device);
@@ -331,21 +342,59 @@ class DriveCommand extends RunCommandBase {
331342
return FlutterCommandResult.success();
332343
}
333344

334-
Map<ProcessSignal, Object> _registerScreenshotCallbacks(Function(ProcessSignal) callback) {
345+
int? get _timeoutSeconds {
346+
final String? timeoutString = stringArg('timeout');
347+
if (timeoutString == null) {
348+
return null;
349+
}
350+
final int? timeoutSeconds = int.tryParse(timeoutString);
351+
if (timeoutSeconds == null || timeoutSeconds <= 0) {
352+
throwToolExit(
353+
'Invalid value "$timeoutString" provided to the option --timeout: '
354+
'expected a positive integer representing seconds.',
355+
);
356+
}
357+
return timeoutSeconds;
358+
}
359+
360+
void _registerScreenshotCallbacks(Device device) {
335361
_logger.printTrace('Registering signal handlers...');
336362
final Map<ProcessSignal, Object> tokens = <ProcessSignal, Object>{};
337363
for (final ProcessSignal signal in signalsToHandle) {
338-
tokens[signal] = signals.addHandler(signal, callback);
364+
tokens[signal] = signals.addHandler(
365+
signal,
366+
(ProcessSignal signal) {
367+
_unregisterScreenshotCallbacks();
368+
_logger.printError('Caught $signal');
369+
return _takeScreenshot(device);
370+
},
371+
);
372+
}
373+
screenshotTokens = tokens;
374+
375+
final int? timeoutSeconds = _timeoutSeconds;
376+
if (timeoutSeconds != null) {
377+
timeoutTimer = Timer(
378+
Duration(seconds: timeoutSeconds),
379+
() {
380+
_unregisterScreenshotCallbacks();
381+
_takeScreenshot(device);
382+
throwToolExit('Timed out after $timeoutSeconds seconds');
383+
}
384+
);
339385
}
340-
return tokens;
341386
}
342387

343-
void _unregisterScreenshotCallbacks(Map<ProcessSignal, Object> tokens) {
344-
_logger.printTrace('Unregistering signal handlers...');
345-
for (final MapEntry<ProcessSignal, Object> entry in tokens.entries) {
346-
signals.removeHandler(entry.key, entry.value);
388+
void _unregisterScreenshotCallbacks() {
389+
if (screenshotTokens != null) {
390+
_logger.printTrace('Unregistering signal handlers...');
391+
for (final MapEntry<ProcessSignal, Object> entry in screenshotTokens!.entries) {
392+
signals.removeHandler(entry.key, entry.value);
393+
}
347394
}
395+
timeoutTimer?.cancel();
348396
}
397+
349398
String? _getTestFile() {
350399
if (argResults!['driver'] != null) {
351400
return stringArgDeprecated('driver');

packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import 'dart:async';
66
import 'dart:io' as io;
77

8+
import 'package:fake_async/fake_async.dart';
89
import 'package:file/memory.dart';
910
import 'package:flutter_tools/src/application_package.dart';
11+
import 'package:flutter_tools/src/base/async_guard.dart';
1012
import 'package:flutter_tools/src/base/common.dart';
1113
import 'package:flutter_tools/src/base/file_system.dart';
1214
import 'package:flutter_tools/src/base/io.dart';
@@ -203,6 +205,72 @@ void main() {
203205
DeviceManager: () => fakeDeviceManager,
204206
});
205207

208+
testUsingContext('drive --timeout takes screenshot and tool exits after timeout', () async {
209+
final DriveCommand command = DriveCommand(
210+
fileSystem: fileSystem,
211+
logger: logger,
212+
platform: platform,
213+
signals: Signals.test(),
214+
flutterDriverFactory: NeverEndingFlutterDriverFactory(() {}),
215+
);
216+
217+
fileSystem.file('lib/main.dart').createSync(recursive: true);
218+
fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
219+
fileSystem.file('pubspec.yaml').createSync();
220+
fileSystem.directory('drive_screenshots').createSync();
221+
222+
final ScreenshotDevice screenshotDevice = ScreenshotDevice();
223+
fakeDeviceManager.devices = <Device>[screenshotDevice];
224+
225+
expect(screenshotDevice.screenshots, isEmpty);
226+
bool caughtToolExit = false;
227+
FakeAsync().run<void>((FakeAsync time) {
228+
// Because the tool exit will be thrown asynchronously by a [Timer],
229+
// use [asyncGuard] to catch it
230+
asyncGuard<void>(
231+
() => createTestCommandRunner(command).run(
232+
<String>[
233+
'drive',
234+
'--no-pub',
235+
'-d',
236+
screenshotDevice.id,
237+
'--use-existing-app',
238+
'http://localhost:8181',
239+
'--screenshot',
240+
'drive_screenshots',
241+
'--timeout',
242+
'300', // 5 minutes
243+
],
244+
),
245+
onError: (Object error) {
246+
expect(error, isA<ToolExit>());
247+
expect(
248+
(error as ToolExit).message,
249+
contains('Timed out after 300 seconds'),
250+
);
251+
caughtToolExit = true;
252+
}
253+
);
254+
time.elapse(const Duration(seconds: 299));
255+
expect(screenshotDevice.screenshots, isEmpty);
256+
time.elapse(const Duration(seconds: 2));
257+
expect(
258+
screenshotDevice.screenshots,
259+
contains(isA<File>().having(
260+
(File file) => file.path,
261+
'path',
262+
'drive_screenshots/drive_01.png',
263+
)),
264+
);
265+
});
266+
expect(caughtToolExit, isTrue);
267+
}, overrides: <Type, Generator>{
268+
FileSystem: () => fileSystem,
269+
ProcessManager: () => FakeProcessManager.any(),
270+
Pub: () => FakePub(),
271+
DeviceManager: () => fakeDeviceManager,
272+
});
273+
206274
testUsingContext('drive --screenshot takes screenshot if sent a registered signal', () async {
207275
final FakeProcessSignal signal = FakeProcessSignal();
208276
final ProcessSignal signalUnderTest = ProcessSignal(signal);

0 commit comments

Comments
 (0)