| // 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 'package:file/memory.dart'; |
| import 'package:flutter_tools/src/artifacts.dart'; |
| import 'package:flutter_tools/src/base/file_system.dart'; |
| import 'package:flutter_tools/src/base/io.dart'; |
| import 'package:flutter_tools/src/base/platform.dart'; |
| import 'package:flutter_tools/src/build_info.dart'; |
| import 'package:flutter_tools/src/dart/pub.dart'; |
| import 'package:flutter_tools/src/devfs.dart'; |
| import 'package:flutter_tools/src/device.dart'; |
| import 'package:flutter_tools/src/reporting/reporting.dart'; |
| import 'package:flutter_tools/src/resident_runner.dart'; |
| import 'package:flutter_tools/src/run_hot.dart'; |
| import 'package:flutter_tools/src/vmservice.dart'; |
| import 'package:unified_analytics/unified_analytics.dart'; |
| import 'package:vm_service/vm_service.dart' as vm_service; |
| |
| import '../src/common.dart'; |
| import '../src/context.dart'; |
| import '../src/fakes.dart'; |
| import '../src/package_config.dart'; |
| import '../src/throwing_pub.dart'; |
| import 'hot_shared.dart'; |
| |
| void main() { |
| group('validateReloadReport', () { |
| testUsingContext('invalid', () async { |
| expect( |
| HotRunner.validateReloadReport( |
| vm_service.ReloadReport.parse(<String, dynamic>{ |
| 'type': 'ReloadReport', |
| 'success': false, |
| 'details': <String, dynamic>{}, |
| }), |
| ), |
| false, |
| ); |
| expect( |
| HotRunner.validateReloadReport( |
| vm_service.ReloadReport.parse(<String, dynamic>{ |
| 'type': 'ReloadReport', |
| 'success': false, |
| 'details': <String, dynamic>{'notices': <Map<String, dynamic>>[]}, |
| }), |
| ), |
| false, |
| ); |
| expect( |
| HotRunner.validateReloadReport( |
| vm_service.ReloadReport.parse(<String, dynamic>{ |
| 'type': 'ReloadReport', |
| 'success': false, |
| 'details': <String, dynamic>{ |
| 'notices': <String, dynamic>{'message': 'error'}, |
| }, |
| }), |
| ), |
| false, |
| ); |
| expect( |
| HotRunner.validateReloadReport( |
| vm_service.ReloadReport.parse(<String, dynamic>{ |
| 'type': 'ReloadReport', |
| 'success': false, |
| 'details': <String, dynamic>{'notices': <Map<String, dynamic>>[]}, |
| }), |
| ), |
| false, |
| ); |
| expect( |
| HotRunner.validateReloadReport( |
| vm_service.ReloadReport.parse(<String, dynamic>{ |
| 'type': 'ReloadReport', |
| 'success': false, |
| 'details': <String, dynamic>{ |
| 'notices': <Map<String, dynamic>>[ |
| <String, dynamic>{'message': false}, |
| ], |
| }, |
| }), |
| ), |
| false, |
| ); |
| expect( |
| HotRunner.validateReloadReport( |
| vm_service.ReloadReport.parse(<String, dynamic>{ |
| 'type': 'ReloadReport', |
| 'success': false, |
| 'details': <String, dynamic>{ |
| 'notices': <Map<String, dynamic>>[ |
| <String, dynamic>{ |
| 'message': <String>['error'], |
| }, |
| ], |
| }, |
| }), |
| ), |
| false, |
| ); |
| expect( |
| HotRunner.validateReloadReport( |
| vm_service.ReloadReport.parse(<String, dynamic>{ |
| 'type': 'ReloadReport', |
| 'success': false, |
| 'details': <String, dynamic>{ |
| 'notices': <Map<String, dynamic>>[ |
| <String, dynamic>{'message': 'error'}, |
| <String, dynamic>{ |
| 'message': <String>['error'], |
| }, |
| ], |
| }, |
| }), |
| ), |
| false, |
| ); |
| expect( |
| HotRunner.validateReloadReport( |
| vm_service.ReloadReport.parse(<String, dynamic>{ |
| 'type': 'ReloadReport', |
| 'success': false, |
| 'details': <String, dynamic>{ |
| 'notices': <Map<String, dynamic>>[ |
| <String, dynamic>{'message': 'error'}, |
| ], |
| }, |
| }), |
| ), |
| false, |
| ); |
| expect( |
| HotRunner.validateReloadReport( |
| vm_service.ReloadReport.parse(<String, dynamic>{'type': 'ReloadReport', 'success': true}), |
| ), |
| true, |
| ); |
| }); |
| |
| testWithoutContext('ReasonForCancelling toString has a hint for specific errors', () { |
| final reasonForCancelling = ReasonForCancelling(message: 'Const class cannot remove fields'); |
| |
| expect(reasonForCancelling.toString(), contains('Try performing a hot restart instead.')); |
| }); |
| }); |
| |
| group('hotRestart', () { |
| final residentCompiler = FakeResidentCompiler(); |
| late MemoryFileSystem fileSystem; |
| late TestUsage testUsage; |
| late FakeAnalytics fakeAnalytics; |
| |
| setUp(() { |
| fileSystem = MemoryFileSystem.test(); |
| testUsage = TestUsage(); |
| fakeAnalytics = getInitializedFakeAnalyticsInstance( |
| fs: fileSystem, |
| fakeFlutterVersion: FakeFlutterVersion(), |
| ); |
| }); |
| |
| group('fails to setup', () { |
| late TestHotRunnerConfig failingTestingConfig; |
| setUp(() { |
| failingTestingConfig = TestHotRunnerConfig( |
| successfulHotRestartSetup: false, |
| successfulHotReloadSetup: false, |
| ); |
| }); |
| |
| testUsingContext( |
| 'setupHotRestart function fails', |
| () async { |
| fileSystem.file('pubspec.yaml').writeAsStringSync(''' |
| name: my_app |
| '''); |
| writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_app'); |
| final device = FakeDevice(); |
| final devices = <FlutterDevice>[FakeFlutterDevice(device)]; |
| final OperationResult result = await HotRunner( |
| devices, |
| debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), |
| target: 'main.dart', |
| analytics: fakeAnalytics, |
| ).restart(fullRestart: true); |
| expect(result.isOk, false); |
| expect(result.message, 'setupHotRestart failed'); |
| expect(failingTestingConfig.updateDevFSCompleteCalled, false); |
| }, |
| overrides: <Type, Generator>{ |
| HotRunnerConfig: () => failingTestingConfig, |
| Artifacts: () => Artifacts.test(), |
| FileSystem: () => fileSystem, |
| Platform: () => FakePlatform(), |
| ProcessManager: () => FakeProcessManager.any(), |
| Pub: ThrowingPub.new, |
| }, |
| ); |
| |
| testUsingContext( |
| 'setupHotReload function fails', |
| () async { |
| fileSystem.file('pubspec.yaml').writeAsStringSync(''' |
| name: my_app |
| '''); |
| writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_app'); |
| final device = FakeDevice(); |
| final fakeFlutterDevice = FakeFlutterDevice(device); |
| final devices = <FlutterDevice>[fakeFlutterDevice]; |
| final OperationResult result = await HotRunner( |
| devices, |
| debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), |
| target: 'main.dart', |
| reassembleHelper: |
| ( |
| List<FlutterDevice?> flutterDevices, |
| Map<FlutterDevice?, List<FlutterView>> viewCache, |
| void Function(String message)? onSlow, |
| String reloadMessage, |
| ) async => |
| ReassembleResult(<FlutterView?, FlutterVmService?>{null: null}, false, true), |
| analytics: fakeAnalytics, |
| ).restart(); |
| expect(result.isOk, false); |
| expect(result.message, 'setupHotReload failed'); |
| expect(failingTestingConfig.updateDevFSCompleteCalled, false); |
| }, |
| overrides: <Type, Generator>{ |
| HotRunnerConfig: () => failingTestingConfig, |
| Artifacts: () => Artifacts.test(), |
| FileSystem: () => fileSystem, |
| Platform: () => FakePlatform(), |
| ProcessManager: () => FakeProcessManager.any(), |
| Pub: ThrowingPub.new, |
| }, |
| ); |
| }); |
| |
| group('shutdown hook tests', () { |
| late TestHotRunnerConfig shutdownTestingConfig; |
| |
| setUp(() { |
| shutdownTestingConfig = TestHotRunnerConfig(); |
| }); |
| |
| testUsingContext( |
| 'shutdown hook called after signal', |
| () async { |
| writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_app'); |
| final device = FakeDevice(); |
| final devices = <FlutterDevice>[ |
| FlutterDevice( |
| device, |
| generator: residentCompiler, |
| buildInfo: BuildInfo.debug, |
| developmentShaderCompiler: const FakeShaderCompiler(), |
| ), |
| ]; |
| await HotRunner( |
| devices, |
| debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), |
| target: 'main.dart', |
| analytics: fakeAnalytics, |
| ).cleanupAfterSignal(); |
| expect(shutdownTestingConfig.shutdownHookCalled, true); |
| }, |
| overrides: <Type, Generator>{ |
| HotRunnerConfig: () => shutdownTestingConfig, |
| Artifacts: () => Artifacts.test(), |
| FileSystem: () => fileSystem, |
| Platform: () => FakePlatform(), |
| ProcessManager: () => FakeProcessManager.any(), |
| }, |
| ); |
| |
| testUsingContext( |
| 'shutdown hook called after app stop', |
| () async { |
| writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_app'); |
| final device = FakeDevice(); |
| final devices = <FlutterDevice>[ |
| FlutterDevice( |
| device, |
| generator: residentCompiler, |
| buildInfo: BuildInfo.debug, |
| developmentShaderCompiler: const FakeShaderCompiler(), |
| ), |
| ]; |
| await HotRunner( |
| devices, |
| debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), |
| target: 'main.dart', |
| analytics: fakeAnalytics, |
| ).preExit(); |
| expect(shutdownTestingConfig.shutdownHookCalled, true); |
| }, |
| overrides: <Type, Generator>{ |
| HotRunnerConfig: () => shutdownTestingConfig, |
| Artifacts: () => Artifacts.test(), |
| FileSystem: () => fileSystem, |
| Platform: () => FakePlatform(), |
| ProcessManager: () => FakeProcessManager.any(), |
| }, |
| ); |
| }); |
| |
| group('successful hot restart', () { |
| late TestHotRunnerConfig testingConfig; |
| setUp(() { |
| testingConfig = TestHotRunnerConfig(successfulHotRestartSetup: true); |
| }); |
| testUsingContext( |
| 'correctly tracks time spent for analytics for hot restart', |
| () async { |
| final device = FakeDevice(); |
| final fakeFlutterDevice = FakeFlutterDevice(device); |
| final devices = <FlutterDevice>[fakeFlutterDevice]; |
| |
| fakeFlutterDevice.updateDevFSReportCallback = () async => UpdateFSReport( |
| success: true, |
| invalidatedSourcesCount: 2, |
| syncedBytes: 4, |
| scannedSourcesCount: 8, |
| compileDuration: const Duration(seconds: 16), |
| transferDuration: const Duration(seconds: 32), |
| ); |
| |
| final fakeStopwatchFactory = FakeStopwatchFactory( |
| stopwatches: <String, Stopwatch>{ |
| 'fullRestartHelper': FakeStopwatch()..elapsed = const Duration(seconds: 64), |
| 'updateDevFS': FakeStopwatch()..elapsed = const Duration(seconds: 128), |
| }, |
| ); |
| |
| (fakeFlutterDevice.devFS! as FakeDevFs).baseUri = Uri.parse('file:///base_uri'); |
| |
| final OperationResult result = await HotRunner( |
| devices, |
| debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), |
| target: 'main.dart', |
| stopwatchFactory: fakeStopwatchFactory, |
| analytics: fakeAnalytics, |
| ).restart(fullRestart: true); |
| |
| expect(result.isOk, true); |
| expect(testUsage.events, <TestUsageEvent>[ |
| const TestUsageEvent( |
| 'hot', |
| 'restart', |
| parameters: CustomDimensions( |
| hotEventTargetPlatform: 'flutter-tester', |
| hotEventSdkName: 'Tester', |
| hotEventEmulator: false, |
| hotEventFullRestart: true, |
| hotEventOverallTimeInMs: 64000, |
| hotEventSyncedBytes: 4, |
| hotEventInvalidatedSourcesCount: 2, |
| hotEventTransferTimeInMs: 32000, |
| hotEventCompileTimeInMs: 16000, |
| hotEventFindInvalidatedTimeInMs: 128000, |
| hotEventScannedSourcesCount: 8, |
| ), |
| ), |
| ]); |
| |
| expect( |
| fakeAnalytics.sentEvents, |
| contains( |
| Event.hotRunnerInfo( |
| label: 'restart', |
| targetPlatform: 'flutter-tester', |
| sdkName: 'Tester', |
| emulator: false, |
| fullRestart: true, |
| syncedBytes: 4, |
| invalidatedSourcesCount: 2, |
| transferTimeInMs: 32000, |
| overallTimeInMs: 64000, |
| compileTimeInMs: 16000, |
| findInvalidatedTimeInMs: 128000, |
| scannedSourcesCount: 8, |
| ), |
| ), |
| ); |
| expect(testingConfig.updateDevFSCompleteCalled, true); |
| }, |
| overrides: <Type, Generator>{ |
| HotRunnerConfig: () => testingConfig, |
| Artifacts: () => Artifacts.test(), |
| FileSystem: () => fileSystem, |
| Platform: () => FakePlatform(), |
| ProcessManager: () => FakeProcessManager.any(), |
| Usage: () => testUsage, |
| }, |
| ); |
| }); |
| |
| group('successful hot reload', () { |
| late TestHotRunnerConfig testingConfig; |
| setUp(() { |
| testingConfig = TestHotRunnerConfig(successfulHotReloadSetup: true); |
| }); |
| testUsingContext( |
| 'correctly tracks time spent for analytics for hot reload', |
| () async { |
| final device = FakeDevice(); |
| final fakeFlutterDevice = FakeFlutterDevice(device); |
| final devices = <FlutterDevice>[fakeFlutterDevice]; |
| |
| fakeFlutterDevice.updateDevFSReportCallback = () async => UpdateFSReport( |
| success: true, |
| invalidatedSourcesCount: 6, |
| syncedBytes: 8, |
| scannedSourcesCount: 16, |
| compileDuration: const Duration(seconds: 16), |
| transferDuration: const Duration(seconds: 32), |
| ); |
| |
| final fakeStopwatchFactory = FakeStopwatchFactory( |
| stopwatches: <String, Stopwatch>{ |
| 'updateDevFS': FakeStopwatch()..elapsed = const Duration(seconds: 64), |
| 'reloadSources:reload': FakeStopwatch()..elapsed = const Duration(seconds: 128), |
| 'reloadSources:reassemble': FakeStopwatch()..elapsed = const Duration(seconds: 256), |
| 'reloadSources:vm': FakeStopwatch()..elapsed = const Duration(seconds: 512), |
| }, |
| ); |
| |
| (fakeFlutterDevice.devFS! as FakeDevFs).baseUri = Uri.parse('file:///base_uri'); |
| |
| final OperationResult result = await HotRunner( |
| devices, |
| debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), |
| target: 'main.dart', |
| stopwatchFactory: fakeStopwatchFactory, |
| analytics: fakeAnalytics, |
| reloadSourcesHelper: |
| ( |
| HotRunner hotRunner, |
| List<FlutterDevice?> flutterDevices, |
| bool? pause, |
| Map<String, dynamic> firstReloadDetails, |
| String? targetPlatform, |
| String? sdkName, |
| bool? emulator, |
| String? reason, |
| Analytics? analytics, |
| ) async { |
| firstReloadDetails['finalLibraryCount'] = 2; |
| firstReloadDetails['receivedLibraryCount'] = 3; |
| firstReloadDetails['receivedClassesCount'] = 4; |
| firstReloadDetails['receivedProceduresCount'] = 5; |
| return OperationResult.ok; |
| }, |
| reassembleHelper: |
| ( |
| List<FlutterDevice?> flutterDevices, |
| Map<FlutterDevice?, List<FlutterView>> viewCache, |
| void Function(String message)? onSlow, |
| String reloadMessage, |
| ) async => |
| ReassembleResult(<FlutterView?, FlutterVmService?>{null: null}, false, true), |
| ).restart(); |
| |
| expect(result.isOk, true); |
| expect(testUsage.events, <TestUsageEvent>[ |
| const TestUsageEvent( |
| 'hot', |
| 'reload', |
| parameters: CustomDimensions( |
| hotEventFinalLibraryCount: 2, |
| hotEventSyncedLibraryCount: 3, |
| hotEventSyncedClassesCount: 4, |
| hotEventSyncedProceduresCount: 5, |
| hotEventSyncedBytes: 8, |
| hotEventInvalidatedSourcesCount: 6, |
| hotEventTransferTimeInMs: 32000, |
| hotEventOverallTimeInMs: 128000, |
| hotEventTargetPlatform: 'flutter-tester', |
| hotEventSdkName: 'Tester', |
| hotEventEmulator: false, |
| hotEventFullRestart: false, |
| hotEventCompileTimeInMs: 16000, |
| hotEventFindInvalidatedTimeInMs: 64000, |
| hotEventScannedSourcesCount: 16, |
| hotEventReassembleTimeInMs: 256000, |
| hotEventReloadVMTimeInMs: 512000, |
| ), |
| ), |
| ]); |
| expect( |
| fakeAnalytics.sentEvents, |
| contains( |
| Event.hotRunnerInfo( |
| label: 'reload', |
| targetPlatform: 'flutter-tester', |
| sdkName: 'Tester', |
| emulator: false, |
| fullRestart: false, |
| finalLibraryCount: 2, |
| syncedLibraryCount: 3, |
| syncedClassesCount: 4, |
| syncedProceduresCount: 5, |
| syncedBytes: 8, |
| invalidatedSourcesCount: 6, |
| transferTimeInMs: 32000, |
| overallTimeInMs: 128000, |
| compileTimeInMs: 16000, |
| findInvalidatedTimeInMs: 64000, |
| scannedSourcesCount: 16, |
| reassembleTimeInMs: 256000, |
| reloadVMTimeInMs: 512000, |
| ), |
| ), |
| ); |
| expect(testingConfig.updateDevFSCompleteCalled, true); |
| }, |
| overrides: <Type, Generator>{ |
| HotRunnerConfig: () => testingConfig, |
| Artifacts: () => Artifacts.test(), |
| FileSystem: () => fileSystem, |
| Platform: () => FakePlatform(), |
| ProcessManager: () => FakeProcessManager.any(), |
| Usage: () => testUsage, |
| }, |
| ); |
| }); |
| |
| group('hot restart that failed to sync dev fs', () { |
| late TestHotRunnerConfig testingConfig; |
| setUp(() { |
| testingConfig = TestHotRunnerConfig(successfulHotRestartSetup: true); |
| }); |
| testUsingContext( |
| 'still calls the devfs complete callback', |
| () async { |
| final device = FakeDevice(); |
| final fakeFlutterDevice = FakeFlutterDevice(device); |
| final devices = <FlutterDevice>[fakeFlutterDevice]; |
| fakeFlutterDevice.updateDevFSReportCallback = () async => |
| throw Exception('updateDevFS failed'); |
| |
| final runner = HotRunner( |
| devices, |
| debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), |
| target: 'main.dart', |
| analytics: fakeAnalytics, |
| ); |
| |
| await expectLater( |
| runner.restart(fullRestart: true), |
| throwsA( |
| isA<Exception>().having( |
| (Exception e) => e.toString(), |
| 'message', |
| 'Exception: updateDevFS failed', |
| ), |
| ), |
| ); |
| expect(testingConfig.updateDevFSCompleteCalled, true); |
| }, |
| overrides: <Type, Generator>{ |
| HotRunnerConfig: () => testingConfig, |
| Artifacts: () => Artifacts.test(), |
| FileSystem: () => fileSystem, |
| Platform: () => FakePlatform(), |
| ProcessManager: () => FakeProcessManager.any(), |
| Usage: () => testUsage, |
| }, |
| ); |
| }); |
| |
| group('hot reload that failed to sync dev fs', () { |
| late TestHotRunnerConfig testingConfig; |
| setUp(() { |
| testingConfig = TestHotRunnerConfig(successfulHotReloadSetup: true); |
| }); |
| testUsingContext( |
| 'still calls the devfs complete callback', |
| () async { |
| final device = FakeDevice(); |
| final fakeFlutterDevice = FakeFlutterDevice(device); |
| final devices = <FlutterDevice>[fakeFlutterDevice]; |
| fakeFlutterDevice.updateDevFSReportCallback = () async => |
| throw Exception('updateDevFS failed'); |
| |
| final runner = HotRunner( |
| devices, |
| debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), |
| target: 'main.dart', |
| analytics: fakeAnalytics, |
| ); |
| |
| await expectLater( |
| runner.restart(), |
| throwsA( |
| isA<Exception>().having( |
| (Exception e) => e.toString(), |
| 'message', |
| 'Exception: updateDevFS failed', |
| ), |
| ), |
| ); |
| expect(testingConfig.updateDevFSCompleteCalled, true); |
| }, |
| overrides: <Type, Generator>{ |
| HotRunnerConfig: () => testingConfig, |
| Artifacts: () => Artifacts.test(), |
| FileSystem: () => fileSystem, |
| Platform: () => FakePlatform(), |
| ProcessManager: () => FakeProcessManager.any(), |
| Usage: () => testUsage, |
| }, |
| ); |
| }); |
| }); |
| |
| group('hot attach', () { |
| late MemoryFileSystem fileSystem; |
| late FakeAnalytics fakeAnalytics; |
| |
| setUp(() { |
| fileSystem = MemoryFileSystem.test(); |
| fakeAnalytics = getInitializedFakeAnalyticsInstance( |
| fs: fileSystem, |
| fakeFlutterVersion: FakeFlutterVersion(), |
| ); |
| }); |
| |
| testUsingContext( |
| 'Exits with code 2 when HttpException is thrown ' |
| 'during VM service connection', |
| () async { |
| writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_app'); |
| |
| final residentCompiler = FakeResidentCompiler(); |
| final device = FakeDevice(); |
| final devices = <FlutterDevice>[ |
| TestFlutterDevice( |
| device: device, |
| generator: residentCompiler, |
| exception: const HttpException( |
| 'Connection closed before full header was received, ' |
| 'uri = http://127.0.0.1:63394/5ZmLv8A59xY=/ws', |
| ), |
| ), |
| ]; |
| |
| final int exitCode = await HotRunner( |
| devices, |
| debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), |
| target: 'main.dart', |
| analytics: fakeAnalytics, |
| ).attach(needsFullRestart: false); |
| expect(exitCode, 2); |
| }, |
| overrides: <Type, Generator>{ |
| HotRunnerConfig: () => TestHotRunnerConfig(), |
| Artifacts: () => Artifacts.test(), |
| FileSystem: () => fileSystem, |
| Platform: () => FakePlatform(), |
| ProcessManager: () => FakeProcessManager.any(), |
| }, |
| ); |
| }); |
| |
| group('hot cleanupAtFinish()', () { |
| late MemoryFileSystem fileSystem; |
| late FakeAnalytics fakeAnalytics; |
| |
| setUp(() { |
| fileSystem = MemoryFileSystem.test(); |
| fakeAnalytics = getInitializedFakeAnalyticsInstance( |
| fs: fileSystem, |
| fakeFlutterVersion: FakeFlutterVersion(), |
| ); |
| }); |
| |
| testUsingContext('disposes each device', () async { |
| final device1 = FakeDevice(); |
| final device2 = FakeDevice(); |
| final flutterDevice1 = FakeFlutterDevice(device1); |
| final flutterDevice2 = FakeFlutterDevice(device2); |
| |
| final devices = <FlutterDevice>[flutterDevice1, flutterDevice2]; |
| |
| await HotRunner( |
| devices, |
| debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), |
| target: 'main.dart', |
| analytics: fakeAnalytics, |
| ).cleanupAtFinish(); |
| |
| expect(device1.disposed, true); |
| expect(device2.disposed, true); |
| |
| expect(flutterDevice1.stoppedEchoingDeviceLog, true); |
| expect(flutterDevice2.stoppedEchoingDeviceLog, true); |
| }); |
| }); |
| } |