// 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. // @dart = 2.8 import 'package:file/memory.dart'; import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/asset.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/compile.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_devtools_handler.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:meta/meta.dart'; import 'package:package_config/package_config.dart'; import 'package:test/fake.dart'; import 'package:vm_service/vm_service.dart' as vm_service; import '../src/common.dart'; import '../src/context.dart'; import '../src/fake_vm_services.dart'; import '../src/fakes.dart'; final vm_service.Isolate fakeUnpausedIsolate = vm_service.Isolate( id: '1', pauseEvent: vm_service.Event( kind: vm_service.EventKind.kResume, timestamp: 0 ), breakpoints: <vm_service.Breakpoint>[], exceptionPauseMode: null, libraries: <vm_service.LibraryRef>[], livePorts: 0, name: 'test', number: '1', pauseOnExit: false, runnable: true, startTime: 0, isSystemIsolate: false, isolateFlags: <vm_service.IsolateFlag>[], ); final FlutterView fakeFlutterView = FlutterView( id: 'a', uiIsolate: fakeUnpausedIsolate, ); final FakeVmServiceRequest listViews = FakeVmServiceRequest( method: kListViewsMethod, jsonResponse: <String, Object>{ 'views': <Object>[ fakeFlutterView.toJson(), ], }, ); 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 = ReasonForCancelling( message: 'Const class cannot remove fields', ); expect(reasonForCancelling.toString(), contains('Try performing a hot restart instead.')); }); }); group('hotRestart', () { final FakeResidentCompiler residentCompiler = FakeResidentCompiler(); FileSystem fileSystem; TestUsage testUsage; setUp(() { fileSystem = MemoryFileSystem.test(); testUsage = TestUsage(); }); group('fails to setup', () { TestHotRunnerConfig failingTestingConfig; setUp(() { failingTestingConfig = TestHotRunnerConfig( successfulHotRestartSetup: false, successfulHotReloadSetup: false, ); }); testUsingContext('setupHotRestart function fails', () async { fileSystem.file('.packages') ..createSync(recursive: true) ..writeAsStringSync('\n'); final FakeDevice device = FakeDevice(); final List<FlutterDevice> devices = <FlutterDevice>[ FlutterDevice(device, generator: residentCompiler, buildInfo: BuildInfo.debug)..devFS = FakeDevFs(), ]; final OperationResult result = await HotRunner( devices, debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), target: 'main.dart', devtoolsHandler: createNoOpHandler, ).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(), }); testUsingContext('setupHotReload function fails', () async { fileSystem.file('.packages') ..createSync(recursive: true) ..writeAsStringSync('\n'); final FakeDevice device = FakeDevice(); final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device); final List<FlutterDevice> devices = <FlutterDevice>[ fakeFlutterDevice, ]; final OperationResult result = await HotRunner( devices, debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), target: 'main.dart', devtoolsHandler: createNoOpHandler, reassembleHelper: ( List<FlutterDevice> flutterDevices, Map<FlutterDevice, List<FlutterView>> viewCache, void Function(String message) onSlow, String reloadMessage, String fastReassembleClassName, ) async => ReassembleResult( <FlutterView, FlutterVmService>{null: null}, false, true, ), ).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(), }); }); group('shutdown hook tests', () { TestHotRunnerConfig shutdownTestingConfig; setUp(() { shutdownTestingConfig = TestHotRunnerConfig(); }); testUsingContext('shutdown hook called after signal', () async { fileSystem.file('.packages') ..createSync(recursive: true) ..writeAsStringSync('\n'); final FakeDevice device = FakeDevice(); final List<FlutterDevice> devices = <FlutterDevice>[ FlutterDevice(device, generator: residentCompiler, buildInfo: BuildInfo.debug), ]; await HotRunner( devices, debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), target: 'main.dart', ).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 { fileSystem.file('.packages') ..createSync(recursive: true) ..writeAsStringSync('\n'); final FakeDevice device = FakeDevice(); final List<FlutterDevice> devices = <FlutterDevice>[ FlutterDevice(device, generator: residentCompiler, buildInfo: BuildInfo.debug), ]; await HotRunner( devices, debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), target: 'main.dart', ).preExit(); expect(shutdownTestingConfig.shutdownHookCalled, true); }, overrides: <Type, Generator>{ HotRunnerConfig: () => shutdownTestingConfig, Artifacts: () => Artifacts.test(), FileSystem: () => fileSystem, Platform: () => FakePlatform(), ProcessManager: () => FakeProcessManager.any(), }); }); group('successful hot restart', () { TestHotRunnerConfig testingConfig; setUp(() { testingConfig = TestHotRunnerConfig( successfulHotRestartSetup: true, ); }); testUsingContext('correctly tracks time spent for analytics for hot restart', () async { final FakeDevice device = FakeDevice(); final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device); final List<FlutterDevice> 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 = 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', devtoolsHandler: createNoOpHandler, stopwatchFactory: fakeStopwatchFactory, ).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, fastReassemble: false, hotEventOverallTimeInMs: 64000, hotEventSyncedBytes: 4, hotEventInvalidatedSourcesCount: 2, hotEventTransferTimeInMs: 32000, hotEventCompileTimeInMs: 16000, hotEventFindInvalidatedTimeInMs: 128000, hotEventScannedSourcesCount: 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', () { TestHotRunnerConfig testingConfig; setUp(() { testingConfig = TestHotRunnerConfig( successfulHotReloadSetup: true, ); }); testUsingContext('correctly tracks time spent for analytics for hot reload', () async { final FakeDevice device = FakeDevice(); final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device); final List<FlutterDevice> 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 = 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', devtoolsHandler: createNoOpHandler, stopwatchFactory: fakeStopwatchFactory, reloadSourcesHelper: ( HotRunner hotRunner, List<FlutterDevice> flutterDevices, bool pause, Map<String, dynamic> firstReloadDetails, String targetPlatform, String sdkName, bool emulator, String reason, ) 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, String fastReassembleClassName, ) 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, fastReassemble: false, hotEventCompileTimeInMs: 16000, hotEventFindInvalidatedTimeInMs: 64000, hotEventScannedSourcesCount: 16, hotEventReassembleTimeInMs: 256000, hotEventReloadVMTimeInMs: 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', () { TestHotRunnerConfig testingConfig; setUp(() { testingConfig = TestHotRunnerConfig( successfulHotRestartSetup: true, ); }); testUsingContext('still calls the devfs complete callback', () async { final FakeDevice device = FakeDevice(); final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device); final List<FlutterDevice> devices = <FlutterDevice>[ fakeFlutterDevice, ]; fakeFlutterDevice.updateDevFSReportCallback = () async => throw Exception('updateDevFS failed'); final HotRunner runner = HotRunner( devices, debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), target: 'main.dart', devtoolsHandler: createNoOpHandler, ); 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', () { TestHotRunnerConfig testingConfig; setUp(() { testingConfig = TestHotRunnerConfig( successfulHotReloadSetup: true, ); }); testUsingContext('still calls the devfs complete callback', () async { final FakeDevice device = FakeDevice(); final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device); final List<FlutterDevice> devices = <FlutterDevice>[ fakeFlutterDevice, ]; fakeFlutterDevice.updateDevFSReportCallback = () async => throw Exception('updateDevFS failed'); final HotRunner runner = HotRunner( devices, debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), target: 'main.dart', devtoolsHandler: createNoOpHandler, ); 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', () { FileSystem fileSystem; setUp(() { fileSystem = MemoryFileSystem.test(); }); testUsingContext('Exits with code 2 when HttpException is thrown ' 'during VM service connection', () async { fileSystem.file('.packages') ..createSync(recursive: true) ..writeAsStringSync('\n'); final FakeResidentCompiler residentCompiler = FakeResidentCompiler(); final FakeDevice device = FakeDevice(); final List<FlutterDevice> 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', ).attach(); expect(exitCode, 2); }, overrides: <Type, Generator>{ HotRunnerConfig: () => TestHotRunnerConfig(), Artifacts: () => Artifacts.test(), FileSystem: () => fileSystem, Platform: () => FakePlatform(), ProcessManager: () => FakeProcessManager.any(), }); }); group('hot cleanupAtFinish()', () { testUsingContext('disposes each device', () async { final FakeDevice device1 = FakeDevice(); final FakeDevice device2 = FakeDevice(); final FakeFlutterDevice flutterDevice1 = FakeFlutterDevice(device1); final FakeFlutterDevice flutterDevice2 = FakeFlutterDevice(device2); final List<FlutterDevice> devices = <FlutterDevice>[ flutterDevice1, flutterDevice2, ]; await HotRunner(devices, debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), target: 'main.dart', ).cleanupAtFinish(); expect(device1.disposed, true); expect(device2.disposed, true); expect(flutterDevice1.stoppedEchoingDeviceLog, true); expect(flutterDevice2.stoppedEchoingDeviceLog, true); }); }); } class FakeDevFs extends Fake implements DevFS { @override Future<void> destroy() async { } @override List<Uri> sources = <Uri>[]; @override DateTime lastCompiled; @override PackageConfig lastPackageConfig; @override Set<String> assetPathsToEvict = <String>{}; @override Uri baseUri; } // Unfortunately Device, despite not being immutable, has an `operator ==`. // Until we fix that, we have to also ignore related lints here. // ignore: avoid_implementing_value_types class FakeDevice extends Fake implements Device { bool disposed = false; @override bool isSupported() => true; @override bool supportsHotReload = true; @override bool supportsHotRestart = true; @override bool supportsFlutterExit = true; @override Future<TargetPlatform> get targetPlatform async => TargetPlatform.tester; @override Future<String> get sdkNameAndVersion async => 'Tester'; @override Future<bool> get isLocalEmulator async => false; @override String get name => 'Fake Device'; @override Future<bool> stopApp( covariant ApplicationPackage app, { String userIdentifier, }) async { return true; } @override Future<void> dispose() async { disposed = true; } } class FakeFlutterDevice extends Fake implements FlutterDevice { FakeFlutterDevice(this.device); bool stoppedEchoingDeviceLog = false; Future<UpdateFSReport> Function() updateDevFSReportCallback; @override final FakeDevice device; @override Future<void> stopEchoingDeviceLog() async { stoppedEchoingDeviceLog = true; } @override DevFS devFS = FakeDevFs(); @override FlutterVmService get vmService => FakeFlutterVmService(); @override ResidentCompiler generator; @override Future<UpdateFSReport> updateDevFS({ Uri mainUri, String target, AssetBundle bundle, DateTime firstBuildTime, bool bundleFirstUpload = false, bool bundleDirty = false, bool fullRestart = false, String projectRootPath, String pathToReload, @required String dillOutputPath, @required List<Uri> invalidatedFiles, @required PackageConfig packageConfig, }) => updateDevFSReportCallback(); } class TestFlutterDevice extends FlutterDevice { TestFlutterDevice({ @required Device device, @required this.exception, @required ResidentCompiler generator, }) : assert(exception != null), super(device, buildInfo: BuildInfo.debug, generator: generator); /// The exception to throw when the connect method is called. final Exception exception; @override Future<void> connect({ ReloadSources reloadSources, Restart restart, CompileExpression compileExpression, GetSkSLMethod getSkSLMethod, PrintStructuredErrorLogMethod printStructuredErrorLogMethod, bool disableServiceAuthCodes = false, bool enableDds = true, bool cacheStartupProfile = false, bool ipv6 = false, int hostVmServicePort, int ddsPort, bool allowExistingDdsInstance = false, }) async { throw exception; } } class TestHotRunnerConfig extends HotRunnerConfig { TestHotRunnerConfig({this.successfulHotRestartSetup, this.successfulHotReloadSetup}); bool successfulHotRestartSetup; bool successfulHotReloadSetup; bool shutdownHookCalled = false; bool updateDevFSCompleteCalled = false; @override Future<bool> setupHotRestart() async { assert(successfulHotRestartSetup != null, 'setupHotRestart is not expected to be called in this test.'); return successfulHotRestartSetup; } @override Future<bool> setupHotReload() async { assert(successfulHotReloadSetup != null, 'setupHotReload is not expected to be called in this test.'); return successfulHotReloadSetup; } @override void updateDevFSComplete() { updateDevFSCompleteCalled = true; } @override Future<void> runPreShutdownOperations() async { shutdownHookCalled = true; } } class FakeResidentCompiler extends Fake implements ResidentCompiler { @override void accept() {} } class FakeFlutterVmService extends Fake implements FlutterVmService { @override vm_service.VmService get service => FakeVmService(); @override Future<List<FlutterView>> getFlutterViews({bool returnEarly = false, Duration delay = const Duration(milliseconds: 50)}) async { return <FlutterView>[]; } } class FakeVmService extends Fake implements vm_service.VmService { @override Future<vm_service.VM> getVM() async => FakeVm(); } class FakeVm extends Fake implements vm_service.VM { @override List<vm_service.IsolateRef> get isolates => <vm_service.IsolateRef>[]; }