diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index fb7aea57798..9bad5a3d472 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -212,9 +212,6 @@ class IOSDevice extends Device { DevicePortForwarder _portForwarder; - @visibleForTesting - IOSDeployDebugger iosDeployDebugger; - @override Future get isLocalEmulator async => false; @@ -398,38 +395,23 @@ class IOSDevice extends Device { timeout: timeoutConfiguration.slowOperation); try { ProtocolDiscovery observatoryDiscovery; - int installationResult = 1; if (debuggingOptions.debuggingEnabled) { _logger.printTrace('Debugging is enabled, connecting to observatory'); - iosDeployDebugger = _iosDeploy.prepareDebuggerForLaunch( - deviceId: id, - bundlePath: bundle.path, - launchArguments: launchArguments, - interfaceType: interfaceType, - ); - - final DeviceLogReader deviceLogReader = getLogReader(app: package); - if (deviceLogReader is IOSDeviceLogReader) { - deviceLogReader.debuggerStream = iosDeployDebugger; - } observatoryDiscovery = ProtocolDiscovery.observatory( - deviceLogReader, + getLogReader(app: package), portForwarder: portForwarder, - throttleDuration: fallbackPollingDelay, - throttleTimeout: fallbackThrottleTimeout ?? const Duration(seconds: 5), hostPort: debuggingOptions.hostVmServicePort, devicePort: debuggingOptions.deviceVmServicePort, ipv6: ipv6, - ); - installationResult = await iosDeployDebugger.launchAndAttach() ? 0 : 1; - } else { - installationResult = await _iosDeploy.launchApp( - deviceId: id, - bundlePath: bundle.path, - launchArguments: launchArguments, - interfaceType: interfaceType, + throttleTimeout: fallbackThrottleTimeout ?? const Duration(seconds: 1), ); } + final int installationResult = await _iosDeploy.runApp( + deviceId: id, + bundlePath: bundle.path, + launchArguments: launchArguments, + interfaceType: interfaceType, + ); if (installationResult != 0) { _logger.printError('Could not run ${bundle.path} on $id.'); _logger.printError('Try launching Xcode and selecting "Product > Run" to fix the problem:'); @@ -483,11 +465,7 @@ class IOSDevice extends Device { IOSApp app, { String userIdentifier, }) async { - // If the debugger is not attached, killing the ios-deploy process won't stop the app. - if (iosDeployDebugger!= null && iosDeployDebugger.debuggerAttached) { - // Avoid null. - return iosDeployDebugger?.exit() == true; - } + // Currently we don't have a way to stop an app running on iOS. return false; } @@ -677,13 +655,6 @@ class IOSDeviceLogReader extends DeviceLogReader { // Matches a syslog line from any app. RegExp _anyLineRegex; - // Logging from native code/Flutter engine is prefixed by timestamp and process metadata: - // 2020-09-15 19:15:10.931434-0700 Runner[541:226276] Did finish launching. - // 2020-09-15 19:15:10.931434-0700 Runner[541:226276] [Category] Did finish launching. - // - // Logging from the dart code has no prefixing metadata. - final RegExp _debuggerLoggingRegex = RegExp(r'^\S* \S* \S*\[[0-9:]*] (.*)'); - StreamController _linesController; List> _loggingSubscriptions; @@ -716,10 +687,6 @@ class IOSDeviceLogReader extends DeviceLogReader { } void logMessage(vm_service.Event event) { - if (_iosDeployDebugger != null && _iosDeployDebugger.debuggerAttached) { - // Prefer the more complete logs from the attached debugger. - return; - } final String message = processVmServiceMessage(event); if (message.isNotEmpty) { _linesController.add(message); @@ -732,26 +699,6 @@ class IOSDeviceLogReader extends DeviceLogReader { ]); } - /// Log reader will listen to [debugger.logLines] and will detach debugger on dispose. - set debuggerStream(IOSDeployDebugger debugger) { - // Logging is gathered from syslog on iOS 13 and earlier. - if (_majorSdkVersion < _minimumUniversalLoggingSdkVersion) { - return; - } - _iosDeployDebugger = debugger; - // Add the debugger logs to the controller created on initialization. - _loggingSubscriptions.add(debugger.logLines.listen( - (String line) => _linesController.add(_debuggerLineHandler(line)), - onError: _linesController.addError, - onDone: _linesController.close, - cancelOnError: true, - )); - } - IOSDeployDebugger _iosDeployDebugger; - - // Strip off the logging metadata (leave the category), or just echo the line. - String _debuggerLineHandler(String line) => _debuggerLoggingRegex?.firstMatch(line)?.group(1) ?? line; - void _listenToSysLog() { // syslog is not written on iOS 13+. if (_majorSdkVersion >= _minimumUniversalLoggingSdkVersion) { @@ -811,7 +758,6 @@ class IOSDeviceLogReader extends DeviceLogReader { loggingSubscription.cancel(); } _idevicesyslogProcess?.kill(); - _iosDeployDebugger?.detach(); } } diff --git a/packages/flutter_tools/lib/src/ios/fallback_discovery.dart b/packages/flutter_tools/lib/src/ios/fallback_discovery.dart index 42f337f5702..56c8147eff9 100644 --- a/packages/flutter_tools/lib/src/ios/fallback_discovery.dart +++ b/packages/flutter_tools/lib/src/ios/fallback_discovery.dart @@ -81,29 +81,6 @@ class FallbackDiscovery { return result; } - try { - final Uri result = await _protocolDiscovery.uri; - if (result != null) { - UsageEvent( - _kEventName, - 'log-success', - flutterUsage: _flutterUsage, - ).send(); - return result; - } - } on ArgumentError { - // In the event of an invalid InternetAddress, this code attempts to catch - // an ArgumentError from protocol_discovery.dart - } on Exception catch (err) { - _logger.printTrace(err.toString()); - } - _logger.printTrace('Failed to connect with log scanning, falling back to mDNS'); - UsageEvent( - _kEventName, - 'log-failure', - flutterUsage: _flutterUsage, - ).send(); - try { final Uri result = await _mDnsObservatoryDiscovery.getObservatoryUri( packageId, @@ -122,12 +99,35 @@ class FallbackDiscovery { } on Exception catch (err) { _logger.printTrace(err.toString()); } - _logger.printTrace('Failed to connect with mDNS'); + _logger.printTrace('Failed to connect with mDNS, falling back to log scanning'); UsageEvent( _kEventName, 'mdns-failure', flutterUsage: _flutterUsage, ).send(); + + try { + final Uri result = await _protocolDiscovery.uri; + if (result != null) { + UsageEvent( + _kEventName, + 'fallback-success', + flutterUsage: _flutterUsage, + ).send(); + return result; + } + } on ArgumentError { + // In the event of an invalid InternetAddress, this code attempts to catch + // an ArgumentError from protocol_discovery.dart + } on Exception catch (err) { + _logger.printTrace(err.toString()); + } + _logger.printTrace('Failed to connect with log scanning'); + UsageEvent( + _kEventName, + 'fallback-failure', + flutterUsage: _flutterUsage, + ).send(); return null; } @@ -148,7 +148,7 @@ class FallbackDiscovery { assumedWsUri = Uri.parse('ws://localhost:$hostPort/ws'); } on Exception catch (err) { _logger.printTrace(err.toString()); - _logger.printTrace('Failed to connect directly, falling back to log scanning'); + _logger.printTrace('Failed to connect directly, falling back to mDNS'); _sendFailureEvent(err, assumedDevicePort); return null; } diff --git a/packages/flutter_tools/lib/src/ios/ios_deploy.dart b/packages/flutter_tools/lib/src/ios/ios_deploy.dart index 3945bdc225a..c1e90a74fc9 100644 --- a/packages/flutter_tools/lib/src/ios/ios_deploy.dart +++ b/packages/flutter_tools/lib/src/ios/ios_deploy.dart @@ -2,20 +2,15 @@ // 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:meta/meta.dart'; import 'package:process/process.dart'; import '../artifacts.dart'; -import '../base/common.dart'; -import '../base/io.dart'; import '../base/logger.dart'; import '../base/platform.dart'; import '../base/process.dart'; import '../build_info.dart'; import '../cache.dart'; -import '../convert.dart'; import 'code_signing.dart'; import 'devices.dart'; @@ -112,47 +107,10 @@ class IOSDeploy { ); } - /// Returns [IOSDeployDebugger] wrapping attached debugger logic. - /// - /// This method does not install the app. Call [IOSDeployDebugger.launchAndAttach()] - /// to install and attach the debugger to the specified app bundle. - IOSDeployDebugger prepareDebuggerForLaunch({ - @required String deviceId, - @required String bundlePath, - @required List launchArguments, - @required IOSDeviceInterface interfaceType, - }) { - // Interactive debug session to support sending the lldb detach command. - final List launchCommand = [ - 'script', - '-t', - '0', - '/dev/null', - _binaryPath, - '--id', - deviceId, - '--bundle', - bundlePath, - '--debug', - if (interfaceType != IOSDeviceInterface.network) - '--no-wifi', - if (launchArguments.isNotEmpty) ...[ - '--args', - launchArguments.join(' '), - ], - ]; - return IOSDeployDebugger( - launchCommand: launchCommand, - logger: _logger, - processUtils: _processUtils, - iosDeployEnv: iosDeployEnv, - ); - } - /// Installs and then runs the specified app bundle. /// /// Uses ios-deploy and returns the exit code. - Future launchApp({ + Future runApp({ @required String deviceId, @required String bundlePath, @required List launchArguments, @@ -211,202 +169,30 @@ class IOSDeploy { return true; } - String _monitorFailure(String stdout) => _monitorIOSDeployFailure(stdout, _logger); -} - -/// lldb attach state flow. -enum _IOSDeployDebuggerState { - detached, - launching, - attached, -} - -/// Wrapper to launch app and attach the debugger with ios-deploy. -class IOSDeployDebugger { - IOSDeployDebugger({ - @required Logger logger, - @required ProcessUtils processUtils, - @required List launchCommand, - @required Map iosDeployEnv, - }) : _processUtils = processUtils, - _logger = logger, - _launchCommand = launchCommand, - _iosDeployEnv = iosDeployEnv, - _debuggerState = _IOSDeployDebuggerState.detached; - - /// Create a [IOSDeployDebugger] for testing. - /// - /// Sets the command to "ios-deploy" and environment to an empty map. - @visibleForTesting - factory IOSDeployDebugger.test({ - @required ProcessManager processManager, - Logger logger, - }) { - final Logger debugLogger = logger ?? BufferLogger.test(); - return IOSDeployDebugger( - logger: debugLogger, - processUtils: ProcessUtils(logger: debugLogger, processManager: processManager), - launchCommand: ['ios-deploy'], - iosDeployEnv: {}, - ); - } - - final Logger _logger; - final ProcessUtils _processUtils; - final List _launchCommand; - final Map _iosDeployEnv; - - Process _iosDeployProcess; - - Stream get logLines => _debuggerOutput.stream; - final StreamController _debuggerOutput = StreamController.broadcast(); - - bool get debuggerAttached => _debuggerState == _IOSDeployDebuggerState.attached; - _IOSDeployDebuggerState _debuggerState; - - // (lldb) run - // https://github.com/ios-control/ios-deploy/blob/1.11.2-beta.1/src/ios-deploy/ios-deploy.m#L51 - static final RegExp _lldbRun = RegExp(r'\(lldb\)\s*run'); - - /// Launch the app on the device, and attach the debugger. - /// - /// Returns whether or not the debugger successfully attached. - Future launchAndAttach() async { - // Return when the debugger attaches, or the ios-deploy process exits. - final Completer debuggerCompleter = Completer(); - try { - _iosDeployProcess = await _processUtils.start( - _launchCommand, - environment: _iosDeployEnv, - ); - String lastLineFromDebugger; - final StreamSubscription stdoutSubscription = _iosDeployProcess.stdout - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((String line) { - _monitorIOSDeployFailure(line, _logger); - - // (lldb) run - // success - // 2020-09-15 13:42:25.185474-0700 Runner[477:181141] flutter: Observatory listening on http://127.0.0.1:57782/ - if (_lldbRun.hasMatch(line)) { - _logger.printTrace(line); - _debuggerState = _IOSDeployDebuggerState.launching; - return; - } - // Next line after "run" must be "success", or the attach failed. - // Example: "error: process launch failed" - if (_debuggerState == _IOSDeployDebuggerState.launching) { - _logger.printTrace(line); - final bool attachSuccess = line == 'success'; - _debuggerState = attachSuccess ? _IOSDeployDebuggerState.attached : _IOSDeployDebuggerState.detached; - if (!debuggerCompleter.isCompleted) { - debuggerCompleter.complete(attachSuccess); - } - return; - } - if (line.contains('PROCESS_STOPPED') || - line.contains('PROCESS_EXITED')) { - // The app exited or crashed, so stop echoing the output. - // Don't pass any further ios-deploy debugging messages to the log reader after it exits. - _debuggerState = _IOSDeployDebuggerState.detached; - _logger.printTrace(line); - return; - } - if (_debuggerState != _IOSDeployDebuggerState.attached) { - _logger.printTrace(line); - return; - } - if (lastLineFromDebugger != null && lastLineFromDebugger.isNotEmpty && line.isEmpty) { - // The lldb console stream from ios-deploy is separated lines by an extra \r\n. - // To avoid all lines being double spaced, if the last line from the - // debugger was not an empty line, skip this empty line. - // This will still cause "legit" logged newlines to be doubled... - } else { - _debuggerOutput.add(line); - } - lastLineFromDebugger = line; - }); - final StreamSubscription stderrSubscription = _iosDeployProcess.stderr - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen((String line) { - _monitorIOSDeployFailure(line, _logger); - _logger.printTrace(line); - }); - unawaited(_iosDeployProcess.exitCode.then((int status) { - _logger.printTrace('ios-deploy exited with code $exitCode'); - _debuggerState = _IOSDeployDebuggerState.detached; - unawaited(stdoutSubscription.cancel()); - unawaited(stderrSubscription.cancel()); - }).whenComplete(() async { - if (_debuggerOutput.hasListener) { - // Tell listeners the process died. - await _debuggerOutput.close(); - } - if (!debuggerCompleter.isCompleted) { - debuggerCompleter.complete(false); - } - _iosDeployProcess = null; - })); - } on ProcessException catch (exception, stackTrace) { - _logger.printTrace('ios-deploy failed: $exception'); - _debuggerState = _IOSDeployDebuggerState.detached; - _debuggerOutput.addError(exception, stackTrace); - } on ArgumentError catch (exception, stackTrace) { - _logger.printTrace('ios-deploy failed: $exception'); - _debuggerState = _IOSDeployDebuggerState.detached; - _debuggerOutput.addError(exception, stackTrace); - } - // Wait until the debugger attaches, or the attempt fails. - return debuggerCompleter.future; - } - - bool exit() { - final bool success = (_iosDeployProcess == null) || _iosDeployProcess.kill(); - _iosDeployProcess = null; - return success; - } - - void detach() { - if (!debuggerAttached) { - return; - } - - try { - // Detach lldb from the app process. - _iosDeployProcess?.stdin?.writeln('process detach'); - _debuggerState = _IOSDeployDebuggerState.detached; - } on SocketException catch (error) { - // Best effort, try to detach, but maybe the app already exited or already detached. - _logger.printTrace('Could not detach from debugger: $error'); - } - } -} - -// Maps stdout line stream. Must return original line. -String _monitorIOSDeployFailure(String stdout, Logger logger) { - // Installation issues. - if (stdout.contains(noProvisioningProfileErrorOne) || stdout.contains(noProvisioningProfileErrorTwo)) { - logger.printError(noProvisioningProfileInstruction, emphasis: true); + // Maps stdout line stream. Must return original line. + String _monitorFailure(String stdout) { + // Installation issues. + if (stdout.contains(noProvisioningProfileErrorOne) || stdout.contains(noProvisioningProfileErrorTwo)) { + _logger.printError(noProvisioningProfileInstruction, emphasis: true); // Launch issues. - } else if (stdout.contains(deviceLockedError)) { - logger.printError(''' + } else if (stdout.contains(deviceLockedError)) { + _logger.printError(''' ═══════════════════════════════════════════════════════════════════════════════════ Your device is locked. Unlock your device first before running. ═══════════════════════════════════════════════════════════════════════════════════''', - emphasis: true); - } else if (stdout.contains(unknownAppLaunchError)) { - logger.printError(''' + emphasis: true); + } else if (stdout.contains(unknownAppLaunchError)) { + _logger.printError(''' ═══════════════════════════════════════════════════════════════════════════════════ Error launching app. Try launching from within Xcode via: open ios/Runner.xcworkspace Your Xcode version may be too old for your iOS version. ═══════════════════════════════════════════════════════════════════════════════════''', - emphasis: true); - } + emphasis: true); + } - return stdout; + return stdout; + } } diff --git a/packages/flutter_tools/lib/src/protocol_discovery.dart b/packages/flutter_tools/lib/src/protocol_discovery.dart index 3d1bc3dd2dc..367bccd7a53 100644 --- a/packages/flutter_tools/lib/src/protocol_discovery.dart +++ b/packages/flutter_tools/lib/src/protocol_discovery.dart @@ -34,7 +34,7 @@ class ProtocolDiscovery { factory ProtocolDiscovery.observatory( DeviceLogReader logReader, { DevicePortForwarder portForwarder, - Duration throttleDuration, + Duration throttleDuration = const Duration(milliseconds: 200), Duration throttleTimeout, @required int hostPort, @required int devicePort, @@ -45,7 +45,7 @@ class ProtocolDiscovery { logReader, kObservatoryService, portForwarder: portForwarder, - throttleDuration: throttleDuration ?? const Duration(milliseconds: 200), + throttleDuration: throttleDuration, throttleTimeout: throttleTimeout, hostPort: hostPort, devicePort: devicePort, @@ -225,7 +225,7 @@ class _BufferedStreamController { /// /// For example, consider a `waitDuration` of `10ms`, and list of event names /// and arrival times: `a (0ms), b (5ms), c (11ms), d (21ms)`. -/// The events `a`, `c`, and `d` will be produced as a result. +/// The events `c` and `d` will be produced as a result. StreamTransformer _throttle({ @required Duration waitDuration, }) { @@ -240,13 +240,10 @@ StreamTransformer _throttle({ handleData: (S value, EventSink sink) { latestLine = value; - final bool isFirstMessage = lastExecution == null; final int currentTime = DateTime.now().millisecondsSinceEpoch; lastExecution ??= currentTime; final int remainingTime = currentTime - lastExecution; - - // Always send the first event immediately. - final int nextExecutionTime = isFirstMessage || remainingTime > waitDuration.inMilliseconds + final int nextExecutionTime = remainingTime > waitDuration.inMilliseconds ? 0 : waitDuration.inMilliseconds - remainingTime; diff --git a/packages/flutter_tools/test/general.shard/ios/ios_deploy_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_deploy_test.dart index e36d466ceeb..121c4f96fa9 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_deploy_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_deploy_test.dart @@ -2,15 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; -import 'dart:convert'; - import 'package:flutter_tools/src/artifacts.dart'; -import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/cache.dart'; -import 'package:flutter_tools/src/ios/devices.dart'; import 'package:flutter_tools/src/ios/ios_deploy.dart'; import 'package:mockito/mockito.dart'; import 'package:process/process.dart'; @@ -26,230 +21,50 @@ void main () { expect(environment['PATH'], startsWith('/usr/bin')); }); - group('IOSDeploy.prepareDebuggerForLaunch', () { - testWithoutContext('calls ios-deploy with correct arguments and returns when debugger attaches', () async { - final FakeProcessManager processManager = FakeProcessManager.list([ - FakeCommand( - command: [ - 'script', - '-t', - '0', - '/dev/null', - 'ios-deploy', - '--id', - '123', - '--bundle', - '/', - '--debug', - '--args', - [ - '--enable-dart-profiling', - ].join(' '), - ], environment: const { - 'PATH': '/usr/bin:/usr/local/bin:/usr/bin', - 'DYLD_LIBRARY_PATH': '/path/to/libs', - }, - stdout: '(lldb) run\nsuccess\nDid finish launching.', - ), - ]); - final IOSDeploy iosDeploy = setUpIOSDeploy(processManager); - final IOSDeployDebugger iosDeployDebugger = iosDeploy.prepareDebuggerForLaunch( - deviceId: '123', - bundlePath: '/', - launchArguments: ['--enable-dart-profiling'], - interfaceType: IOSDeviceInterface.network, - ); + testWithoutContext('IOSDeploy.uninstallApp calls ios-deploy with correct arguments and returns 0 on success', () async { + const String deviceId = '123'; + const String bundleId = 'com.example.app'; + final FakeProcessManager processManager = FakeProcessManager.list([ + const FakeCommand(command: [ + 'ios-deploy', + '--id', + deviceId, + '--uninstall_only', + '--bundle_id', + bundleId, + ]) + ]); + final IOSDeploy iosDeploy = setUpIOSDeploy(processManager); + final int exitCode = await iosDeploy.uninstallApp( + deviceId: deviceId, + bundleId: bundleId, + ); - expect(await iosDeployDebugger.launchAndAttach(), isTrue); - expect(await iosDeployDebugger.logLines.toList(), ['Did finish launching.']); - expect(processManager.hasRemainingExpectations, false); - }); + expect(exitCode, 0); + expect(processManager.hasRemainingExpectations, false); }); - group('IOSDeployDebugger', () { - group('launch', () { - BufferLogger logger; + testWithoutContext('IOSDeploy.uninstallApp returns non-zero exit code when ios-deploy does the same', () async { + const String deviceId = '123'; + const String bundleId = 'com.example.app'; + final FakeProcessManager processManager = FakeProcessManager.list([ + const FakeCommand(command: [ + 'ios-deploy', + '--id', + deviceId, + '--uninstall_only', + '--bundle_id', + bundleId, + ], exitCode: 1) + ]); + final IOSDeploy iosDeploy = setUpIOSDeploy(processManager); + final int exitCode = await iosDeploy.uninstallApp( + deviceId: deviceId, + bundleId: bundleId, + ); - setUp(() { - logger = BufferLogger.test(); - }); - - testWithoutContext('debugger attached', () async { - final FakeProcessManager processManager = FakeProcessManager.list([ - const FakeCommand( - command: ['ios-deploy'], - stdout: '(lldb) run\r\nsuccess\r\nsuccess\r\nLog on attach1\r\n\r\nLog on attach2\r\n\r\n\r\n\r\nPROCESS_STOPPED\r\nLog after process exit', - ), - ]); - final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test( - processManager: processManager, - logger: logger, - ); - final List receivedLogLines = []; - final Stream logLines = iosDeployDebugger.logLines - ..listen(receivedLogLines.add); - - expect(await iosDeployDebugger.launchAndAttach(), isTrue); - await logLines.toList(); - expect(receivedLogLines, [ - 'success', // ignore first "success" from lldb, but log subsequent ones from real logging. - 'Log on attach1', - 'Log on attach2', - '', '']); - }); - - testWithoutContext('attach failed', () async { - final FakeProcessManager processManager = FakeProcessManager.list([ - const FakeCommand( - command: ['ios-deploy'], - // A success after an error should never happen, but test that we're handling random "successes" anyway. - stdout: '(lldb) run\r\nerror: process launch failed\r\nsuccess\r\nLog on attach1', - ), - ]); - final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test( - processManager: processManager, - logger: logger, - ); - final List receivedLogLines = []; - final Stream logLines = iosDeployDebugger.logLines - ..listen(receivedLogLines.add); - - expect(await iosDeployDebugger.launchAndAttach(), isFalse); - await logLines.toList(); - // Debugger lines are double spaced, separated by an extra \r\n. Skip the extra lines. - // Still include empty lines other than the extra added newlines. - expect(receivedLogLines, isEmpty); - }); - - testWithoutContext('no provisioning profile 1, stdout', () async { - final FakeProcessManager processManager = FakeProcessManager.list([ - const FakeCommand( - command: ['ios-deploy'], - stdout: 'Error 0xe8008015', - ), - ]); - final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test( - processManager: processManager, - logger: logger, - ); - - await iosDeployDebugger.launchAndAttach(); - expect(logger.errorText, contains('No Provisioning Profile was found')); - }); - - testWithoutContext('no provisioning profile 2, stderr', () async { - final FakeProcessManager processManager = FakeProcessManager.list([ - const FakeCommand( - command: ['ios-deploy'], - stderr: 'Error 0xe8000067', - ), - ]); - final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test( - processManager: processManager, - logger: logger, - ); - await iosDeployDebugger.launchAndAttach(); - expect(logger.errorText, contains('No Provisioning Profile was found')); - }); - - testWithoutContext('device locked', () async { - final FakeProcessManager processManager = FakeProcessManager.list([ - const FakeCommand( - command: ['ios-deploy'], - stdout: 'e80000e2', - ), - ]); - final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test( - processManager: processManager, - logger: logger, - ); - await iosDeployDebugger.launchAndAttach(); - expect(logger.errorText, contains('Your device is locked.')); - }); - - testWithoutContext('device locked', () async { - final FakeProcessManager processManager = FakeProcessManager.list([ - const FakeCommand( - command: ['ios-deploy'], - stdout: 'Error 0xe8000022', - ), - ]); - final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test( - processManager: processManager, - logger: logger, - ); - await iosDeployDebugger.launchAndAttach(); - expect(logger.errorText, contains('Try launching from within Xcode')); - }); - }); - - testWithoutContext('detach', () async { - final StreamController> stdin = StreamController>(); - final Stream stdinStream = stdin.stream.transform(const Utf8Decoder()); - final FakeProcessManager processManager = FakeProcessManager.list([ - FakeCommand( - command: const [ - 'ios-deploy', - ], - stdout: '(lldb) run\nsuccess', - stdin: IOSink(stdin.sink), - ), - ]); - final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test( - processManager: processManager, - ); - await iosDeployDebugger.launchAndAttach(); - iosDeployDebugger.detach(); - expect(await stdinStream.first, 'process detach'); - }); - }); - - group('IOSDeploy.uninstallApp', () { - testWithoutContext('calls ios-deploy with correct arguments and returns 0 on success', () async { - const String deviceId = '123'; - const String bundleId = 'com.example.app'; - final FakeProcessManager processManager = FakeProcessManager.list([ - const FakeCommand(command: [ - 'ios-deploy', - '--id', - deviceId, - '--uninstall_only', - '--bundle_id', - bundleId, - ]) - ]); - final IOSDeploy iosDeploy = setUpIOSDeploy(processManager); - final int exitCode = await iosDeploy.uninstallApp( - deviceId: deviceId, - bundleId: bundleId, - ); - - expect(exitCode, 0); - expect(processManager.hasRemainingExpectations, false); - }); - - testWithoutContext('returns non-zero exit code when ios-deploy does the same', () async { - const String deviceId = '123'; - const String bundleId = 'com.example.app'; - final FakeProcessManager processManager = FakeProcessManager.list([ - const FakeCommand(command: [ - 'ios-deploy', - '--id', - deviceId, - '--uninstall_only', - '--bundle_id', - bundleId, - ], exitCode: 1) - ]); - final IOSDeploy iosDeploy = setUpIOSDeploy(processManager); - final int exitCode = await iosDeploy.uninstallApp( - deviceId: deviceId, - bundleId: bundleId, - ); - - expect(exitCode, 1); - expect(processManager.hasRemainingExpectations, false); - }); + expect(exitCode, 1); + expect(processManager.hasRemainingExpectations, false); }); } diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart index 1bdfa8ae4d9..bd95e87df0a 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart @@ -10,7 +10,6 @@ import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/ios/devices.dart'; -import 'package:flutter_tools/src/ios/ios_deploy.dart'; import 'package:flutter_tools/src/ios/mac.dart'; import 'package:mockito/mockito.dart'; import 'package:vm_service/vm_service.dart'; @@ -31,299 +30,167 @@ void main() { artifacts = MockArtifacts(); logger = BufferLogger.test(); when(artifacts.getArtifactPath(Artifact.idevicesyslog, platform: TargetPlatform.ios)) - .thenReturn('idevice-syslog'); + .thenReturn('idevice-syslog'); }); - group('syslog stream', () { - testWithoutContext('decodeSyslog decodes a syslog-encoded line', () { - final String decoded = decodeSyslog( - r'I \M-b\M^]\M-$\M-o\M-8\M^O syslog \M-B\M-/\' - r'134_(\M-c\M^C\M^D)_/\M-B\M-/ \M-l\M^F\240!'); + testWithoutContext('decodeSyslog decodes a syslog-encoded line', () { + final String decoded = decodeSyslog( + r'I \M-b\M^]\M-$\M-o\M-8\M^O syslog \M-B\M-/\' + r'134_(\M-c\M^C\M^D)_/\M-B\M-/ \M-l\M^F\240!'); - expect(decoded, r'I ❤️ syslog ¯\_(ツ)_/¯ 솠!'); - }); + expect(decoded, r'I ❤️ syslog ¯\_(ツ)_/¯ 솠!'); + }); - testWithoutContext('decodeSyslog passes through un-decodeable lines as-is', () { - final String decoded = decodeSyslog(r'I \M-b\M^O syslog!'); + testWithoutContext('decodeSyslog passes through un-decodeable lines as-is', () { + final String decoded = decodeSyslog(r'I \M-b\M^O syslog!'); - expect(decoded, r'I \M-b\M^O syslog!'); - }); + expect(decoded, r'I \M-b\M^O syslog!'); + }); - testWithoutContext('IOSDeviceLogReader suppresses non-Flutter lines from output with syslog', () async { - processManager.addCommand( - const FakeCommand( - command: [ - 'idevice-syslog', '-u', '1234', - ], - stdout: ''' + testWithoutContext('IOSDeviceLogReader suppresses non-Flutter lines from output with syslog', () async { + processManager.addCommand( + const FakeCommand( + command: [ + 'idevice-syslog', '-u', '1234', + ], + stdout: ''' Runner(Flutter)[297] : A is for ari Runner(libsystem_asl.dylib)[297] : libMobileGestalt MobileGestaltSupport.m:153: pid 123 (Runner) does not have sandbox access for frZQaeyWLUvLjeuEK43hmg and IS NOT appropriately entitled Runner(libsystem_asl.dylib)[297] : libMobileGestalt MobileGestalt.c:550: no access to InverseDeviceID (see ) Runner(Flutter)[297] : I is for ichigo Runner(UIKit)[297] : E is for enpitsu" ''' - ), - ); - final DeviceLogReader logReader = IOSDeviceLogReader.test( - iMobileDevice: IMobileDevice( - artifacts: artifacts, - processManager: processManager, - cache: fakeCache, - logger: logger, - ), - ); - final List lines = await logReader.logLines.toList(); + ), + ); + final DeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: processManager, + cache: fakeCache, + logger: logger, + ), + ); + final List lines = await logReader.logLines.toList(); - expect(lines, ['A is for ari', 'I is for ichigo']); - }); + expect(lines, ['A is for ari', 'I is for ichigo']); + }); - testWithoutContext('IOSDeviceLogReader includes multi-line Flutter logs in the output with syslog', () async { - processManager.addCommand( - const FakeCommand( - command: [ - 'idevice-syslog', '-u', '1234', - ], - stdout: ''' + testWithoutContext('IOSDeviceLogReader includes multi-line Flutter logs in the output with syslog', () async { + processManager.addCommand( + const FakeCommand( + command: [ + 'idevice-syslog', '-u', '1234', + ], + stdout: ''' Runner(Flutter)[297] : This is a multi-line message, with another Flutter message following it. Runner(Flutter)[297] : This is a multi-line message, with a non-Flutter log message following it. Runner(libsystem_asl.dylib)[297] : libMobileGestalt ''' - ), - ); - final DeviceLogReader logReader = IOSDeviceLogReader.test( - iMobileDevice: IMobileDevice( - artifacts: artifacts, - processManager: processManager, - cache: fakeCache, - logger: logger, - ), - ); - final List lines = await logReader.logLines.toList(); + ), + ); + final DeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: processManager, + cache: fakeCache, + logger: logger, + ), + ); + final List lines = await logReader.logLines.toList(); - expect(lines, [ - 'This is a multi-line message,', - ' with another Flutter message following it.', - 'This is a multi-line message,', - ' with a non-Flutter log message following it.', - ]); - }); + expect(lines, [ + 'This is a multi-line message,', + ' with another Flutter message following it.', + 'This is a multi-line message,', + ' with a non-Flutter log message following it.', + ]); + }); - testWithoutContext('includes multi-line Flutter logs in the output', () async { - processManager.addCommand( - const FakeCommand( - command: [ - 'idevice-syslog', '-u', '1234', - ], - stdout: ''' + testWithoutContext('includes multi-line Flutter logs in the output', () async { + processManager.addCommand( + const FakeCommand( + command: [ + 'idevice-syslog', '-u', '1234', + ], + stdout: ''' Runner(Flutter)[297] : This is a multi-line message, with another Flutter message following it. Runner(Flutter)[297] : This is a multi-line message, with a non-Flutter log message following it. Runner(libsystem_asl.dylib)[297] : libMobileGestalt ''', - ), - ); + ), + ); - final DeviceLogReader logReader = IOSDeviceLogReader.test( - iMobileDevice: IMobileDevice( - artifacts: artifacts, - processManager: processManager, - cache: fakeCache, - logger: logger, - ), - ); - final List lines = await logReader.logLines.toList(); + final DeviceLogReader logReader = IOSDeviceLogReader.test( + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: processManager, + cache: fakeCache, + logger: logger, + ), + ); + final List lines = await logReader.logLines.toList(); - expect(lines, [ - 'This is a multi-line message,', - ' with another Flutter message following it.', - 'This is a multi-line message,', - ' with a non-Flutter log message following it.', - ]); - }); + expect(lines, [ + 'This is a multi-line message,', + ' with another Flutter message following it.', + 'This is a multi-line message,', + ' with a non-Flutter log message following it.', + ]); }); - group('VM service', () { - testWithoutContext('IOSDeviceLogReader can listen to VM Service logs', () async { - final MockVmService vmService = MockVmService(); - final DeviceLogReader logReader = IOSDeviceLogReader.test( - useSyslog: false, - iMobileDevice: IMobileDevice( - artifacts: artifacts, - processManager: processManager, - cache: fakeCache, - logger: logger, - ), - ); - final StreamController stdoutController = StreamController(); - final StreamController stderController = StreamController(); - final Completer stdoutCompleter = Completer(); - final Completer stderrCompleter = Completer(); - when(vmService.streamListen('Stdout')).thenAnswer((Invocation invocation) { - return stdoutCompleter.future; - }); - when(vmService.streamListen('Stderr')).thenAnswer((Invocation invocation) { - return stderrCompleter.future; - }); - when(vmService.onStdoutEvent).thenAnswer((Invocation invocation) { - return stdoutController.stream; - }); - when(vmService.onStderrEvent).thenAnswer((Invocation invocation) { - return stderController.stream; - }); - logReader.connectedVMService = vmService; - - stdoutCompleter.complete(Success()); - stderrCompleter.complete(Success()); - stdoutController.add(Event( - kind: 'Stdout', - timestamp: 0, - bytes: base64.encode(utf8.encode(' This is a message ')), - )); - stderController.add(Event( - kind: 'Stderr', - timestamp: 0, - bytes: base64.encode(utf8.encode(' And this is an error ')), - )); - - // Wait for stream listeners to fire. - await expectLater(logReader.logLines, emitsInAnyOrder([ - equals(' This is a message '), - equals(' And this is an error '), - ])); + testWithoutContext('IOSDeviceLogReader can listen to VM Service logs', () async { + final MockVmService vmService = MockVmService(); + final DeviceLogReader logReader = IOSDeviceLogReader.test( + useSyslog: false, + iMobileDevice: IMobileDevice( + artifacts: artifacts, + processManager: processManager, + cache: fakeCache, + logger: logger, + ), + ); + final StreamController stdoutController = StreamController(); + final StreamController stderController = StreamController(); + final Completer stdoutCompleter = Completer(); + final Completer stderrCompleter = Completer(); + when(vmService.streamListen('Stdout')).thenAnswer((Invocation invocation) { + return stdoutCompleter.future; }); - - testWithoutContext('IOSDeviceLogReader ignores VM Service logs when attached to debugger', () async { - final MockVmService vmService = MockVmService(); - final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( - useSyslog: false, - iMobileDevice: IMobileDevice( - artifacts: artifacts, - processManager: processManager, - cache: fakeCache, - logger: logger, - ), - ); - final StreamController stdoutController = StreamController(); - final StreamController stderController = StreamController(); - final Completer stdoutCompleter = Completer(); - final Completer stderrCompleter = Completer(); - when(vmService.streamListen('Stdout')).thenAnswer((Invocation invocation) { - return stdoutCompleter.future; - }); - when(vmService.streamListen('Stderr')).thenAnswer((Invocation invocation) { - return stderrCompleter.future; - }); - when(vmService.onStdoutEvent).thenAnswer((Invocation invocation) { - return stdoutController.stream; - }); - when(vmService.onStderrEvent).thenAnswer((Invocation invocation) { - return stderController.stream; - }); - logReader.connectedVMService = vmService; - - stdoutCompleter.complete(Success()); - stderrCompleter.complete(Success()); - stdoutController.add(Event( - kind: 'Stdout', - timestamp: 0, - bytes: base64.encode(utf8.encode(' This is a message ')), - )); - stderController.add(Event( - kind: 'Stderr', - timestamp: 0, - bytes: base64.encode(utf8.encode(' And this is an error ')), - )); - - final MockIOSDeployDebugger iosDeployDebugger = MockIOSDeployDebugger(); - when(iosDeployDebugger.debuggerAttached).thenReturn(true); - - final Stream debuggingLogs = Stream.fromIterable([ - 'Message from debugger' - ]); - when(iosDeployDebugger.logLines).thenAnswer((Invocation invocation) => debuggingLogs); - logReader.debuggerStream = iosDeployDebugger; - - // Wait for stream listeners to fire. - await expectLater(logReader.logLines, emitsInAnyOrder([ - equals('Message from debugger'), - ])); + when(vmService.streamListen('Stderr')).thenAnswer((Invocation invocation) { + return stderrCompleter.future; }); - }); - - group('debugger stream', () { - testWithoutContext('IOSDeviceLogReader removes metadata prefix from lldb output', () async { - final Stream debuggingLogs = Stream.fromIterable([ - '2020-09-15 19:15:10.931434-0700 Runner[541:226276] Did finish launching.', - '2020-09-15 19:15:10.931434-0700 Runner[541:226276] [Category] Did finish launching from logging category.', - 'stderr from dart', - '', - ]); - - final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( - iMobileDevice: IMobileDevice( - artifacts: artifacts, - processManager: processManager, - cache: fakeCache, - logger: logger, - ), - useSyslog: false, - ); - final MockIOSDeployDebugger iosDeployDebugger = MockIOSDeployDebugger(); - when(iosDeployDebugger.logLines).thenAnswer((Invocation invocation) => debuggingLogs); - logReader.debuggerStream = iosDeployDebugger; - final Future> logLines = logReader.logLines.toList(); - - expect(await logLines, [ - 'Did finish launching.', - '[Category] Did finish launching from logging category.', - 'stderr from dart', - '', - ]); + when(vmService.onStdoutEvent).thenAnswer((Invocation invocation) { + return stdoutController.stream; }); - - testWithoutContext('errors on debugger stream closes log stream', () async { - final Stream debuggingLogs = Stream.error('ios-deploy error'); - final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( - iMobileDevice: IMobileDevice( - artifacts: artifacts, - processManager: processManager, - cache: fakeCache, - logger: logger, - ), - useSyslog: false, - ); - final Completer streamComplete = Completer(); - final MockIOSDeployDebugger iosDeployDebugger = MockIOSDeployDebugger(); - when(iosDeployDebugger.logLines).thenAnswer((Invocation invocation) => debuggingLogs); - logReader.logLines.listen(null, onError: (Object error) => streamComplete.complete()); - logReader.debuggerStream = iosDeployDebugger; - - await streamComplete.future; + when(vmService.onStderrEvent).thenAnswer((Invocation invocation) { + return stderController.stream; }); + logReader.connectedVMService = vmService; - testWithoutContext('detaches debugger', () async { - final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( - iMobileDevice: IMobileDevice( - artifacts: artifacts, - processManager: processManager, - cache: fakeCache, - logger: logger, - ), - useSyslog: false, - ); - final MockIOSDeployDebugger iosDeployDebugger = MockIOSDeployDebugger(); - when(iosDeployDebugger.logLines).thenAnswer((Invocation invocation) => const Stream.empty()); - logReader.debuggerStream = iosDeployDebugger; + stdoutCompleter.complete(Success()); + stderrCompleter.complete(Success()); + stdoutController.add(Event( + kind: 'Stdout', + timestamp: 0, + bytes: base64.encode(utf8.encode(' This is a message ')), + )); + stderController.add(Event( + kind: 'Stderr', + timestamp: 0, + bytes: base64.encode(utf8.encode(' And this is an error ')), + )); - logReader.dispose(); - verify(iosDeployDebugger.detach()); - }); + // Wait for stream listeners to fire. + await expectLater(logReader.logLines, emitsInAnyOrder([ + equals(' This is a message '), + equals(' And this is an error '), + ])); }); } class MockArtifacts extends Mock implements Artifacts {} class MockVmService extends Mock implements VmService {} -class MockIOSDeployDebugger extends Mock implements IOSDeployDebugger {} diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart index 806f7ffc155..12c87774421 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart @@ -67,25 +67,19 @@ const FakeCommand kLaunchReleaseCommand = FakeCommand( // The command used to actually launch the app with args in debug. const FakeCommand kLaunchDebugCommand = FakeCommand(command: [ - 'script', - '-t', - '0', - '/dev/null', 'ios-deploy', '--id', '123', '--bundle', '/', - '--debug', '--no-wifi', + '--justlaunch', '--args', '--enable-dart-profiling --enable-service-port-fallback --disable-service-auth-codes --observatory-port=60700 --enable-checked-mode --verify-entry-points' ], environment: { 'PATH': '/usr/bin:null', 'DYLD_LIBRARY_PATH': '/path/to/libraries', -}, -stdout: '(lldb) run\nsuccess', -); +}); void main() { // TODO(jonahwilliams): This test doesn't really belong here but @@ -108,7 +102,7 @@ void main() { }); // Still uses context for analytics and mDNS. - testUsingContext('IOSDevice.startApp succeeds in debug mode via mDNS discovery when log reading fails', () async { + testUsingContext('IOSDevice.startApp succeeds in debug mode via mDNS discovery', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final FakeProcessManager processManager = FakeProcessManager.list([ kDeployCommand, @@ -151,7 +145,6 @@ void main() { debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), platformArgs: {}, fallbackPollingDelay: Duration.zero, - fallbackThrottleTimeout: const Duration(milliseconds: 10), ); verify(globals.flutterUsage.sendEvent('ios-handshake', 'mdns-success')).called(1); @@ -164,58 +157,7 @@ void main() { }); // Still uses context for analytics and mDNS. - testUsingContext('IOSDevice.startApp succeeds in debug mode via log reading', () async { - final FileSystem fileSystem = MemoryFileSystem.test(); - final FakeProcessManager processManager = FakeProcessManager.list([ - kDeployCommand, - kLaunchDebugCommand, - ]); - final IOSDevice device = setUpIOSDevice( - processManager: processManager, - fileSystem: fileSystem, - vmServiceConnector: (String string, {Log log}) async { - throw const io.SocketException( - 'OS Error: Connection refused, errno = 61, address = localhost, port ' - '= 58943', - ); - }, - ); - final IOSApp iosApp = PrebuiltIOSApp( - projectBundleId: 'app', - bundleName: 'Runner', - bundleDir: fileSystem.currentDirectory, - ); - final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader(); - - device.portForwarder = const NoOpDevicePortForwarder(); - device.setLogReader(iosApp, deviceLogReader); - - // Start writing messages to the log reader. - Timer.run(() { - deviceLogReader.addLine('Foo'); - deviceLogReader.addLine('Observatory listening on http://127.0.0.1:456'); - }); - - final LaunchResult launchResult = await device.startApp(iosApp, - prebuiltApplication: true, - debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), - platformArgs: {}, - fallbackPollingDelay: Duration.zero, - fallbackThrottleTimeout: const Duration(milliseconds: 10), - ); - - expect(launchResult.started, true); - expect(launchResult.hasObservatory, true); - verify(globals.flutterUsage.sendEvent('ios-handshake', 'log-success')).called(1); - verifyNever(globals.flutterUsage.sendEvent('ios-handshake', 'mdns-failure')); - expect(await device.stopApp(iosApp), false); - }, overrides: { - MDnsObservatoryDiscovery: () => MockMDnsObservatoryDiscovery(), - Usage: () => MockUsage(), - }); - - // Still uses context for analytics and mDNS. - testUsingContext('IOSDevice.startApp fails in debug mode when Observatory URI is malformed', () async { + testUsingContext('IOSDevice.startApp succeeds in debug mode when mDNS fails', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final FakeProcessManager processManager = FakeProcessManager.list([ kDeployCommand, @@ -244,15 +186,69 @@ void main() { // Now that the reader is used, start writing messages to it. Timer.run(() { deviceLogReader.addLine('Foo'); - deviceLogReader.addLine('Observatory listening on http://127.0.0.1:456abc'); + deviceLogReader.addLine('Observatory listening on http://127.0.0.1:456'); }); + when(MDnsObservatoryDiscovery.instance.getObservatoryUri(any, any, usesIpv6: anyNamed('usesIpv6'))) + .thenAnswer((Invocation invocation) async => null); + + final LaunchResult launchResult = await device.startApp(iosApp, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + platformArgs: {}, + fallbackPollingDelay: Duration.zero, + ); + + expect(launchResult.started, true); + expect(launchResult.hasObservatory, true); + verify(globals.flutterUsage.sendEvent('ios-handshake', 'mdns-failure')).called(1); + verify(globals.flutterUsage.sendEvent('ios-handshake', 'fallback-success')).called(1); + expect(await device.stopApp(iosApp), false); + }, overrides: { + Usage: () => MockUsage(), + MDnsObservatoryDiscovery: () => MockMDnsObservatoryDiscovery(), + }); + + // Still uses context for analytics and mDNS. + testUsingContext('IOSDevice.startApp fails in debug mode when mDNS fails and ' + 'when Observatory URI is malformed', () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final FakeProcessManager processManager = FakeProcessManager.list([ + kDeployCommand, + kLaunchDebugCommand, + ]); + final IOSDevice device = setUpIOSDevice( + processManager: processManager, + fileSystem: fileSystem, + vmServiceConnector: (String string, {Log log}) async { + throw const io.SocketException( + 'OS Error: Connection refused, errno = 61, address = localhost, port ' + '= 58943', + ); + }, + ); + final IOSApp iosApp = PrebuiltIOSApp( + projectBundleId: 'app', + bundleName: 'Runner', + bundleDir: fileSystem.currentDirectory, + ); + final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader(); + + device.portForwarder = const NoOpDevicePortForwarder(); + device.setLogReader(iosApp, deviceLogReader); + + // Now that the reader is used, start writing messages to it. + Timer.run(() { + deviceLogReader.addLine('Foo'); + deviceLogReader.addLine('Observatory listening on http:/:/127.0.0.1:456'); + }); + when(MDnsObservatoryDiscovery.instance.getObservatoryUri(any, any, usesIpv6: anyNamed('usesIpv6'))) + .thenAnswer((Invocation invocation) async => null); final LaunchResult launchResult = await device.startApp(iosApp, prebuiltApplication: true, debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), platformArgs: {}, fallbackPollingDelay: Duration.zero, - // fallbackThrottleTimeout: const Duration(milliseconds: 10), ); expect(launchResult.started, false); @@ -263,8 +259,8 @@ void main() { label: anyNamed('label'), value: anyNamed('value'), )).called(1); - verify(globals.flutterUsage.sendEvent('ios-handshake', 'log-failure')).called(1); verify(globals.flutterUsage.sendEvent('ios-handshake', 'mdns-failure')).called(1); + verify(globals.flutterUsage.sendEvent('ios-handshake', 'fallback-failure')).called(1); }, overrides: { MDnsObservatoryDiscovery: () => MockMDnsObservatoryDiscovery(), Usage: () => MockUsage(), @@ -392,7 +388,6 @@ void main() { debuggingOptions: DebuggingOptions.disabled(BuildInfo.release), platformArgs: {}, fallbackPollingDelay: Duration.zero, - fallbackThrottleTimeout: const Duration(milliseconds: 10), ); expect(launchResult.started, true); @@ -410,17 +405,13 @@ void main() { kDeployCommand, FakeCommand( command: [ - 'script', - '-t', - '0', - '/dev/null', 'ios-deploy', '--id', '123', '--bundle', '/', - '--debug', '--no-wifi', + '--justlaunch', // The arguments below are determined by what is passed into // the debugging options argument to startApp. '--args', @@ -445,8 +436,7 @@ void main() { ], environment: const { 'PATH': '/usr/bin:null', 'DYLD_LIBRARY_PATH': '/path/to/libraries', - }, - stdout: '(lldb) run\nsuccess', + } ) ]); final IOSDevice device = setUpIOSDevice( @@ -465,15 +455,22 @@ void main() { bundleName: 'Runner', bundleDir: fileSystem.currentDirectory, ); - final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader(); + final Uri uri = Uri( + scheme: 'http', + host: '127.0.0.1', + port: 1234, + path: 'observatory', + ); + device.setLogReader(iosApp, FakeDeviceLogReader()); device.portForwarder = const NoOpDevicePortForwarder(); - device.setLogReader(iosApp, deviceLogReader); - // Start writing messages to the log reader. - Timer.run(() { - deviceLogReader.addLine('Observatory listening on http://127.0.0.1:1234'); - }); + when(MDnsObservatoryDiscovery.instance.getObservatoryUri( + any, + any, + usesIpv6: anyNamed('usesIpv6'), + hostVmservicePort: anyNamed('hostVmservicePort') + )).thenAnswer((Invocation invocation) async => uri); final LaunchResult launchResult = await device.startApp(iosApp, prebuiltApplication: true, @@ -495,7 +492,6 @@ void main() { ), platformArgs: {}, fallbackPollingDelay: Duration.zero, - fallbackThrottleTimeout: const Duration(milliseconds: 10), ); expect(launchResult.started, true);