[flutter_tools] allow flutter drive to take screenshots when sent a terminating signal (#114118)
This commit is contained in:
parent
80d4e5a05a
commit
d40f6d1e68
@ -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';
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
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 @@
|
|||||||
// 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() {
|
|||||||
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() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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() {
|
|||||||
fileSystem: fileSystem,
|
fileSystem: fileSystem,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
platform: platform,
|
platform: platform,
|
||||||
|
signals: signals,
|
||||||
flutterDriverFactory: FailingFakeFlutterDriverFactory(),
|
flutterDriverFactory: FailingFakeFlutterDriverFactory(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -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() {
|
|||||||
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() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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 {
|
|||||||
// 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 {
|
|||||||
}) 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 {
|
|||||||
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 {
|
|||||||
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;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user