// 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 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:dwds/dwds.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/asset.dart'; import 'package:flutter_tools/src/base/dds.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/time.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/globals.dart' as globals; import 'package:flutter_tools/src/isolated/devfs_web.dart'; import 'package:flutter_tools/src/isolated/resident_web_runner.dart'; import 'package:flutter_tools/src/project.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/vmservice.dart'; import 'package:flutter_tools/src/web/chrome.dart'; import 'package:flutter_tools/src/web/web_device.dart'; import 'package:meta/meta.dart'; import 'package:package_config/package_config.dart'; import 'package:package_config/package_config_types.dart'; import 'package:test/fake.dart'; import 'package:vm_service/vm_service.dart' as vm_service; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; import '../src/common.dart'; import '../src/context.dart'; import '../src/fake_vm_services.dart'; const List kAttachLogExpectations = [ FakeVmServiceRequest( method: 'streamListen', args: { 'streamId': 'Stdout', }, ), FakeVmServiceRequest( method: 'streamListen', args: { 'streamId': 'Stderr', }, ), ]; const List kAttachIsolateExpectations = [ FakeVmServiceRequest(method: 'streamListen', args: { 'streamId': 'Isolate', }), FakeVmServiceRequest(method: 'registerService', args: { 'service': 'reloadSources', 'alias': 'Flutter Tools', }), FakeVmServiceRequest(method: 'registerService', args: { 'service': 'flutterVersion', 'alias': 'Flutter Tools', }), FakeVmServiceRequest(method: 'registerService', args: { 'service': 'flutterMemoryInfo', 'alias': 'Flutter Tools', }), FakeVmServiceRequest( method: 'streamListen', args: { 'streamId': 'Extension', }, ), ]; const List kAttachExpectations = [ ...kAttachLogExpectations, ...kAttachIsolateExpectations, ]; void main() { FakeDebugConnection debugConnection; FakeChromeDevice chromeDevice; FakeAppConnection appConnection; FakeFlutterDevice flutterDevice; FakeWebDevFS webDevFS; FakeResidentCompiler residentCompiler; FakeChromeConnection chromeConnection; FakeChromeTab chromeTab; FakeWebServerDevice webServerDevice; FakeDevice mockDevice; FakeVmServiceHost fakeVmServiceHost; FileSystem fileSystem; ProcessManager processManager; TestUsage testUsage; setUp(() { testUsage = TestUsage(); fileSystem = MemoryFileSystem.test(); processManager = FakeProcessManager.any(); debugConnection = FakeDebugConnection(); mockDevice = FakeDevice(); appConnection = FakeAppConnection(); webDevFS = FakeWebDevFS(); residentCompiler = FakeResidentCompiler(); chromeConnection = FakeChromeConnection(); chromeTab = FakeChromeTab('index.html'); webServerDevice = FakeWebServerDevice(); flutterDevice = FakeFlutterDevice() .._devFS = webDevFS ..device = mockDevice ..generator = residentCompiler; fileSystem.file('.packages').writeAsStringSync('\n'); }); void setupMocks() { fileSystem.file('pubspec.yaml').createSync(); fileSystem.file('lib/main.dart').createSync(recursive: true); fileSystem.file('web/index.html').createSync(recursive: true); webDevFS.report = UpdateFSReport(success: true); debugConnection.fakeVmServiceHost = () => fakeVmServiceHost; webDevFS.result = ConnectionResult( appConnection, debugConnection, debugConnection.vmService, ); debugConnection.uri = 'ws://127.0.0.1/abcd/'; chromeConnection.tabs.add(chromeTab); } testUsingContext( 'runner with web server device does not support debugging without --start-paused', () { final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice); flutterDevice.device = WebServerDevice( logger: BufferLogger.test(), ); fakeVmServiceHost = FakeVmServiceHost(requests: []); final ResidentRunner profileResidentWebRunner = ResidentWebRunner( flutterDevice, flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), ipv6: true, fileSystem: fileSystem, logger: BufferLogger.test(), usage: globals.flutterUsage, systemClock: globals.systemClock, ); expect(profileResidentWebRunner.debuggingEnabled, false); flutterDevice.device = FakeChromeDevice(); expect(residentWebRunner.debuggingEnabled, true); expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext( 'runner with web server device supports debugging with --start-paused', () { fakeVmServiceHost = FakeVmServiceHost(requests: []); setupMocks(); flutterDevice.device = WebServerDevice( logger: BufferLogger.test(), ); final ResidentRunner profileResidentWebRunner = ResidentWebRunner( flutterDevice, flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, startPaused: true), ipv6: true, fileSystem: fileSystem, logger: BufferLogger.test(), usage: globals.flutterUsage, systemClock: globals.systemClock, ); expect(profileResidentWebRunner.uri, webDevFS.baseUri); expect(profileResidentWebRunner.debuggingEnabled, true); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('profile does not supportsServiceProtocol', () { final ResidentRunner residentWebRunner = ResidentWebRunner( flutterDevice, flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), ipv6: true, fileSystem: fileSystem, logger: BufferLogger.test(), usage: globals.flutterUsage, systemClock: globals.systemClock, ); fakeVmServiceHost = FakeVmServiceHost(requests: []); flutterDevice.device = chromeDevice; final ResidentRunner profileResidentWebRunner = ResidentWebRunner( flutterDevice, flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), debuggingOptions: DebuggingOptions.enabled(BuildInfo.profile), ipv6: true, fileSystem: fileSystem, logger: BufferLogger.test(), usage: globals.flutterUsage, systemClock: globals.systemClock, ); expect(profileResidentWebRunner.supportsServiceProtocol, false); expect(residentWebRunner.supportsServiceProtocol, true); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Can successfully run and connect to vmservice', () async { final BufferLogger logger = BufferLogger.test(); final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, logger: logger); fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList()); setupMocks(); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); final DebugConnectionInfo debugConnectionInfo = await connectionInfoCompleter.future; expect(appConnection.ranMain, true); expect(logger.statusText, contains('Debug service listening on ws://127.0.0.1/abcd/')); expect(debugConnectionInfo.wsUri.toString(), 'ws://127.0.0.1/abcd/'); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('WebRunner copies compiled app.dill to cache during startup', () async { final DebuggingOptions debuggingOptions = DebuggingOptions.enabled( const BuildInfo(BuildMode.debug, null, treeShakeIcons: false), ); final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, debuggingOptions: debuggingOptions); fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList()); setupMocks(); residentWebRunner.artifactDirectory .childFile('app.dill') .writeAsStringSync('ABC'); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; expect( await fileSystem .file(fileSystem.path.join('build', 'cache.dill')) .readAsString(), 'ABC'); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext( 'WebRunner copies compiled app.dill to cache during startup with track-widget-creation', () async { final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice); fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList()); setupMocks(); residentWebRunner.artifactDirectory .childFile('app.dill') .writeAsStringSync('ABC'); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; expect( await fileSystem .file(fileSystem.path.join('build', 'cache.dill.track.dill')) .readAsString(), 'ABC'); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); // Regression test for https://github.com/flutter/flutter/issues/60613 testUsingContext( 'ResidentWebRunner calls appFailedToStart if initial compilation fails', () async { fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList()); setupMocks(); final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice); fileSystem .file(globals.fs.path.join('lib', 'main.dart')) .createSync(recursive: true); webDevFS.report = UpdateFSReport(); expect(await residentWebRunner.run(), 1); // Completing this future ensures that the daemon can exit correctly. expect(await residentWebRunner.waitForAppToFinish(), 1); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext( 'Can successfully run without an index.html including status warning', () async { final BufferLogger logger = BufferLogger.test(); fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList()); setupMocks(); fileSystem.file(fileSystem.path.join('web', 'index.html')).deleteSync(); final ResidentWebRunner residentWebRunner = ResidentWebRunner( flutterDevice, flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), ipv6: true, stayResident: false, fileSystem: fileSystem, logger: logger, usage: globals.flutterUsage, systemClock: globals.systemClock, ); expect(await residentWebRunner.run(), 0); expect(logger.statusText, contains('This application is not configured to build on the web')); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Can successfully run and disconnect with --no-resident', () async { fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList()); setupMocks(); final ResidentRunner residentWebRunner = ResidentWebRunner( flutterDevice, flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), ipv6: true, stayResident: false, fileSystem: fileSystem, logger: BufferLogger.test(), usage: globals.flutterUsage, systemClock: globals.systemClock, ); expect(await residentWebRunner.run(), 0); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Listens to stdout and stderr streams before running main', () async { final BufferLogger logger = BufferLogger.test(); final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, logger: logger); fakeVmServiceHost = FakeVmServiceHost(requests: [ ...kAttachLogExpectations, FakeVmServiceStreamResponse( streamId: 'Stdout', event: vm_service.Event( timestamp: 0, kind: vm_service.EventStreams.kStdout, bytes: base64.encode(utf8.encode('THIS MESSAGE IS IMPORTANT'))), ), FakeVmServiceStreamResponse( streamId: 'Stderr', event: vm_service.Event( timestamp: 0, kind: vm_service.EventStreams.kStderr, bytes: base64.encode(utf8.encode('SO IS THIS'))), ), ...kAttachIsolateExpectations, ]); setupMocks(); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; expect(logger.statusText, contains('THIS MESSAGE IS IMPORTANT')); expect(logger.statusText, contains('SO IS THIS')); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Listens to extension events with structured errors', () async { final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, logger: testLogger); final Map extensionData = { 'test': 'data', 'renderedErrorText': 'error text', }; final Map emptyExtensionData = { 'test': 'data', 'renderedErrorText': '', }; final Map nonStructuredErrorData = { 'other': 'other stuff', }; fakeVmServiceHost = FakeVmServiceHost(requests: [ ...kAttachExpectations, FakeVmServiceStreamResponse( streamId: 'Extension', event: vm_service.Event( timestamp: 0, extensionKind: 'Flutter.Error', extensionData: vm_service.ExtensionData.parse(extensionData), kind: vm_service.EventStreams.kExtension, ), ), // Empty error text should not break anything. FakeVmServiceStreamResponse( streamId: 'Extension', event: vm_service.Event( timestamp: 0, extensionKind: 'Flutter.Error', extensionData: vm_service.ExtensionData.parse(emptyExtensionData), kind: vm_service.EventStreams.kExtension, ), ), // This is not Flutter.Error kind data, so it should not be logged. FakeVmServiceStreamResponse( streamId: 'Extension', event: vm_service.Event( timestamp: 0, extensionKind: 'Other', extensionData: vm_service.ExtensionData.parse(nonStructuredErrorData), kind: vm_service.EventStreams.kExtension, ), ), ]); setupMocks(); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; await null; expect(testLogger.statusText, contains('\nerror text')); expect(testLogger.statusText, isNot(contains('other stuff'))); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Does not run main with --start-paused', () async { final ResidentRunner residentWebRunner = ResidentWebRunner( flutterDevice, flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, startPaused: true), ipv6: true, fileSystem: fileSystem, logger: BufferLogger.test(), usage: globals.flutterUsage, systemClock: globals.systemClock, ); fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList()); setupMocks(); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; expect(appConnection.ranMain, false); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Can hot reload after attaching', () async { final BufferLogger logger = BufferLogger.test(); final ResidentRunner residentWebRunner = setUpResidentRunner( flutterDevice, logger: logger, systemClock: SystemClock.fixed(DateTime(2001)), ); fakeVmServiceHost = FakeVmServiceHost(requests: [ ...kAttachExpectations, const FakeVmServiceRequest( method: 'hotRestart', jsonResponse: { 'type': 'Success', }), const FakeVmServiceRequest( method: 'streamListen', args: { 'streamId': 'Isolate', }, ), ]); setupMocks(); final TestChromiumLauncher chromiumLauncher = TestChromiumLauncher(); final Chromium chrome = Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher); chromiumLauncher.setInstance(chrome); flutterDevice.device = GoogleChromeDevice( fileSystem: fileSystem, chromiumLauncher: chromiumLauncher, logger: BufferLogger.test(), platform: FakePlatform(), processManager: FakeProcessManager.any(), ); webDevFS.report = UpdateFSReport(success: true); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); final DebugConnectionInfo debugConnectionInfo = await connectionInfoCompleter.future; expect(debugConnectionInfo, isNotNull); final OperationResult result = await residentWebRunner.restart(); expect(logger.statusText, contains('Restarted application in')); expect(result.code, 0); expect(webDevFS.mainUri.toString(), contains('entrypoint.dart')); // ensure that analytics are sent. expect(testUsage.events, [ TestUsageEvent('hot', 'restart', parameters: CustomDimensions.fromMap({ 'cd27': 'web-javascript', 'cd28': '', 'cd29': 'false', 'cd30': 'true', 'cd13': '0', 'cd48': 'false' })), ]); expect(testUsage.timings, const [ TestTimingEvent('hot', 'web-incremental-restart', Duration.zero), ]); }, overrides: { Usage: () => testUsage, FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Can hot restart after attaching', () async { final BufferLogger logger = BufferLogger.test(); final ResidentRunner residentWebRunner = setUpResidentRunner( flutterDevice, logger: logger, systemClock: SystemClock.fixed(DateTime(2001)), ); fakeVmServiceHost = FakeVmServiceHost(requests: [ ...kAttachExpectations, const FakeVmServiceRequest( method: 'hotRestart', jsonResponse: { 'type': 'Success', }), ]); setupMocks(); final TestChromiumLauncher chromiumLauncher = TestChromiumLauncher(); final Chromium chrome = Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher); chromiumLauncher.setInstance(chrome); flutterDevice.device = GoogleChromeDevice( fileSystem: fileSystem, chromiumLauncher: chromiumLauncher, logger: BufferLogger.test(), platform: FakePlatform(), processManager: FakeProcessManager.any(), ); webDevFS.report = UpdateFSReport(success: true); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; final OperationResult result = await residentWebRunner.restart(fullRestart: true); // Ensure that generated entrypoint is generated correctly. expect(webDevFS.mainUri, isNotNull); final String entrypointContents = fileSystem.file(webDevFS.mainUri).readAsStringSync(); expect(entrypointContents, contains('// Flutter web bootstrap script')); expect(entrypointContents, contains("import 'dart:ui' as ui;")); expect(entrypointContents, contains('await ui.webOnlyWarmupEngine(')); expect(logger.statusText, contains('Restarted application in')); expect(result.code, 0); // ensure that analytics are sent. expect(testUsage.events, [ TestUsageEvent('hot', 'restart', parameters: CustomDimensions.fromMap({ 'cd27': 'web-javascript', 'cd28': '', 'cd29': 'false', 'cd30': 'true', 'cd13': '0', 'cd48': 'false' })), ]); expect(testUsage.timings, const [ TestTimingEvent('hot', 'web-incremental-restart', Duration.zero), ]); }, overrides: { Usage: () => testUsage, FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Can hot restart after attaching with web-server device', () async { final BufferLogger logger = BufferLogger.test(); final ResidentRunner residentWebRunner = setUpResidentRunner( flutterDevice, logger: logger, systemClock: SystemClock.fixed(DateTime(2001)), ); fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations); setupMocks(); flutterDevice.device = webServerDevice; webDevFS.report = UpdateFSReport(success: true); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; final OperationResult result = await residentWebRunner.restart(fullRestart: true); expect(logger.statusText, contains('Restarted application in')); expect(result.code, 0); // web-server device does not send restart analytics expect(testUsage.events, isEmpty); expect(testUsage.timings, isEmpty); }, overrides: { Usage: () => testUsage, FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('web resident runner is debuggable', () { final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice); fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList()); expect(residentWebRunner.debuggingEnabled, true); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Exits when initial compile fails', () async { final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice); fakeVmServiceHost = FakeVmServiceHost(requests: []); setupMocks(); webDevFS.report = UpdateFSReport(); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); expect(await residentWebRunner.run(), 1); expect(testUsage.events, isEmpty); expect(testUsage.timings, isEmpty); }, overrides: { Usage: () => testUsage, FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext( 'Faithfully displays stdout messages with leading/trailing spaces', () async { final BufferLogger logger = BufferLogger.test(); final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, logger: logger); fakeVmServiceHost = FakeVmServiceHost(requests: [ ...kAttachLogExpectations, FakeVmServiceStreamResponse( streamId: 'Stdout', event: vm_service.Event( timestamp: 0, kind: vm_service.EventStreams.kStdout, bytes: base64.encode( utf8.encode( ' This is a message with 4 leading and trailing spaces '), ), ), ), ...kAttachIsolateExpectations, ]); setupMocks(); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; expect( logger.statusText, contains( ' This is a message with 4 leading and trailing spaces ')); expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Fails on compilation errors in hot restart', () async { final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice); fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList()); setupMocks(); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; webDevFS.report = UpdateFSReport(); final OperationResult result = await residentWebRunner.restart(fullRestart: true); expect(result.code, 1); expect(result.message, contains('Failed to recompile application.')); expect(testUsage.events, isEmpty); expect(testUsage.timings, isEmpty); }, overrides: { Usage: () => testUsage, FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext( 'Fails non-fatally on vmservice response error for hot restart', () async { final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice); fakeVmServiceHost = FakeVmServiceHost(requests: [ ...kAttachExpectations, const FakeVmServiceRequest( method: 'hotRestart', jsonResponse: { 'type': 'Failed', }, ), ]); setupMocks(); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; final OperationResult result = await residentWebRunner.restart(); expect(result.code, 0); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Fails fatally on Vm Service error response', () async { final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice); fakeVmServiceHost = FakeVmServiceHost(requests: [ ...kAttachExpectations, const FakeVmServiceRequest( method: 'hotRestart', // Failed response, errorCode: RPCErrorCodes.kInternalError, ), ]); setupMocks(); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; final OperationResult result = await residentWebRunner.restart(); expect(result.code, 1); expect(result.message, contains(RPCErrorCodes.kInternalError.toString())); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('printHelp without details shows hot restart help message', () async { final BufferLogger logger = BufferLogger.test(); final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, logger: logger); fakeVmServiceHost = FakeVmServiceHost(requests: []); residentWebRunner.printHelp(details: false); expect(logger.statusText, contains('To hot restart changes')); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('cleanup of resources is safe to call multiple times', () async { final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice); mockDevice.dds = DartDevelopmentService(); fakeVmServiceHost = FakeVmServiceHost(requests: [ ...kAttachExpectations, ]); setupMocks(); final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; await residentWebRunner.exit(); await residentWebRunner.exit(); expect(debugConnection.didClose, false); expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('cleans up Chrome if tab is closed', () async { final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice); fakeVmServiceHost = FakeVmServiceHost(requests: [ ...kAttachExpectations, ]); setupMocks(); final Completer connectionInfoCompleter = Completer(); final Future result = residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, ); await connectionInfoCompleter.future; debugConnection.completer.complete(); await result; expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Prints target and device name on run', () async { final BufferLogger logger = BufferLogger.test(); final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, logger: logger); fakeVmServiceHost = FakeVmServiceHost(requests: [ ...kAttachExpectations, ]); setupMocks(); mockDevice.name = 'Chromez'; final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; expect( logger.statusText, contains( 'Launching ${fileSystem.path.join('lib', 'main.dart')} on ' 'Chromez in debug mode', )); expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Sends launched app.webLaunchUrl event for Chrome device', () async { final BufferLogger logger = BufferLogger.test(); fakeVmServiceHost = FakeVmServiceHost(requests: [ ...kAttachLogExpectations, ...kAttachIsolateExpectations, ]); setupMocks(); final FakeChromeConnection chromeConnection = FakeChromeConnection(); final TestChromiumLauncher chromiumLauncher = TestChromiumLauncher(); final Chromium chrome = Chromium(1, chromeConnection, chromiumLauncher: chromiumLauncher); chromiumLauncher.setInstance(chrome); flutterDevice.device = GoogleChromeDevice( fileSystem: fileSystem, chromiumLauncher: chromiumLauncher, logger: logger, platform: FakePlatform(), processManager: FakeProcessManager.any(), ); webDevFS.baseUri = Uri.parse('http://localhost:8765/app/'); final FakeChromeTab chromeTab = FakeChromeTab('index.html'); chromeConnection.tabs.add(chromeTab); final ResidentWebRunner runner = ResidentWebRunner( flutterDevice, flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), ipv6: true, fileSystem: fileSystem, logger: logger, usage: globals.flutterUsage, systemClock: globals.systemClock, ); final Completer connectionInfoCompleter = Completer(); unawaited(runner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; // Ensure we got the URL and that it was already launched. expect( logger.eventText, contains(json.encode( { 'name': 'app.webLaunchUrl', 'args': { 'url': 'http://localhost:8765/app/', 'launched': true, }, }, ))); expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext( 'Sends unlaunched app.webLaunchUrl event for Web Server device', () async { final BufferLogger logger = BufferLogger.test(); fakeVmServiceHost = FakeVmServiceHost(requests: []); setupMocks(); flutterDevice.device = WebServerDevice( logger: logger, ); webDevFS.baseUri = Uri.parse('http://localhost:8765/app/'); final ResidentWebRunner runner = ResidentWebRunner( flutterDevice, flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), ipv6: true, fileSystem: fileSystem, logger: logger, usage: globals.flutterUsage, systemClock: globals.systemClock, ); final Completer connectionInfoCompleter = Completer(); unawaited(runner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; // Ensure we got the URL and that it was not already launched. expect( logger.eventText, contains(json.encode( { 'name': 'app.webLaunchUrl', 'args': { 'url': 'http://localhost:8765/app/', 'launched': false, }, }, ))); expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); // While this file should be ignored on web, generating it here will cause a // perf regression in hot restart. testUsingContext('Does not generate dart_plugin_registrant.dart', () async { // Create necessary files for [DartPluginRegistrantTarget] final File packageConfig = globals.fs.directory('.dart_tool').childFile('package_config.json'); packageConfig.createSync(recursive: true); packageConfig.writeAsStringSync(''' { "configVersion": 2, "packages": [ { "name": "path_provider_linux", "rootUri": "../../../path_provider_linux", "packageUri": "lib/", "languageVersion": "2.12" } ] } '''); // Start with a dart_plugin_registrant.dart file. globals.fs .directory('.dart_tool') .childDirectory('flutter_build') .childFile('dart_plugin_registrant.dart') .createSync(recursive: true); final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice); await residentWebRunner.runSourceGenerators(); // dart_plugin_registrant.dart should be untouched, indicating that its // generation didn't run. If it had run, the file would have been removed as // there are no plugins in the project. expect(project.dartPluginRegistrant.existsSync(), true); expect(project.dartPluginRegistrant.readAsStringSync(), ''); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Successfully turns WebSocketException into ToolExit', () async { final BufferLogger logger = BufferLogger.test(); final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, logger: logger); fakeVmServiceHost = FakeVmServiceHost(requests: []); setupMocks(); webDevFS.exception = const WebSocketException(); await expectLater(residentWebRunner.run, throwsToolExit()); expect(logger.errorText, contains('WebSocketException')); expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Successfully turns AppConnectionException into ToolExit', () async { final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice); fakeVmServiceHost = FakeVmServiceHost(requests: []); setupMocks(); webDevFS.exception = AppConnectionException(''); await expectLater(residentWebRunner.run, throwsToolExit()); expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Successfully turns ChromeDebugError into ToolExit', () async { final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice); fakeVmServiceHost = FakeVmServiceHost(requests: []); setupMocks(); webDevFS.exception = ChromeDebugException({}); await expectLater(residentWebRunner.run, throwsToolExit()); expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Rethrows unknown Exception type from dwds', () async { final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice); fakeVmServiceHost = FakeVmServiceHost(requests: []); setupMocks(); webDevFS.exception = Exception(); await expectLater(residentWebRunner.run, throwsException); expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); testUsingContext('Rethrows unknown Error type from dwds tooling', () async { final BufferLogger logger = BufferLogger.test(); final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice, logger: logger); fakeVmServiceHost = FakeVmServiceHost(requests: []); setupMocks(); webDevFS.exception = StateError(''); await expectLater(residentWebRunner.run, throwsStateError); expect(fakeVmServiceHost.hasRemainingExpectations, false); }, overrides: { FileSystem: () => fileSystem, ProcessManager: () => processManager, }); } ResidentRunner setUpResidentRunner( FlutterDevice flutterDevice, { Logger logger, SystemClock systemClock, DebuggingOptions debuggingOptions, }) { return ResidentWebRunner( flutterDevice, flutterProject: FlutterProject.fromDirectoryTest(globals.fs.currentDirectory), debuggingOptions: debuggingOptions ?? DebuggingOptions.enabled(BuildInfo.debug), ipv6: true, usage: globals.flutterUsage, systemClock: systemClock ?? SystemClock.fixed(DateTime.now()), fileSystem: globals.fs, logger: logger ?? BufferLogger.test(), devtoolsHandler: createNoOpHandler, ); } // 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 FakeWebServerDevice extends FakeDevice implements WebServerDevice {} // 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 { @override String name; int count = 0; @override Future get sdkNameAndVersion async => 'SDK Name and Version'; @override DartDevelopmentService dds; @override Future startApp( covariant ApplicationPackage package, { String mainPath, String route, DebuggingOptions debuggingOptions, Map platformArgs, bool prebuiltApplication = false, bool ipv6 = false, String userIdentifier, }) async { return LaunchResult.succeeded(); } @override Future stopApp( covariant ApplicationPackage app, { String userIdentifier, }) async { if (count > 0) { throw StateError('stopApp called more than once.'); } count += 1; return true; } } class FakeDebugConnection extends Fake implements DebugConnection { FakeVmServiceHost Function() fakeVmServiceHost; @override vm_service.VmService get vmService => fakeVmServiceHost.call().vmService.service; @override String uri; final Completer completer = Completer(); bool didClose = false; @override Future get onDone => completer.future; @override Future close() async { didClose = true; } } class FakeAppConnection extends Fake implements AppConnection { bool ranMain = false; @override void runMain() { ranMain = true; } } // 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 FakeChromeDevice extends Fake implements ChromiumDevice {} class FakeWipDebugger extends Fake implements WipDebugger {} class FakeResidentCompiler extends Fake implements ResidentCompiler { @override Future recompile( Uri mainUri, List invalidatedFiles, { @required String outputPath, @required PackageConfig packageConfig, @required String projectRootPath, @required FileSystem fs, bool suppressErrors = false, bool checkDartPluginRegistry = false, File dartPluginRegistrant, }) async { return const CompilerOutput('foo.dill', 0, []); } @override void accept() {} @override void reset() {} @override Future reject() async { return const CompilerOutput('foo.dill', 0, []); } @override void addFileSystemRoot(String root) {} } class FakeWebDevFS extends Fake implements WebDevFS { Object exception; ConnectionResult result; UpdateFSReport report; Uri mainUri; @override List sources = []; @override Uri baseUri = Uri.parse('http://localhost:12345'); @override DateTime lastCompiled = DateTime.now(); @override PackageConfig lastPackageConfig = PackageConfig.empty; @override Future create() async { return baseUri; } @override Future update({ @required Uri mainUri, @required ResidentCompiler generator, @required bool trackWidgetCreation, @required String pathToReload, @required List invalidatedFiles, @required PackageConfig packageConfig, @required String dillOutputPath, DevFSWriter devFSWriter, String target, AssetBundle bundle, DateTime firstBuildTime, bool bundleFirstUpload = false, bool fullRestart = false, String projectRootPath, File dartPluginRegistrant, }) async { this.mainUri = mainUri; return report; } @override Future connect(bool useDebugExtension) async { if (exception != null) { assert(exception is Exception || exception is Error); // ignore: only_throw_errors, exception is either Error or Exception here. throw exception; } return result; } } class FakeChromeConnection extends Fake implements ChromeConnection { final List tabs = []; @override Future getTab(bool Function(ChromeTab tab) accept, {Duration retryFor}) async { return tabs.firstWhere(accept); } @override Future> getTabs({Duration retryFor}) async { return tabs; } } class FakeChromeTab extends Fake implements ChromeTab { FakeChromeTab(this.url); @override final String url; final FakeWipConnection connection = FakeWipConnection(); @override Future connect() async { return connection; } } class FakeWipConnection extends Fake implements WipConnection { @override final WipDebugger debugger = FakeWipDebugger(); } /// A test implementation of the [ChromiumLauncher] that launches a fixed instance. class TestChromiumLauncher implements ChromiumLauncher { TestChromiumLauncher(); bool _hasInstance = false; void setInstance(Chromium chromium) { _hasInstance = true; currentCompleter.complete(chromium); } @override Completer currentCompleter = Completer(); @override bool canFindExecutable() { return true; } @override Future get connectedInstance => currentCompleter.future; @override String findExecutable() { return 'chrome'; } @override bool get hasChromeInstance => _hasInstance; @override Future launch( String url, { bool headless = false, int debugPort, bool skipCheck = false, Directory cacheDir, List webBrowserFlags = const [], }) async { return currentCompleter.future; } @override Future connect(Chromium chrome, bool skipCheck) { return currentCompleter.future; } } class FakeFlutterDevice extends Fake implements FlutterDevice { Uri testUri; UpdateFSReport report = UpdateFSReport( success: true, invalidatedSourcesCount: 1, ); Exception reportError; @override ResidentCompiler generator; @override Stream get observatoryUris => Stream.value(testUri); @override FlutterVmService vmService; DevFS _devFS; @override DevFS get devFS => _devFS; @override set devFS(DevFS value) {} @override Device device; @override Future stopEchoingDeviceLog() async {} @override Future initLogReader() async {} @override Future setupDevFS(String fsName, Directory rootDirectory) async { return testUri; } @override Future exitApps( {Duration timeoutDelay = const Duration(seconds: 10)}) async {} @override Future connect({ ReloadSources reloadSources, Restart restart, CompileExpression compileExpression, GetSkSLMethod getSkSLMethod, PrintStructuredErrorLogMethod printStructuredErrorLogMethod, int hostVmServicePort, int ddsPort, bool disableServiceAuthCodes = false, bool enableDds = true, bool cacheStartupProfile = false, @required bool allowExistingDdsInstance, bool ipv6 = false, }) async {} @override Future updateDevFS({ Uri mainUri, String target, AssetBundle bundle, DateTime firstBuildTime, bool bundleFirstUpload = false, bool bundleDirty = false, bool fullRestart = false, String projectRootPath, String pathToReload, String dillOutputPath, List invalidatedFiles, PackageConfig packageConfig, File dartPluginRegistrant, }) async { if (reportError != null) { throw reportError; } return report; } @override Future updateReloadStatus(bool wasReloadSuccessful) async {} }