diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 2f134cfced8..26c0173fd96 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -439,17 +440,36 @@ class DriveExamplesCommand extends PackageLoopingCommand { bool passed = true; for (final String target in individualRunTargets) { - final int exitCode = await processRunner.runAndStream( + final Timer timeoutTimer = Timer(const Duration(minutes: 10), () async { + final String screenshotBasename = + 'test-timeout-screenshot_${target.replaceAll(platform.pathSeparator, '_')}.png'; + printWarning( + 'Test is taking a long time, taking screenshot $screenshotBasename...'); + await processRunner.runAndStream( flutterCommand, [ - 'test', + 'screenshot', ...deviceFlags, - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - if (logsDirectory != null) '--debug-logs-dir=${logsDirectory.path}', - target, + if (logsDirectory != null) + '--out=${logsDirectory.childFile(screenshotBasename).path}', ], - workingDir: example.directory); + workingDir: example.directory, + ); + }); + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'test', + ...deviceFlags, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + if (logsDirectory != null) '--debug-logs-dir=${logsDirectory.path}', + target, + ], + workingDir: example.directory, + ); + + timeoutTimer.cancel(); passed = passed && (exitCode == 0); } return passed; diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index ee24eb90d4f..d5ea5b93447 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: dev_dependencies: build_runner: ^2.2.1 + fake_async: ^1.3.1 matcher: ^0.12.15 mockito: ^5.4.4 diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index ac1b8709a9d..664481a4bf7 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -3,13 +3,16 @@ // found in the LICENSE file. import 'dart:convert'; +import 'dart:io' as io; import 'package:args/command_runner.dart'; +import 'package:fake_async/fake_async.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:flutter_plugin_tools/src/drive_examples_command.dart'; +import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; import 'package:test/test.dart'; @@ -387,6 +390,82 @@ void main() { ])); }); + test('saves a screenshot if test is taking too long', () async { + setMockFlutterDevicesOutput(); + final RepositoryPackage plugin = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/integration_test/bar_test.dart', + 'example/ios/ios.m', + ], + platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline), + platformIOS: const PlatformDetails(PlatformSupport.inline), + }, + ); + + final FakeAsync fakeAsync = FakeAsync(); + processRunner.mockProcessesForExecutable['flutter']! + .addAll([ + FakeProcessInfo( + _FakeDelayingProcess( + delayDuration: const Duration(minutes: 11), + fakeAsync: fakeAsync), + ['test']), + FakeProcessInfo(MockProcess(), ['screenshot']), + ]); + + final Directory pluginExampleDirectory = getExampleDir(plugin); + + List output = []; + fakeAsync.run((_) { + () async { + output = await runCapturingPrint( + runner, ['drive-examples', '--ios']); + }(); + }); + fakeAsync.flushTimers(); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains( + 'Test is taking a long time, taking screenshot test-timeout-screenshot_integration_test.png...'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['devices', '--machine'], null), + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'test', + '-d', + _fakeIOSDevice, + '--debug-logs-dir=/path/to/logs', + 'integration_test', + ], + pluginExampleDirectory.path, + ), + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'screenshot', + '-d', + _fakeIOSDevice, + '--out=/path/to/logs/test-timeout-screenshot_integration_test.png', + ], + pluginExampleDirectory.path, + ), + ])); + }); + test('driving when plugin does not support Linux is a no-op', () async { createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/plugin_test.dart', @@ -1634,3 +1713,20 @@ void main() { }); }); } + +class _FakeDelayingProcess extends Fake implements io.Process { + /// Creates a mock process that takes [delayDuration] time to exit successfully. + _FakeDelayingProcess( + {required Duration delayDuration, required FakeAsync fakeAsync}) + : _delayDuration = delayDuration, + _fakeAsync = fakeAsync; + + final Duration _delayDuration; + final FakeAsync _fakeAsync; + + @override + Future get exitCode async { + _fakeAsync.elapse(_delayDuration); + return 0; + } +}