Unverified Commit d5b715d7 authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Stream logging from attached debugger on iOS 13+ (#66399)

parent 658e6c8a
...@@ -2,11 +2,15 @@ ...@@ -2,11 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:io';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
group('example', () { group('example', () {
test('passed', () { test('passed', () {
print('This is print');
stderr.writeln('This is writeln');
expect(true, true); expect(true, true);
}); });
test('failed', () { test('failed', () {
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
...@@ -19,9 +20,10 @@ const Pattern passedMessageMatch = '+0: example passed'; ...@@ -19,9 +20,10 @@ const Pattern passedMessageMatch = '+0: example passed';
const Pattern failedMessageMatch = '+1: example failed [E]'; const Pattern failedMessageMatch = '+1: example failed [E]';
const Pattern skippedMessageMatch = '+1 -1: example skipped'; const Pattern skippedMessageMatch = '+1 -1: example skipped';
const Pattern finishedMessageMatch = '+1 ~1 -1: Some tests failed.'; const Pattern finishedMessageMatch = '+1 ~1 -1: Some tests failed.';
const Pattern printMessageMatch = 'This is print';
const Pattern writelnMessageMatch = 'This is writeln';
Future<void> main() async { Future<void> main() async {
deviceOperatingSystem = DeviceOperatingSystem.android;
await task(createFlutterRunTask); await task(createFlutterRunTask);
} }
...@@ -31,19 +33,26 @@ Future<TaskResult> createFlutterRunTask() async { ...@@ -31,19 +33,26 @@ Future<TaskResult> createFlutterRunTask() async {
bool failedTest = false; bool failedTest = false;
bool skippedTest = false; bool skippedTest = false;
bool finishedMessage = false; bool finishedMessage = false;
bool printMessage = false;
bool writelnMessage = false;
final Device device = await devices.workingDevice; final Device device = await devices.workingDevice;
await device.unlock(); await device.unlock();
final List<String> options = <String>[ final List<String> options = <String>[
'-t', runTestSource.absolute.path, '-d', device.deviceId, '-t', runTestSource.absolute.path, '-d', device.deviceId, '-v',
]; ];
await inDirectory<void>(flutterGalleryDir, () async { await inDirectory<void>(flutterGalleryDir, () async {
startProcess( final Process run = await startProcess(
path.join(flutterDirectory.path, 'bin', 'flutter'), path.join(flutterDirectory.path, 'bin', 'flutter'),
flutterCommandArgs('run', options), flutterCommandArgs('run', options),
environment: null, environment: null,
); );
final Completer<void> finished = Completer<void>(); final Completer<void> finished = Completer<void>();
final StreamSubscription<void> subscription = device.logcat.listen((String line) { final StreamSubscription<void> subscription = run.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
print('stdout: $line');
// tests execute in order. // tests execute in order.
if (line.contains(passedMessageMatch)) { if (line.contains(passedMessageMatch)) {
passedTest = true; passedTest = true;
...@@ -51,6 +60,10 @@ Future<TaskResult> createFlutterRunTask() async { ...@@ -51,6 +60,10 @@ Future<TaskResult> createFlutterRunTask() async {
failedTest = true; failedTest = true;
} else if (line.contains(skippedMessageMatch)) { } else if (line.contains(skippedMessageMatch)) {
skippedTest = true; skippedTest = true;
} else if (line.contains(printMessageMatch)) {
printMessage = true;
} else if (line.contains(writelnMessageMatch)) {
writelnMessage = true;
} else if (line.contains(finishedMessageMatch)) { } else if (line.contains(finishedMessageMatch)) {
finishedMessage = true; finishedMessage = true;
finished.complete(); finished.complete();
...@@ -58,8 +71,9 @@ Future<TaskResult> createFlutterRunTask() async { ...@@ -58,8 +71,9 @@ Future<TaskResult> createFlutterRunTask() async {
}); });
await finished.future.timeout(const Duration(minutes: 1)); await finished.future.timeout(const Duration(minutes: 1));
subscription.cancel(); subscription.cancel();
run.kill();
}); });
return passedTest && failedTest && skippedTest && finishedMessage return passedTest && failedTest && skippedTest && finishedMessage && printMessage && writelnMessage
? TaskResult.success(<String, dynamic>{}) ? TaskResult.success(<String, dynamic>{})
: TaskResult.failure('Test did not execute as expected.'); : TaskResult.failure('Test did not execute as expected.');
} }
...@@ -213,6 +213,9 @@ class IOSDevice extends Device { ...@@ -213,6 +213,9 @@ class IOSDevice extends Device {
DevicePortForwarder _portForwarder; DevicePortForwarder _portForwarder;
@visibleForTesting
IOSDeployDebugger iosDeployDebugger;
@override @override
Future<bool> get isLocalEmulator async => false; Future<bool> get isLocalEmulator async => false;
...@@ -396,23 +399,45 @@ class IOSDevice extends Device { ...@@ -396,23 +399,45 @@ class IOSDevice extends Device {
timeout: timeoutConfiguration.slowOperation); timeout: timeoutConfiguration.slowOperation);
try { try {
ProtocolDiscovery observatoryDiscovery; ProtocolDiscovery observatoryDiscovery;
int installationResult = 1;
if (debuggingOptions.debuggingEnabled) { if (debuggingOptions.debuggingEnabled) {
_logger.printTrace('Debugging is enabled, connecting to observatory'); _logger.printTrace('Debugging is enabled, connecting to observatory');
final DeviceLogReader deviceLogReader = getLogReader(app: package);
// If the device supports syslog reading, prefer launching the app without
// attaching the debugger to avoid the overhead of the unnecessary extra running process.
if (majorSdkVersion >= IOSDeviceLogReader.minimumUniversalLoggingSdkVersion) {
iosDeployDebugger = _iosDeploy.prepareDebuggerForLaunch(
deviceId: id,
bundlePath: bundle.path,
launchArguments: launchArguments,
interfaceType: interfaceType,
);
if (deviceLogReader is IOSDeviceLogReader) {
deviceLogReader.debuggerStream = iosDeployDebugger;
}
}
observatoryDiscovery = ProtocolDiscovery.observatory( observatoryDiscovery = ProtocolDiscovery.observatory(
getLogReader(app: package), deviceLogReader,
portForwarder: portForwarder, portForwarder: portForwarder,
throttleDuration: fallbackPollingDelay,
throttleTimeout: fallbackThrottleTimeout ?? const Duration(seconds: 5),
hostPort: debuggingOptions.hostVmServicePort, hostPort: debuggingOptions.hostVmServicePort,
devicePort: debuggingOptions.deviceVmServicePort, devicePort: debuggingOptions.deviceVmServicePort,
ipv6: ipv6, ipv6: ipv6,
throttleTimeout: fallbackThrottleTimeout ?? const Duration(seconds: 1),
); );
} }
final int installationResult = await _iosDeploy.runApp( if (iosDeployDebugger == null) {
installationResult = await _iosDeploy.launchApp(
deviceId: id, deviceId: id,
bundlePath: bundle.path, bundlePath: bundle.path,
launchArguments: launchArguments, launchArguments: launchArguments,
interfaceType: interfaceType, interfaceType: interfaceType,
); );
} else {
installationResult = await iosDeployDebugger.launchAndAttach() ? 0 : 1;
}
if (installationResult != 0) { if (installationResult != 0) {
_logger.printError('Could not run ${bundle.path} on $id.'); _logger.printError('Could not run ${bundle.path} on $id.');
_logger.printError('Try launching Xcode and selecting "Product > Run" to fix the problem:'); _logger.printError('Try launching Xcode and selecting "Product > Run" to fix the problem:');
...@@ -466,7 +491,11 @@ class IOSDevice extends Device { ...@@ -466,7 +491,11 @@ class IOSDevice extends Device {
IOSApp app, { IOSApp app, {
String userIdentifier, String userIdentifier,
}) async { }) async {
// Currently we don't have a way to stop an app running on iOS. // 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;
}
return false; return false;
} }
...@@ -656,6 +685,13 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -656,6 +685,13 @@ class IOSDeviceLogReader extends DeviceLogReader {
// Matches a syslog line from any app. // Matches a syslog line from any app.
RegExp _anyLineRegex; 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<String> _linesController; StreamController<String> _linesController;
List<StreamSubscription<void>> _loggingSubscriptions; List<StreamSubscription<void>> _loggingSubscriptions;
...@@ -672,10 +708,10 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -672,10 +708,10 @@ class IOSDeviceLogReader extends DeviceLogReader {
_connectedVMService = connectedVmService; _connectedVMService = connectedVmService;
} }
static const int _minimumUniversalLoggingSdkVersion = 13; static const int minimumUniversalLoggingSdkVersion = 13;
Future<void> _listenToUnifiedLoggingEvents(vm_service.VmService connectedVmService) async { Future<void> _listenToUnifiedLoggingEvents(vm_service.VmService connectedVmService) async {
if (_majorSdkVersion < _minimumUniversalLoggingSdkVersion) { if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) {
return; return;
} }
try { try {
...@@ -692,6 +728,10 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -692,6 +728,10 @@ class IOSDeviceLogReader extends DeviceLogReader {
} }
void logMessage(vm_service.Event event) { 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); final String message = processVmServiceMessage(event);
if (message.isNotEmpty) { if (message.isNotEmpty) {
_linesController.add(message); _linesController.add(message);
...@@ -704,9 +744,29 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -704,9 +744,29 @@ 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() { void _listenToSysLog() {
// syslog is not written on iOS 13+. // syslog is not written on iOS 13+.
if (_majorSdkVersion >= _minimumUniversalLoggingSdkVersion) { if (_majorSdkVersion >= minimumUniversalLoggingSdkVersion) {
return; return;
} }
_iMobileDevice.startLogger(_deviceId).then<void>((Process process) { _iMobileDevice.startLogger(_deviceId).then<void>((Process process) {
...@@ -763,6 +823,7 @@ class IOSDeviceLogReader extends DeviceLogReader { ...@@ -763,6 +823,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
loggingSubscription.cancel(); loggingSubscription.cancel();
} }
_idevicesyslogProcess?.kill(); _idevicesyslogProcess?.kill();
_iosDeployDebugger?.detach();
} }
} }
......
...@@ -82,50 +82,50 @@ class FallbackDiscovery { ...@@ -82,50 +82,50 @@ class FallbackDiscovery {
} }
try { try {
final Uri result = await _mDnsObservatoryDiscovery.getObservatoryUri( final Uri result = await _protocolDiscovery.uri;
packageId,
device,
usesIpv6: usesIpv6,
hostVmservicePort: hostVmservicePort,
);
if (result != null) { if (result != null) {
UsageEvent( UsageEvent(
_kEventName, _kEventName,
'mdns-success', 'log-success',
flutterUsage: _flutterUsage, flutterUsage: _flutterUsage,
).send(); ).send();
return result; 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) { } on Exception catch (err) {
_logger.printTrace(err.toString()); _logger.printTrace(err.toString());
} }
_logger.printTrace('Failed to connect with mDNS, falling back to log scanning'); _logger.printTrace('Failed to connect with log scanning, falling back to mDNS');
UsageEvent( UsageEvent(
_kEventName, _kEventName,
'mdns-failure', 'log-failure',
flutterUsage: _flutterUsage, flutterUsage: _flutterUsage,
).send(); ).send();
try { try {
final Uri result = await _protocolDiscovery.uri; final Uri result = await _mDnsObservatoryDiscovery.getObservatoryUri(
packageId,
device,
usesIpv6: usesIpv6,
hostVmservicePort: hostVmservicePort,
);
if (result != null) { if (result != null) {
UsageEvent( UsageEvent(
_kEventName, _kEventName,
'fallback-success', 'mdns-success',
flutterUsage: _flutterUsage, flutterUsage: _flutterUsage,
).send(); ).send();
return result; 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) { } on Exception catch (err) {
_logger.printTrace(err.toString()); _logger.printTrace(err.toString());
} }
_logger.printTrace('Failed to connect with log scanning'); _logger.printTrace('Failed to connect with mDNS');
UsageEvent( UsageEvent(
_kEventName, _kEventName,
'fallback-failure', 'mdns-failure',
flutterUsage: _flutterUsage, flutterUsage: _flutterUsage,
).send(); ).send();
return null; return null;
...@@ -148,7 +148,7 @@ class FallbackDiscovery { ...@@ -148,7 +148,7 @@ class FallbackDiscovery {
assumedWsUri = Uri.parse('ws://localhost:$hostPort/ws'); assumedWsUri = Uri.parse('ws://localhost:$hostPort/ws');
} on Exception catch (err) { } on Exception catch (err) {
_logger.printTrace(err.toString()); _logger.printTrace(err.toString());
_logger.printTrace('Failed to connect directly, falling back to mDNS'); _logger.printTrace('Failed to connect directly, falling back to log scanning');
_sendFailureEvent(err, assumedDevicePort); _sendFailureEvent(err, assumedDevicePort);
return null; return null;
} }
......
...@@ -34,7 +34,7 @@ class ProtocolDiscovery { ...@@ -34,7 +34,7 @@ class ProtocolDiscovery {
factory ProtocolDiscovery.observatory( factory ProtocolDiscovery.observatory(
DeviceLogReader logReader, { DeviceLogReader logReader, {
DevicePortForwarder portForwarder, DevicePortForwarder portForwarder,
Duration throttleDuration = const Duration(milliseconds: 200), Duration throttleDuration,
Duration throttleTimeout, Duration throttleTimeout,
@required int hostPort, @required int hostPort,
@required int devicePort, @required int devicePort,
...@@ -45,7 +45,7 @@ class ProtocolDiscovery { ...@@ -45,7 +45,7 @@ class ProtocolDiscovery {
logReader, logReader,
kObservatoryService, kObservatoryService,
portForwarder: portForwarder, portForwarder: portForwarder,
throttleDuration: throttleDuration, throttleDuration: throttleDuration ?? const Duration(milliseconds: 200),
throttleTimeout: throttleTimeout, throttleTimeout: throttleTimeout,
hostPort: hostPort, hostPort: hostPort,
devicePort: devicePort, devicePort: devicePort,
...@@ -225,7 +225,7 @@ class _BufferedStreamController<T> { ...@@ -225,7 +225,7 @@ class _BufferedStreamController<T> {
/// ///
/// For example, consider a `waitDuration` of `10ms`, and list of event names /// For example, consider a `waitDuration` of `10ms`, and list of event names
/// and arrival times: `a (0ms), b (5ms), c (11ms), d (21ms)`. /// and arrival times: `a (0ms), b (5ms), c (11ms), d (21ms)`.
/// The events `c` and `d` will be produced as a result. /// The events `a`, `c`, and `d` will be produced as a result.
StreamTransformer<S, S> _throttle<S>({ StreamTransformer<S, S> _throttle<S>({
@required Duration waitDuration, @required Duration waitDuration,
}) { }) {
...@@ -240,10 +240,13 @@ StreamTransformer<S, S> _throttle<S>({ ...@@ -240,10 +240,13 @@ StreamTransformer<S, S> _throttle<S>({
handleData: (S value, EventSink<S> sink) { handleData: (S value, EventSink<S> sink) {
latestLine = value; latestLine = value;
final bool isFirstMessage = lastExecution == null;
final int currentTime = DateTime.now().millisecondsSinceEpoch; final int currentTime = DateTime.now().millisecondsSinceEpoch;
lastExecution ??= currentTime; lastExecution ??= currentTime;
final int remainingTime = currentTime - lastExecution; final int remainingTime = currentTime - lastExecution;
final int nextExecutionTime = remainingTime > waitDuration.inMilliseconds
// Always send the first event immediately.
final int nextExecutionTime = isFirstMessage || remainingTime > waitDuration.inMilliseconds
? 0 ? 0
: waitDuration.inMilliseconds - remainingTime; : waitDuration.inMilliseconds - remainingTime;
......
...@@ -2,10 +2,15 @@ ...@@ -2,10 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'package:flutter_tools/src/artifacts.dart'; 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/logger.dart';
import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/cache.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:flutter_tools/src/ios/ios_deploy.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
...@@ -21,7 +26,206 @@ void main () { ...@@ -21,7 +26,206 @@ void main () {
expect(environment['PATH'], startsWith('/usr/bin')); expect(environment['PATH'], startsWith('/usr/bin'));
}); });
testWithoutContext('IOSDeploy.uninstallApp calls ios-deploy with correct arguments and returns 0 on success', () async { group('IOSDeploy.prepareDebuggerForLaunch', () {
testWithoutContext('calls ios-deploy with correct arguments and returns when debugger attaches', () async {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'ios-deploy',
'--id',
'123',
'--bundle',
'/',
'--debug',
'--args',
<String>[
'--enable-dart-profiling',
].join(' '),
], environment: const <String, String>{
'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: <String>['--enable-dart-profiling'],
interfaceType: IOSDeviceInterface.network,
);
expect(await iosDeployDebugger.launchAndAttach(), isTrue);
expect(await iosDeployDebugger.logLines.toList(), <String>['Did finish launching.']);
expect(processManager.hasRemainingExpectations, false);
});
});
group('IOSDeployDebugger', () {
group('launch', () {
BufferLogger logger;
setUp(() {
logger = BufferLogger.test();
});
testWithoutContext('debugger attached', () async {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>['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<String> receivedLogLines = <String>[];
final Stream<String> logLines = iosDeployDebugger.logLines
..listen(receivedLogLines.add);
expect(await iosDeployDebugger.launchAndAttach(), isTrue);
await logLines.toList();
expect(receivedLogLines, <String>[
'success', // ignore first "success" from lldb, but log subsequent ones from real logging.
'Log on attach1',
'Log on attach2',
'', '']);
});
testWithoutContext('app exit', () async {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>['ios-deploy'],
stdout: '(lldb) run\r\nsuccess\r\nLog on attach\r\nProcess 100 exited with status = 0\r\nLog after process exit',
),
]);
final IOSDeployDebugger iosDeployDebugger = IOSDeployDebugger.test(
processManager: processManager,
logger: logger,
);
final List<String> receivedLogLines = <String>[];
final Stream<String> logLines = iosDeployDebugger.logLines
..listen(receivedLogLines.add);
expect(await iosDeployDebugger.launchAndAttach(), isTrue);
await logLines.toList();
expect(receivedLogLines, <String>['Log on attach']);
});
testWithoutContext('attach failed', () async {
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>['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<String> receivedLogLines = <String>[];
final Stream<String> 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(<FakeCommand>[
const FakeCommand(
command: <String>['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(<FakeCommand>[
const FakeCommand(
command: <String>['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(<FakeCommand>[
const FakeCommand(
command: <String>['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(<FakeCommand>[
const FakeCommand(
command: <String>['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<List<int>> stdin = StreamController<List<int>>();
final Stream<String> stdinStream = stdin.stream.transform<String>(const Utf8Decoder());
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: const <String>[
'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 deviceId = '123';
const String bundleId = 'com.example.app'; const String bundleId = 'com.example.app';
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
...@@ -44,7 +248,7 @@ void main () { ...@@ -44,7 +248,7 @@ void main () {
expect(processManager.hasRemainingExpectations, false); expect(processManager.hasRemainingExpectations, false);
}); });
testWithoutContext('IOSDeploy.uninstallApp returns non-zero exit code when ios-deploy does the same', () async { testWithoutContext('returns non-zero exit code when ios-deploy does the same', () async {
const String deviceId = '123'; const String deviceId = '123';
const String bundleId = 'com.example.app'; const String bundleId = 'com.example.app';
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
...@@ -66,6 +270,7 @@ void main () { ...@@ -66,6 +270,7 @@ void main () {
expect(exitCode, 1); expect(exitCode, 1);
expect(processManager.hasRemainingExpectations, false); expect(processManager.hasRemainingExpectations, false);
}); });
});
} }
IOSDeploy setUpIOSDeploy(ProcessManager processManager) { IOSDeploy setUpIOSDeploy(ProcessManager processManager) {
......
...@@ -10,6 +10,7 @@ import 'package:flutter_tools/src/build_info.dart'; ...@@ -10,6 +10,7 @@ import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/ios/devices.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:flutter_tools/src/ios/mac.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:vm_service/vm_service.dart'; import 'package:vm_service/vm_service.dart';
...@@ -33,6 +34,7 @@ void main() { ...@@ -33,6 +34,7 @@ void main() {
.thenReturn('idevice-syslog'); .thenReturn('idevice-syslog');
}); });
group('syslog stream', () {
testWithoutContext('decodeSyslog decodes a syslog-encoded line', () { testWithoutContext('decodeSyslog decodes a syslog-encoded line', () {
final String decoded = decodeSyslog( final String decoded = decodeSyslog(
r'I \M-b\M^]\M-$\M-o\M-8\M^O syslog \M-B\M-/\' r'I \M-b\M^]\M-$\M-o\M-8\M^O syslog \M-B\M-/\'
...@@ -141,7 +143,9 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt ...@@ -141,7 +143,9 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
' with a non-Flutter log message following it.', ' with a non-Flutter log message following it.',
]); ]);
}); });
});
group('VM service', () {
testWithoutContext('IOSDeviceLogReader can listen to VM Service logs', () async { testWithoutContext('IOSDeviceLogReader can listen to VM Service logs', () async {
final MockVmService vmService = MockVmService(); final MockVmService vmService = MockVmService();
final DeviceLogReader logReader = IOSDeviceLogReader.test( final DeviceLogReader logReader = IOSDeviceLogReader.test(
...@@ -191,7 +195,136 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt ...@@ -191,7 +195,136 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
])); ]));
verify(vmService.streamListen('Debug')); verify(vmService.streamListen('Debug'));
}); });
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<Event> stdoutController = StreamController<Event>();
final StreamController<Event> stderController = StreamController<Event>();
final Completer<Success> stdoutCompleter = Completer<Success>();
final Completer<Success> stderrCompleter = Completer<Success>();
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<String> debuggingLogs = Stream<String>.fromIterable(<String>[
'Message from debugger'
]);
when(iosDeployDebugger.logLines).thenAnswer((Invocation invocation) => debuggingLogs);
logReader.debuggerStream = iosDeployDebugger;
// Wait for stream listeners to fire.
await expectLater(logReader.logLines, emitsInAnyOrder(<Matcher>[
equals('Message from debugger'),
]));
});
});
group('debugger stream', () {
testWithoutContext('IOSDeviceLogReader removes metadata prefix from lldb output', () async {
final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
'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<List<String>> logLines = logReader.logLines.toList();
expect(await logLines, <String>[
'Did finish launching.',
'[Category] Did finish launching from logging category.',
'stderr from dart',
'',
]);
});
testWithoutContext('errors on debugger stream closes log stream', () async {
final Stream<String> debuggingLogs = Stream<String>.error('ios-deploy error');
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
iMobileDevice: IMobileDevice(
artifacts: artifacts,
processManager: processManager,
cache: fakeCache,
logger: logger,
),
useSyslog: false,
);
final Completer<void> streamComplete = Completer<void>();
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;
});
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<String>.empty());
logReader.debuggerStream = iosDeployDebugger;
logReader.dispose();
verify(iosDeployDebugger.detach());
});
});
} }
class MockArtifacts extends Mock implements Artifacts {} class MockArtifacts extends Mock implements Artifacts {}
class MockVmService extends Mock implements VmService {} class MockVmService extends Mock implements VmService {}
class MockIOSDeployDebugger extends Mock implements IOSDeployDebugger {}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment