Skip to content

Commit d40f6d1

Browse files
[flutter_tools] allow flutter drive to take screenshots when sent a terminating signal (#114118)
1 parent 80d4e5a commit d40f6d1

File tree

3 files changed

+193
-7
lines changed

3 files changed

+193
-7
lines changed

packages/flutter_tools/lib/executable.dart

+1
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ List<FlutterCommand> generateCommands({
189189
fileSystem: globals.fs,
190190
logger: globals.logger,
191191
platform: globals.platform,
192+
signals: globals.signals,
192193
),
193194
EmulatorsCommand(),
194195
FormatCommand(verboseHelp: verboseHelp),

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

+32-1
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import '../application_package.dart';
1212
import '../artifacts.dart';
1313
import '../base/common.dart';
1414
import '../base/file_system.dart';
15+
import '../base/io.dart';
1516
import '../base/logger.dart';
1617
import '../base/platform.dart';
18+
import '../base/signals.dart';
1719
import '../build_info.dart';
1820
import '../dart/package_map.dart';
1921
import '../device.dart';
@@ -48,9 +50,11 @@ class DriveCommand extends RunCommandBase {
4850
DriveCommand({
4951
bool verboseHelp = false,
5052
@visibleForTesting FlutterDriverFactory? flutterDriverFactory,
53+
@visibleForTesting this.signalsToHandle = const <ProcessSignal>{ProcessSignal.sigint, ProcessSignal.sigterm},
5154
required FileSystem fileSystem,
5255
required Logger? logger,
5356
required Platform platform,
57+
required this.signals,
5458
}) : _flutterDriverFactory = flutterDriverFactory,
5559
_fileSystem = fileSystem,
5660
_logger = logger,
@@ -149,6 +153,11 @@ class DriveCommand extends RunCommandBase {
149153
valueHelp: 'profile_memory.json');
150154
}
151155

156+
final Signals signals;
157+
158+
/// The [ProcessSignal]s that will lead to a screenshot being taken (if the option is provided).
159+
final Set<ProcessSignal> signalsToHandle;
160+
152161
// `pub` must always be run due to the test script running from source,
153162
// even if an application binary is used. Default to true unless the user explicitly
154163
// specified not to.
@@ -270,7 +279,7 @@ class DriveCommand extends RunCommandBase {
270279
);
271280
}
272281

273-
final int testResult = await driverService.startTest(
282+
final Future<int> testResultFuture = driverService.startTest(
274283
testFile,
275284
stringsArg('test-arguments'),
276285
<String, String>{},
@@ -286,6 +295,13 @@ class DriveCommand extends RunCommandBase {
286295
androidEmulator: boolArgDeprecated('android-emulator'),
287296
profileMemory: stringArgDeprecated('profile-memory'),
288297
);
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+
});
303+
final int testResult = await testResultFuture;
304+
_unregisterScreenshotCallbacks(screenshotTokens);
289305
if (testResult != 0 && screenshot != null) {
290306
// Take a screenshot while the app is still running.
291307
await _takeScreenshot(device);
@@ -315,6 +331,21 @@ class DriveCommand extends RunCommandBase {
315331
return FlutterCommandResult.success();
316332
}
317333

334+
Map<ProcessSignal, Object> _registerScreenshotCallbacks(Function(ProcessSignal) callback) {
335+
_logger!.printTrace('Registering signal handlers...');
336+
final Map<ProcessSignal, Object> tokens = <ProcessSignal, Object>{};
337+
for (final ProcessSignal signal in signalsToHandle) {
338+
tokens[signal] = signals.addHandler(signal, callback);
339+
}
340+
return tokens;
341+
}
342+
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);
347+
}
348+
}
318349
String? _getTestFile() {
319350
if (argResults!['driver'] != null) {
320351
return stringArgDeprecated('driver');

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

+160-6
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
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';
6+
import 'dart:io' as io;
7+
58
import 'package:file/memory.dart';
69
import 'package:flutter_tools/src/application_package.dart';
710
import 'package:flutter_tools/src/base/common.dart';
811
import 'package:flutter_tools/src/base/file_system.dart';
12+
import 'package:flutter_tools/src/base/io.dart';
913
import 'package:flutter_tools/src/base/logger.dart';
1014
import 'package:flutter_tools/src/base/platform.dart';
15+
import 'package:flutter_tools/src/base/signals.dart';
1116
import 'package:flutter_tools/src/build_info.dart';
1217
import 'package:flutter_tools/src/cache.dart';
1318
import 'package:flutter_tools/src/commands/drive.dart';
@@ -27,12 +32,14 @@ void main() {
2732
late BufferLogger logger;
2833
late Platform platform;
2934
late FakeDeviceManager fakeDeviceManager;
35+
late Signals signals;
3036

3137
setUp(() {
3238
fileSystem = MemoryFileSystem.test();
3339
logger = BufferLogger.test();
3440
platform = FakePlatform();
3541
fakeDeviceManager = FakeDeviceManager();
42+
signals = FakeSignals();
3643
});
3744

3845
setUpAll(() {
@@ -44,7 +51,12 @@ void main() {
4451
});
4552

4653
testUsingContext('warns if screenshot is not supported but continues test', () async {
47-
final DriveCommand command = DriveCommand(fileSystem: fileSystem, logger: logger, platform: platform);
54+
final DriveCommand command = DriveCommand(
55+
fileSystem: fileSystem,
56+
logger: logger,
57+
platform: platform,
58+
signals: signals,
59+
);
4860
fileSystem.file('lib/main.dart').createSync(recursive: true);
4961
fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
5062
fileSystem.file('pubspec.yaml').createSync();
@@ -76,7 +88,12 @@ void main() {
7688
});
7789

7890
testUsingContext('takes screenshot and rethrows on drive exception', () async {
79-
final DriveCommand command = DriveCommand(fileSystem: fileSystem, logger: logger, platform: platform);
91+
final DriveCommand command = DriveCommand(
92+
fileSystem: fileSystem,
93+
logger: logger,
94+
platform: platform,
95+
signals: signals,
96+
);
8097
fileSystem.file('lib/main.dart').createSync(recursive: true);
8198
fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
8299
fileSystem.file('pubspec.yaml').createSync();
@@ -111,6 +128,7 @@ void main() {
111128
fileSystem: fileSystem,
112129
logger: logger,
113130
platform: platform,
131+
signals: signals,
114132
flutterDriverFactory: FailingFakeFlutterDriverFactory(),
115133
);
116134

@@ -149,7 +167,13 @@ void main() {
149167
});
150168

151169
testUsingContext('drive --screenshot errors but does not fail if screenshot fails', () async {
152-
final DriveCommand command = DriveCommand(fileSystem: fileSystem, logger: logger, platform: platform);
170+
final DriveCommand command = DriveCommand(
171+
fileSystem: fileSystem,
172+
logger: logger,
173+
platform: platform,
174+
signals: signals,
175+
);
176+
153177
fileSystem.file('lib/main.dart').createSync(recursive: true);
154178
fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
155179
fileSystem.file('pubspec.yaml').createSync();
@@ -179,8 +203,71 @@ void main() {
179203
DeviceManager: () => fakeDeviceManager,
180204
});
181205

206+
testUsingContext('drive --screenshot takes screenshot if sent a registered signal', () async {
207+
final FakeProcessSignal signal = FakeProcessSignal();
208+
final ProcessSignal signalUnderTest = ProcessSignal(signal);
209+
final DriveCommand command = DriveCommand(
210+
fileSystem: fileSystem,
211+
logger: logger,
212+
platform: platform,
213+
signals: Signals.test(),
214+
flutterDriverFactory: NeverEndingFlutterDriverFactory(() {
215+
signal.controller.add(signal);
216+
}),
217+
signalsToHandle: <ProcessSignal>{signalUnderTest},
218+
);
219+
220+
fileSystem.file('lib/main.dart').createSync(recursive: true);
221+
fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
222+
fileSystem.file('pubspec.yaml').createSync();
223+
fileSystem.directory('drive_screenshots').createSync();
224+
225+
final ScreenshotDevice screenshotDevice = ScreenshotDevice();
226+
fakeDeviceManager.devices = <Device>[screenshotDevice];
227+
228+
expect(screenshotDevice.screenshots, isEmpty);
229+
230+
// This command will never complete. In reality, a real signal would have
231+
// shut down the Dart process.
232+
unawaited(
233+
createTestCommandRunner(command).run(
234+
<String>[
235+
'drive',
236+
'--no-pub',
237+
'-d',
238+
screenshotDevice.id,
239+
'--use-existing-app',
240+
'http://localhost:8181',
241+
'--screenshot',
242+
'drive_screenshots',
243+
],
244+
),
245+
);
246+
247+
await screenshotDevice.firstScreenshot;
248+
expect(
249+
screenshotDevice.screenshots,
250+
contains(isA<File>().having(
251+
(File file) => file.path,
252+
'path',
253+
'drive_screenshots/drive_01.png',
254+
)),
255+
);
256+
}, overrides: <Type, Generator>{
257+
FileSystem: () => fileSystem,
258+
ProcessManager: () => FakeProcessManager.any(),
259+
Pub: () => FakePub(),
260+
DeviceManager: () => fakeDeviceManager,
261+
});
262+
182263
testUsingContext('shouldRunPub is true unless user specifies --no-pub', () async {
183-
final DriveCommand command = DriveCommand(fileSystem: fileSystem, logger: logger, platform: platform);
264+
final DriveCommand command = DriveCommand(
265+
fileSystem: fileSystem,
266+
logger: logger,
267+
platform: platform,
268+
signals: signals,
269+
);
270+
184271
fileSystem.file('lib/main.dart').createSync(recursive: true);
185272
fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
186273
fileSystem.file('pubspec.yaml').createSync();
@@ -207,7 +294,13 @@ void main() {
207294
});
208295

209296
testUsingContext('flags propagate to debugging options', () async {
210-
final DriveCommand command = DriveCommand(fileSystem: fileSystem, logger: logger, platform: platform);
297+
final DriveCommand command = DriveCommand(
298+
fileSystem: fileSystem,
299+
logger: logger,
300+
platform: platform,
301+
signals: signals,
302+
);
303+
211304
fileSystem.file('lib/main.dart').createSync(recursive: true);
212305
fileSystem.file('test_driver/main_test.dart').createSync(recursive: true);
213306
fileSystem.file('pubspec.yaml').createSync();
@@ -270,6 +363,13 @@ class ThrowingScreenshotDevice extends ScreenshotDevice {
270363
// Until we fix that, we have to also ignore related lints here.
271364
// ignore: avoid_implementing_value_types
272365
class ScreenshotDevice extends Fake implements Device {
366+
final List<File> screenshots = <File>[];
367+
368+
final Completer<void> _firstScreenshotCompleter = Completer<void>();
369+
370+
/// A Future that completes when [takeScreenshot] is called the first time.
371+
Future<void> get firstScreenshot => _firstScreenshotCompleter.future;
372+
273373
@override
274374
final String name = 'FakeDevice';
275375

@@ -299,7 +399,12 @@ class ScreenshotDevice extends Fake implements Device {
299399
}) async => LaunchResult.succeeded();
300400

301401
@override
302-
Future<void> takeScreenshot(File outputFile) async {}
402+
Future<void> takeScreenshot(File outputFile) async {
403+
if (!_firstScreenshotCompleter.isCompleted) {
404+
_firstScreenshotCompleter.complete();
405+
}
406+
screenshots.add(outputFile);
407+
}
303408
}
304409

305410
class FakePub extends Fake implements Pub {
@@ -331,6 +436,48 @@ class FakeDeviceManager extends Fake implements DeviceManager {
331436
Future<List<Device>> findTargetDevices(FlutterProject? flutterProject, {Duration? timeout, bool promptUserToChooseDevice = true}) async => devices;
332437
}
333438

439+
/// A [FlutterDriverFactory] that creates a [NeverEndingDriverService].
440+
class NeverEndingFlutterDriverFactory extends Fake implements FlutterDriverFactory {
441+
NeverEndingFlutterDriverFactory(this.callback);
442+
443+
final void Function() callback;
444+
445+
@override
446+
DriverService createDriverService(bool web) => NeverEndingDriverService(callback);
447+
}
448+
449+
/// A [DriverService] that will return a Future from [startTest] that will never complete.
450+
///
451+
/// This is to similate when the test will take a long time, but a signal is
452+
/// expected to interrupt the process.
453+
class NeverEndingDriverService extends Fake implements DriverService {
454+
NeverEndingDriverService(this.callback);
455+
456+
final void Function() callback;
457+
@override
458+
Future<void> reuseApplication(Uri vmServiceUri, Device device, DebuggingOptions debuggingOptions, bool ipv6) async { }
459+
460+
@override
461+
Future<int> startTest(
462+
String testFile,
463+
List<String> arguments,
464+
Map<String, String> environment,
465+
PackageConfig packageConfig, {
466+
bool? headless,
467+
String? chromeBinary,
468+
String? browserName,
469+
bool? androidEmulator,
470+
int? driverPort,
471+
List<String>? webBrowserFlags,
472+
List<String>? browserDimension,
473+
String? profileMemory,
474+
}) async {
475+
callback();
476+
// return a Future that will never complete.
477+
return Completer<int>().future;
478+
}
479+
}
480+
334481
class FailingFakeFlutterDriverFactory extends Fake implements FlutterDriverFactory {
335482
@override
336483
DriverService createDriverService(bool web) => FailingFakeDriverService();
@@ -356,3 +503,10 @@ class FailingFakeDriverService extends Fake implements DriverService {
356503
String? profileMemory,
357504
}) async => 1;
358505
}
506+
507+
class FakeProcessSignal extends Fake implements io.ProcessSignal {
508+
final StreamController<io.ProcessSignal> controller = StreamController<io.ProcessSignal>();
509+
510+
@override
511+
Stream<io.ProcessSignal> watch() => controller.stream;
512+
}

0 commit comments

Comments
 (0)