Skip to content

Commit fc4adc7

Browse files
authored
[ci] Take screenshot when native drive test is taking longer than 10 minutes (flutter#8050)
If the `flutter test` run takes more than 10 minutes (default timeout is 12 minutes) take a screenshot and attach to the logs directory. Helped debug flutter/flutter#153578 (comment). Probably `flutter test` should have a screenshot option similar to `flutter drive`, but at the moment it doesn't.
1 parent afb1aff commit fc4adc7

File tree

3 files changed

+124
-7
lines changed

3 files changed

+124
-7
lines changed

script/tool/lib/src/drive_examples_command.dart

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:async';
56
import 'dart:convert';
67
import 'dart:io';
78

@@ -439,17 +440,36 @@ class DriveExamplesCommand extends PackageLoopingCommand {
439440

440441
bool passed = true;
441442
for (final String target in individualRunTargets) {
442-
final int exitCode = await processRunner.runAndStream(
443+
final Timer timeoutTimer = Timer(const Duration(minutes: 10), () async {
444+
final String screenshotBasename =
445+
'test-timeout-screenshot_${target.replaceAll(platform.pathSeparator, '_')}.png';
446+
printWarning(
447+
'Test is taking a long time, taking screenshot $screenshotBasename...');
448+
await processRunner.runAndStream(
443449
flutterCommand,
444450
<String>[
445-
'test',
451+
'screenshot',
446452
...deviceFlags,
447-
if (enableExperiment.isNotEmpty)
448-
'--enable-experiment=$enableExperiment',
449-
if (logsDirectory != null) '--debug-logs-dir=${logsDirectory.path}',
450-
target,
453+
if (logsDirectory != null)
454+
'--out=${logsDirectory.childFile(screenshotBasename).path}',
451455
],
452-
workingDir: example.directory);
456+
workingDir: example.directory,
457+
);
458+
});
459+
final int exitCode = await processRunner.runAndStream(
460+
flutterCommand,
461+
<String>[
462+
'test',
463+
...deviceFlags,
464+
if (enableExperiment.isNotEmpty)
465+
'--enable-experiment=$enableExperiment',
466+
if (logsDirectory != null) '--debug-logs-dir=${logsDirectory.path}',
467+
target,
468+
],
469+
workingDir: example.directory,
470+
);
471+
472+
timeoutTimer.cancel();
453473
passed = passed && (exitCode == 0);
454474
}
455475
return passed;

script/tool/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ dependencies:
2626

2727
dev_dependencies:
2828
build_runner: ^2.2.1
29+
fake_async: ^1.3.1
2930
matcher: ^0.12.15
3031
mockito: ^5.4.4
3132

script/tool/test/drive_examples_command_test.dart

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
// found in the LICENSE file.
44

55
import 'dart:convert';
6+
import 'dart:io' as io;
67

78
import 'package:args/command_runner.dart';
9+
import 'package:fake_async/fake_async.dart';
810
import 'package:file/file.dart';
911
import 'package:file/memory.dart';
1012
import 'package:flutter_plugin_tools/src/common/core.dart';
1113
import 'package:flutter_plugin_tools/src/common/plugin_utils.dart';
1214
import 'package:flutter_plugin_tools/src/drive_examples_command.dart';
15+
import 'package:mockito/mockito.dart';
1316
import 'package:platform/platform.dart';
1417
import 'package:test/test.dart';
1518

@@ -387,6 +390,82 @@ void main() {
387390
]));
388391
});
389392

393+
test('saves a screenshot if test is taking too long', () async {
394+
setMockFlutterDevicesOutput();
395+
final RepositoryPackage plugin = createFakePlugin(
396+
'plugin',
397+
packagesDir,
398+
extraFiles: <String>[
399+
'example/integration_test/bar_test.dart',
400+
'example/ios/ios.m',
401+
],
402+
platformSupport: <String, PlatformDetails>{
403+
platformAndroid: const PlatformDetails(PlatformSupport.inline),
404+
platformIOS: const PlatformDetails(PlatformSupport.inline),
405+
},
406+
);
407+
408+
final FakeAsync fakeAsync = FakeAsync();
409+
processRunner.mockProcessesForExecutable['flutter']!
410+
.addAll(<FakeProcessInfo>[
411+
FakeProcessInfo(
412+
_FakeDelayingProcess(
413+
delayDuration: const Duration(minutes: 11),
414+
fakeAsync: fakeAsync),
415+
<String>['test']),
416+
FakeProcessInfo(MockProcess(), <String>['screenshot']),
417+
]);
418+
419+
final Directory pluginExampleDirectory = getExampleDir(plugin);
420+
421+
List<String> output = <String>[];
422+
fakeAsync.run((_) {
423+
() async {
424+
output = await runCapturingPrint(
425+
runner, <String>['drive-examples', '--ios']);
426+
}();
427+
});
428+
fakeAsync.flushTimers();
429+
430+
expect(
431+
output,
432+
containsAllInOrder(<Matcher>[
433+
contains('Running for plugin'),
434+
contains(
435+
'Test is taking a long time, taking screenshot test-timeout-screenshot_integration_test.png...'),
436+
contains('No issues found!'),
437+
]),
438+
);
439+
440+
expect(
441+
processRunner.recordedCalls,
442+
orderedEquals(<ProcessCall>[
443+
ProcessCall(getFlutterCommand(mockPlatform),
444+
const <String>['devices', '--machine'], null),
445+
ProcessCall(
446+
getFlutterCommand(mockPlatform),
447+
const <String>[
448+
'test',
449+
'-d',
450+
_fakeIOSDevice,
451+
'--debug-logs-dir=/path/to/logs',
452+
'integration_test',
453+
],
454+
pluginExampleDirectory.path,
455+
),
456+
ProcessCall(
457+
getFlutterCommand(mockPlatform),
458+
const <String>[
459+
'screenshot',
460+
'-d',
461+
_fakeIOSDevice,
462+
'--out=/path/to/logs/test-timeout-screenshot_integration_test.png',
463+
],
464+
pluginExampleDirectory.path,
465+
),
466+
]));
467+
});
468+
390469
test('driving when plugin does not support Linux is a no-op', () async {
391470
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
392471
'example/integration_test/plugin_test.dart',
@@ -1634,3 +1713,20 @@ void main() {
16341713
});
16351714
});
16361715
}
1716+
1717+
class _FakeDelayingProcess extends Fake implements io.Process {
1718+
/// Creates a mock process that takes [delayDuration] time to exit successfully.
1719+
_FakeDelayingProcess(
1720+
{required Duration delayDuration, required FakeAsync fakeAsync})
1721+
: _delayDuration = delayDuration,
1722+
_fakeAsync = fakeAsync;
1723+
1724+
final Duration _delayDuration;
1725+
final FakeAsync _fakeAsync;
1726+
1727+
@override
1728+
Future<int> get exitCode async {
1729+
_fakeAsync.elapse(_delayDuration);
1730+
return 0;
1731+
}
1732+
}

0 commit comments

Comments
 (0)