| // 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:fake_async/fake_async.dart'; |
| import 'package:file/memory.dart'; |
| import 'package:flutter_tools/src/android/android_device.dart'; |
| import 'package:flutter_tools/src/application_package.dart'; |
| import 'package:flutter_tools/src/artifacts.dart'; |
| import 'package:flutter_tools/src/base/common.dart'; |
| import 'package:flutter_tools/src/base/dds.dart'; |
| import 'package:flutter_tools/src/base/file_system.dart'; |
| import 'package:flutter_tools/src/base/io.dart'; |
| import 'package:flutter_tools/src/base/logger.dart'; |
| import 'package:flutter_tools/src/base/platform.dart'; |
| import 'package:flutter_tools/src/base/signals.dart'; |
| import 'package:flutter_tools/src/base/terminal.dart'; |
| import 'package:flutter_tools/src/build_info.dart'; |
| import 'package:flutter_tools/src/cache.dart'; |
| import 'package:flutter_tools/src/commands/attach.dart'; |
| import 'package:flutter_tools/src/compile.dart'; |
| import 'package:flutter_tools/src/device.dart'; |
| import 'package:flutter_tools/src/device_port_forwarder.dart'; |
| import 'package:flutter_tools/src/device_vm_service_discovery_for_attach.dart'; |
| import 'package:flutter_tools/src/ios/application_package.dart'; |
| import 'package:flutter_tools/src/ios/devices.dart'; |
| import 'package:flutter_tools/src/ios/simulators.dart'; |
| import 'package:flutter_tools/src/macos/macos_ipad_device.dart'; |
| import 'package:flutter_tools/src/mdns_discovery.dart'; |
| import 'package:flutter_tools/src/project.dart'; |
| import 'package:flutter_tools/src/resident_runner.dart'; |
| import 'package:flutter_tools/src/run_hot.dart'; |
| import 'package:multicast_dns/multicast_dns.dart'; |
| import 'package:test/fake.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/fake_devices.dart'; |
| import '../../src/test_flutter_command_runner.dart'; |
| |
| class FakeStdio extends Fake implements Stdio { |
| @override |
| var stdinHasTerminal = false; |
| } |
| |
| class FakeProcessInfo extends Fake implements ProcessInfo { |
| @override |
| var maxRss = 0; |
| } |
| |
| void main() { |
| tearDown(() { |
| MacOSDesignedForIPadDevices.allowDiscovery = false; |
| }); |
| |
| group('attach', () { |
| late StreamLogger logger; |
| late FileSystem testFileSystem; |
| late TestDeviceManager testDeviceManager; |
| late Artifacts artifacts; |
| late Stdio stdio; |
| late Terminal terminal; |
| late Signals signals; |
| late Platform platform; |
| late ProcessInfo processInfo; |
| |
| setUp(() { |
| Cache.disableLocking(); |
| logger = StreamLogger(); |
| platform = FakePlatform(); |
| testFileSystem = MemoryFileSystem.test(); |
| testFileSystem.directory('lib').createSync(); |
| testFileSystem.file(testFileSystem.path.join('lib', 'main.dart')).createSync(); |
| artifacts = Artifacts.test(fileSystem: testFileSystem); |
| stdio = FakeStdio(); |
| terminal = FakeTerminal(); |
| signals = FakeSignals(); |
| processInfo = FakeProcessInfo(); |
| testDeviceManager = TestDeviceManager(logger: logger); |
| }); |
| |
| group('with one device and no specified target file', () { |
| const devicePort = 499; |
| const hostPort = 42; |
| final int future = DateTime.now().add(const Duration(days: 1)).millisecondsSinceEpoch; |
| |
| late FakeDeviceLogReader fakeLogReader; |
| late RecordingPortForwarder portForwarder; |
| late FakeDartDevelopmentService fakeDds; |
| late FakeAndroidDevice device; |
| |
| setUp(() { |
| fakeLogReader = FakeDeviceLogReader(); |
| portForwarder = RecordingPortForwarder(defaultHostPort: hostPort); |
| fakeDds = FakeDartDevelopmentService(); |
| device = FakeAndroidDevice(id: '1') |
| ..portForwarder = portForwarder |
| ..dds = fakeDds; |
| }); |
| |
| tearDown(() { |
| fakeLogReader.dispose(); |
| }); |
| |
| testUsingContext( |
| 'succeeds with iOS device with protocol discovery', |
| () async { |
| final device = FakeIOSDevice( |
| portForwarder: portForwarder, |
| majorSdkVersion: 12, |
| onGetLogReader: () { |
| fakeLogReader.addLine('Foo'); |
| fakeLogReader.addLine( |
| 'The Dart VM service is listening on http://127.0.0.1:$devicePort', |
| ); |
| return fakeLogReader; |
| }, |
| ); |
| testDeviceManager.devices = <Device>[device]; |
| final completer = Completer<void>(); |
| final StreamSubscription<String> loggerSubscription = logger.stream.listen(( |
| String message, |
| ) { |
| if (message == '[verbose] VM Service URL on device: http://127.0.0.1:$devicePort') { |
| // The "VM Service URL on device" message is output by the ProtocolDiscovery when it found the VM Service. |
| completer.complete(); |
| } |
| }); |
| final hotRunner = FakeHotRunner(); |
| hotRunner.onAttach = |
| ( |
| Completer<DebugConnectionInfo>? connectionInfoCompleter, |
| Completer<void>? appStartedCompleter, |
| bool enableDevTools, |
| ) async => 0; |
| hotRunner.exited = false; |
| hotRunner.isWaitingForVmService = false; |
| final hotRunnerFactory = FakeHotRunnerFactory()..hotRunner = hotRunner; |
| |
| await createTestCommandRunner( |
| AttachCommand( |
| hotRunnerFactory: hotRunnerFactory, |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ), |
| ).run(<String>['attach']); |
| |
| await completer.future; |
| |
| expect(portForwarder.forwardedPorts, <TypeMatcher<ForwardedPort>>[ |
| isA<ForwardedPort>() |
| .having((ForwardedPort d) => d.devicePort, 'devicePort', devicePort) |
| .having((ForwardedPort d) => d.hostPort, 'hostPort', hostPort), |
| ]); |
| |
| await fakeLogReader.dispose(); |
| await loggerSubscription.cancel(); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Logger: () => logger, |
| DeviceManager: () => testDeviceManager, |
| MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery( |
| mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}), |
| preliminaryMDnsClient: FakeMDnsClient( |
| <PtrResourceRecord>[], |
| <String, List<SrvResourceRecord>>{}, |
| ), |
| logger: logger, |
| analytics: const NoOpAnalytics(), |
| ), |
| }, |
| ); |
| |
| testUsingContext( |
| 'restores terminal to singleCharMode == false on command exit', |
| () async { |
| final device = FakeIOSDevice( |
| portForwarder: portForwarder, |
| majorSdkVersion: 12, |
| onGetLogReader: () { |
| fakeLogReader.addLine('Foo'); |
| fakeLogReader.addLine( |
| 'The Dart VM service is listening on http://127.0.0.1:$devicePort', |
| ); |
| return fakeLogReader; |
| }, |
| ); |
| testDeviceManager.devices = <Device>[device]; |
| final completer = Completer<void>(); |
| final StreamSubscription<String> loggerSubscription = logger.stream.listen(( |
| String message, |
| ) { |
| if (message == '[verbose] VM Service URL on device: http://127.0.0.1:$devicePort') { |
| // The "VM Service URL on device" message is output by the ProtocolDiscovery when it found the VM Service. |
| completer.complete(); |
| } |
| }); |
| final hotRunner = FakeHotRunner(); |
| hotRunner.onAttach = |
| ( |
| Completer<DebugConnectionInfo>? connectionInfoCompleter, |
| Completer<void>? appStartedCompleter, |
| bool enableDevTools, |
| ) async { |
| appStartedCompleter?.complete(); |
| return 0; |
| }; |
| hotRunner.exited = false; |
| hotRunner.isWaitingForVmService = false; |
| final hotRunnerFactory = FakeHotRunnerFactory()..hotRunner = hotRunner; |
| |
| await createTestCommandRunner( |
| AttachCommand( |
| hotRunnerFactory: hotRunnerFactory, |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ), |
| ).run(<String>['attach']); |
| await completer.future; |
| await Future.wait<void>(<Future<void>>[ |
| fakeLogReader.dispose(), |
| loggerSubscription.cancel(), |
| ]); |
| |
| expect(terminal.singleCharMode, isFalse); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Logger: () => logger, |
| DeviceManager: () => testDeviceManager, |
| MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery( |
| mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}), |
| preliminaryMDnsClient: FakeMDnsClient( |
| <PtrResourceRecord>[], |
| <String, List<SrvResourceRecord>>{}, |
| ), |
| logger: logger, |
| analytics: const NoOpAnalytics(), |
| ), |
| Signals: () => FakeSignals(), |
| }, |
| ); |
| |
| testUsingContext( |
| 'local engine artifacts are passed to runner', |
| () async { |
| const localEngineSrc = '/path/to/local/engine/src'; |
| const localEngineDir = 'host_debug_unopt'; |
| testFileSystem |
| .directory('$localEngineSrc/out/$localEngineDir') |
| .createSync(recursive: true); |
| final device = FakeIOSDevice( |
| portForwarder: portForwarder, |
| majorSdkVersion: 12, |
| onGetLogReader: () { |
| fakeLogReader.addLine('Foo'); |
| fakeLogReader.addLine( |
| 'The Dart VM service is listening on http://127.0.0.1:$devicePort', |
| ); |
| return fakeLogReader; |
| }, |
| ); |
| testDeviceManager.devices = <Device>[device]; |
| final completer = Completer<void>(); |
| final StreamSubscription<String> loggerSubscription = logger.stream.listen(( |
| String message, |
| ) { |
| if (message == '[verbose] VM Service URL on device: http://127.0.0.1:$devicePort') { |
| // The "VM Service URL on device" message is output by the ProtocolDiscovery when it found the VM Service. |
| completer.complete(); |
| } |
| }); |
| final hotRunner = FakeHotRunner(); |
| hotRunner.onAttach = |
| ( |
| Completer<DebugConnectionInfo>? connectionInfoCompleter, |
| Completer<void>? appStartedCompleter, |
| bool enableDevTools, |
| ) async => 0; |
| hotRunner.exited = false; |
| hotRunner.isWaitingForVmService = false; |
| var passedArtifactTest = false; |
| final hotRunnerFactory = FakeHotRunnerFactory() |
| ..hotRunner = hotRunner |
| .._artifactTester = (Artifacts artifacts) { |
| expect(artifacts, isA<CachedLocalEngineArtifacts>()); |
| // expecting this to be true ensures this test ran |
| passedArtifactTest = true; |
| }; |
| |
| await createTestCommandRunner( |
| AttachCommand( |
| hotRunnerFactory: hotRunnerFactory, |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ), |
| ).run(<String>[ |
| 'attach', |
| '--local-engine-src-path=$localEngineSrc', |
| '--local-engine=$localEngineDir', |
| '--local-engine-host=$localEngineDir', |
| ]); |
| await completer.future; |
| await Future.wait<void>(<Future<void>>[ |
| fakeLogReader.dispose(), |
| loggerSubscription.cancel(), |
| ]); |
| expect(passedArtifactTest, isTrue); |
| }, |
| overrides: <Type, Generator>{ |
| Artifacts: () => artifacts, |
| DeviceManager: () => testDeviceManager, |
| FileSystem: () => testFileSystem, |
| Logger: () => logger, |
| MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery( |
| mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}), |
| preliminaryMDnsClient: FakeMDnsClient( |
| <PtrResourceRecord>[], |
| <String, List<SrvResourceRecord>>{}, |
| ), |
| logger: logger, |
| analytics: const NoOpAnalytics(), |
| ), |
| ProcessManager: () => FakeProcessManager.empty(), |
| }, |
| ); |
| |
| testUsingContext( |
| 'succeeds with iOS device with mDNS', |
| () async { |
| final device = FakeIOSDevice( |
| portForwarder: portForwarder, |
| majorSdkVersion: 16, |
| onGetLogReader: () { |
| fakeLogReader.addLine('Foo'); |
| fakeLogReader.addLine( |
| 'The Dart VM service is listening on http://127.0.0.1:$devicePort', |
| ); |
| return fakeLogReader; |
| }, |
| ); |
| testDeviceManager.devices = <Device>[device]; |
| final hotRunner = FakeHotRunner(); |
| hotRunner.onAttach = |
| ( |
| Completer<DebugConnectionInfo>? connectionInfoCompleter, |
| Completer<void>? appStartedCompleter, |
| bool enableDevTools, |
| ) async => 0; |
| hotRunner.exited = false; |
| hotRunner.isWaitingForVmService = false; |
| final hotRunnerFactory = FakeHotRunnerFactory()..hotRunner = hotRunner; |
| |
| await createTestCommandRunner( |
| AttachCommand( |
| hotRunnerFactory: hotRunnerFactory, |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ), |
| ).run(<String>['attach']); |
| await fakeLogReader.dispose(); |
| |
| // Listen to the URI before checking port forwarder. Port forwarding |
| // is done as a side effect when generating the uri. |
| final FlutterDevice flutterDevice = hotRunnerFactory.devices.first; |
| final Uri? vmServiceUri = await flutterDevice.vmServiceUris?.first; |
| expect(vmServiceUri.toString(), 'http://127.0.0.1:$hostPort/xyz/'); |
| |
| expect(portForwarder.forwardedPorts, <TypeMatcher<ForwardedPort>>[ |
| isA<ForwardedPort>() |
| .having((ForwardedPort d) => d.devicePort, 'devicePort', devicePort) |
| .having((ForwardedPort d) => d.hostPort, 'hostPort', hostPort), |
| ]); |
| expect(hotRunnerFactory.devices, hasLength(1)); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Logger: () => logger, |
| DeviceManager: () => testDeviceManager, |
| MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery( |
| mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}), |
| preliminaryMDnsClient: FakeMDnsClient( |
| <PtrResourceRecord>[PtrResourceRecord('foo', future, domainName: 'bar')], |
| <String, List<SrvResourceRecord>>{ |
| 'bar': <SrvResourceRecord>[ |
| SrvResourceRecord( |
| 'bar', |
| future, |
| port: devicePort, |
| weight: 1, |
| priority: 1, |
| target: 'appId', |
| ), |
| ], |
| }, |
| txtResponse: <String, List<TxtResourceRecord>>{ |
| 'bar': <TxtResourceRecord>[ |
| TxtResourceRecord('bar', future, text: 'authCode=xyz\n'), |
| ], |
| }, |
| ), |
| logger: logger, |
| analytics: const NoOpAnalytics(), |
| ), |
| }, |
| ); |
| |
| testUsingContext( |
| 'succeeds with iOS device with mDNS wireless device', |
| () async { |
| final device = FakeIOSDevice( |
| portForwarder: portForwarder, |
| majorSdkVersion: 16, |
| connectionInterface: DeviceConnectionInterface.wireless, |
| ); |
| testDeviceManager.devices = <Device>[device]; |
| final hotRunner = FakeHotRunner(); |
| hotRunner.onAttach = |
| ( |
| Completer<DebugConnectionInfo>? connectionInfoCompleter, |
| Completer<void>? appStartedCompleter, |
| bool enableDevTools, |
| ) async => 0; |
| hotRunner.exited = false; |
| hotRunner.isWaitingForVmService = false; |
| final hotRunnerFactory = FakeHotRunnerFactory()..hotRunner = hotRunner; |
| |
| await createTestCommandRunner( |
| AttachCommand( |
| hotRunnerFactory: hotRunnerFactory, |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ), |
| ).run(<String>['attach']); |
| await fakeLogReader.dispose(); |
| |
| // Listen to the URI before checking port forwarder. Port forwarding |
| // is done as a side effect when generating the uri. |
| final FlutterDevice flutterDevice = hotRunnerFactory.devices.first; |
| final Uri? vmServiceUri = await flutterDevice.vmServiceUris?.first; |
| expect(vmServiceUri.toString(), 'http://111.111.111.111:123/xyz/'); |
| |
| expect(portForwarder.forwardedPorts, isEmpty); |
| expect(hotRunnerFactory.devices, hasLength(1)); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Logger: () => logger, |
| DeviceManager: () => testDeviceManager, |
| MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery( |
| mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}), |
| preliminaryMDnsClient: FakeMDnsClient( |
| <PtrResourceRecord>[PtrResourceRecord('foo', future, domainName: 'srv-foo')], |
| <String, List<SrvResourceRecord>>{ |
| 'srv-foo': <SrvResourceRecord>[ |
| SrvResourceRecord( |
| 'srv-foo', |
| future, |
| port: 123, |
| weight: 1, |
| priority: 1, |
| target: 'target-foo', |
| ), |
| ], |
| }, |
| ipResponse: <String, List<IPAddressResourceRecord>>{ |
| 'target-foo': <IPAddressResourceRecord>[ |
| IPAddressResourceRecord( |
| 'target-foo', |
| 0, |
| address: InternetAddress.tryParse('111.111.111.111')!, |
| ), |
| ], |
| }, |
| txtResponse: <String, List<TxtResourceRecord>>{ |
| 'srv-foo': <TxtResourceRecord>[ |
| TxtResourceRecord('srv-foo', future, text: 'authCode=xyz\n'), |
| ], |
| }, |
| ), |
| logger: logger, |
| analytics: const NoOpAnalytics(), |
| ), |
| }, |
| ); |
| |
| testUsingContext( |
| 'succeeds with iOS device with mDNS wireless device with debug-port', |
| () async { |
| final device = FakeIOSDevice( |
| portForwarder: portForwarder, |
| majorSdkVersion: 16, |
| connectionInterface: DeviceConnectionInterface.wireless, |
| ); |
| testDeviceManager.devices = <Device>[device]; |
| final hotRunner = FakeHotRunner(); |
| hotRunner.onAttach = |
| ( |
| Completer<DebugConnectionInfo>? connectionInfoCompleter, |
| Completer<void>? appStartedCompleter, |
| bool enableDevTools, |
| ) async => 0; |
| hotRunner.exited = false; |
| hotRunner.isWaitingForVmService = false; |
| final hotRunnerFactory = FakeHotRunnerFactory()..hotRunner = hotRunner; |
| |
| await createTestCommandRunner( |
| AttachCommand( |
| hotRunnerFactory: hotRunnerFactory, |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ), |
| ).run(<String>['attach', '--debug-port', '123']); |
| await fakeLogReader.dispose(); |
| |
| // Listen to the URI before checking port forwarder. Port forwarding |
| // is done as a side effect when generating the uri. |
| final FlutterDevice flutterDevice = hotRunnerFactory.devices.first; |
| final Uri? vmServiceUri = await flutterDevice.vmServiceUris?.first; |
| expect(vmServiceUri.toString(), 'http://111.111.111.111:123/xyz/'); |
| |
| expect(portForwarder.forwardedPorts, isEmpty); |
| expect(hotRunnerFactory.devices, hasLength(1)); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Logger: () => logger, |
| DeviceManager: () => testDeviceManager, |
| MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery( |
| mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}), |
| preliminaryMDnsClient: FakeMDnsClient( |
| <PtrResourceRecord>[ |
| PtrResourceRecord('bar', future, domainName: 'srv-bar'), |
| PtrResourceRecord('foo', future, domainName: 'srv-foo'), |
| ], |
| <String, List<SrvResourceRecord>>{ |
| 'srv-bar': <SrvResourceRecord>[ |
| SrvResourceRecord( |
| 'srv-bar', |
| future, |
| port: 321, |
| weight: 1, |
| priority: 1, |
| target: 'target-bar', |
| ), |
| ], |
| 'srv-foo': <SrvResourceRecord>[ |
| SrvResourceRecord( |
| 'srv-foo', |
| future, |
| port: 123, |
| weight: 1, |
| priority: 1, |
| target: 'target-foo', |
| ), |
| ], |
| }, |
| ipResponse: <String, List<IPAddressResourceRecord>>{ |
| 'target-foo': <IPAddressResourceRecord>[ |
| IPAddressResourceRecord( |
| 'target-foo', |
| 0, |
| address: InternetAddress.tryParse('111.111.111.111')!, |
| ), |
| ], |
| }, |
| txtResponse: <String, List<TxtResourceRecord>>{ |
| 'srv-foo': <TxtResourceRecord>[ |
| TxtResourceRecord('srv-foo', future, text: 'authCode=xyz\n'), |
| ], |
| }, |
| ), |
| logger: logger, |
| analytics: const NoOpAnalytics(), |
| ), |
| }, |
| ); |
| |
| testUsingContext( |
| 'succeeds with iOS device with mDNS wireless device with debug-url', |
| () async { |
| final device = FakeIOSDevice( |
| portForwarder: portForwarder, |
| majorSdkVersion: 16, |
| connectionInterface: DeviceConnectionInterface.wireless, |
| ); |
| testDeviceManager.devices = <Device>[device]; |
| final hotRunner = FakeHotRunner(); |
| hotRunner.onAttach = |
| ( |
| Completer<DebugConnectionInfo>? connectionInfoCompleter, |
| Completer<void>? appStartedCompleter, |
| bool enableDevTools, |
| ) async => 0; |
| hotRunner.exited = false; |
| hotRunner.isWaitingForVmService = false; |
| final hotRunnerFactory = FakeHotRunnerFactory()..hotRunner = hotRunner; |
| |
| await createTestCommandRunner( |
| AttachCommand( |
| hotRunnerFactory: hotRunnerFactory, |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ), |
| ).run(<String>['attach', '--debug-url', 'https://0.0.0.0:123']); |
| await fakeLogReader.dispose(); |
| |
| // Listen to the URI before checking port forwarder. Port forwarding |
| // is done as a side effect when generating the uri. |
| final FlutterDevice flutterDevice = hotRunnerFactory.devices.first; |
| final Uri? vmServiceUri = await flutterDevice.vmServiceUris?.first; |
| expect(vmServiceUri.toString(), 'http://111.111.111.111:123/xyz/'); |
| |
| expect(portForwarder.forwardedPorts, isEmpty); |
| expect(hotRunnerFactory.devices, hasLength(1)); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Logger: () => logger, |
| DeviceManager: () => testDeviceManager, |
| MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery( |
| mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}), |
| preliminaryMDnsClient: FakeMDnsClient( |
| <PtrResourceRecord>[ |
| PtrResourceRecord('bar', future, domainName: 'srv-bar'), |
| PtrResourceRecord('foo', future, domainName: 'srv-foo'), |
| ], |
| <String, List<SrvResourceRecord>>{ |
| 'srv-bar': <SrvResourceRecord>[ |
| SrvResourceRecord( |
| 'srv-bar', |
| future, |
| port: 321, |
| weight: 1, |
| priority: 1, |
| target: 'target-bar', |
| ), |
| ], |
| 'srv-foo': <SrvResourceRecord>[ |
| SrvResourceRecord( |
| 'srv-foo', |
| future, |
| port: 123, |
| weight: 1, |
| priority: 1, |
| target: 'target-foo', |
| ), |
| ], |
| }, |
| ipResponse: <String, List<IPAddressResourceRecord>>{ |
| 'target-foo': <IPAddressResourceRecord>[ |
| IPAddressResourceRecord( |
| 'target-foo', |
| 0, |
| address: InternetAddress.tryParse('111.111.111.111')!, |
| ), |
| ], |
| }, |
| txtResponse: <String, List<TxtResourceRecord>>{ |
| 'srv-foo': <TxtResourceRecord>[ |
| TxtResourceRecord('srv-foo', future, text: 'authCode=xyz\n'), |
| ], |
| }, |
| ), |
| logger: logger, |
| analytics: const NoOpAnalytics(), |
| ), |
| }, |
| ); |
| |
| testUsingContext( |
| 'finds VM Service port and forwards', |
| () async { |
| device.onGetLogReader = () { |
| fakeLogReader.addLine('Foo'); |
| fakeLogReader.addLine( |
| 'The Dart VM service is listening on http://127.0.0.1:$devicePort', |
| ); |
| return fakeLogReader; |
| }; |
| testDeviceManager.devices = <Device>[device]; |
| final completer = Completer<void>(); |
| final StreamSubscription<String> loggerSubscription = logger.stream.listen(( |
| String message, |
| ) { |
| if (message == '[verbose] VM Service URL on device: http://127.0.0.1:$devicePort') { |
| // The "VM Service URL on device" message is output by the ProtocolDiscovery when it found the VM Service. |
| completer.complete(); |
| } |
| }); |
| final Future<void> task = createTestCommandRunner( |
| AttachCommand( |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ), |
| ).run(<String>['attach']); |
| await completer.future; |
| |
| expect(portForwarder.forwardedPorts, <TypeMatcher<ForwardedPort>>[ |
| isA<ForwardedPort>() |
| .having((ForwardedPort d) => d.devicePort, 'devicePort', devicePort) |
| .having((ForwardedPort d) => d.hostPort, 'hostPort', hostPort), |
| ]); |
| |
| await fakeLogReader.dispose(); |
| await expectLoggerInterruptEndsTask(task, logger); |
| await loggerSubscription.cancel(); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Logger: () => logger, |
| DeviceManager: () => testDeviceManager, |
| }, |
| ); |
| |
| testUsingContext( |
| 'Fails with tool exit on bad VmService uri', |
| () async { |
| device.onGetLogReader = () { |
| fakeLogReader.addLine('Foo'); |
| fakeLogReader.addLine( |
| 'The Dart VM service is listening on http://127.0.0.1:$devicePort', |
| ); |
| fakeLogReader.dispose(); |
| return fakeLogReader; |
| }; |
| testDeviceManager.devices = <Device>[device]; |
| expect( |
| () => createTestCommandRunner( |
| AttachCommand( |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ), |
| ).run(<String>['attach']), |
| throwsToolExit(), |
| ); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Logger: () => logger, |
| DeviceManager: () => testDeviceManager, |
| }, |
| ); |
| |
| testUsingContext( |
| 'accepts filesystem parameters', |
| () async { |
| device.onGetLogReader = () { |
| fakeLogReader.addLine('Foo'); |
| fakeLogReader.addLine( |
| 'The Dart VM service is listening on http://127.0.0.1:$devicePort', |
| ); |
| return fakeLogReader; |
| }; |
| testDeviceManager.devices = <Device>[device]; |
| |
| const filesystemScheme = 'foo'; |
| const filesystemRoot = '/build-output/'; |
| const projectRoot = '/build-output/project-root'; |
| const outputDill = '/tmp/output.dill'; |
| |
| final hotRunner = FakeHotRunner(); |
| hotRunner.onAttach = |
| ( |
| Completer<DebugConnectionInfo>? connectionInfoCompleter, |
| Completer<void>? appStartedCompleter, |
| bool enableDevTools, |
| ) async => 0; |
| hotRunner.exited = false; |
| hotRunner.isWaitingForVmService = false; |
| |
| final hotRunnerFactory = FakeHotRunnerFactory()..hotRunner = hotRunner; |
| |
| final command = AttachCommand( |
| hotRunnerFactory: hotRunnerFactory, |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ); |
| await createTestCommandRunner(command).run(<String>[ |
| 'attach', |
| '--filesystem-scheme', |
| filesystemScheme, |
| '--filesystem-root', |
| filesystemRoot, |
| '--project-root', |
| projectRoot, |
| '--output-dill', |
| outputDill, |
| '-v', // enables verbose logging |
| ]); |
| |
| // Validate the attach call built a fake runner with the right |
| // project root and output dill. |
| expect(hotRunnerFactory.projectRootPath, projectRoot); |
| expect(hotRunnerFactory.dillOutputPath, outputDill); |
| expect(hotRunnerFactory.devices, hasLength(1)); |
| |
| // Validate that the attach call built a flutter device with the right |
| // output dill, filesystem scheme, and filesystem root. |
| final FlutterDevice flutterDevice = hotRunnerFactory.devices.first; |
| |
| expect(flutterDevice.buildInfo.fileSystemScheme, filesystemScheme); |
| expect(flutterDevice.buildInfo.fileSystemRoots, const <String>[filesystemRoot]); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| DeviceManager: () => testDeviceManager, |
| }, |
| ); |
| |
| testUsingContext( |
| 'exits when ipv6 is specified and debug-port is not on non-iOS device', |
| () async { |
| testDeviceManager.devices = <Device>[device]; |
| |
| final command = AttachCommand( |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ); |
| await expectLater( |
| createTestCommandRunner(command).run(<String>['attach', '--ipv6']), |
| throwsToolExit( |
| message: |
| 'When the --debug-port or --debug-url is unknown, this command determines ' |
| 'the value of --ipv6 on its own.', |
| ), |
| ); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| DeviceManager: () => testDeviceManager, |
| }, |
| ); |
| |
| testUsingContext( |
| 'succeeds when ipv6 is specified and debug-port is not on iOS device', |
| () async { |
| final device = FakeIOSDevice( |
| portForwarder: portForwarder, |
| majorSdkVersion: 12, |
| onGetLogReader: () { |
| fakeLogReader.addLine('Foo'); |
| fakeLogReader.addLine('The Dart VM service is listening on http://[::1]:$devicePort'); |
| return fakeLogReader; |
| }, |
| ); |
| testDeviceManager.devices = <Device>[device]; |
| final completer = Completer<void>(); |
| final StreamSubscription<String> loggerSubscription = logger.stream.listen(( |
| String message, |
| ) { |
| if (message == '[verbose] VM Service URL on device: http://[::1]:$devicePort') { |
| // The "VM Service URL on device" message is output by the ProtocolDiscovery when it found the VM Service. |
| completer.complete(); |
| } |
| }); |
| final hotRunner = FakeHotRunner(); |
| hotRunner.onAttach = |
| ( |
| Completer<DebugConnectionInfo>? connectionInfoCompleter, |
| Completer<void>? appStartedCompleter, |
| bool enableDevTools, |
| ) async => 0; |
| hotRunner.exited = false; |
| hotRunner.isWaitingForVmService = false; |
| final hotRunnerFactory = FakeHotRunnerFactory()..hotRunner = hotRunner; |
| |
| await createTestCommandRunner( |
| AttachCommand( |
| hotRunnerFactory: hotRunnerFactory, |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ), |
| ).run(<String>['attach', '--ipv6']); |
| await completer.future; |
| |
| expect(portForwarder.forwardedPorts, <TypeMatcher<ForwardedPort>>[ |
| isA<ForwardedPort>() |
| .having((ForwardedPort d) => d.devicePort, 'devicePort', devicePort) |
| .having((ForwardedPort d) => d.hostPort, 'hostPort', hostPort), |
| ]); |
| |
| await fakeLogReader.dispose(); |
| await loggerSubscription.cancel(); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Logger: () => logger, |
| DeviceManager: () => testDeviceManager, |
| MDnsVmServiceDiscovery: () => MDnsVmServiceDiscovery( |
| mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}), |
| preliminaryMDnsClient: FakeMDnsClient( |
| <PtrResourceRecord>[], |
| <String, List<SrvResourceRecord>>{}, |
| ), |
| logger: logger, |
| analytics: const NoOpAnalytics(), |
| ), |
| }, |
| ); |
| |
| testUsingContext( |
| 'exits when vm-service-port is specified and debug-port is not', |
| () async { |
| device.onGetLogReader = () { |
| fakeLogReader.addLine('Foo'); |
| fakeLogReader.addLine( |
| 'The Dart VM service is listening on http://127.0.0.1:$devicePort', |
| ); |
| return fakeLogReader; |
| }; |
| testDeviceManager.devices = <Device>[device]; |
| |
| final command = AttachCommand( |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ); |
| await expectLater( |
| createTestCommandRunner(command).run(<String>['attach', '--vm-service-port', '100']), |
| throwsToolExit( |
| message: |
| 'When the --debug-port or --debug-url is unknown, this command does not use ' |
| 'the value of --vm-service-port.', |
| ), |
| ); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| DeviceManager: () => testDeviceManager, |
| }, |
| ); |
| }); |
| |
| group('forwarding to given port', () { |
| const devicePort = 499; |
| const hostPort = 42; |
| late RecordingPortForwarder portForwarder; |
| late FakeAndroidDevice device; |
| |
| setUp(() { |
| final fakeDds = FakeDartDevelopmentService(); |
| portForwarder = RecordingPortForwarder(defaultHostPort: 42); |
| device = FakeAndroidDevice(id: '1') |
| ..portForwarder = portForwarder |
| ..dds = fakeDds; |
| }); |
| |
| testUsingContext( |
| 'succeeds in ipv4 mode', |
| () async { |
| testDeviceManager.devices = <Device>[device]; |
| |
| final completer = Completer<void>(); |
| final StreamSubscription<String> loggerSubscription = logger.stream.listen(( |
| String message, |
| ) { |
| if (message == '[verbose] Connecting to service protocol: http://127.0.0.1:42/') { |
| // Wait until resident_runner.dart tries to connect. |
| // There's nothing to connect _to_, so that's as far as we care to go. |
| completer.complete(); |
| } |
| }); |
| final Future<void> task = createTestCommandRunner( |
| AttachCommand( |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ), |
| ).run(<String>['attach', '--debug-port', '$devicePort']); |
| await completer.future; |
| |
| expect(portForwarder.forwardedPorts, <TypeMatcher<ForwardedPort>>[ |
| isA<ForwardedPort>() |
| .having((ForwardedPort d) => d.devicePort, 'devicePort', devicePort) |
| .having((ForwardedPort d) => d.hostPort, 'hostPort', hostPort), |
| ]); |
| |
| await expectLoggerInterruptEndsTask(task, logger); |
| await loggerSubscription.cancel(); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Logger: () => logger, |
| DeviceManager: () => testDeviceManager, |
| }, |
| ); |
| |
| testUsingContext( |
| 'succeeds in ipv6 mode', |
| () async { |
| testDeviceManager.devices = <Device>[device]; |
| |
| final completer = Completer<void>(); |
| final StreamSubscription<String> loggerSubscription = logger.stream.listen(( |
| String message, |
| ) { |
| if (message == '[verbose] Connecting to service protocol: http://[::1]:42/') { |
| // Wait until resident_runner.dart tries to connect. |
| // There's nothing to connect _to_, so that's as far as we care to go. |
| completer.complete(); |
| } |
| }); |
| final Future<void> task = createTestCommandRunner( |
| AttachCommand( |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ), |
| ).run(<String>['attach', '--debug-port', '$devicePort', '--ipv6']); |
| await completer.future; |
| |
| expect(portForwarder.forwardedPorts, <TypeMatcher<ForwardedPort>>[ |
| isA<ForwardedPort>() |
| .having((ForwardedPort d) => d.devicePort, 'devicePort', devicePort) |
| .having((ForwardedPort d) => d.hostPort, 'hostPort', hostPort), |
| ]); |
| |
| await expectLoggerInterruptEndsTask(task, logger); |
| await loggerSubscription.cancel(); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Logger: () => logger, |
| DeviceManager: () => testDeviceManager, |
| }, |
| ); |
| |
| testUsingContext( |
| 'skips in ipv4 mode with a provided VM Service port', |
| () async { |
| testDeviceManager.devices = <Device>[device]; |
| |
| final completer = Completer<void>(); |
| final StreamSubscription<String> loggerSubscription = logger.stream.listen(( |
| String message, |
| ) { |
| if (message == '[verbose] Connecting to service protocol: http://127.0.0.1:42/') { |
| // Wait until resident_runner.dart tries to connect. |
| // There's nothing to connect _to_, so that's as far as we care to go. |
| completer.complete(); |
| } |
| }); |
| final Future<void> task = |
| createTestCommandRunner( |
| AttachCommand( |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ), |
| ).run(<String>[ |
| 'attach', |
| '--debug-port', |
| '$devicePort', |
| '--vm-service-port', |
| '$hostPort', |
| // Ensure DDS doesn't use hostPort by binding to a random port. |
| '--dds-port', |
| '0', |
| ]); |
| await completer.future; |
| |
| expect(portForwarder.forwardedPorts, isEmpty); |
| |
| await expectLoggerInterruptEndsTask(task, logger); |
| await loggerSubscription.cancel(); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Logger: () => logger, |
| DeviceManager: () => testDeviceManager, |
| }, |
| ); |
| |
| testUsingContext( |
| 'skips in ipv6 mode with a provided VM Service port', |
| () async { |
| testDeviceManager.devices = <Device>[device]; |
| |
| final completer = Completer<void>(); |
| final StreamSubscription<String> loggerSubscription = logger.stream.listen(( |
| String message, |
| ) { |
| if (message == '[verbose] Connecting to service protocol: http://[::1]:42/') { |
| // Wait until resident_runner.dart tries to connect. |
| // There's nothing to connect _to_, so that's as far as we care to go. |
| completer.complete(); |
| } |
| }); |
| final Future<void> task = |
| createTestCommandRunner( |
| AttachCommand( |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ), |
| ).run(<String>[ |
| 'attach', |
| '--debug-port', |
| '$devicePort', |
| '--vm-service-port', |
| '$hostPort', |
| '--ipv6', |
| // Ensure DDS doesn't use hostPort by binding to a random port. |
| '--dds-port', |
| '0', |
| ]); |
| await completer.future; |
| |
| expect(portForwarder.forwardedPorts, isEmpty); |
| |
| await expectLoggerInterruptEndsTask(task, logger); |
| await loggerSubscription.cancel(); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Logger: () => logger, |
| DeviceManager: () => testDeviceManager, |
| }, |
| ); |
| }); |
| |
| testUsingContext( |
| 'exits when no device connected', |
| () async { |
| final command = AttachCommand( |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ); |
| await expectLater( |
| createTestCommandRunner(command).run(<String>['attach']), |
| throwsToolExit(), |
| ); |
| expect(testLogger.statusText, containsIgnoringWhitespace('No supported devices connected')); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| DeviceManager: () => testDeviceManager, |
| }, |
| ); |
| |
| testUsingContext( |
| 'fails when targeted device is not Android with --device-user', |
| () async { |
| final device = FakeIOSDevice(); |
| testDeviceManager.devices = <Device>[device]; |
| expect( |
| createTestCommandRunner( |
| AttachCommand( |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ), |
| ).run(<String>['attach', '--device-user', '10']), |
| throwsToolExit(message: '--device-user is only supported for Android'), |
| ); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| DeviceManager: () => testDeviceManager, |
| }, |
| ); |
| |
| testUsingContext( |
| 'exits when multiple devices connected', |
| () async { |
| final command = AttachCommand( |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ); |
| testDeviceManager.devices = <Device>[ |
| FakeAndroidDevice(id: 'xx1'), |
| FakeAndroidDevice(id: 'yy2'), |
| ]; |
| await expectLater( |
| createTestCommandRunner(command).run(<String>['attach']), |
| throwsToolExit(), |
| ); |
| expect(testLogger.statusText, containsIgnoringWhitespace('More than one device')); |
| expect(testLogger.statusText, contains('xx1')); |
| expect(testLogger.statusText, contains('yy2')); |
| expect(MacOSDesignedForIPadDevices.allowDiscovery, isTrue); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| DeviceManager: () => testDeviceManager, |
| AnsiTerminal: () => FakeTerminal(stdinHasTerminal: false), |
| }, |
| ); |
| |
| testUsingContext( |
| 'Catches service disappeared error', |
| () async { |
| final device = FakeAndroidDevice(id: '1') |
| ..portForwarder = const NoOpDevicePortForwarder() |
| ..onGetLogReader = () => NoOpDeviceLogReader('test'); |
| final hotRunner = FakeHotRunner(); |
| final hotRunnerFactory = FakeHotRunnerFactory()..hotRunner = hotRunner; |
| hotRunner.onAttach = |
| ( |
| Completer<DebugConnectionInfo>? connectionInfoCompleter, |
| Completer<void>? appStartedCompleter, |
| bool enableDevTools, |
| ) async { |
| await null; |
| throw vm_service.RPCError( |
| 'flutter._listViews', |
| vm_service.RPCErrorKind.kServiceDisappeared.code, |
| '', |
| ); |
| }; |
| |
| testDeviceManager.devices = <Device>[device]; |
| testFileSystem.file('lib/main.dart').createSync(); |
| |
| final command = AttachCommand( |
| hotRunnerFactory: hotRunnerFactory, |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ); |
| await expectLater( |
| createTestCommandRunner(command).run(<String>['attach']), |
| throwsToolExit(message: 'Lost connection to device.'), |
| ); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| DeviceManager: () => testDeviceManager, |
| }, |
| ); |
| |
| testUsingContext( |
| 'Catches "Service connection disposed" error by code', |
| () async { |
| final device = FakeAndroidDevice(id: '1') |
| ..portForwarder = const NoOpDevicePortForwarder() |
| ..onGetLogReader = () => NoOpDeviceLogReader('test'); |
| final hotRunner = FakeHotRunner(); |
| final hotRunnerFactory = FakeHotRunnerFactory()..hotRunner = hotRunner; |
| hotRunner.onAttach = |
| ( |
| Completer<DebugConnectionInfo>? connectionInfoCompleter, |
| Completer<void>? appStartedCompleter, |
| bool enableDevTools, |
| ) async { |
| await null; |
| throw vm_service.RPCError( |
| 'flutter._listViews', |
| vm_service.RPCErrorKind.kConnectionDisposed.code, |
| 'dummy text not matched', |
| ); |
| }; |
| |
| testDeviceManager.devices = <Device>[device]; |
| testFileSystem.file('lib/main.dart').createSync(); |
| |
| final command = AttachCommand( |
| hotRunnerFactory: hotRunnerFactory, |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ); |
| await expectLater( |
| createTestCommandRunner(command).run(<String>['attach']), |
| throwsToolExit(message: 'Lost connection to device.'), |
| ); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| DeviceManager: () => testDeviceManager, |
| }, |
| ); |
| |
| testUsingContext( |
| 'Catches "Service connection disposed" error by text', |
| () async { |
| final device = FakeAndroidDevice(id: '1') |
| ..portForwarder = const NoOpDevicePortForwarder() |
| ..onGetLogReader = () => NoOpDeviceLogReader('test'); |
| final hotRunner = FakeHotRunner(); |
| final hotRunnerFactory = FakeHotRunnerFactory()..hotRunner = hotRunner; |
| hotRunner.onAttach = |
| ( |
| Completer<DebugConnectionInfo>? connectionInfoCompleter, |
| Completer<void>? appStartedCompleter, |
| bool enableDevTools, |
| ) async { |
| await null; |
| throw vm_service.RPCError( |
| 'flutter._listViews', |
| vm_service.RPCErrorKind.kServerError.code, |
| 'Service connection disposed', |
| ); |
| }; |
| |
| testDeviceManager.devices = <Device>[device]; |
| testFileSystem.file('lib/main.dart').createSync(); |
| |
| final command = AttachCommand( |
| hotRunnerFactory: hotRunnerFactory, |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ); |
| await expectLater( |
| createTestCommandRunner(command).run(<String>['attach']), |
| throwsToolExit(message: 'Lost connection to device.'), |
| ); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| DeviceManager: () => testDeviceManager, |
| }, |
| ); |
| |
| testUsingContext( |
| 'Does not catch generic RPC error', |
| () async { |
| final device = FakeAndroidDevice(id: '1') |
| ..portForwarder = const NoOpDevicePortForwarder() |
| ..onGetLogReader = () => NoOpDeviceLogReader('test'); |
| final hotRunner = FakeHotRunner(); |
| final hotRunnerFactory = FakeHotRunnerFactory()..hotRunner = hotRunner; |
| |
| hotRunner.onAttach = |
| ( |
| Completer<DebugConnectionInfo>? connectionInfoCompleter, |
| Completer<void>? appStartedCompleter, |
| bool enableDevTools, |
| ) async { |
| await null; |
| throw vm_service.RPCError( |
| 'flutter._listViews', |
| vm_service.RPCErrorKind.kInvalidParams.code, |
| '', |
| ); |
| }; |
| |
| testDeviceManager.devices = <Device>[device]; |
| testFileSystem.file('lib/main.dart').createSync(); |
| |
| final command = AttachCommand( |
| hotRunnerFactory: hotRunnerFactory, |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ); |
| await expectLater( |
| createTestCommandRunner(command).run(<String>['attach']), |
| throwsA(isA<vm_service.RPCError>()), |
| ); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| DeviceManager: () => testDeviceManager, |
| }, |
| ); |
| |
| group('prints warning when too slow', () { |
| late SlowWarningCallbackBufferLogger logger; |
| |
| setUp(() { |
| logger = SlowWarningCallbackBufferLogger.test(); |
| }); |
| |
| testUsingContext( |
| 'to find on iOS Simulator', |
| () async { |
| final device = FakeIOSSimulator(); |
| testDeviceManager.devices = <Device>[device]; |
| FakeAsync().run((FakeAsync fakeAsync) { |
| createTestCommandRunner( |
| AttachCommand( |
| stdio: stdio, |
| logger: logger, |
| terminal: terminal, |
| signals: signals, |
| platform: platform, |
| processInfo: processInfo, |
| fileSystem: testFileSystem, |
| ), |
| ).run(<String>['attach']); |
| |
| logger.expectedWarning = |
| 'The Dart VM Service was not discovered after 30 seconds. ' |
| 'This may be due to limited mDNS support in the iOS Simulator.\n\n' |
| 'Click "Allow" to the prompt on your device asking if you would like to find and connect devices on your local network. ' |
| 'If you selected "Don\'t Allow", you can turn it on in Settings > Your App Name > Local Network. ' |
| "If you don't see your app in the Settings, uninstall the app and rerun to see the prompt again.\n\n" |
| 'If you do not receive a prompt, either run "flutter attach" before starting the ' |
| 'app or use the Dart VM service URL from the Xcode console with ' |
| '"flutter attach --debug-url=<URL>".\n'; |
| fakeAsync.elapse(const Duration(seconds: 30)); |
| }); |
| }, |
| overrides: <Type, Generator>{ |
| FileSystem: () => testFileSystem, |
| ProcessManager: () => FakeProcessManager.any(), |
| Logger: () => logger, |
| DeviceManager: () => testDeviceManager, |
| }, |
| ); |
| }); |
| }); |
| } |
| |
| class FakeHotRunner extends Fake implements HotRunner { |
| late Future<int> Function(Completer<DebugConnectionInfo>?, Completer<void>?, bool) onAttach; |
| |
| @override |
| var exited = false; |
| |
| @override |
| var isWaitingForVmService = true; |
| |
| @override |
| Future<int> attach({ |
| Completer<DebugConnectionInfo>? connectionInfoCompleter, |
| Completer<void>? appStartedCompleter, |
| bool enableDevTools = false, |
| bool needsFullRestart = true, |
| }) { |
| return onAttach(connectionInfoCompleter, appStartedCompleter, enableDevTools); |
| } |
| |
| @override |
| var supportsServiceProtocol = false; |
| |
| @override |
| var stayResident = true; |
| |
| @override |
| void printHelp({required bool details, bool reloadIsRestart = false}) {} |
| } |
| |
| class FakeHotRunnerFactory extends Fake implements HotRunnerFactory { |
| late HotRunner hotRunner; |
| String? dillOutputPath; |
| String? projectRootPath; |
| late List<FlutterDevice> devices; |
| void Function(Artifacts artifacts)? _artifactTester; |
| |
| @override |
| HotRunner build( |
| List<FlutterDevice> devices, { |
| required String target, |
| required DebuggingOptions debuggingOptions, |
| bool benchmarkMode = false, |
| File? applicationBinary, |
| bool hostIsIde = false, |
| String? projectRootPath, |
| String? packagesFilePath, |
| String? dillOutputPath, |
| bool stayResident = true, |
| bool ipv6 = false, |
| FlutterProject? flutterProject, |
| Analytics? analytics, |
| String? nativeAssetsYamlFile, |
| }) { |
| if (_artifactTester != null) { |
| for (final device in devices) { |
| _artifactTester!((device.generator! as DefaultResidentCompiler).artifacts); |
| } |
| } |
| this.devices = devices; |
| this.dillOutputPath = dillOutputPath; |
| this.projectRootPath = projectRootPath; |
| return hotRunner; |
| } |
| } |
| |
| class RecordingPortForwarder implements DevicePortForwarder { |
| RecordingPortForwarder({required this.defaultHostPort}); |
| final int defaultHostPort; |
| |
| @override |
| Future<void> dispose() async { |
| forwardedPorts.clear(); |
| } |
| |
| @override |
| Future<int> forward(int devicePort, {int? hostPort}) async { |
| hostPort ??= defaultHostPort; |
| final forwardedPort = ForwardedPort(hostPort, devicePort); |
| forwardedPorts.add(forwardedPort); |
| return forwardedPort.hostPort; |
| } |
| |
| @override |
| var forwardedPorts = <ForwardedPort>[]; |
| |
| @override |
| Future<void> unforward(ForwardedPort forwardedPort) async { |
| // Find a matching forwarded port. |
| int? n; |
| for (final (int i, ForwardedPort possibleMatch) in forwardedPorts.indexed) { |
| if (possibleMatch.hostPort != forwardedPort.hostPort || |
| possibleMatch.devicePort != forwardedPort.devicePort) { |
| continue; |
| } |
| if (n != null) { |
| throw StateError('Multiple matching ports for $forwardedPort'); |
| } |
| n = i; |
| } |
| if (n == null) { |
| throw StateError('No port found for $forwardedPort'); |
| } |
| forwardedPorts.removeAt(n); |
| } |
| } |
| |
| class StreamLogger extends Logger { |
| @override |
| bool get isVerbose => true; |
| |
| @override |
| void printError( |
| String message, { |
| StackTrace? stackTrace, |
| bool? emphasis, |
| TerminalColor? color, |
| int? indent, |
| int? hangingIndent, |
| bool? wrap, |
| }) { |
| hadErrorOutput = true; |
| _log('[stderr] $message'); |
| } |
| |
| @override |
| void printWarning( |
| String message, { |
| bool? emphasis, |
| TerminalColor? color, |
| int? indent, |
| int? hangingIndent, |
| bool? wrap, |
| bool fatal = true, |
| }) { |
| hadWarningOutput = hadWarningOutput || fatal; |
| _log('[stderr] $message'); |
| } |
| |
| @override |
| void printStatus( |
| String message, { |
| bool? emphasis, |
| TerminalColor? color, |
| bool? newline, |
| int? indent, |
| int? hangingIndent, |
| bool? wrap, |
| }) { |
| _log('[stdout] $message'); |
| } |
| |
| @override |
| void printBox(String message, {String? title}) { |
| if (title == null) { |
| _log('[stdout] $message'); |
| } else { |
| _log('[stdout] $title: $message'); |
| } |
| } |
| |
| @override |
| void printTrace(String message) { |
| _log('[verbose] $message'); |
| } |
| |
| @override |
| Status startProgress( |
| String message, { |
| Duration? timeout, |
| String? progressId, |
| bool multilineOutput = false, |
| bool includeTiming = true, |
| int progressIndicatorPadding = kDefaultStatusPadding, |
| }) { |
| _log('[progress] $message'); |
| return SilentStatus(stopwatch: Stopwatch())..start(); |
| } |
| |
| @override |
| Status startSpinner({ |
| VoidCallback? onFinish, |
| Duration? timeout, |
| SlowWarningCallback? slowWarningCallback, |
| TerminalColor? warningColor, |
| }) { |
| return SilentStatus(stopwatch: Stopwatch(), onFinish: onFinish)..start(); |
| } |
| |
| var _interrupt = false; |
| |
| void interrupt() { |
| _interrupt = true; |
| } |
| |
| final _controller = StreamController<String>.broadcast(); |
| |
| void _log(String message) { |
| _controller.add(message); |
| if (_interrupt) { |
| _interrupt = false; |
| throw const LoggerInterrupted(); |
| } |
| } |
| |
| Stream<String> get stream => _controller.stream; |
| |
| @override |
| void sendEvent(String name, [Map<String, dynamic>? args]) {} |
| |
| @override |
| bool get supportsColor => throw UnimplementedError(); |
| |
| @override |
| bool get hasTerminal => false; |
| |
| @override |
| void clear() => _log('[stdout] ${terminal.clearScreen()}\n'); |
| |
| @override |
| Terminal get terminal => Terminal.test(); |
| } |
| |
| class LoggerInterrupted implements Exception { |
| const LoggerInterrupted(); |
| } |
| |
| Future<void> expectLoggerInterruptEndsTask(Future<void> task, StreamLogger logger) async { |
| logger.interrupt(); // an exception during the task should cause it to fail... |
| await expectLater( |
| () => task, |
| throwsA(isA<ToolExit>().having((ToolExit error) => error.exitCode, 'exitCode', 2)), |
| ); |
| } |
| |
| class FakeDartDevelopmentService extends Fake implements DartDevelopmentService { |
| @override |
| Future<void> get done => noopCompleter.future; |
| final noopCompleter = Completer<void>(); |
| |
| @override |
| Future<void> startDartDevelopmentService( |
| Uri vmServiceUri, { |
| int? ddsPort, |
| FlutterDevice? device, |
| bool? ipv6, |
| bool? disableServiceAuthCodes, |
| bool enableDevTools = false, |
| bool cacheStartupProfile = false, |
| String? google3WorkspaceRoot, |
| Uri? devToolsServerAddress, |
| }) async {} |
| |
| @override |
| Uri get uri => Uri.parse('http://localhost:8181'); |
| } |
| |
| class FakeAndroidDevice extends Fake implements AndroidDevice { |
| FakeAndroidDevice({required this.id}); |
| |
| @override |
| late DartDevelopmentService dds; |
| |
| @override |
| final String id; |
| |
| @override |
| String get name => 'd$id'; |
| |
| @override |
| String get displayName => name; |
| |
| @override |
| Future<bool> get isLocalEmulator async => false; |
| |
| @override |
| Future<String> get sdkNameAndVersion async => 'Android 46'; |
| |
| @override |
| Future<String> get targetPlatformDisplayName async => 'android'; |
| |
| @override |
| Future<TargetPlatform> get targetPlatform async => TargetPlatform.android_arm; |
| |
| @override |
| DeviceConnectionInterface get connectionInterface => DeviceConnectionInterface.attached; |
| |
| @override |
| Future<bool> isSupported() async => true; |
| |
| @override |
| bool get isConnected => true; |
| |
| @override |
| bool get supportsHotRestart => true; |
| |
| @override |
| bool get supportsFlutterExit => false; |
| |
| @override |
| bool isSupportedForProject(FlutterProject flutterProject) => true; |
| |
| @override |
| DevicePortForwarder? portForwarder; |
| |
| DeviceLogReader Function()? onGetLogReader; |
| |
| @override |
| FutureOr<DeviceLogReader> getLogReader({ApplicationPackage? app, bool includePastLogs = false}) { |
| if (onGetLogReader == null) { |
| throw UnimplementedError( |
| 'Called getLogReader but no onGetLogReader callback was supplied in the constructor to FakeAndroidDevice.', |
| ); |
| } |
| return onGetLogReader!(); |
| } |
| |
| @override |
| final PlatformType platformType = PlatformType.android; |
| |
| @override |
| Category get category => Category.mobile; |
| |
| @override |
| bool get ephemeral => true; |
| |
| @override |
| VMServiceDiscoveryForAttach getVMServiceDiscoveryForAttach({ |
| String? appId, |
| String? fuchsiaModule, |
| int? filterDevicePort, |
| int? expectedHostPort, |
| required bool ipv6, |
| required Logger logger, |
| }) => LogScanningVMServiceDiscoveryForAttach( |
| Future<DeviceLogReader>.value(getLogReader()), |
| portForwarder: portForwarder, |
| devicePort: filterDevicePort, |
| hostPort: expectedHostPort, |
| ipv6: ipv6, |
| logger: logger, |
| ); |
| |
| @override |
| Future<void> dispose() async { |
| await portForwarder?.dispose(); |
| } |
| } |
| |
| class FakeIOSDevice extends Fake implements IOSDevice { |
| FakeIOSDevice({ |
| DevicePortForwarder? portForwarder, |
| this.onGetLogReader, |
| this.connectionInterface = DeviceConnectionInterface.attached, |
| this.majorSdkVersion = 0, |
| }) : _portForwarder = portForwarder; |
| |
| final DevicePortForwarder? _portForwarder; |
| @override |
| int majorSdkVersion; |
| |
| @override |
| final DeviceConnectionInterface connectionInterface; |
| |
| @override |
| bool get isWirelesslyConnected => connectionInterface == DeviceConnectionInterface.wireless; |
| |
| @override |
| DevicePortForwarder get portForwarder => _portForwarder!; |
| |
| @override |
| DartDevelopmentService get dds => throw UnimplementedError('getter dds not implemented'); |
| |
| final DeviceLogReader Function()? onGetLogReader; |
| |
| @override |
| DeviceLogReader getLogReader({ |
| IOSApp? app, |
| bool includePastLogs = false, |
| bool usingCISystem = false, |
| }) { |
| if (onGetLogReader == null) { |
| throw UnimplementedError( |
| 'Called getLogReader but no onGetLogReader callback was supplied in the constructor to FakeIOSDevice', |
| ); |
| } |
| return onGetLogReader!(); |
| } |
| |
| @override |
| final name = 'name'; |
| |
| @override |
| String get displayName => name; |
| |
| @override |
| Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios; |
| |
| @override |
| final PlatformType platformType = PlatformType.ios; |
| |
| @override |
| Future<bool> isSupported() async => true; |
| |
| @override |
| bool isSupportedForProject(FlutterProject project) => true; |
| |
| @override |
| bool get isConnected => true; |
| |
| @override |
| bool get ephemeral => true; |
| |
| @override |
| VMServiceDiscoveryForAttach getVMServiceDiscoveryForAttach({ |
| String? appId, |
| String? fuchsiaModule, |
| int? filterDevicePort, |
| int? expectedHostPort, |
| required bool ipv6, |
| required Logger logger, |
| }) { |
| final bool compatibleWithProtocolDiscovery = |
| majorSdkVersion < IOSDeviceLogReader.minimumUniversalLoggingSdkVersion && |
| !isWirelesslyConnected; |
| final mdnsVMServiceDiscoveryForAttach = MdnsVMServiceDiscoveryForAttach( |
| device: this, |
| appId: appId, |
| deviceVmservicePort: filterDevicePort, |
| hostVmservicePort: expectedHostPort, |
| usesIpv6: ipv6, |
| useDeviceIPAsHost: isWirelesslyConnected, |
| ); |
| |
| if (compatibleWithProtocolDiscovery) { |
| return DelegateVMServiceDiscoveryForAttach(<VMServiceDiscoveryForAttach>[ |
| mdnsVMServiceDiscoveryForAttach, |
| LogScanningVMServiceDiscoveryForAttach( |
| Future<DeviceLogReader>.value(getLogReader()), |
| portForwarder: portForwarder, |
| devicePort: filterDevicePort, |
| hostPort: expectedHostPort, |
| ipv6: ipv6, |
| logger: logger, |
| ), |
| ]); |
| } else { |
| return mdnsVMServiceDiscoveryForAttach; |
| } |
| } |
| } |
| |
| class FakeIOSSimulator extends Fake implements IOSSimulator { |
| @override |
| final name = 'name'; |
| |
| @override |
| String get displayName => name; |
| |
| @override |
| Future<bool> isSupported() async => true; |
| |
| @override |
| bool isSupportedForProject(FlutterProject flutterProject) => true; |
| |
| @override |
| bool get isConnected => true; |
| |
| @override |
| DeviceConnectionInterface get connectionInterface => DeviceConnectionInterface.attached; |
| |
| @override |
| bool get ephemeral => true; |
| |
| @override |
| Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios; |
| |
| @override |
| final PlatformType platformType = PlatformType.ios; |
| |
| @override |
| bool get isWirelesslyConnected => false; |
| |
| @override |
| DevicePortForwarder portForwarder = RecordingPortForwarder(defaultHostPort: 42); |
| |
| @override |
| VMServiceDiscoveryForAttach getVMServiceDiscoveryForAttach({ |
| String? appId, |
| String? fuchsiaModule, |
| int? filterDevicePort, |
| int? expectedHostPort, |
| required bool ipv6, |
| required Logger logger, |
| }) { |
| final mdnsVMServiceDiscoveryForAttach = MdnsVMServiceDiscoveryForAttach( |
| device: this, |
| appId: appId, |
| deviceVmservicePort: filterDevicePort, |
| hostVmservicePort: expectedHostPort, |
| usesIpv6: ipv6, |
| useDeviceIPAsHost: isWirelesslyConnected, |
| ); |
| return mdnsVMServiceDiscoveryForAttach; |
| } |
| } |
| |
| class FakeMDnsClient extends Fake implements MDnsClient { |
| FakeMDnsClient( |
| this.ptrRecords, |
| this.srvResponse, { |
| this.txtResponse = const <String, List<TxtResourceRecord>>{}, |
| this.ipResponse = const <String, List<IPAddressResourceRecord>>{}, |
| this.osErrorOnStart = false, |
| }); |
| |
| final List<PtrResourceRecord> ptrRecords; |
| final Map<String, List<SrvResourceRecord>> srvResponse; |
| final Map<String, List<TxtResourceRecord>> txtResponse; |
| final Map<String, List<IPAddressResourceRecord>> ipResponse; |
| final bool osErrorOnStart; |
| |
| @override |
| Future<void> start({ |
| InternetAddress? listenAddress, |
| NetworkInterfacesFactory? interfacesFactory, |
| int mDnsPort = 5353, |
| InternetAddress? mDnsAddress, |
| Function? onError, |
| }) async { |
| if (osErrorOnStart) { |
| throw const OSError('Operation not supported on socket', 102); |
| } |
| } |
| |
| @override |
| Stream<T> lookup<T extends ResourceRecord>( |
| ResourceRecordQuery query, { |
| Duration timeout = const Duration(seconds: 5), |
| }) { |
| if (T == PtrResourceRecord && |
| query.fullyQualifiedName == MDnsVmServiceDiscovery.dartVmServiceName) { |
| return Stream<PtrResourceRecord>.fromIterable(ptrRecords) as Stream<T>; |
| } |
| if (T == SrvResourceRecord) { |
| final String key = query.fullyQualifiedName; |
| return Stream<SrvResourceRecord>.fromIterable(srvResponse[key] ?? <SrvResourceRecord>[]) |
| as Stream<T>; |
| } |
| if (T == TxtResourceRecord) { |
| final String key = query.fullyQualifiedName; |
| return Stream<TxtResourceRecord>.fromIterable(txtResponse[key] ?? <TxtResourceRecord>[]) |
| as Stream<T>; |
| } |
| if (T == IPAddressResourceRecord) { |
| final String key = query.fullyQualifiedName; |
| return Stream<IPAddressResourceRecord>.fromIterable( |
| ipResponse[key] ?? <IPAddressResourceRecord>[], |
| ) |
| as Stream<T>; |
| } |
| throw UnsupportedError('Unsupported query type $T'); |
| } |
| |
| @override |
| void stop() {} |
| } |
| |
| class TestDeviceManager extends DeviceManager { |
| TestDeviceManager({required super.logger}); |
| var devices = <Device>[]; |
| |
| @override |
| List<DeviceDiscovery> get deviceDiscoverers { |
| final discoverer = FakePollingDeviceDiscovery(); |
| devices.forEach(discoverer.addDevice); |
| return <DeviceDiscovery>[discoverer]; |
| } |
| } |
| |
| class FakeTerminal extends Fake implements AnsiTerminal { |
| FakeTerminal({this.stdinHasTerminal = true}); |
| |
| @override |
| final bool stdinHasTerminal; |
| |
| @override |
| var usesTerminalUi = false; |
| |
| @override |
| var singleCharMode = false; |
| |
| @override |
| Stream<String> get keystrokes => StreamController<String>().stream; |
| } |
| |
| class SlowWarningCallbackBufferLogger extends BufferLogger { |
| SlowWarningCallbackBufferLogger.test() : super.test(); |
| |
| String? expectedWarning; |
| |
| @override |
| Status startSpinner({ |
| VoidCallback? onFinish, |
| Duration? timeout, |
| SlowWarningCallback? slowWarningCallback, |
| TerminalColor? warningColor, |
| }) { |
| expect(slowWarningCallback, isNotNull); |
| expect(slowWarningCallback!(), expectedWarning); |
| return SilentStatus(stopwatch: Stopwatch(), onFinish: onFinish)..start(); |
| } |
| } |