// 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:file/memory.dart'; import 'package:flutter_tools/src/android/android_device.dart'; import 'package:flutter_tools/src/android/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/device.dart'; import 'package:flutter_tools/src/device_port_forwarder.dart'; import 'package:flutter_tools/src/ios/application_package.dart'; import 'package:flutter_tools/src/ios/devices.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/reporting/reporting.dart'; import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/run_hot.dart'; import 'package:flutter_tools/src/vmservice.dart'; import 'package:multicast_dns/multicast_dns.dart'; import 'package:test/fake.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 bool stdinHasTerminal = false; } class FakeProcessInfo extends Fake implements ProcessInfo { @override int 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(); stdio = FakeStdio(); terminal = FakeTerminal(); signals = Signals.test(); processInfo = FakeProcessInfo(); testDeviceManager = TestDeviceManager(logger: BufferLogger.test()); }); group('with one device and no specified target file', () { const int devicePort = 499; const int hostPort = 42; late FakeDeviceLogReader fakeLogReader; late RecordingPortForwarder portForwarder; late FakeDartDevelopmentService fakeDds; late FakeAndroidDevice device; setUp(() { fakeLogReader = FakeDeviceLogReader(); portForwarder = RecordingPortForwarder(hostPort); fakeDds = FakeDartDevelopmentService(); device = FakeAndroidDevice(id: '1') ..portForwarder = portForwarder ..dds = fakeDds; }); tearDown(() { fakeLogReader.dispose(); }); testUsingContext('succeeds with iOS device', () async { final FakeIOSDevice device = FakeIOSDevice( logReader: fakeLogReader, portForwarder: portForwarder, 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<void> completer = Completer<void>(); final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) { if (message == '[verbose] Observatory URL on device: http://127.0.0.1:$devicePort') { // The "Observatory URL on device" message is output by the ProtocolDiscovery when it found the observatory. completer.complete(); } }); final Future<void> task = createTestCommandRunner(AttachCommand( artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, signals: signals, platform: platform, processInfo: processInfo, fileSystem: testFileSystem, )).run(<String>['attach']); await completer.future; expect(portForwarder.devicePort, devicePort); expect(portForwarder.hostPort, hostPort); await fakeLogReader.dispose(); await expectLoggerInterruptEndsTask(task, logger); await loggerSubscription.cancel(); }, overrides: <Type, Generator>{ FileSystem: () => testFileSystem, ProcessManager: () => FakeProcessManager.any(), Logger: () => logger, DeviceManager: () => testDeviceManager, MDnsObservatoryDiscovery: () => MDnsObservatoryDiscovery( mdnsClient: FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{}), logger: logger, flutterUsage: TestUsage(), ), }); testUsingContext('finds observatory 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<void> completer = Completer<void>(); final StreamSubscription<String> loggerSubscription = logger.stream.listen((String message) { if (message == '[verbose] Observatory URL on device: http://127.0.0.1:$devicePort') { // The "Observatory URL on device" message is output by the ProtocolDiscovery when it found the observatory. completer.complete(); } }); final Future<void> task = createTestCommandRunner(AttachCommand( artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, signals: signals, platform: platform, processInfo: processInfo, fileSystem: testFileSystem, )).run(<String>['attach']); await completer.future; expect(portForwarder.devicePort, devicePort); expect(portForwarder.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 Observatory 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( artifacts: artifacts, 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 String filesystemScheme = 'foo'; const String filesystemRoot = '/build-output/'; const String projectRoot = '/build-output/project-root'; const String outputDill = '/tmp/output.dill'; final FakeHotRunner hotRunner = FakeHotRunner(); hotRunner.onAttach = ( Completer<DebugConnectionInfo>? connectionInfoCompleter, Completer<void>? appStartedCompleter, bool allowExistingDdsInstance, bool enableDevTools, ) async => 0; hotRunner.exited = false; hotRunner.isWaitingForObservatory = false; final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory() ..hotRunner = hotRunner; final AttachCommand command = AttachCommand( hotRunnerFactory: hotRunnerFactory, artifacts: artifacts, 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', () async { testDeviceManager.devices = <Device>[device]; final AttachCommand command = AttachCommand( artifacts: artifacts, 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('exits when observatory-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 AttachCommand command = AttachCommand( artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, signals: signals, platform: platform, processInfo: processInfo, fileSystem: testFileSystem, ); await expectLater( createTestCommandRunner(command).run(<String>['attach', '--observatory-port', '100']), throwsToolExit( message: 'When the --debug-port or --debug-url is unknown, this command does not use ' 'the value of --observatory-port.', ), ); }, overrides: <Type, Generator>{ FileSystem: () => testFileSystem, ProcessManager: () => FakeProcessManager.any(), DeviceManager: () => testDeviceManager, },); }); group('forwarding to given port', () { const int devicePort = 499; const int hostPort = 42; late RecordingPortForwarder portForwarder; late FakeAndroidDevice device; setUp(() { final FakeDartDevelopmentService fakeDds = FakeDartDevelopmentService(); portForwarder = RecordingPortForwarder(hostPort); device = FakeAndroidDevice(id: '1') ..portForwarder = portForwarder ..dds = fakeDds; }); testUsingContext('succeeds in ipv4 mode', () async { testDeviceManager.devices = <Device>[device]; final Completer<void> 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( artifacts: artifacts, 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.devicePort, devicePort); expect(portForwarder.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<void> 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( artifacts: artifacts, 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.devicePort, devicePort); expect(portForwarder.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 observatory port', () async { testDeviceManager.devices = <Device>[device]; final Completer<void> 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( artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, signals: signals, platform: platform, processInfo: processInfo, fileSystem: testFileSystem, )).run( <String>[ 'attach', '--debug-port', '$devicePort', '--observatory-port', '$hostPort', // Ensure DDS doesn't use hostPort by binding to a random port. '--dds-port', '0', ], ); await completer.future; expect(portForwarder.devicePort, null); expect(portForwarder.hostPort, 42); 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 observatory port', () async { testDeviceManager.devices = <Device>[device]; final Completer<void> 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( artifacts: artifacts, stdio: stdio, logger: logger, terminal: terminal, signals: signals, platform: platform, processInfo: processInfo, fileSystem: testFileSystem, )).run( <String>[ 'attach', '--debug-port', '$devicePort', '--observatory-port', '$hostPort', '--ipv6', // Ensure DDS doesn't use hostPort by binding to a random port. '--dds-port', '0', ], ); await completer.future; expect(portForwarder.devicePort, null); expect(portForwarder.hostPort, 42); 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 AttachCommand command = AttachCommand( artifacts: artifacts, 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 FakeIOSDevice device = FakeIOSDevice(); testDeviceManager.devices = <Device>[device]; expect(createTestCommandRunner(AttachCommand( artifacts: artifacts, 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 AttachCommand command = AttachCommand( artifacts: artifacts, 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 FakeAndroidDevice device = FakeAndroidDevice(id: '1') ..portForwarder = const NoOpDevicePortForwarder() ..onGetLogReader = () => NoOpDeviceLogReader('test'); final FakeHotRunner hotRunner = FakeHotRunner(); final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory() ..hotRunner = hotRunner; hotRunner.onAttach = ( Completer<DebugConnectionInfo>? connectionInfoCompleter, Completer<void>? appStartedCompleter, bool allowExistingDdsInstance, bool enableDevTools, ) async { await null; throw vm_service.RPCError('flutter._listViews', RPCErrorCodes.kServiceDisappeared, ''); }; testDeviceManager.devices = <Device>[device]; testFileSystem.file('lib/main.dart').createSync(); final AttachCommand command = AttachCommand( hotRunnerFactory: hotRunnerFactory, artifacts: artifacts, 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 FakeAndroidDevice device = FakeAndroidDevice(id: '1') ..portForwarder = const NoOpDevicePortForwarder() ..onGetLogReader = () => NoOpDeviceLogReader('test'); final FakeHotRunner hotRunner = FakeHotRunner(); final FakeHotRunnerFactory hotRunnerFactory = FakeHotRunnerFactory() ..hotRunner = hotRunner; hotRunner.onAttach = ( Completer<DebugConnectionInfo>? connectionInfoCompleter, Completer<void>? appStartedCompleter, bool allowExistingDdsInstance, bool enableDevTools, ) async { await null; throw vm_service.RPCError('flutter._listViews', RPCErrorCodes.kInvalidParams, ''); }; testDeviceManager.devices = <Device>[device]; testFileSystem.file('lib/main.dart').createSync(); final AttachCommand command = AttachCommand( hotRunnerFactory: hotRunnerFactory, artifacts: artifacts, 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, }); }); } class FakeHotRunner extends Fake implements HotRunner { late Future<int> Function(Completer<DebugConnectionInfo>?, Completer<void>?, bool, bool) onAttach; @override bool exited = false; @override bool isWaitingForObservatory = true; @override Future<int> attach({ Completer<DebugConnectionInfo>? connectionInfoCompleter, Completer<void>? appStartedCompleter, bool allowExistingDdsInstance = false, bool enableDevTools = false, bool needsFullRestart = true, }) { return onAttach(connectionInfoCompleter, appStartedCompleter, allowExistingDdsInstance, enableDevTools); } } class FakeHotRunnerFactory extends Fake implements HotRunnerFactory { late HotRunner hotRunner; String? dillOutputPath; String? projectRootPath; late List<FlutterDevice> devices; @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, }) { this.devices = devices; this.dillOutputPath = dillOutputPath; this.projectRootPath = projectRootPath; return hotRunner; } } class RecordingPortForwarder implements DevicePortForwarder { RecordingPortForwarder([this.hostPort]); int? devicePort; int? hostPort; @override Future<void> dispose() async { } @override Future<int> forward(int devicePort, {int? hostPort}) async { this.devicePort = devicePort; this.hostPort ??= hostPort; return this.hostPort!; } @override List<ForwardedPort> get forwardedPorts => <ForwardedPort>[]; @override Future<void> unforward(ForwardedPort forwardedPort) async { } } 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, }) { hadWarningOutput = true; _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, }) { return SilentStatus( stopwatch: Stopwatch(), onFinish: onFinish, )..start(); } bool _interrupt = false; void interrupt() { _interrupt = true; } final StreamController<String> _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 Completer<void> noopCompleter = Completer<void>(); @override Future<void> startDartDevelopmentService( Uri observatoryUri, { required Logger logger, int? hostPort, bool? ipv6, bool? disableServiceAuthCodes, bool cacheStartupProfile = false, }) async {} @override Uri get uri => Uri.parse('http://localhost:8181'); } // 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 FakeAndroidDevice extends Fake implements AndroidDevice { FakeAndroidDevice({required this.id}); @override late DartDevelopmentService dds; @override final String id; @override String get name => 'd$id'; @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 bool isSupported() => 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({ AndroidApk? 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 OverrideArtifacts? get artifactOverrides => null; @override final PlatformType platformType = PlatformType.android; @override Category get category => Category.mobile; @override bool get ephemeral => 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 FakeIOSDevice extends Fake implements IOSDevice { FakeIOSDevice({ DevicePortForwarder? portForwarder, DeviceLogReader? logReader, this.onGetLogReader, }) : _portForwarder = portForwarder, _logReader = logReader; final DevicePortForwarder? _portForwarder; @override DevicePortForwarder get portForwarder => _portForwarder!; @override DartDevelopmentService get dds => throw UnimplementedError('getter dds not implemented'); final DeviceLogReader? _logReader; DeviceLogReader get logReader => _logReader!; final DeviceLogReader Function()? onGetLogReader; @override DeviceLogReader getLogReader({ IOSApp? app, bool includePastLogs = false, }) { if (onGetLogReader == null) { throw UnimplementedError( 'Called getLogReader but no onGetLogReader callback was supplied in the constructor to FakeIOSDevice', ); } return onGetLogReader!(); } @override OverrideArtifacts? get artifactOverrides => null; @override final String name = 'name'; @override Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios; @override final PlatformType platformType = PlatformType.ios; @override bool isSupported() => true; @override bool isSupportedForProject(FlutterProject project) => true; } class FakeMDnsClient extends Fake implements MDnsClient { FakeMDnsClient(this.ptrRecords, this.srvResponse, { this.txtResponse = const <String, List<TxtResourceRecord>>{}, this.osErrorOnStart = false, }); final List<PtrResourceRecord> ptrRecords; final Map<String, List<SrvResourceRecord>> srvResponse; final Map<String, List<TxtResourceRecord>> txtResponse; final bool osErrorOnStart; @override Future<void> start({ InternetAddress? listenAddress, NetworkInterfacesFactory? interfacesFactory, int mDnsPort = 5353, InternetAddress? mDnsAddress, }) 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 == MDnsObservatoryDiscovery.dartObservatoryName) { 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>; } throw UnsupportedError('Unsupported query type $T'); } @override void stop() {} } class TestDeviceManager extends DeviceManager { TestDeviceManager({required this.logger}) : super(logger: logger); List<Device> devices = <Device>[]; final BufferLogger logger; @override List<DeviceDiscovery> get deviceDiscoverers { final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery(); devices.forEach(discoverer.addDevice); return <DeviceDiscovery>[discoverer]; } } class FakeTerminal extends Fake implements AnsiTerminal { FakeTerminal({this.stdinHasTerminal = true}); @override final bool stdinHasTerminal; @override bool usesTerminalUi = false; }