// 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/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: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 'hot_shared.dart'; void main() { group('validateReloadReport', () { testUsingContext('invalid', () async { expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse({ 'type': 'ReloadReport', 'success': false, 'details': {}, })), false); expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse({ 'type': 'ReloadReport', 'success': false, 'details': { 'notices': >[ ], }, })), false); expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse({ 'type': 'ReloadReport', 'success': false, 'details': { 'notices': { 'message': 'error', }, }, })), false); expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse({ 'type': 'ReloadReport', 'success': false, 'details': { 'notices': >[], }, })), false); expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse({ 'type': 'ReloadReport', 'success': false, 'details': { 'notices': >[ {'message': false}, ], }, })), false); expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse({ 'type': 'ReloadReport', 'success': false, 'details': { 'notices': >[ {'message': ['error']}, ], }, })), false); expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse({ 'type': 'ReloadReport', 'success': false, 'details': { 'notices': >[ {'message': 'error'}, {'message': ['error']}, ], }, })), false); expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse({ 'type': 'ReloadReport', 'success': false, 'details': { 'notices': >[ {'message': 'error'}, ], }, })), false); expect(HotRunner.validateReloadReport(vm_service.ReloadReport.parse({ '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(); 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.directory('.dart_tool').childFile('package_config.json') ..createSync(recursive: true) ..writeAsStringSync(''' { "configVersion": 2, "packages": [] } '''); final FakeDevice device = FakeDevice(); final List devices = [ FakeFlutterDevice(device), ]; final OperationResult result = await HotRunner( devices, debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), target: 'main.dart', devtoolsHandler: createNoOpHandler, useImplicitPubspecResolution: true, analytics: fakeAnalytics, ).restart(fullRestart: true); expect(result.isOk, false); expect(result.message, 'setupHotRestart failed'); expect(failingTestingConfig.updateDevFSCompleteCalled, false); }, overrides: { HotRunnerConfig: () => failingTestingConfig, Artifacts: () => Artifacts.test(), FileSystem: () => fileSystem, Platform: () => FakePlatform(), ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('setupHotReload function fails', () async { fileSystem.directory('.dart_tool').childFile('package_config.json') ..createSync(recursive: true) ..writeAsStringSync(''' { "configVersion": 2, "packages": [] } '''); final FakeDevice device = FakeDevice(); final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device); final List devices = [ fakeFlutterDevice, ]; final OperationResult result = await HotRunner( devices, debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), target: 'main.dart', devtoolsHandler: createNoOpHandler, useImplicitPubspecResolution: true, reassembleHelper: ( List flutterDevices, Map> viewCache, void Function(String message)? onSlow, String reloadMessage, ) async => ReassembleResult( {null: null}, false, true, ), analytics: fakeAnalytics, ).restart(); expect(result.isOk, false); expect(result.message, 'setupHotReload failed'); expect(failingTestingConfig.updateDevFSCompleteCalled, false); }, overrides: { HotRunnerConfig: () => failingTestingConfig, Artifacts: () => Artifacts.test(), FileSystem: () => fileSystem, Platform: () => FakePlatform(), ProcessManager: () => FakeProcessManager.any(), }); }); group('shutdown hook tests', () { late TestHotRunnerConfig shutdownTestingConfig; setUp(() { shutdownTestingConfig = TestHotRunnerConfig(); }); testUsingContext('shutdown hook called after signal', () async { fileSystem.directory('.dart_tool').childFile('package_config.json') ..createSync(recursive: true) ..writeAsStringSync(''' { "configVersion": 2, "packages": [] } '''); final FakeDevice device = FakeDevice(); final List devices = [ FlutterDevice(device, generator: residentCompiler, buildInfo: BuildInfo.debug, developmentShaderCompiler: const FakeShaderCompiler()), ]; await HotRunner( devices, debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), target: 'main.dart', analytics: fakeAnalytics, useImplicitPubspecResolution: true, ).cleanupAfterSignal(); expect(shutdownTestingConfig.shutdownHookCalled, true); }, overrides: { HotRunnerConfig: () => shutdownTestingConfig, Artifacts: () => Artifacts.test(), FileSystem: () => fileSystem, Platform: () => FakePlatform(), ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('shutdown hook called after app stop', () async { fileSystem.directory('.dart_tool').childFile('package_config.json') ..createSync(recursive: true) ..writeAsStringSync(''' { "configVersion": 2, "packages": [] } '''); final FakeDevice device = FakeDevice(); final List devices = [ FlutterDevice(device, generator: residentCompiler, buildInfo: BuildInfo.debug, developmentShaderCompiler: const FakeShaderCompiler()), ]; await HotRunner( devices, debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), target: 'main.dart', analytics: fakeAnalytics, useImplicitPubspecResolution: true, ).preExit(); expect(shutdownTestingConfig.shutdownHookCalled, true); }, overrides: { 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 FakeDevice device = FakeDevice(); final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device); final List devices = [ 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: { '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, analytics: fakeAnalytics, useImplicitPubspecResolution: true, ).restart(fullRestart: true); expect(result.isOk, true); expect(testUsage.events, [ 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: { 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 FakeDevice device = FakeDevice(); final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device); final List devices = [ 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: { '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, analytics: fakeAnalytics, useImplicitPubspecResolution: true, reloadSourcesHelper: ( HotRunner hotRunner, List flutterDevices, bool? pause, Map firstReloadDetails, String? targetPlatform, String? sdkName, bool? emulator, String? reason, Usage usage, Analytics? analytics, ) async { firstReloadDetails['finalLibraryCount'] = 2; firstReloadDetails['receivedLibraryCount'] = 3; firstReloadDetails['receivedClassesCount'] = 4; firstReloadDetails['receivedProceduresCount'] = 5; return OperationResult.ok; }, reassembleHelper: ( List flutterDevices, Map> viewCache, void Function(String message)? onSlow, String reloadMessage, ) async => ReassembleResult( {null: null}, false, true, ), ).restart(); expect(result.isOk, true); expect(testUsage.events, [ 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: { 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 FakeDevice device = FakeDevice(); final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device); final List devices = [ fakeFlutterDevice, ]; fakeFlutterDevice.updateDevFSReportCallback = () async => throw Exception('updateDevFS failed'); final HotRunner runner = HotRunner( devices, debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), target: 'main.dart', devtoolsHandler: createNoOpHandler, analytics: fakeAnalytics, useImplicitPubspecResolution: true, ); await expectLater(runner.restart(fullRestart: true), throwsA(isA().having((Exception e) => e.toString(), 'message', 'Exception: updateDevFS failed'))); expect(testingConfig.updateDevFSCompleteCalled, true); }, overrides: { 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 FakeDevice device = FakeDevice(); final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device); final List devices = [ fakeFlutterDevice, ]; fakeFlutterDevice.updateDevFSReportCallback = () async => throw Exception('updateDevFS failed'); final HotRunner runner = HotRunner( devices, debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug), target: 'main.dart', devtoolsHandler: createNoOpHandler, analytics: fakeAnalytics, useImplicitPubspecResolution: true, ); await expectLater(runner.restart(), throwsA(isA().having((Exception e) => e.toString(), 'message', 'Exception: updateDevFS failed'))); expect(testingConfig.updateDevFSCompleteCalled, true); }, overrides: { 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 { fileSystem.directory('.dart_tool').childFile('package_config.json') ..createSync(recursive: true) ..writeAsStringSync(''' { "configVersion": 2, "packages": [] } '''); final FakeResidentCompiler residentCompiler = FakeResidentCompiler(); final FakeDevice device = FakeDevice(); final List devices = [ 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, useImplicitPubspecResolution: true, ).attach(needsFullRestart: false); expect(exitCode, 2); }, overrides: { 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 FakeDevice device1 = FakeDevice(); final FakeDevice device2 = FakeDevice(); final FakeFlutterDevice flutterDevice1 = FakeFlutterDevice(device1); final FakeFlutterDevice flutterDevice2 = FakeFlutterDevice(device2); final List devices = [ flutterDevice1, flutterDevice2, ]; await HotRunner(devices, debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), target: 'main.dart', analytics: fakeAnalytics, useImplicitPubspecResolution: true, ).cleanupAtFinish(); expect(device1.disposed, true); expect(device2.disposed, true); expect(flutterDevice1.stoppedEchoingDeviceLog, true); expect(flutterDevice2.stoppedEchoingDeviceLog, true); }); }); }