Unverified Commit d40f6d1e authored by Christopher Fujino's avatar Christopher Fujino Committed by GitHub

[flutter_tools] allow flutter drive to take screenshots when sent a terminating signal (#114118)

parent 80d4e5a0
...@@ -189,6 +189,7 @@ List<FlutterCommand> generateCommands({ ...@@ -189,6 +189,7 @@ List<FlutterCommand> generateCommands({
fileSystem: globals.fs, fileSystem: globals.fs,
logger: globals.logger, logger: globals.logger,
platform: globals.platform, platform: globals.platform,
signals: globals.signals,
), ),
EmulatorsCommand(), EmulatorsCommand(),
FormatCommand(verboseHelp: verboseHelp), FormatCommand(verboseHelp: verboseHelp),
......
...@@ -12,8 +12,10 @@ import '../application_package.dart'; ...@@ -12,8 +12,10 @@ import '../application_package.dart';
import '../artifacts.dart'; import '../artifacts.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/platform.dart'; import '../base/platform.dart';
import '../base/signals.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../dart/package_map.dart'; import '../dart/package_map.dart';
import '../device.dart'; import '../device.dart';
...@@ -48,9 +50,11 @@ class DriveCommand extends RunCommandBase { ...@@ -48,9 +50,11 @@ class DriveCommand extends RunCommandBase {
DriveCommand({ DriveCommand({
bool verboseHelp = false, bool verboseHelp = false,
@visibleForTesting FlutterDriverFactory? flutterDriverFactory, @visibleForTesting FlutterDriverFactory? flutterDriverFactory,
@visibleForTesting this.signalsToHandle = const <ProcessSignal>{ProcessSignal.sigint, ProcessSignal.sigterm},
required FileSystem fileSystem, required FileSystem fileSystem,
required Logger? logger, required Logger? logger,
required Platform platform, required Platform platform,
required this.signals,
}) : _flutterDriverFactory = flutterDriverFactory, }) : _flutterDriverFactory = flutterDriverFactory,
_fileSystem = fileSystem, _fileSystem = fileSystem,
_logger = logger, _logger = logger,
...@@ -149,6 +153,11 @@ class DriveCommand extends RunCommandBase { ...@@ -149,6 +153,11 @@ class DriveCommand extends RunCommandBase {
valueHelp: 'profile_memory.json'); valueHelp: 'profile_memory.json');
} }
final Signals signals;
/// The [ProcessSignal]s that will lead to a screenshot being taken (if the option is provided).
final Set<ProcessSignal> signalsToHandle;
// `pub` must always be run due to the test script running from source, // `pub` must always be run due to the test script running from source,
// even if an application binary is used. Default to true unless the user explicitly // even if an application binary is used. Default to true unless the user explicitly
// specified not to. // specified not to.
...@@ -270,7 +279,7 @@ class DriveCommand extends RunCommandBase { ...@@ -270,7 +279,7 @@ class DriveCommand extends RunCommandBase {
); );
} }
final int testResult = await driverService.startTest( final Future<int> testResultFuture = driverService.startTest(
testFile, testFile,
stringsArg('test-arguments'), stringsArg('test-arguments'),
<String, String>{}, <String, String>{},
...@@ -286,6 +295,13 @@ class DriveCommand extends RunCommandBase { ...@@ -286,6 +295,13 @@ class DriveCommand extends RunCommandBase {
androidEmulator: boolArgDeprecated('android-emulator'), androidEmulator: boolArgDeprecated('android-emulator'),
profileMemory: stringArgDeprecated('profile-memory'), profileMemory: stringArgDeprecated('profile-memory'),
); );
// If the test is sent a signal, take a screenshot before exiting
final Map<ProcessSignal, Object> screenshotTokens = _registerScreenshotCallbacks((ProcessSignal signal) async {
_logger!.printError('Caught $signal');
await _takeScreenshot(device);
});
final int testResult = await testResultFuture;
_unregisterScreenshotCallbacks(screenshotTokens);
if (testResult != 0 && screenshot != null) { if (testResult != 0 && screenshot != null) {
// Take a screenshot while the app is still running. // Take a screenshot while the app is still running.
await _takeScreenshot(device); await _takeScreenshot(device);
...@@ -315,6 +331,21 @@ class DriveCommand extends RunCommandBase { ...@@ -315,6 +331,21 @@ class DriveCommand extends RunCommandBase {
return FlutterCommandResult.success(); return FlutterCommandResult.success();
} }
Map<ProcessSignal, Object> _registerScreenshotCallbacks(Function(ProcessSignal) callback) {
_logger!.printTrace('Registering signal handlers...');
final Map<ProcessSignal, Object> tokens = <ProcessSignal, Object>{};
for (final ProcessSignal signal in signalsToHandle) {
tokens[signal] = signals.addHandler(signal, callback);
}
return tokens;
}
void _unregisterScreenshotCallbacks(Map<ProcessSignal, Object> tokens) {
_logger!.printTrace('Unregistering signal handlers...');
for (final MapEntry<ProcessSignal, Object> entry in tokens.entries) {
signals.removeHandler(entry.key, entry.value);
}
}
String? _getTestFile() { String? _getTestFile() {
if (argResults!['driver'] != null) { if (argResults!['driver'] != null) {
return stringArgDeprecated('driver'); return stringArgDeprecated('driver');
......
...@@ -2,12 +2,17 @@ ...@@ -2,12 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'dart:io' as io;
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/signals.dart';
import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/drive.dart'; import 'package:flutter_tools/src/commands/drive.dart';
...@@ -27,12 +32,14 @@ void main() { ...@@ -27,12 +32,14 @@ void main() {
late BufferLogger logger; late BufferLogger logger;
late Platform platform; late Platform platform;
late FakeDeviceManager fakeDeviceManager; late FakeDeviceManager fakeDeviceManager;
late Signals signals;
setUp(() { setUp(() {
fileSystem = MemoryFileSystem.test(); fileSystem = MemoryFileSystem.test();
logger = BufferLogger.test(); logger = BufferLogger.test();
platform = FakePlatform(); platform = FakePlatform();
fakeDeviceManager = FakeDeviceManager(); fakeDeviceManager = FakeDeviceManager();
signals = FakeSignals();
}); });
setUpAll(() { setUpAll(() {
...@@ -44,7 +51,12 @@ void main() { ...@@ -44,7 +51,12 @@ void main() {
}); });
testUsingContext('warns if screenshot is not supported but continues test', () async { testUsingContext('warns if screenshot is not supported but continues test', () async {
final DriveCommand command = DriveCommand(fileSystem: fileSystem, logger: logger, platform: platform); final DriveCommand command = DriveCommand(
fileSystem: fileSystem,
logger: logger,
platform: platform,
signals: signals,
);
fileSystem.file('lib/main.dart').createSync(recursive: true); fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('test_driver/main_test.dart').createSync(recursive: true); fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('pubspec.yaml').createSync();
...@@ -76,7 +88,12 @@ void main() { ...@@ -76,7 +88,12 @@ void main() {
}); });
testUsingContext('takes screenshot and rethrows on drive exception', () async { testUsingContext('takes screenshot and rethrows on drive exception', () async {
final DriveCommand command = DriveCommand(fileSystem: fileSystem, logger: logger, platform: platform); final DriveCommand command = DriveCommand(
fileSystem: fileSystem,
logger: logger,
platform: platform,
signals: signals,
);
fileSystem.file('lib/main.dart').createSync(recursive: true); fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('test_driver/main_test.dart').createSync(recursive: true); fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('pubspec.yaml').createSync();
...@@ -111,6 +128,7 @@ void main() { ...@@ -111,6 +128,7 @@ void main() {
fileSystem: fileSystem, fileSystem: fileSystem,
logger: logger, logger: logger,
platform: platform, platform: platform,
signals: signals,
flutterDriverFactory: FailingFakeFlutterDriverFactory(), flutterDriverFactory: FailingFakeFlutterDriverFactory(),
); );
...@@ -149,7 +167,13 @@ void main() { ...@@ -149,7 +167,13 @@ void main() {
}); });
testUsingContext('drive --screenshot errors but does not fail if screenshot fails', () async { testUsingContext('drive --screenshot errors but does not fail if screenshot fails', () async {
final DriveCommand command = DriveCommand(fileSystem: fileSystem, logger: logger, platform: platform); final DriveCommand command = DriveCommand(
fileSystem: fileSystem,
logger: logger,
platform: platform,
signals: signals,
);
fileSystem.file('lib/main.dart').createSync(recursive: true); fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('test_driver/main_test.dart').createSync(recursive: true); fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('pubspec.yaml').createSync();
...@@ -179,8 +203,71 @@ void main() { ...@@ -179,8 +203,71 @@ void main() {
DeviceManager: () => fakeDeviceManager, DeviceManager: () => fakeDeviceManager,
}); });
testUsingContext('drive --screenshot takes screenshot if sent a registered signal', () async {
final FakeProcessSignal signal = FakeProcessSignal();
final ProcessSignal signalUnderTest = ProcessSignal(signal);
final DriveCommand command = DriveCommand(
fileSystem: fileSystem,
logger: logger,
platform: platform,
signals: Signals.test(),
flutterDriverFactory: NeverEndingFlutterDriverFactory(() {
signal.controller.add(signal);
}),
signalsToHandle: <ProcessSignal>{signalUnderTest},
);
fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
fileSystem.directory('drive_screenshots').createSync();
final ScreenshotDevice screenshotDevice = ScreenshotDevice();
fakeDeviceManager.devices = <Device>[screenshotDevice];
expect(screenshotDevice.screenshots, isEmpty);
// This command will never complete. In reality, a real signal would have
// shut down the Dart process.
unawaited(
createTestCommandRunner(command).run(
<String>[
'drive',
'--no-pub',
'-d',
screenshotDevice.id,
'--use-existing-app',
'http://localhost:8181',
'--screenshot',
'drive_screenshots',
],
),
);
await screenshotDevice.firstScreenshot;
expect(
screenshotDevice.screenshots,
contains(isA<File>().having(
(File file) => file.path,
'path',
'drive_screenshots/drive_01.png',
)),
);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Pub: () => FakePub(),
DeviceManager: () => fakeDeviceManager,
});
testUsingContext('shouldRunPub is true unless user specifies --no-pub', () async { testUsingContext('shouldRunPub is true unless user specifies --no-pub', () async {
final DriveCommand command = DriveCommand(fileSystem: fileSystem, logger: logger, platform: platform); final DriveCommand command = DriveCommand(
fileSystem: fileSystem,
logger: logger,
platform: platform,
signals: signals,
);
fileSystem.file('lib/main.dart').createSync(recursive: true); fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('test_driver/main_test.dart').createSync(recursive: true); fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('pubspec.yaml').createSync();
...@@ -207,7 +294,13 @@ void main() { ...@@ -207,7 +294,13 @@ void main() {
}); });
testUsingContext('flags propagate to debugging options', () async { testUsingContext('flags propagate to debugging options', () async {
final DriveCommand command = DriveCommand(fileSystem: fileSystem, logger: logger, platform: platform); final DriveCommand command = DriveCommand(
fileSystem: fileSystem,
logger: logger,
platform: platform,
signals: signals,
);
fileSystem.file('lib/main.dart').createSync(recursive: true); fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('test_driver/main_test.dart').createSync(recursive: true); fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('pubspec.yaml').createSync();
...@@ -270,6 +363,13 @@ class ThrowingScreenshotDevice extends ScreenshotDevice { ...@@ -270,6 +363,13 @@ class ThrowingScreenshotDevice extends ScreenshotDevice {
// Until we fix that, we have to also ignore related lints here. // Until we fix that, we have to also ignore related lints here.
// ignore: avoid_implementing_value_types // ignore: avoid_implementing_value_types
class ScreenshotDevice extends Fake implements Device { class ScreenshotDevice extends Fake implements Device {
final List<File> screenshots = <File>[];
final Completer<void> _firstScreenshotCompleter = Completer<void>();
/// A Future that completes when [takeScreenshot] is called the first time.
Future<void> get firstScreenshot => _firstScreenshotCompleter.future;
@override @override
final String name = 'FakeDevice'; final String name = 'FakeDevice';
...@@ -299,7 +399,12 @@ class ScreenshotDevice extends Fake implements Device { ...@@ -299,7 +399,12 @@ class ScreenshotDevice extends Fake implements Device {
}) async => LaunchResult.succeeded(); }) async => LaunchResult.succeeded();
@override @override
Future<void> takeScreenshot(File outputFile) async {} Future<void> takeScreenshot(File outputFile) async {
if (!_firstScreenshotCompleter.isCompleted) {
_firstScreenshotCompleter.complete();
}
screenshots.add(outputFile);
}
} }
class FakePub extends Fake implements Pub { class FakePub extends Fake implements Pub {
...@@ -331,6 +436,48 @@ class FakeDeviceManager extends Fake implements DeviceManager { ...@@ -331,6 +436,48 @@ class FakeDeviceManager extends Fake implements DeviceManager {
Future<List<Device>> findTargetDevices(FlutterProject? flutterProject, {Duration? timeout, bool promptUserToChooseDevice = true}) async => devices; Future<List<Device>> findTargetDevices(FlutterProject? flutterProject, {Duration? timeout, bool promptUserToChooseDevice = true}) async => devices;
} }
/// A [FlutterDriverFactory] that creates a [NeverEndingDriverService].
class NeverEndingFlutterDriverFactory extends Fake implements FlutterDriverFactory {
NeverEndingFlutterDriverFactory(this.callback);
final void Function() callback;
@override
DriverService createDriverService(bool web) => NeverEndingDriverService(callback);
}
/// A [DriverService] that will return a Future from [startTest] that will never complete.
///
/// This is to similate when the test will take a long time, but a signal is
/// expected to interrupt the process.
class NeverEndingDriverService extends Fake implements DriverService {
NeverEndingDriverService(this.callback);
final void Function() callback;
@override
Future<void> reuseApplication(Uri vmServiceUri, Device device, DebuggingOptions debuggingOptions, bool ipv6) async { }
@override
Future<int> startTest(
String testFile,
List<String> arguments,
Map<String, String> environment,
PackageConfig packageConfig, {
bool? headless,
String? chromeBinary,
String? browserName,
bool? androidEmulator,
int? driverPort,
List<String>? webBrowserFlags,
List<String>? browserDimension,
String? profileMemory,
}) async {
callback();
// return a Future that will never complete.
return Completer<int>().future;
}
}
class FailingFakeFlutterDriverFactory extends Fake implements FlutterDriverFactory { class FailingFakeFlutterDriverFactory extends Fake implements FlutterDriverFactory {
@override @override
DriverService createDriverService(bool web) => FailingFakeDriverService(); DriverService createDriverService(bool web) => FailingFakeDriverService();
...@@ -356,3 +503,10 @@ class FailingFakeDriverService extends Fake implements DriverService { ...@@ -356,3 +503,10 @@ class FailingFakeDriverService extends Fake implements DriverService {
String? profileMemory, String? profileMemory,
}) async => 1; }) async => 1;
} }
class FakeProcessSignal extends Fake implements io.ProcessSignal {
final StreamController<io.ProcessSignal> controller = StreamController<io.ProcessSignal>();
@override
Stream<io.ProcessSignal> watch() => controller.stream;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment