// 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_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 '../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({ '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.file('pubspec.yaml').writeAsStringSync(''' name: my_app '''); writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_app'); 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, 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(), Pub: ThrowingPub.new, }, ); testUsingContext( 'setupHotReload function fails', () async { fileSystem.file('pubspec.yaml').writeAsStringSync(''' name: my_app '''); writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_app'); 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, 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(), 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 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, ).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 { writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_app'); 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, ).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, ).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, reloadSourcesHelper: ( HotRunner hotRunner, List flutterDevices, bool? pause, Map 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 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, ); 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, ); 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 { writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_app'); 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, ).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, ).cleanupAtFinish(); expect(device1.disposed, true); expect(device2.disposed, true); expect(flutterDevice1.stoppedEchoingDeviceLog, true); expect(flutterDevice2.stoppedEchoingDeviceLog, true); }); }); }