| // 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 'dart:async'; |
| |
| import 'package:flutter_tools/src/base/dds.dart'; |
| import 'package:flutter_tools/src/base/logger.dart'; |
| import 'package:flutter_tools/src/build_info.dart'; |
| import 'package:flutter_tools/src/cache.dart'; |
| import 'package:flutter_tools/src/device.dart'; |
| import 'package:flutter_tools/src/devtools_launcher.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:test/fake.dart'; |
| import 'package:vm_service/vm_service.dart' as vm_service; |
| |
| import '../src/common.dart'; |
| import '../src/fake_process_manager.dart'; |
| import '../src/fake_vm_services.dart'; |
| import '../src/fakes.dart'; |
| |
| final vm_service.Isolate isolate = vm_service.Isolate( |
| id: '1', |
| pauseEvent: vm_service.Event( |
| kind: vm_service.EventKind.kResume, |
| timestamp: 0 |
| ), |
| breakpoints: <vm_service.Breakpoint>[], |
| libraries: <vm_service.LibraryRef>[ |
| vm_service.LibraryRef( |
| id: '1', |
| uri: 'file:///hello_world/main.dart', |
| name: '', |
| ), |
| ], |
| livePorts: 0, |
| name: 'test', |
| number: '1', |
| pauseOnExit: false, |
| runnable: true, |
| startTime: 0, |
| isSystemIsolate: false, |
| isolateFlags: <vm_service.IsolateFlag>[], |
| extensionRPCs: <String>['ext.flutter.connectedVmServiceUri'], |
| ); |
| |
| final FakeVmServiceRequest listViews = FakeVmServiceRequest( |
| method: kListViewsMethod, |
| jsonResponse: <String, Object>{ |
| 'views': <Object>[ |
| FlutterView( |
| id: 'a', |
| uiIsolate: isolate, |
| ).toJson(), |
| ], |
| }, |
| ); |
| |
| void main() { |
| Cache.flutterRoot = ''; |
| |
| testWithoutContext('Does not serve devtools if launcher is null', () async { |
| final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler( |
| null, |
| FakeResidentRunner(), |
| BufferLogger.test(), |
| ); |
| |
| await handler.serveAndAnnounceDevTools(flutterDevices: <FlutterDevice>[]); |
| |
| expect(handler.activeDevToolsServer, null); |
| }); |
| |
| testWithoutContext('Does not serve devtools if ResidentRunner does not support the service protocol', () async { |
| final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler( |
| FakeDevtoolsLauncher(), |
| FakeResidentRunner()..supportsServiceProtocol = false, |
| BufferLogger.test(), |
| ); |
| |
| await handler.serveAndAnnounceDevTools(flutterDevices: <FlutterDevice>[]); |
| |
| expect(handler.activeDevToolsServer, null); |
| }); |
| |
| testWithoutContext('Can use devtools with existing devtools URI', () async { |
| final DevtoolsServerLauncher launcher = DevtoolsServerLauncher( |
| processManager: FakeProcessManager.empty(), |
| dartExecutable: 'dart', |
| logger: BufferLogger.test(), |
| botDetector: const FakeBotDetector(false), |
| ); |
| final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler( |
| // Uses real devtools instance which should be a no-op if |
| // URI is already set. |
| launcher, |
| FakeResidentRunner(), |
| BufferLogger.test(), |
| ); |
| |
| await handler.serveAndAnnounceDevTools( |
| devToolsServerAddress: Uri.parse('http://localhost:8181'), |
| flutterDevices: <FlutterDevice>[], |
| ); |
| |
| expect(handler.activeDevToolsServer!.host, 'localhost'); |
| expect(handler.activeDevToolsServer!.port, 8181); |
| }); |
| |
| testWithoutContext('serveAndAnnounceDevTools with attached device does not fail on null vm service', () async { |
| final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler( |
| FakeDevtoolsLauncher() |
| ..activeDevToolsServer = DevToolsServerAddress('localhost', 8080) |
| ..devToolsUrl = Uri.parse('http://localhost:8080'), |
| FakeResidentRunner(), |
| BufferLogger.test(), |
| ); |
| |
| // VM Service is intentionally null |
| final FakeFlutterDevice device = FakeFlutterDevice(); |
| |
| await handler.serveAndAnnounceDevTools( |
| flutterDevices: <FlutterDevice>[device], |
| ); |
| }); |
| |
| testWithoutContext('serveAndAnnounceDevTools with invokes devtools and vm_service setter', () async { |
| final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler( |
| FakeDevtoolsLauncher() |
| ..activeDevToolsServer = DevToolsServerAddress('localhost', 8080) |
| ..devToolsUrl = Uri.parse('http://localhost:8080'), |
| FakeResidentRunner(), |
| BufferLogger.test(), |
| ); |
| final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[ |
| const FakeVmServiceRequest( |
| method: 'streamListen', |
| args: <String, Object>{ |
| 'streamId': 'Isolate', |
| } |
| ), |
| listViews, |
| FakeVmServiceRequest( |
| method: 'getIsolate', |
| jsonResponse: isolate.toJson(), |
| args: <String, Object>{ |
| 'isolateId': '1', |
| }, |
| ), |
| const FakeVmServiceRequest( |
| method: 'streamCancel', |
| args: <String, Object>{ |
| 'streamId': 'Isolate', |
| }, |
| ), |
| listViews, |
| const FakeVmServiceRequest( |
| method: 'ext.flutter.activeDevToolsServerAddress', |
| args: <String, Object>{ |
| 'isolateId': '1', |
| 'value': 'http://localhost:8080', |
| }, |
| ), |
| listViews, |
| const FakeVmServiceRequest( |
| method: 'ext.flutter.connectedVmServiceUri', |
| args: <String, Object>{ |
| 'isolateId': '1', |
| 'value': 'http://localhost:1234', |
| }, |
| ), |
| ], httpAddress: Uri.parse('http://localhost:1234')); |
| |
| final FakeFlutterDevice device = FakeFlutterDevice() |
| ..vmService = fakeVmServiceHost.vmService; |
| |
| await handler.serveAndAnnounceDevTools( |
| flutterDevices: <FlutterDevice>[device], |
| ); |
| }); |
| |
| testWithoutContext('serveAndAnnounceDevTools will bail if launching devtools fails', () async { |
| final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler( |
| FakeDevtoolsLauncher()..activeDevToolsServer = null, |
| FakeResidentRunner(), |
| BufferLogger.test(), |
| ); |
| final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[], httpAddress: Uri.parse('http://localhost:1234')); |
| |
| final FakeFlutterDevice device = FakeFlutterDevice() |
| ..vmService = fakeVmServiceHost.vmService; |
| |
| await handler.serveAndAnnounceDevTools( |
| flutterDevices: <FlutterDevice>[device], |
| ); |
| }); |
| |
| testWithoutContext('serveAndAnnounceDevTools with web device', () async { |
| final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler( |
| FakeDevtoolsLauncher() |
| ..activeDevToolsServer = DevToolsServerAddress('localhost', 8080) |
| ..devToolsUrl = Uri.parse('http://localhost:8080'), |
| FakeResidentRunner(), |
| BufferLogger.test(), |
| ); |
| final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[ |
| const FakeVmServiceRequest( |
| method: 'streamListen', |
| args: <String, Object>{ |
| 'streamId': 'Isolate', |
| } |
| ), |
| listViews, |
| FakeVmServiceRequest( |
| method: 'getIsolate', |
| jsonResponse: isolate.toJson(), |
| args: <String, Object>{ |
| 'isolateId': '1', |
| }, |
| ), |
| const FakeVmServiceRequest( |
| method: 'streamCancel', |
| args: <String, Object>{ |
| 'streamId': 'Isolate', |
| }, |
| ), |
| const FakeVmServiceRequest( |
| method: 'ext.flutter.activeDevToolsServerAddress', |
| args: <String, Object>{ |
| 'value': 'http://localhost:8080', |
| }, |
| ), |
| const FakeVmServiceRequest( |
| method: 'ext.flutter.connectedVmServiceUri', |
| args: <String, Object>{ |
| 'value': 'http://localhost:1234', |
| }, |
| ), |
| ], httpAddress: Uri.parse('http://localhost:1234')); |
| |
| final FakeFlutterDevice device = FakeFlutterDevice() |
| ..vmService = fakeVmServiceHost.vmService |
| ..targetPlatform = TargetPlatform.web_javascript; |
| |
| await handler.serveAndAnnounceDevTools( |
| flutterDevices: <FlutterDevice>[device], |
| ); |
| }); |
| |
| testWithoutContext('serveAndAnnounceDevTools with skips calling service extensions when VM service disappears', () async { |
| final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler( |
| FakeDevtoolsLauncher()..activeDevToolsServer = DevToolsServerAddress('localhost', 8080), |
| FakeResidentRunner(), |
| BufferLogger.test(), |
| ); |
| final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[ |
| const FakeVmServiceRequest( |
| method: 'streamListen', |
| args: <String, Object>{ |
| 'streamId': 'Isolate', |
| }, |
| ), |
| const FakeVmServiceRequest( |
| method: kListViewsMethod, |
| errorCode: RPCErrorCodes.kServiceDisappeared, |
| ), |
| const FakeVmServiceRequest( |
| method: 'streamCancel', |
| args: <String, Object>{ |
| 'streamId': 'Isolate', |
| }, |
| errorCode: RPCErrorCodes.kServiceDisappeared, |
| ), |
| ], httpAddress: Uri.parse('http://localhost:1234')); |
| |
| final FakeFlutterDevice device = FakeFlutterDevice() |
| ..vmService = fakeVmServiceHost.vmService; |
| |
| await handler.serveAndAnnounceDevTools( |
| flutterDevices: <FlutterDevice>[device], |
| ); |
| }); |
| |
| testWithoutContext('serveAndAnnounceDevTools with multiple devices and VM service disappears on one', () async { |
| final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler( |
| FakeDevtoolsLauncher() |
| ..activeDevToolsServer = DevToolsServerAddress('localhost', 8080) |
| ..devToolsUrl = Uri.parse('http://localhost:8080'), |
| FakeResidentRunner(), |
| BufferLogger.test(), |
| ); |
| |
| final FakeVmServiceHost vmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[ |
| const FakeVmServiceRequest( |
| method: 'streamListen', |
| args: <String, Object>{ |
| 'streamId': 'Isolate', |
| }, |
| ), |
| listViews, |
| FakeVmServiceRequest( |
| method: 'getIsolate', |
| jsonResponse: isolate.toJson(), |
| args: <String, Object>{ |
| 'isolateId': '1', |
| }, |
| ), |
| const FakeVmServiceRequest( |
| method: 'streamCancel', |
| args: <String, Object>{ |
| 'streamId': 'Isolate', |
| }, |
| ), |
| listViews, |
| const FakeVmServiceRequest( |
| method: 'ext.flutter.activeDevToolsServerAddress', |
| args: <String, Object>{ |
| 'isolateId': '1', |
| 'value': 'http://localhost:8080', |
| }, |
| ), |
| listViews, |
| const FakeVmServiceRequest( |
| method: 'ext.flutter.connectedVmServiceUri', |
| args: <String, Object>{ |
| 'isolateId': '1', |
| 'value': 'http://localhost:1234', |
| }, |
| ), |
| ], httpAddress: Uri.parse('http://localhost:1234')); |
| |
| final FakeVmServiceHost vmServiceHostThatDisappears = FakeVmServiceHost(requests: <VmServiceExpectation>[ |
| const FakeVmServiceRequest( |
| method: 'streamListen', |
| args: <String, Object>{ |
| 'streamId': 'Isolate', |
| }, |
| ), |
| const FakeVmServiceRequest( |
| method: kListViewsMethod, |
| errorCode: RPCErrorCodes.kServiceDisappeared, |
| ), |
| const FakeVmServiceRequest( |
| method: 'streamCancel', |
| args: <String, Object>{ |
| 'streamId': 'Isolate', |
| }, |
| errorCode: RPCErrorCodes.kServiceDisappeared, |
| ), |
| ], httpAddress: Uri.parse('http://localhost:5678')); |
| |
| await handler.serveAndAnnounceDevTools( |
| flutterDevices: <FlutterDevice>[ |
| FakeFlutterDevice() |
| ..vmService = vmServiceHostThatDisappears.vmService, |
| FakeFlutterDevice() |
| ..vmService = vmServiceHost.vmService, |
| ], |
| ); |
| }); |
| |
| testWithoutContext('Does not launch devtools in browser if launcher is null', () async { |
| final FlutterResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler( |
| null, |
| FakeResidentRunner(), |
| BufferLogger.test(), |
| ); |
| |
| handler.launchDevToolsInBrowser(flutterDevices: <FlutterDevice>[]); |
| expect(handler.launchedInBrowser, isFalse); |
| expect(handler.activeDevToolsServer, null); |
| }); |
| |
| testWithoutContext('Does not launch devtools in browser if ResidentRunner does not support the service protocol', () async { |
| final FlutterResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler( |
| FakeDevtoolsLauncher(), |
| FakeResidentRunner()..supportsServiceProtocol = false, |
| BufferLogger.test(), |
| ); |
| |
| handler.launchDevToolsInBrowser(flutterDevices: <FlutterDevice>[]); |
| expect(handler.launchedInBrowser, isFalse); |
| expect(handler.activeDevToolsServer, null); |
| }); |
| |
| testWithoutContext('launchDevToolsInBrowser launches after _devToolsLauncher.ready completes', () async { |
| final Completer<void> completer = Completer<void>(); |
| final FlutterResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler( |
| FakeDevtoolsLauncher() |
| ..devToolsUrl = null |
| // We need to set [activeDevToolsServer] to simulate the state we would |
| // be in after serving devtools completes. |
| ..activeDevToolsServer = DevToolsServerAddress('localhost', 8080) |
| ..readyCompleter = completer, |
| FakeResidentRunner(), |
| BufferLogger.test(), |
| ); |
| |
| expect(handler.launchDevToolsInBrowser(flutterDevices: <FlutterDevice>[]), isTrue); |
| expect(handler.launchedInBrowser, isFalse); |
| |
| completer.complete(); |
| // Await a short delay to give DevTools time to launch. |
| await Future<void>.delayed(const Duration(microseconds: 100)); |
| |
| expect(handler.launchedInBrowser, isTrue); |
| }); |
| |
| testWithoutContext('launchDevToolsInBrowser launches successfully', () async { |
| final FlutterResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler( |
| FakeDevtoolsLauncher() |
| ..devToolsUrl = Uri(host: 'localhost', port: 8080) |
| ..activeDevToolsServer = DevToolsServerAddress('localhost', 8080), |
| FakeResidentRunner(), |
| BufferLogger.test(), |
| ); |
| |
| expect(handler.launchDevToolsInBrowser(flutterDevices: <FlutterDevice>[]), isTrue); |
| expect(handler.launchedInBrowser, isTrue); |
| }); |
| |
| testWithoutContext('Converts a VmService URI with a query parameter to a pretty display string', () { |
| const String value = 'http://127.0.0.1:9100?uri=http%3A%2F%2F127.0.0.1%3A57922%2F_MXpzytpH20%3D%2F'; |
| final Uri uri = Uri.parse(value); |
| |
| expect(urlToDisplayString(uri), 'http://127.0.0.1:9100?uri=http://127.0.0.1:57922/_MXpzytpH20=/'); |
| }); |
| } |
| |
| class FakeDevtoolsLauncher extends Fake implements DevtoolsLauncher { |
| @override |
| DevToolsServerAddress? activeDevToolsServer; |
| |
| @override |
| Uri? devToolsUrl; |
| |
| @override |
| Future<DevToolsServerAddress?> serve() async => null; |
| |
| @override |
| Future<void> get ready => readyCompleter.future; |
| |
| Completer<void> readyCompleter = Completer<void>()..complete(); |
| } |
| |
| class FakeResidentRunner extends Fake implements ResidentRunner { |
| @override |
| bool supportsServiceProtocol = true; |
| |
| @override |
| bool reportedDebuggers = false; |
| |
| @override |
| DebuggingOptions debuggingOptions = DebuggingOptions.disabled(BuildInfo.debug); |
| } |
| |
| class FakeFlutterDevice extends Fake implements FlutterDevice { |
| @override |
| final Device device = FakeDevice(); |
| |
| @override |
| FlutterVmService? vmService; |
| |
| @override |
| TargetPlatform targetPlatform = TargetPlatform.android_arm; |
| } |
| |
| // 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 |
| DartDevelopmentService get dds => FakeDartDevelopmentService(); |
| } |
| |
| class FakeDartDevelopmentService extends Fake implements DartDevelopmentService { |
| bool started = false; |
| bool disposed = false; |
| |
| @override |
| final Uri uri = Uri.parse('http://127.0.0.1:1234/'); |
| |
| @override |
| Future<void> startDartDevelopmentService( |
| Uri observatoryUri, { |
| required Logger logger, |
| int? hostPort, |
| bool? ipv6, |
| bool? disableServiceAuthCodes, |
| bool cacheStartupProfile = false, |
| }) async { |
| started = true; |
| } |
| |
| @override |
| Future<void> shutdown() async { |
| disposed = true; |
| } |
| |
| @override |
| void setExternalDevToolsUri(Uri uri) {} |
| } |