Unverified Commit fdda777e authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] support screenshot on all device types (#80616)

Co-authored-by: 's avatarZachary Anderson <zanderso@users.noreply.github.com>
parent 71cb40f4
...@@ -30,6 +30,7 @@ import 'build_system/targets/localizations.dart'; ...@@ -30,6 +30,7 @@ import 'build_system/targets/localizations.dart';
import 'bundle.dart'; import 'bundle.dart';
import 'cache.dart'; import 'cache.dart';
import 'compile.dart'; import 'compile.dart';
import 'convert.dart';
import 'devfs.dart'; import 'devfs.dart';
import 'device.dart'; import 'device.dart';
import 'features.dart'; import 'features.dart';
...@@ -938,10 +939,18 @@ abstract class ResidentHandlers { ...@@ -938,10 +939,18 @@ abstract class ResidentHandlers {
/// If the device has a connected vmservice, this method will attempt to hide /// If the device has a connected vmservice, this method will attempt to hide
/// and restore the debug banner before taking the screenshot. /// and restore the debug banner before taking the screenshot.
/// ///
/// Throws an [AssertionError] if [Device.supportsScreenshot] is not true. /// If the device type does not support a "native" screenshot, then this
/// will fallback to a rasterizer screenshot from the engine. This has the
/// downside of being unable to display the contents of platform views.
///
/// This method will return without writing the screenshot file if any
/// RPC errors are encountered, printing them to stderr. This is true even
/// if an error occurs after the data has already been received, such as
/// from restoring the debug banner.
Future<void> screenshot(FlutterDevice device) async { Future<void> screenshot(FlutterDevice device) async {
assert(device.device.supportsScreenshot); if (!device.device.supportsScreenshot && !supportsServiceProtocol) {
return;
}
final Status status = logger.startProgress( final Status status = logger.startProgress(
'Taking screenshot for ${device.device.name}...', 'Taking screenshot for ${device.device.name}...',
); );
...@@ -950,50 +959,79 @@ abstract class ResidentHandlers { ...@@ -950,50 +959,79 @@ abstract class ResidentHandlers {
'flutter', 'flutter',
'png', 'png',
); );
List<FlutterView> views = <FlutterView>[];
try {
bool result;
if (device.device.supportsScreenshot) {
result = await _toggleDebugBanner(device, () => device.device.takeScreenshot(outputFile));
} else {
result = await _takeVmServiceScreenshot(device, outputFile);
}
if (!result) {
return;
}
final int sizeKB = outputFile.lengthSync() ~/ 1024;
status.stop();
logger.printStatus(
'Screenshot written to ${fileSystem.path.relative(outputFile.path)} (${sizeKB}kB).',
);
} on Exception catch (error) {
status.cancel();
logger.printError('Error taking screenshot: $error');
}
}
Future<bool> _takeVmServiceScreenshot(FlutterDevice device, File outputFile) async {
final bool isWebDevice = device.targetPlatform == TargetPlatform.web_javascript;
assert(supportsServiceProtocol);
return _toggleDebugBanner(device, () async {
final vm_service.Response response = isWebDevice
? await device.vmService.callMethodWrapper('ext.dwds.screenshot')
: await device.vmService.screenshot();
if (response == null) {
throw Exception('Failed to take screenshot');
}
final String data = response.json[isWebDevice ? 'data' : 'screenshot'] as String;
outputFile.writeAsBytesSync(base64.decode(data));
});
}
Future<bool> _toggleDebugBanner(FlutterDevice device, Future<void> Function() cb) async {
List<vm_service.IsolateRef> views = <vm_service.IsolateRef>[];
if (supportsServiceProtocol) {
views = await device._getCurrentIsolates();
}
Future<bool> setDebugBanner(bool value) async { Future<bool> setDebugBanner(bool value) async {
try { try {
for (final FlutterView view in views) { for (final vm_service.IsolateRef view in views) {
await device.vmService.flutterDebugAllowBanner( await device.vmService.flutterDebugAllowBanner(
value, value,
isolateId: view.uiIsolate.id, isolateId: view.id,
); );
} }
return true; return true;
} on Exception catch (error) { } on vm_service.RPCError catch (error) {
status.cancel();
logger.printError('Error communicating with Flutter on the device: $error'); logger.printError('Error communicating with Flutter on the device: $error');
return false; return false;
} }
} }
if (!await setDebugBanner(false)) {
return false;
}
bool succeeded = true;
try { try {
if (supportsServiceProtocol && isRunningDebug) { await cb();
// Ensure that the vmService access is guarded by supportsServiceProtocol, it } finally {
// will be null in release mode. if (!await setDebugBanner(true)) {
views = await device.vmService.getFlutterViews(); succeeded = false;
if (!await setDebugBanner(false)) {
return;
}
}
try {
await device.device.takeScreenshot(outputFile);
} finally {
if (supportsServiceProtocol && isRunningDebug) {
await setDebugBanner(true);
}
} }
final int sizeKB = outputFile.lengthSync() ~/ 1024;
status.stop();
logger.printStatus(
'Screenshot written to ${fileSystem.path.relative(outputFile.path)} (${sizeKB}kB).',
);
} on Exception catch (error) {
status.cancel();
logger.printError('Error taking screenshot: $error');
} }
return succeeded;
} }
/// Remove sigusr signal handlers. /// Remove sigusr signal handlers.
Future<void> cleanupAfterSignal(); Future<void> cleanupAfterSignal();
...@@ -1633,9 +1671,7 @@ class TerminalHandler { ...@@ -1633,9 +1671,7 @@ class TerminalHandler {
return true; return true;
case 's': case 's':
for (final FlutterDevice device in residentRunner.flutterDevices) { for (final FlutterDevice device in residentRunner.flutterDevices) {
if (device.device.supportsScreenshot) { await residentRunner.screenshot(device);
await residentRunner.screenshot(device);
}
} }
return true; return true;
case 'S': case 'S':
......
...@@ -1645,143 +1645,6 @@ void main() { ...@@ -1645,143 +1645,6 @@ void main() {
DevtoolsLauncher: () => mockDevtoolsLauncher, DevtoolsLauncher: () => mockDevtoolsLauncher,
}); });
testUsingContext('ResidentRunner can take screenshot on debug device', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews,
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'false',
},
),
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'true',
},
)
]);
await residentRunner.screenshot(mockFlutterDevice);
expect(testLogger.statusText, contains('1kB'));
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}));
testUsingContext('ResidentRunner can take screenshot on release device', () => testbed.run(() async {
residentRunner = ColdRunner(
<FlutterDevice>[
mockFlutterDevice,
],
stayResident: false,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
target: 'main.dart',
devtoolsHandler: createNoOpHandler,
);
await residentRunner.screenshot(mockFlutterDevice);
expect(testLogger.statusText, contains('1kB'));
}));
testUsingContext('ResidentRunner bails taking screenshot on debug device if debugAllowBanner throws RpcError', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews,
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'false',
},
// Failed response,
errorCode: RPCErrorCodes.kInternalError,
)
]);
await residentRunner.screenshot(mockFlutterDevice);
expect(testLogger.errorText, contains('Error'));
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}));
testUsingContext('ResidentRunner bails taking screenshot on debug device if debugAllowBanner during second request', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews,
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'false',
},
),
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'true',
},
// Failed response,
errorCode: RPCErrorCodes.kInternalError,
)
]);
await residentRunner.screenshot(mockFlutterDevice);
expect(testLogger.errorText, contains('Error'));
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}));
testUsingContext('ResidentRunner bails taking screenshot on debug device if takeScreenshot throws', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews,
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'false',
},
),
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'true',
},
),
]);
// Ensure that takeScreenshot will throw an exception.
mockDevice.failScreenshot = true;
await residentRunner.screenshot(mockFlutterDevice);
expect(testLogger.errorText, contains('Error'));
}));
testUsingContext("ResidentRunner can't take screenshot on device without support", () => testbed.run(() {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
mockDevice.supportsScreenshot = false;
expect(() => residentRunner.screenshot(mockFlutterDevice),
throwsAssertionError);
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}));
testUsingContext('ResidentRunner does not toggle banner in non-debug mode', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
residentRunner = HotRunner(
<FlutterDevice>[
mockFlutterDevice,
],
stayResident: false,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
target: 'main.dart',
devtoolsHandler: createNoOpHandler,
);
await residentRunner.screenshot(mockFlutterDevice);
expect(testLogger.statusText, contains('1kB'));
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}));
testUsingContext('FlutterDevice will not exit a paused isolate', () => testbed.run(() async { testUsingContext('FlutterDevice will not exit a paused isolate', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[ fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
FakeVmServiceRequest( FakeVmServiceRequest(
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
import 'dart:async'; import 'dart:async';
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:vm_service/vm_service.dart' as vm_service; import 'package:vm_service/vm_service.dart' as vm_service;
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart'; import 'package:file_testing/file_testing.dart';
...@@ -581,12 +582,6 @@ void main() { ...@@ -581,12 +582,6 @@ void main() {
await terminalHandler.processTerminalInput('P'); await terminalHandler.processTerminalInput('P');
}); });
testWithoutContext('s - screenshot', () async {
final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[]);
await terminalHandler.processTerminalInput('s');
});
testWithoutContext('S - debugDumpSemanticsTreeInTraversalOrder', () async { testWithoutContext('S - debugDumpSemanticsTreeInTraversalOrder', () async {
final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[
listViews, listViews,
...@@ -953,6 +948,283 @@ void main() { ...@@ -953,6 +948,283 @@ void main() {
expect(terminalHandler.logger.statusText, equals('')); expect(terminalHandler.logger.statusText, equals(''));
}); });
testWithoutContext('s, can take screenshot on debug device that supports screenshot', () async {
final BufferLogger logger = BufferLogger.test();
final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[
listViews,
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'false',
},
),
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'true',
},
)
], logger: logger, supportsScreenshot: true);
await terminalHandler.processTerminalInput('s');
expect(logger.statusText, contains('Screenshot written to flutter_01.png (0kB)'));
});
testWithoutContext('s, can take screenshot on debug device that does not support screenshot', () async {
final BufferLogger logger = BufferLogger.test();
final FileSystem fileSystem = MemoryFileSystem.test();
final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[
listViews,
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'false',
},
),
FakeVmServiceRequest(
method: '_flutter.screenshot',
args: <String, Object>{},
jsonResponse: <String, Object>{
'screenshot': base64.encode(<int>[1, 2, 3, 4]),
},
),
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'true',
},
)
], logger: logger, supportsScreenshot: false, fileSystem: fileSystem);
await terminalHandler.processTerminalInput('s');
expect(logger.statusText, contains('Screenshot written to flutter_01.png (0kB)'));
expect(fileSystem.currentDirectory.childFile('flutter_01.png').readAsBytesSync(), <int>[1, 2, 3, 4]);
});
testWithoutContext('s, can take screenshot on debug web device that does not support screenshot', () async {
final BufferLogger logger = BufferLogger.test();
final FileSystem fileSystem = MemoryFileSystem.test();
final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[
getVM,
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'false',
},
),
FakeVmServiceRequest(
method: 'ext.dwds.screenshot',
args: <String, Object>{},
jsonResponse: <String, Object>{
'data': base64.encode(<int>[1, 2, 3, 4]),
},
),
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'true',
},
)
], logger: logger, supportsScreenshot: false, web: true, fileSystem: fileSystem);
await terminalHandler.processTerminalInput('s');
expect(logger.statusText, contains('Screenshot written to flutter_01.png (0kB)'));
expect(fileSystem.currentDirectory.childFile('flutter_01.png').readAsBytesSync(), <int>[1, 2, 3, 4]);
});
testWithoutContext('s, can take screenshot on device that does not support service protocol', () async {
final BufferLogger logger = BufferLogger.test();
final FileSystem fileSystem = MemoryFileSystem.test();
final TerminalHandler terminalHandler = setUpTerminalHandler(
<FakeVmServiceRequest>[],
logger: logger,
supportsScreenshot: true,
supportsServiceProtocol: false,
fileSystem: fileSystem,
);
await terminalHandler.processTerminalInput('s');
expect(logger.statusText, contains('Screenshot written to flutter_01.png (0kB)'));
expect(fileSystem.currentDirectory.childFile('flutter_01.png').readAsBytesSync(), <int>[1, 2, 3, 4]);
});
testWithoutContext('s, does not take a screenshot on a device that does not support screenshot or the service protocol', () async {
final BufferLogger logger = BufferLogger.test();
final FileSystem fileSystem = MemoryFileSystem.test();
final TerminalHandler terminalHandler = setUpTerminalHandler(
<FakeVmServiceRequest>[],
logger: logger,
supportsScreenshot: false,
supportsServiceProtocol: false,
fileSystem: fileSystem,
);
await terminalHandler.processTerminalInput('s');
expect(logger.statusText, '\n');
expect(fileSystem.currentDirectory.childFile('flutter_01.png'), isNot(exists));
});
testWithoutContext('s, does not take a screenshot on a web device that does not support screenshot or the service protocol', () async {
final BufferLogger logger = BufferLogger.test();
final FileSystem fileSystem = MemoryFileSystem.test();
final TerminalHandler terminalHandler = setUpTerminalHandler(
<FakeVmServiceRequest>[],
logger: logger,
supportsScreenshot: false,
supportsServiceProtocol: false,
web: true,
fileSystem: fileSystem,
);
await terminalHandler.processTerminalInput('s');
expect(logger.statusText, '\n');
expect(fileSystem.currentDirectory.childFile('flutter_01.png'), isNot(exists));
});
testWithoutContext('s, bails taking screenshot on debug device if debugAllowBanner throws RpcError', () async {
final BufferLogger logger = BufferLogger.test();
final FileSystem fileSystem = MemoryFileSystem.test();
final TerminalHandler terminalHandler = setUpTerminalHandler(
<FakeVmServiceRequest>[
listViews,
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'false',
},
// Failed response,
errorCode: RPCErrorCodes.kInternalError,
),
],
logger: logger,
supportsScreenshot: false,
fileSystem: fileSystem,
);
await terminalHandler.processTerminalInput('s');
expect(logger.errorText, contains('Error'));
});
testWithoutContext('s, bails taking screenshot on debug device if flutter.screenshot throws RpcError, restoring banner', () async {
final BufferLogger logger = BufferLogger.test();
final FileSystem fileSystem = MemoryFileSystem.test();
final TerminalHandler terminalHandler = setUpTerminalHandler(
<FakeVmServiceRequest>[
listViews,
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'false',
},
),
const FakeVmServiceRequest(
method: '_flutter.screenshot',
// Failed response,
errorCode: RPCErrorCodes.kInternalError,
),
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'true',
},
),
],
logger: logger,
supportsScreenshot: false,
fileSystem: fileSystem,
);
await terminalHandler.processTerminalInput('s');
expect(logger.errorText, contains('Error'));
});
testWithoutContext('s, bails taking screenshot on debug device if dwds.screenshot throws RpcError, restoring banner', () async {
final BufferLogger logger = BufferLogger.test();
final FileSystem fileSystem = MemoryFileSystem.test();
final TerminalHandler terminalHandler = setUpTerminalHandler(
<FakeVmServiceRequest>[
getVM,
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'false',
},
),
const FakeVmServiceRequest(
method: 'ext.dwds.screenshot',
// Failed response,
errorCode: RPCErrorCodes.kInternalError,
),
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'true',
},
),
],
logger: logger,
supportsScreenshot: false,
web: true,
fileSystem: fileSystem,
);
await terminalHandler.processTerminalInput('s');
expect(logger.errorText, contains('Error'));
});
testWithoutContext('s, bails taking screenshot on debug device if debugAllowBanner during second request', () async {
final BufferLogger logger = BufferLogger.test();
final FileSystem fileSystem = MemoryFileSystem.test();
final TerminalHandler terminalHandler = setUpTerminalHandler(
<FakeVmServiceRequest>[
listViews,
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'false',
},
),
FakeVmServiceRequest(
method: 'ext.flutter.debugAllowBanner',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
'enabled': 'true',
},
// Failed response,
errorCode: RPCErrorCodes.kInternalError,
),
],
logger: logger,
supportsScreenshot: true,
fileSystem: fileSystem,
);
await terminalHandler.processTerminalInput('s');
expect(logger.errorText, contains('Error'));
});
testWithoutContext('pidfile creation', () { testWithoutContext('pidfile creation', () {
final BufferLogger testLogger = BufferLogger.test(); final BufferLogger testLogger = BufferLogger.test();
final Signals signals = _TestSignals(Signals.defaultExitSignals); final Signals signals = _TestSignals(Signals.defaultExitSignals);
...@@ -1086,6 +1358,9 @@ class FakeDevice extends Fake implements Device { ...@@ -1086,6 +1358,9 @@ class FakeDevice extends Fake implements Device {
@override @override
bool supportsScreenshot = false; bool supportsScreenshot = false;
@override
String get name => 'Fake Device';
@override @override
Future<void> takeScreenshot(File file) async { Future<void> takeScreenshot(File file) async {
if (!supportsScreenshot) { if (!supportsScreenshot) {
...@@ -1102,16 +1377,19 @@ TerminalHandler setUpTerminalHandler(List<FakeVmServiceRequest> requests, { ...@@ -1102,16 +1377,19 @@ TerminalHandler setUpTerminalHandler(List<FakeVmServiceRequest> requests, {
bool supportsHotReload = true, bool supportsHotReload = true,
bool web = false, bool web = false,
bool fatalReloadError = false, bool fatalReloadError = false,
bool supportsScreenshot = false,
int reloadExitCode = 0, int reloadExitCode = 0,
BuildMode buildMode = BuildMode.debug, BuildMode buildMode = BuildMode.debug,
Logger logger,
FileSystem fileSystem,
}) { }) {
final BufferLogger testLogger = BufferLogger.test(); final Logger testLogger = logger ?? BufferLogger.test();
final Signals signals = Signals.test(); final Signals signals = Signals.test();
final Terminal terminal = Terminal.test(); final Terminal terminal = Terminal.test();
final FileSystem fileSystem = MemoryFileSystem.test(); final FileSystem localFileSystem = fileSystem ?? MemoryFileSystem.test();
final ProcessInfo processInfo = ProcessInfo.test(MemoryFileSystem.test()); final ProcessInfo processInfo = ProcessInfo.test(MemoryFileSystem.test());
final FlutterDevice device = FlutterDevice( final FlutterDevice device = FlutterDevice(
FakeDevice(), FakeDevice()..supportsScreenshot = supportsScreenshot,
buildInfo: BuildInfo(buildMode, '', treeShakeIcons: false), buildInfo: BuildInfo(buildMode, '', treeShakeIcons: false),
generator: FakeResidentCompiler(), generator: FakeResidentCompiler(),
targetPlatform: web targetPlatform: web
...@@ -1119,7 +1397,7 @@ TerminalHandler setUpTerminalHandler(List<FakeVmServiceRequest> requests, { ...@@ -1119,7 +1397,7 @@ TerminalHandler setUpTerminalHandler(List<FakeVmServiceRequest> requests, {
: TargetPlatform.android_arm, : TargetPlatform.android_arm,
); );
device.vmService = FakeVmServiceHost(requests: requests).vmService; device.vmService = FakeVmServiceHost(requests: requests).vmService;
final FakeResidentRunner residentRunner = FakeResidentRunner(device, testLogger, fileSystem) final FakeResidentRunner residentRunner = FakeResidentRunner(device, testLogger, localFileSystem)
..supportsServiceProtocol = supportsServiceProtocol ..supportsServiceProtocol = supportsServiceProtocol
..supportsRestart = supportsRestart ..supportsRestart = supportsRestart
..canHotReload = supportsHotReload ..canHotReload = supportsHotReload
......
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