// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/context.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart' as io; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/globals.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/run_cold.dart'; import 'package:flutter_tools/src/run_hot.dart'; import 'package:flutter_tools/src/vmservice.dart'; import 'package:json_rpc_2/json_rpc_2.dart'; import 'package:mockito/mockito.dart'; import '../src/common.dart'; import '../src/context.dart'; import '../src/testbed.dart'; void main() { final Uri testUri = Uri.parse('foo://bar'); Testbed testbed; MockFlutterDevice mockFlutterDevice; MockVMService mockVMService; MockDevFS mockDevFS; MockFlutterView mockFlutterView; ResidentRunner residentRunner; MockDevice mockDevice; MockIsolate mockIsolate; setUp(() { testbed = Testbed(setup: () { fs.file(fs.path.join('build', 'app.dill')) ..createSync(recursive: true) ..writeAsStringSync('ABC'); residentRunner = HotRunner( [ mockFlutterDevice, ], stayResident: false, debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), ); }); mockFlutterDevice = MockFlutterDevice(); mockDevice = MockDevice(); mockVMService = MockVMService(); mockDevFS = MockDevFS(); mockFlutterView = MockFlutterView(); mockIsolate = MockIsolate(); // DevFS Mocks when(mockDevFS.lastCompiled).thenReturn(DateTime(2000)); when(mockDevFS.sources).thenReturn([]); when(mockDevFS.baseUri).thenReturn(Uri()); when(mockDevFS.destroy()).thenAnswer((Invocation invocation) async { }); when(mockDevFS.assetPathsToEvict).thenReturn({}); // FlutterDevice Mocks. when(mockFlutterDevice.updateDevFS( // Intentionally provide empty list to match above mock. invalidatedFiles: [], mainPath: anyNamed('mainPath'), target: anyNamed('target'), bundle: anyNamed('bundle'), firstBuildTime: anyNamed('firstBuildTime'), bundleFirstUpload: anyNamed('bundleFirstUpload'), bundleDirty: anyNamed('bundleDirty'), fullRestart: anyNamed('fullRestart'), projectRootPath: anyNamed('projectRootPath'), pathToReload: anyNamed('pathToReload'), dillOutputPath: anyNamed('dillOutputPath'), )).thenAnswer((Invocation invocation) async { return UpdateFSReport( success: true, syncedBytes: 0, invalidatedSourcesCount: 0, ); }); when(mockFlutterDevice.devFS).thenReturn(mockDevFS); when(mockFlutterDevice.views).thenReturn([ mockFlutterView, ]); when(mockFlutterDevice.device).thenReturn(mockDevice); when(mockFlutterView.uiIsolate).thenReturn(mockIsolate); when(mockFlutterView.runFromSource(any, any, any)).thenAnswer((Invocation invocation) async {}); when(mockFlutterDevice.stopEchoingDeviceLog()).thenAnswer((Invocation invocation) async { }); when(mockFlutterDevice.observatoryUris).thenAnswer((_) => Stream.value(testUri)); when(mockFlutterDevice.connect( reloadSources: anyNamed('reloadSources'), restart: anyNamed('restart'), compileExpression: anyNamed('compileExpression'), )).thenAnswer((Invocation invocation) async { }); when(mockFlutterDevice.setupDevFS(any, any, packagesFilePath: anyNamed('packagesFilePath'))) .thenAnswer((Invocation invocation) async { return testUri; }); when(mockFlutterDevice.vmService).thenReturn(mockVMService); when(mockFlutterDevice.refreshViews()).thenAnswer((Invocation invocation) async { }); when(mockFlutterDevice.reloadSources(any, pause: anyNamed('pause'))).thenReturn(>>[ Future>.value({ 'type': 'ReloadReport', 'success': true, 'details': { 'loadedLibraryCount': 1, 'finalLibraryCount': 1, 'receivedLibraryCount': 1, 'receivedClassesCount': 1, 'receivedProceduresCount': 1, }, }), ]); // VMService mocks. when(mockVMService.wsAddress).thenReturn(testUri); when(mockVMService.done).thenAnswer((Invocation invocation) { final Completer result = Completer.sync(); return result.future; }); when(mockIsolate.resume()).thenAnswer((Invocation invocation) { return Future>.value(null); }); when(mockIsolate.flutterExit()).thenAnswer((Invocation invocation) { return Future>.value(null); }); when(mockIsolate.reload()).thenAnswer((Invocation invocation) { return Future.value(null); }); }); test('ResidentRunner can attach to device successfully', () => testbed.run(() async { final Completer onConnectionInfo = Completer.sync(); final Completer onAppStart = Completer.sync(); final Future result = residentRunner.attach( appStartedCompleter: onAppStart, connectionInfoCompleter: onConnectionInfo, ); final Future connectionInfo = onConnectionInfo.future; expect(await result, 0); verify(mockFlutterDevice.initLogReader()).called(1); expect(onConnectionInfo.isCompleted, true); expect((await connectionInfo).baseUri, 'foo://bar'); expect(onAppStart.isCompleted, true); })); test('ResidentRunner can handle an RPC exception from hot reload', () => testbed.run(() async { when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async { return 'Example'; }); when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async { return TargetPlatform.android_arm; }); when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async { return false; }); final Completer onConnectionInfo = Completer.sync(); final Completer onAppStart = Completer.sync(); unawaited(residentRunner.attach( appStartedCompleter: onAppStart, connectionInfoCompleter: onConnectionInfo, )); await onAppStart.future; when(mockFlutterDevice.updateDevFS( mainPath: anyNamed('mainPath'), target: anyNamed('target'), bundle: anyNamed('bundle'), firstBuildTime: anyNamed('firstBuildTime'), bundleFirstUpload: anyNamed('bundleFirstUpload'), bundleDirty: anyNamed('bundleDirty'), fullRestart: anyNamed('fullRestart'), projectRootPath: anyNamed('projectRootPath'), pathToReload: anyNamed('pathToReload'), invalidatedFiles: anyNamed('invalidatedFiles'), dillOutputPath: anyNamed('dillOutputPath'), )).thenThrow(RpcException(666, 'something bad happened')); final OperationResult result = await residentRunner.restart(fullRestart: false); expect(result.fatal, true); expect(result.code, 1); verify(flutterUsage.sendEvent('hot', 'exception', parameters: { cdKey(CustomDimensions.hotEventTargetPlatform): getNameForTargetPlatform(TargetPlatform.android_arm), cdKey(CustomDimensions.hotEventSdkName): 'Example', cdKey(CustomDimensions.hotEventEmulator): 'false', cdKey(CustomDimensions.hotEventFullRestart): 'false', })).called(1); }, overrides: { Usage: () => MockUsage(), })); test('ResidentRunner copies dill file from build output into temp directory', () => testbed.run(() async { expect(residentRunner.artifactDirectory.childFile('app.dill').readAsStringSync(), 'ABC'); })); test('ResidentRunner can send target platform to analytics from hot reload', () => testbed.run(() async { when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async { return 'Example'; }); when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async { return TargetPlatform.android_arm; }); when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async { return false; }); final Completer onConnectionInfo = Completer.sync(); final Completer onAppStart = Completer.sync(); unawaited(residentRunner.attach( appStartedCompleter: onAppStart, connectionInfoCompleter: onConnectionInfo, )); final OperationResult result = await residentRunner.restart(fullRestart: false); expect(result.fatal, false); expect(result.code, 0); expect(verify(flutterUsage.sendEvent('hot', 'reload', parameters: captureAnyNamed('parameters'))).captured[0], containsPair(cdKey(CustomDimensions.hotEventTargetPlatform), getNameForTargetPlatform(TargetPlatform.android_arm)), ); }, overrides: { Usage: () => MockUsage(), })); test('ResidentRunner can send target platform to analytics from full restart', () => testbed.run(() async { when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async { return 'Example'; }); when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async { return TargetPlatform.android_arm; }); when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async { return false; }); when(mockDevice.supportsHotRestart).thenReturn(true); final Completer onConnectionInfo = Completer.sync(); final Completer onAppStart = Completer.sync(); unawaited(residentRunner.attach( appStartedCompleter: onAppStart, connectionInfoCompleter: onConnectionInfo, )); final OperationResult result = await residentRunner.restart(fullRestart: true); expect(result.fatal, false); expect(result.code, 0); expect(verify(flutterUsage.sendEvent('hot', 'restart', parameters: captureAnyNamed('parameters'))).captured[0], containsPair(cdKey(CustomDimensions.hotEventTargetPlatform), getNameForTargetPlatform(TargetPlatform.android_arm)), ); }, overrides: { Usage: () => MockUsage(), })); test('ResidentRunner Can handle an RPC exception from hot restart', () => testbed.run(() async { when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async { return 'Example'; }); when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async { return TargetPlatform.android_arm; }); when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async { return false; }); when(mockDevice.supportsHotRestart).thenReturn(true); final Completer onConnectionInfo = Completer.sync(); final Completer onAppStart = Completer.sync(); unawaited(residentRunner.attach( appStartedCompleter: onAppStart, connectionInfoCompleter: onConnectionInfo, )); await onAppStart.future; when(mockFlutterDevice.updateDevFS( mainPath: anyNamed('mainPath'), target: anyNamed('target'), bundle: anyNamed('bundle'), firstBuildTime: anyNamed('firstBuildTime'), bundleFirstUpload: anyNamed('bundleFirstUpload'), bundleDirty: anyNamed('bundleDirty'), fullRestart: anyNamed('fullRestart'), projectRootPath: anyNamed('projectRootPath'), pathToReload: anyNamed('pathToReload'), invalidatedFiles: anyNamed('invalidatedFiles'), dillOutputPath: anyNamed('dillOutputPath'), )).thenThrow(RpcException(666, 'something bad happened')); final OperationResult result = await residentRunner.restart(fullRestart: true); expect(result.fatal, true); expect(result.code, 1); verify(flutterUsage.sendEvent('hot', 'exception', parameters: { cdKey(CustomDimensions.hotEventTargetPlatform): getNameForTargetPlatform(TargetPlatform.android_arm), cdKey(CustomDimensions.hotEventSdkName): 'Example', cdKey(CustomDimensions.hotEventEmulator): 'false', cdKey(CustomDimensions.hotEventFullRestart): 'true', })).called(1); }, overrides: { Usage: () => MockUsage(), })); test('ResidentRunner uses temp directory when there is no output dill path', () => testbed.run(() { expect(residentRunner.artifactDirectory.path, contains('flutter_tool.')); final ResidentRunner otherRunner = HotRunner( [ mockFlutterDevice, ], stayResident: false, debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), dillOutputPath: fs.path.join('foobar', 'app.dill'), ); expect(otherRunner.artifactDirectory.path, contains('foobar')); })); test('ResidentRunner printHelpDetails', () => testbed.run(() { when(mockDevice.supportsHotRestart).thenReturn(true); when(mockDevice.supportsScreenshot).thenReturn(true); residentRunner.printHelp(details: true); // supports service protocol expect(residentRunner.supportsServiceProtocol, true); expect(testLogger.statusText, contains('"w"')); expect(testLogger.statusText, contains('"t"')); expect(testLogger.statusText, contains('"P"')); expect(testLogger.statusText, contains('"a"')); // isRunningDebug expect(residentRunner.isRunningDebug, true); expect(testLogger.statusText, contains('"L"')); expect(testLogger.statusText, contains('"S"')); expect(testLogger.statusText, contains('"U"')); expect(testLogger.statusText, contains('"i"')); expect(testLogger.statusText, contains('"p"')); expect(testLogger.statusText, contains('"o"')); expect(testLogger.statusText, contains('"z"')); // screenshot expect(testLogger.statusText, contains('"s"')); })); test('ResidentRunner can take screenshot on debug device', () => testbed.run(() async { when(mockDevice.supportsScreenshot).thenReturn(true); when(mockDevice.takeScreenshot(any)) .thenAnswer((Invocation invocation) async { final File file = invocation.positionalArguments.first as File; file.writeAsBytesSync(List.generate(1024, (int i) => i)); }); await residentRunner.screenshot(mockFlutterDevice); // disables debug banner. verify(mockIsolate.flutterDebugAllowBanner(false)).called(1); // Enables debug banner. verify(mockIsolate.flutterDebugAllowBanner(true)).called(1); expect(testLogger.statusText, contains('1kB')); })); test('ResidentRunner bails taking screenshot on debug device if debugAllowBanner throws pre', () => testbed.run(() async { when(mockDevice.supportsScreenshot).thenReturn(true); when(mockIsolate.flutterDebugAllowBanner(false)).thenThrow(Exception()); await residentRunner.screenshot(mockFlutterDevice); expect(testLogger.errorText, contains('Error')); })); test('ResidentRunner bails taking screenshot on debug device if debugAllowBanner throws post', () => testbed.run(() async { when(mockDevice.supportsScreenshot).thenReturn(true); when(mockIsolate.flutterDebugAllowBanner(true)).thenThrow(Exception()); await residentRunner.screenshot(mockFlutterDevice); expect(testLogger.errorText, contains('Error')); })); test('ResidentRunner bails taking screenshot on debug device if takeScreenshot throws', () => testbed.run(() async { when(mockDevice.supportsScreenshot).thenReturn(true); when(mockDevice.takeScreenshot(any)).thenThrow(Exception()); await residentRunner.screenshot(mockFlutterDevice); expect(testLogger.errorText, contains('Error')); })); test('ResidentRunner can\'t take screenshot on device without support', () => testbed.run(() { when(mockDevice.supportsScreenshot).thenReturn(false); expect(() => residentRunner.screenshot(mockFlutterDevice), throwsA(isInstanceOf())); })); test('ResidentRunner does not toggle banner in non-debug mode', () => testbed.run(() async { residentRunner = HotRunner( [ mockFlutterDevice, ], stayResident: false, debuggingOptions: DebuggingOptions.disabled(BuildInfo.release), ); when(mockDevice.supportsScreenshot).thenReturn(true); when(mockDevice.takeScreenshot(any)) .thenAnswer((Invocation invocation) async { final File file = invocation.positionalArguments.first as File; file.writeAsBytesSync(List.generate(1024, (int i) => i)); }); await residentRunner.screenshot(mockFlutterDevice); // doesn't disabled debug banner. verifyNever(mockIsolate.flutterDebugAllowBanner(false)); // doesn't enable debug banner. verifyNever(mockIsolate.flutterDebugAllowBanner(true)); expect(testLogger.statusText, contains('1kB')); })); test('FlutterDevice will not exit a paused isolate', () => testbed.run(() async { final TestFlutterDevice flutterDevice = TestFlutterDevice( mockDevice, [ mockFlutterView ], ); final MockServiceEvent mockServiceEvent = MockServiceEvent(); when(mockServiceEvent.isPauseEvent).thenReturn(true); when(mockIsolate.pauseEvent).thenReturn(mockServiceEvent); when(mockDevice.supportsFlutterExit).thenReturn(true); await flutterDevice.exitApps(); verifyNever(mockIsolate.flutterExit()); verify(mockDevice.stopApp(any)).called(1); })); test('FlutterDevice will exit an un-paused isolate', () => testbed.run(() async { final TestFlutterDevice flutterDevice = TestFlutterDevice( mockDevice, [mockFlutterView ], ); final MockServiceEvent mockServiceEvent = MockServiceEvent(); when(mockServiceEvent.isPauseEvent).thenReturn(false); when(mockIsolate.pauseEvent).thenReturn(mockServiceEvent); when(mockDevice.supportsFlutterExit).thenReturn(true); await flutterDevice.exitApps(); verify(mockIsolate.flutterExit()).called(1); })); test('ResidentRunner refreshViews calls flutter device', () => testbed.run(() async { await residentRunner.refreshViews(); verify(mockFlutterDevice.refreshViews()).called(1); })); test('ResidentRunner debugDumpApp calls flutter device', () => testbed.run(() async { await residentRunner.debugDumpApp(); verify(mockFlutterDevice.refreshViews()).called(1); verify(mockFlutterDevice.debugDumpApp()).called(1); })); test('ResidentRunner debugDumpRenderTree calls flutter device', () => testbed.run(() async { await residentRunner.debugDumpRenderTree(); verify(mockFlutterDevice.refreshViews()).called(1); verify(mockFlutterDevice.debugDumpRenderTree()).called(1); })); test('ResidentRunner debugDumpLayerTree calls flutter device', () => testbed.run(() async { await residentRunner.debugDumpLayerTree(); verify(mockFlutterDevice.refreshViews()).called(1); verify(mockFlutterDevice.debugDumpLayerTree()).called(1); })); test('ResidentRunner debugDumpSemanticsTreeInTraversalOrder calls flutter device', () => testbed.run(() async { await residentRunner.debugDumpSemanticsTreeInTraversalOrder(); verify(mockFlutterDevice.refreshViews()).called(1); verify(mockFlutterDevice.debugDumpSemanticsTreeInTraversalOrder()).called(1); })); test('ResidentRunner debugDumpSemanticsTreeInInverseHitTestOrder calls flutter device', () => testbed.run(() async { await residentRunner.debugDumpSemanticsTreeInInverseHitTestOrder(); verify(mockFlutterDevice.refreshViews()).called(1); verify(mockFlutterDevice.debugDumpSemanticsTreeInInverseHitTestOrder()).called(1); })); test('ResidentRunner debugToggleDebugPaintSizeEnabled calls flutter device', () => testbed.run(() async { await residentRunner.debugToggleDebugPaintSizeEnabled(); verify(mockFlutterDevice.refreshViews()).called(1); verify(mockFlutterDevice.toggleDebugPaintSizeEnabled()).called(1); })); test('ResidentRunner debugToggleDebugCheckElevationsEnabled calls flutter device', () => testbed.run(() async { await residentRunner.debugToggleDebugCheckElevationsEnabled(); verify(mockFlutterDevice.refreshViews()).called(1); verify(mockFlutterDevice.toggleDebugCheckElevationsEnabled()).called(1); })); test('ResidentRunner debugTogglePerformanceOverlayOverride calls flutter device', () => testbed.run(()async { await residentRunner.debugTogglePerformanceOverlayOverride(); verify(mockFlutterDevice.refreshViews()).called(1); verify(mockFlutterDevice.debugTogglePerformanceOverlayOverride()).called(1); })); test('ResidentRunner debugToggleWidgetInspector calls flutter device', () => testbed.run(() async { await residentRunner.debugToggleWidgetInspector(); verify(mockFlutterDevice.refreshViews()).called(1); verify(mockFlutterDevice.toggleWidgetInspector()).called(1); })); test('ResidentRunner debugToggleProfileWidgetBuilds calls flutter device', () => testbed.run(() async { await residentRunner.debugToggleProfileWidgetBuilds(); verify(mockFlutterDevice.refreshViews()).called(1); verify(mockFlutterDevice.toggleProfileWidgetBuilds()).called(1); })); test('HotRunner writes vm service file when providing debugging option', () => testbed.run(() async { fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); residentRunner = HotRunner( [ mockFlutterDevice, ], stayResident: false, debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, vmserviceOutFile: 'foo'), ); when(mockFlutterDevice.runHot( hotRunner: anyNamed('hotRunner'), route: anyNamed('route'), )).thenAnswer((Invocation invocation) async { return 0; }); await residentRunner.run(); expect(await fs.file('foo').readAsString(), testUri.toString()); })); test('HotRunner handles failure to write vmservice file', () => testbed.run(() async { fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); residentRunner = HotRunner( [ mockFlutterDevice, ], stayResident: false, debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, vmserviceOutFile: 'foo'), ); when(mockFlutterDevice.runHot( hotRunner: anyNamed('hotRunner'), route: anyNamed('route'), )).thenAnswer((Invocation invocation) async { return 0; }); await residentRunner.run(); expect(testLogger.errorText, contains('Failed to write vmservice-out-file at foo')); }, overrides: { FileSystem: () => ThrowingForwardingFileSystem(MemoryFileSystem()), })); test('ColdRunner writes vm service file when providing debugging option', () => testbed.run(() async { fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); residentRunner = ColdRunner( [ mockFlutterDevice, ], stayResident: false, debuggingOptions: DebuggingOptions.enabled(BuildInfo.profile, vmserviceOutFile: 'foo'), ); when(mockFlutterDevice.runCold( coldRunner: anyNamed('coldRunner'), route: anyNamed('route'), )).thenAnswer((Invocation invocation) async { return 0; }); await residentRunner.run(); expect(await fs.file('foo').readAsString(), testUri.toString()); })); test('FlutterDevice uses dartdevc configuration when targeting web', () => testbed.run(() async { final MockDevice mockDevice = MockDevice(); when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async { return TargetPlatform.web_javascript; }); final DefaultResidentCompiler residentCompiler = (await FlutterDevice.create( mockDevice, buildMode: BuildMode.debug, flutterProject: FlutterProject.current(), target: null, trackWidgetCreation: true, )).generator as DefaultResidentCompiler; expect(residentCompiler.targetModel, TargetModel.dartdevc); expect(residentCompiler.sdkRoot, artifacts.getArtifactPath(Artifact.flutterWebSdk, mode: BuildMode.debug) + '/'); expect( residentCompiler.platformDill, fs.file(artifacts.getArtifactPath(Artifact.webPlatformKernelDill, mode: BuildMode.debug)) .absolute.uri.toString(), ); }, overrides: { FeatureFlags: () => TestFeatureFlags(isWebIncrementalCompilerEnabled: true), })); test('connect sets up log reader', () => testbed.run(() async { final MockDevice mockDevice = MockDevice(); final MockDeviceLogReader mockLogReader = MockDeviceLogReader(); when(mockDevice.getLogReader(app: anyNamed('app'))).thenReturn(mockLogReader); final TestFlutterDevice flutterDevice = TestFlutterDevice( mockDevice, [], observatoryUris: Stream.value(testUri), ); await flutterDevice.connect(); verify(mockLogReader.connectedVMService = mockVMService); }, overrides: { VMServiceConnector: () => (Uri httpUri, { ReloadSources reloadSources, Restart restart, CompileExpression compileExpression, io.CompressionOptions compression, Device device, }) async => mockVMService, })); } class MockFlutterDevice extends Mock implements FlutterDevice {} class MockFlutterView extends Mock implements FlutterView {} class MockVMService extends Mock implements VMService {} class MockDevFS extends Mock implements DevFS {} class MockIsolate extends Mock implements Isolate {} class MockDevice extends Mock implements Device {} class MockDeviceLogReader extends Mock implements DeviceLogReader {} class MockUsage extends Mock implements Usage {} class MockProcessManager extends Mock implements ProcessManager {} class MockServiceEvent extends Mock implements ServiceEvent {} class TestFlutterDevice extends FlutterDevice { TestFlutterDevice(Device device, this.views, { Stream observatoryUris }) : super(device, buildMode: BuildMode.debug, trackWidgetCreation: false) { _observatoryUris = observatoryUris; } @override final List views; @override Stream get observatoryUris => _observatoryUris; Stream _observatoryUris; } class ThrowingForwardingFileSystem extends ForwardingFileSystem { ThrowingForwardingFileSystem(FileSystem delegate) : super(delegate); @override File file(dynamic path) { if (path == 'foo') { throw const FileSystemException(); } return delegate.file(path); } }