Unverified Commit ec0b7443 authored by Victoria Ashworth's avatar Victoria Ashworth Committed by GitHub

Fix log filtering and CI tests for iOS 17 physical devices (#132491)

Fixes a couple of issues introduced in new iOS 17 physical device tooling: https://github.com/flutter/flutter/pull/131865.

1) Duplicate messages were being filtered out too aggressively. 

For example, if on the counter app, you printed "Increment!" on button click, it would only print once no matter how many times you clicked.

Sometimes more than one log source is used at a time and the original intention was to filter duplicates between two log sources, so it wouldn't print the same message from both logs. However, it would also filter when the same message was added more than once via the same log.

The new solution distinguishes a "primary" and a "fallback" log source and prefers to use the primary source unless it's not working, in which it'll use the fallback. If the fallback is faster than the primary, the primary will exclude the logs received by the fallback in a 1-to-1 fashion to prevent too-aggressive filtering. Once a flutter-message has been received by the primary source, fallback messages will be ignored.

Note: iOS < 17 did not regress.

2) There was a race condition between the shutdown hooks and exiting XcodeDebug that was causing a crash when deleting a file that doesn't exist. This only affects CI  - for the new integration tests and when testing with iOS 17 physical devices.
parent 8b8f262d
......@@ -690,7 +690,7 @@ class IOSDevice extends Device {
mDNSLookupTimer.cancel();
}
} else {
if (isCoreDevice && vmServiceDiscovery != null) {
if ((isCoreDevice || forceXcodeDebugWorkflow) && vmServiceDiscovery != null) {
// When searching for the Dart VM url, search for it via ProtocolDiscovery
// (device logs) and mDNS simultaneously, since both can be flaky at times.
final Future<Uri?> vmUrlFromMDns = MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
......@@ -1071,7 +1071,8 @@ class IOSDeviceLogReader extends DeviceLogReader {
// Sometimes (race condition?) we try to send a log after the controller has
// been closed. See https://github.com/flutter/flutter/issues/99021 for more
// context.
void _addToLinesController(String message, IOSDeviceLogSource source) {
@visibleForTesting
void addToLinesController(String message, IOSDeviceLogSource source) {
if (!linesController.isClosed) {
if (_excludeLog(message, source)) {
return;
......@@ -1080,32 +1081,53 @@ class IOSDeviceLogReader extends DeviceLogReader {
}
}
/// Used to track messages prefixed with "flutter:" when [useBothLogDeviceReaders]
/// is true.
final List<String> _streamFlutterMessages = <String>[];
/// Used to track messages prefixed with "flutter:" from the fallback log source.
final List<String> _fallbackStreamFlutterMessages = <String>[];
/// Used to track if a message prefixed with "flutter:" has been received from the primary log.
bool primarySourceFlutterLogReceived = false;
/// There are three potential logging sources: `idevicesyslog`, `ios-deploy`,
/// and Unified Logging (Dart VM). When using more than one of these logging
/// sources at a time, exclude logs with a `flutter:` prefix if they have
/// already been added to the stream. This is to prevent duplicates from
/// being printed.
/// sources at a time, prefer to use the primary source. However, if the
/// primary source is not working, use the fallback.
bool _excludeLog(String message, IOSDeviceLogSource source) {
if (!usingMultipleLoggingSources) {
// If no fallback, don't exclude any logs.
if (logSources.fallbackSource == null) {
return false;
}
if (message.startsWith('flutter:')) {
if (_streamFlutterMessages.contains(message)) {
// If log is from primary source, don't exclude it unless the fallback was
// quicker and added the message first.
if (source == logSources.primarySource) {
if (!primarySourceFlutterLogReceived && message.startsWith('flutter:')) {
primarySourceFlutterLogReceived = true;
}
// If the message was already added by the fallback, exclude it to
// prevent duplicates.
final bool foundAndRemoved = _fallbackStreamFlutterMessages.remove(message);
if (foundAndRemoved) {
return true;
}
_streamFlutterMessages.add(message);
} else if (useIOSDeployLogging && source == IOSDeviceLogSource.idevicesyslog) {
// If using both `ios-deploy` and `idevicesyslog` simultaneously, exclude
// the message if its source is `idevicesyslog`. This is done because
//`ios-deploy` and `idevicesyslog` often have different prefixes, which
// makes duplicate matching difficult. Instead, exclude any non-flutter-prefixed
// `idevicesyslog` messages, which are not critical for CI tests.
return false;
}
// If a flutter log was received from the primary source, that means it's
// working so don't use any messages from the fallback.
if (primarySourceFlutterLogReceived) {
return true;
}
// When using logs from fallbacks, skip any logs not prefixed with "flutter:".
// This is done because different sources often have different prefixes for
// non-flutter messages, which makes duplicate matching difficult. Also,
// non-flutter messages are not critical for CI tests.
if (!message.startsWith('flutter:')) {
return true;
}
_fallbackStreamFlutterMessages.add(message);
return false;
}
......@@ -1128,114 +1150,91 @@ class IOSDeviceLogReader extends DeviceLogReader {
static const int minimumUniversalLoggingSdkVersion = 13;
/// Use `idevicesyslog` to stream logs from the device when one of the
/// following criteria is met:
///
/// 1) The device is a physically attached CoreDevice.
/// 2) The device has iOS 16 or greater and it's being debugged from CI.
/// 3) The device has iOS 12 or lower.
/// Determine the primary and fallback source for device logs.
///
/// Syslog stopped working on iOS 13 (https://github.com/flutter/flutter/issues/41133).
/// However, from at least iOS 16, it has began working again. It's unclear
/// why it started working again.
/// There are three potential logging sources: `idevicesyslog`, `ios-deploy`,
/// and Unified Logging (Dart VM).
@visibleForTesting
bool get useSyslogLogging {
// When forcing XcodeDebug workflow, use `idevicesyslog`.
if (_forceXcodeDebug) {
return true;
}
_IOSDeviceLogSources get logSources {
// `ios-deploy` stopped working with iOS 17 / Xcode 15, so use `idevicesyslog` instead.
// However, `idevicesyslog` does not work with iOS 17 wireless devices.
if (_isCoreDevice && !_isWirelesslyConnected) {
return true;
// However, `idevicesyslog` is sometimes unreliable so use Dart VM as a fallback.
// Also, `idevicesyslog` does not work with iOS 17 wireless devices, so use the
// Dart VM for wireless devices.
if (_isCoreDevice || _forceXcodeDebug) {
if (_isWirelesslyConnected) {
return _IOSDeviceLogSources(
primarySource: IOSDeviceLogSource.unifiedLogging,
);
}
// Use both logs from `idevicesyslog` and `ios-deploy` when debugging from CI system
// since sometimes `ios-deploy` does not return the device logs:
// https://github.com/flutter/flutter/issues/121231
if (_usingCISystem && _majorSdkVersion >= 16) {
return true;
return _IOSDeviceLogSources(
primarySource: IOSDeviceLogSource.idevicesyslog,
fallbackSource: IOSDeviceLogSource.unifiedLogging,
);
}
// Use `idevicesyslog` for iOS 12 or less.
// Syslog stopped working on iOS 13 (https://github.com/flutter/flutter/issues/41133).
// However, from at least iOS 16, it has began working again. It's unclear
// why it started working again.
if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) {
return true;
}
return false;
return _IOSDeviceLogSources(
primarySource: IOSDeviceLogSource.idevicesyslog,
);
}
/// Use the Dart VM to stream logs from the device when one of the following
/// criteria is met:
///
/// 1) The device is a CoreDevice and wirelessly connected.
/// 2) The device has iOS 13 or greater and [_iosDeployDebugger] is null or
/// the [_iosDeployDebugger] debugger is not attached.
///
/// This value may change if [_iosDeployDebugger] changes.
@visibleForTesting
bool get useUnifiedLogging {
// Can't use Unified Logging if it's not going to listen to the Dart VM.
if (!_shouldListenForUnifiedLoggingEvents) {
return false;
// Use `idevicesyslog` as a fallback to `ios-deploy` when debugging from
// CI system since sometimes `ios-deploy` does not return the device logs:
// https://github.com/flutter/flutter/issues/121231
if (_usingCISystem && _majorSdkVersion >= 16) {
return _IOSDeviceLogSources(
primarySource: IOSDeviceLogSource.iosDeploy,
fallbackSource: IOSDeviceLogSource.idevicesyslog,
);
}
// `idevicesyslog` doesn't work on wireless devices, so use logs from Dart VM instead.
if (_isCoreDevice) {
return true;
// Use `ios-deploy` to stream logs from the device when the device is not a
// CoreDevice and has iOS 13 or greater.
// When using `ios-deploy` and the Dart VM, prefer the more complete logs
// from the attached debugger, if available.
if (connectedVMService != null && (_iosDeployDebugger == null || !_iosDeployDebugger!.debuggerAttached)) {
return _IOSDeviceLogSources(
primarySource: IOSDeviceLogSource.unifiedLogging,
fallbackSource: IOSDeviceLogSource.iosDeploy,
);
}
// Prefer the more complete logs from the attached debugger, if they are available.
if (_majorSdkVersion >= minimumUniversalLoggingSdkVersion && (_iosDeployDebugger == null || !_iosDeployDebugger!.debuggerAttached)) {
return true;
return _IOSDeviceLogSources(
primarySource: IOSDeviceLogSource.iosDeploy,
fallbackSource: IOSDeviceLogSource.unifiedLogging,
);
}
return false;
/// Whether `idevicesyslog` is used as either the primary or fallback source for device logs.
@visibleForTesting
bool get useSyslogLogging {
return logSources.primarySource == IOSDeviceLogSource.idevicesyslog ||
logSources.fallbackSource == IOSDeviceLogSource.idevicesyslog;
}
/// Determine whether to listen to the Dart VM for logging events. Returns
/// true when one of the following criteria is met:
/// Whether the Dart VM is used as either the primary or fallback source for device logs.
///
/// 1) The device is a CoreDevice and wirelessly connected.
/// 2) The device has iOS 13 or greater.
bool get _shouldListenForUnifiedLoggingEvents {
// `idevicesyslog` doesn't work on wireless devices, so use logs from Dart VM instead.
if (_isCoreDevice) {
return true;
}
if (_majorSdkVersion >= minimumUniversalLoggingSdkVersion) {
return true;
}
return false;
/// Unified Logging only works after the Dart VM has been connected to.
@visibleForTesting
bool get useUnifiedLogging {
return logSources.primarySource == IOSDeviceLogSource.unifiedLogging ||
logSources.fallbackSource == IOSDeviceLogSource.unifiedLogging;
}
/// Use `ios-deploy` to stream logs from the device when the device is not a
/// CoreDevice and has iOS 13 or greater.
/// Whether `ios-deploy` is used as either the primary or fallback source for device logs.
@visibleForTesting
bool get useIOSDeployLogging {
if (_majorSdkVersion < minimumUniversalLoggingSdkVersion || _isCoreDevice) {
return false;
}
return true;
}
@visibleForTesting
/// Returns true when using multiple sources for streaming the device logs.
bool get usingMultipleLoggingSources {
final int numberOfSources = (useIOSDeployLogging ? 1 : 0) + (useSyslogLogging ? 1 : 0) + (useUnifiedLogging ? 1 : 0);
if (numberOfSources > 1) {
return true;
}
return false;
return logSources.primarySource == IOSDeviceLogSource.iosDeploy ||
logSources.fallbackSource == IOSDeviceLogSource.iosDeploy;
}
/// Listen to Dart VM for logs on iOS 13 or greater.
///
/// Only send logs to stream if [_iosDeployDebugger] is null or
/// the [_iosDeployDebugger] debugger is not attached.
Future<void> _listenToUnifiedLoggingEvents(FlutterVmService connectedVmService) async {
if (!_shouldListenForUnifiedLoggingEvents) {
if (!useUnifiedLogging) {
return;
}
try {
......@@ -1252,13 +1251,9 @@ class IOSDeviceLogReader extends DeviceLogReader {
}
void logMessage(vm_service.Event event) {
if (!useUnifiedLogging) {
// Prefer the more complete logs from the attached debugger.
return;
}
final String message = processVmServiceMessage(event);
if (message.isNotEmpty) {
_addToLinesController(message, IOSDeviceLogSource.unifiedLogging);
addToLinesController(message, IOSDeviceLogSource.unifiedLogging);
}
}
......@@ -1283,7 +1278,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
}
// Add the debugger logs to the controller created on initialization.
_loggingSubscriptions.add(debugger.logLines.listen(
(String line) => _addToLinesController(
(String line) => addToLinesController(
_debuggerLineHandler(line),
IOSDeviceLogSource.iosDeploy,
),
......@@ -1338,7 +1333,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
return (String line) {
if (printing) {
if (!_anyLineRegex.hasMatch(line)) {
_addToLinesController(decodeSyslog(line), IOSDeviceLogSource.idevicesyslog);
addToLinesController(decodeSyslog(line), IOSDeviceLogSource.idevicesyslog);
return;
}
......@@ -1350,7 +1345,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
if (match != null) {
final String logLine = line.substring(match.end);
// Only display the log line after the initial device and executable information.
_addToLinesController(decodeSyslog(logLine), IOSDeviceLogSource.idevicesyslog);
addToLinesController(decodeSyslog(logLine), IOSDeviceLogSource.idevicesyslog);
printing = true;
}
};
......@@ -1375,6 +1370,16 @@ enum IOSDeviceLogSource {
unifiedLogging,
}
class _IOSDeviceLogSources {
_IOSDeviceLogSources({
required this.primarySource,
this.fallbackSource,
});
final IOSDeviceLogSource primarySource;
final IOSDeviceLogSource? fallbackSource;
}
/// A [DevicePortForwarder] specialized for iOS usage with iproxy.
class IOSDevicePortForwarder extends DevicePortForwarder {
......
......@@ -7,6 +7,7 @@ import 'dart:async';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import '../base/error_handling_io.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
......@@ -186,7 +187,12 @@ class XcodeDebug {
if (currentDebuggingProject != null) {
final XcodeDebugProject project = currentDebuggingProject!;
if (project.isTemporaryProject) {
project.xcodeProject.parent.deleteSync(recursive: true);
// Only delete if it exists. This is to prevent crashes when racing
// with shutdown hooks to delete temporary files.
ErrorHandlingFileSystem.deleteIfExists(
project.xcodeProject.parent,
recursive: true,
);
}
currentDebuggingProject = null;
}
......
......@@ -190,7 +190,7 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
]));
});
testWithoutContext('IOSDeviceLogReader ignores VM Service logs when attached to debugger', () async {
testWithoutContext('IOSDeviceLogReader ignores VM Service logs when attached to and received flutter logs from debugger', () async {
final Event stdoutEvent = Event(
kind: 'Stdout',
timestamp: 0,
......@@ -229,14 +229,14 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
iosDeployDebugger.debuggerAttached = true;
final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
'Message from debugger',
'flutter: Message from debugger',
]);
iosDeployDebugger.logLines = debuggingLogs;
logReader.debuggerStream = iosDeployDebugger;
// Wait for stream listeners to fire.
await expectLater(logReader.logLines, emitsInAnyOrder(<Matcher>[
equals('Message from debugger'),
equals('flutter: Message from debugger'),
]));
});
});
......@@ -365,7 +365,8 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
expect(logReader.useSyslogLogging, isTrue);
expect(logReader.useUnifiedLogging, isTrue);
expect(logReader.useIOSDeployLogging, isFalse);
expect(logReader.usingMultipleLoggingSources, isTrue);
expect(logReader.logSources.primarySource, IOSDeviceLogSource.idevicesyslog);
expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.unifiedLogging);
});
testWithoutContext('for wirelessly attached CoreDevice', () {
......@@ -384,7 +385,8 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
expect(logReader.useSyslogLogging, isFalse);
expect(logReader.useUnifiedLogging, isTrue);
expect(logReader.useIOSDeployLogging, isFalse);
expect(logReader.usingMultipleLoggingSources, isFalse);
expect(logReader.logSources.primarySource, IOSDeviceLogSource.unifiedLogging);
expect(logReader.logSources.fallbackSource, isNull);
});
testWithoutContext('for iOS 12 or less device', () {
......@@ -401,10 +403,11 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
expect(logReader.useSyslogLogging, isTrue);
expect(logReader.useUnifiedLogging, isFalse);
expect(logReader.useIOSDeployLogging, isFalse);
expect(logReader.usingMultipleLoggingSources, isFalse);
expect(logReader.logSources.primarySource, IOSDeviceLogSource.idevicesyslog);
expect(logReader.logSources.fallbackSource, isNull);
});
testWithoutContext('for iOS 13 or greater non-CoreDevice', () {
testWithoutContext('for iOS 13 or greater non-CoreDevice and _iosDeployDebugger not attached', () {
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
iMobileDevice: IMobileDevice(
artifacts: artifacts,
......@@ -418,7 +421,40 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
expect(logReader.useSyslogLogging, isFalse);
expect(logReader.useUnifiedLogging, isTrue);
expect(logReader.useIOSDeployLogging, isTrue);
expect(logReader.usingMultipleLoggingSources, isTrue);
expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy);
expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.unifiedLogging);
});
testWithoutContext('for iOS 13 or greater non-CoreDevice, _iosDeployDebugger not attached, and VM is connected', () {
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
iMobileDevice: IMobileDevice(
artifacts: artifacts,
processManager: processManager,
cache: fakeCache,
logger: logger,
),
majorSdkVersion: 13,
);
final FlutterVmService vmService = FakeVmServiceHost(requests: <VmServiceExpectation>[
const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
'streamId': 'Debug',
}),
const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
'streamId': 'Stdout',
}),
const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
'streamId': 'Stderr',
}),
]).vmService;
logReader.connectedVMService = vmService;
expect(logReader.useSyslogLogging, isFalse);
expect(logReader.useUnifiedLogging, isTrue);
expect(logReader.useIOSDeployLogging, isTrue);
expect(logReader.logSources.primarySource, IOSDeviceLogSource.unifiedLogging);
expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.iosDeploy);
});
testWithoutContext('for iOS 13 or greater non-CoreDevice and _iosDeployDebugger is attached', () {
......@@ -436,10 +472,25 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
iosDeployDebugger.debuggerAttached = true;
logReader.debuggerStream = iosDeployDebugger;
final FlutterVmService vmService = FakeVmServiceHost(requests: <VmServiceExpectation>[
const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
'streamId': 'Debug',
}),
const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
'streamId': 'Stdout',
}),
const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
'streamId': 'Stderr',
}),
]).vmService;
logReader.connectedVMService = vmService;
expect(logReader.useSyslogLogging, isFalse);
expect(logReader.useUnifiedLogging, isFalse);
expect(logReader.useUnifiedLogging, isTrue);
expect(logReader.useIOSDeployLogging, isTrue);
expect(logReader.usingMultipleLoggingSources, isFalse);
expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy);
expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.unifiedLogging);
});
testWithoutContext('for iOS 16 or greater non-CoreDevice', () {
......@@ -453,10 +504,15 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
majorSdkVersion: 16,
);
final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger();
iosDeployDebugger.debuggerAttached = true;
logReader.debuggerStream = iosDeployDebugger;
expect(logReader.useSyslogLogging, isFalse);
expect(logReader.useUnifiedLogging, isTrue);
expect(logReader.useIOSDeployLogging, isTrue);
expect(logReader.usingMultipleLoggingSources, isTrue);
expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy);
expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.unifiedLogging);
});
testWithoutContext('for iOS 16 or greater non-CoreDevice in CI', () {
......@@ -472,24 +528,27 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
);
expect(logReader.useSyslogLogging, isTrue);
expect(logReader.useUnifiedLogging, isTrue);
expect(logReader.useUnifiedLogging, isFalse);
expect(logReader.useIOSDeployLogging, isTrue);
expect(logReader.usingMultipleLoggingSources, isTrue);
expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy);
expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.idevicesyslog);
});
testWithoutContext('syslog sends flutter messages to stream when useSyslogLogging is true', () async {
group('when useSyslogLogging', () {
testWithoutContext('is true syslog sends flutter messages to stream', () async {
processManager.addCommand(
FakeCommand(
command: <String>[
ideviceSyslogPath, '-u', '1234',
],
stdout: '''
Runner(Flutter)[297] <Notice>: A is for ari
Runner(Flutter)[297] <Notice>: I is for ichigo
May 30 13:56:28 Runner(Flutter)[2037] <Notice>: flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/
May 30 13:56:28 Runner(Flutter)[2037] <Notice>: flutter: This is a test
May 30 13:56:28 Runner(Flutter)[2037] <Notice>: [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.
'''
Runner(Flutter)[297] <Notice>: A is for ari
Runner(Flutter)[297] <Notice>: I is for ichigo
May 30 13:56:28 Runner(Flutter)[2037] <Notice>: flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/
May 30 13:56:28 Runner(Flutter)[2037] <Notice>: flutter: This is a test
May 30 13:56:28 Runner(Flutter)[2037] <Notice>: [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.
'''
),
);
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
......@@ -512,12 +571,7 @@ May 30 13:56:28 Runner(Flutter)[2037] <Notice>: [VERBOSE-2:FlutterDarwinContextM
]);
});
testWithoutContext('IOSDeviceLogReader only uses ios-deploy debugger when attached and not in CI', () async {
final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
'(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.',
'',
]);
testWithoutContext('is false syslog does not send flutter messages to stream', () async {
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
iMobileDevice: IMobileDevice(
artifacts: artifacts,
......@@ -527,72 +581,413 @@ May 30 13:56:28 Runner(Flutter)[2037] <Notice>: [VERBOSE-2:FlutterDarwinContextM
),
majorSdkVersion: 16,
);
final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger();
iosDeployDebugger.debuggerAttached = true;
iosDeployDebugger.logLines = debuggingLogs;
iosDeployDebugger.logLines = Stream<String>.fromIterable(<String>[]);
logReader.debuggerStream = iosDeployDebugger;
final Future<List<String>> logLines = logReader.logLines.toList();
final List<String> lines = await logLines;
expect(logReader.useIOSDeployLogging, isTrue);
final List<String> lines = await logReader.logLines.toList();
expect(logReader.useSyslogLogging, isFalse);
expect(logReader.useUnifiedLogging, isFalse);
expect(logReader.usingMultipleLoggingSources, isFalse);
expect(processManager, hasNoRemainingExpectations);
expect(
lines.contains(
'(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.',
expect(lines, isEmpty);
});
});
group('when useIOSDeployLogging', () {
testWithoutContext('is true ios-deploy sends flutter messages to stream', () async {
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
iMobileDevice: IMobileDevice(
artifacts: artifacts,
processManager: processManager,
cache: fakeCache,
logger: logger,
),
isTrue,
majorSdkVersion: 16,
);
final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger();
final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
'flutter: Message from debugger',
]);
iosDeployDebugger.logLines = debuggingLogs;
logReader.debuggerStream = iosDeployDebugger;
final List<String> lines = await logReader.logLines.toList();
expect(logReader.useIOSDeployLogging, isTrue);
expect(processManager, hasNoRemainingExpectations);
expect(lines, <String>[
'flutter: Message from debugger',
]);
});
testWithoutContext('IOSDeviceLogReader uses both syslog and ios-deploy debugger for CI and filters duplicate messages', () async {
processManager.addCommand(
FakeCommand(
command: <String>[
ideviceSyslogPath, '-u', '1234',
],
stdout: '''
May 30 13:56:28 Runner(Flutter)[2037] <Notice>: flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/
May 30 13:56:28 Runner(Flutter)[2037] <Notice>: flutter: Check for duplicate
May 30 13:56:28 Runner(Flutter)[2037] <Notice>: [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.
'''
testWithoutContext('is false ios-deploy does not send flutter messages to stream', () async {
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
iMobileDevice: IMobileDevice(
artifacts: artifacts,
processManager: FakeProcessManager.any(),
cache: fakeCache,
logger: logger,
),
majorSdkVersion: 12,
);
final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger();
final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
'2023-06-01 12:49:01.445093-0500 Runner[2225:533240] flutter: Check for duplicate',
'(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.',
'flutter: Message from debugger',
]);
iosDeployDebugger.logLines = debuggingLogs;
logReader.debuggerStream = iosDeployDebugger;
final List<String> lines = await logReader.logLines.toList();
expect(logReader.useIOSDeployLogging, isFalse);
expect(processManager, hasNoRemainingExpectations);
expect(lines, isEmpty);
});
});
group('when useUnifiedLogging', () {
testWithoutContext('is true Dart VM sends flutter messages to stream', () async {
final Event stdoutEvent = Event(
kind: 'Stdout',
timestamp: 0,
bytes: base64.encode(utf8.encode('flutter: A flutter message')),
);
final Event stderrEvent = Event(
kind: 'Stderr',
timestamp: 0,
bytes: base64.encode(utf8.encode('flutter: A second flutter message')),
);
final FlutterVmService vmService = FakeVmServiceHost(requests: <VmServiceExpectation>[
const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
'streamId': 'Debug',
}),
const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
'streamId': 'Stdout',
}),
const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
'streamId': 'Stderr',
}),
FakeVmServiceStreamResponse(event: stdoutEvent, streamId: 'Stdout'),
FakeVmServiceStreamResponse(event: stderrEvent, streamId: 'Stderr'),
]).vmService;
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
useSyslog: false,
iMobileDevice: IMobileDevice(
artifacts: artifacts,
processManager: processManager,
cache: fakeCache,
logger: logger,
),
);
logReader.connectedVMService = vmService;
// Wait for stream listeners to fire.
expect(logReader.useUnifiedLogging, isTrue);
expect(processManager, hasNoRemainingExpectations);
await expectLater(logReader.logLines, emitsInAnyOrder(<Matcher>[
equals('flutter: A flutter message'),
equals('flutter: A second flutter message'),
]));
});
testWithoutContext('is false Dart VM does not send flutter messages to stream', () async {
final Event stdoutEvent = Event(
kind: 'Stdout',
timestamp: 0,
bytes: base64.encode(utf8.encode('flutter: A flutter message')),
);
final Event stderrEvent = Event(
kind: 'Stderr',
timestamp: 0,
bytes: base64.encode(utf8.encode('flutter: A second flutter message')),
);
final FlutterVmService vmService = FakeVmServiceHost(requests: <VmServiceExpectation>[
const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
'streamId': 'Debug',
}),
const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
'streamId': 'Stdout',
}),
const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
'streamId': 'Stderr',
}),
FakeVmServiceStreamResponse(event: stdoutEvent, streamId: 'Stdout'),
FakeVmServiceStreamResponse(event: stderrEvent, streamId: 'Stderr'),
]).vmService;
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
iMobileDevice: IMobileDevice(
artifacts: artifacts,
processManager: FakeProcessManager.any(),
cache: fakeCache,
logger: logger,
),
majorSdkVersion: 12,
);
logReader.connectedVMService = vmService;
final List<String> lines = await logReader.logLines.toList();
// Wait for stream listeners to fire.
expect(logReader.useUnifiedLogging, isFalse);
expect(processManager, hasNoRemainingExpectations);
expect(lines, isEmpty);
});
});
group('and when to exclude logs:', () {
testWithoutContext('all primary messages are included except if fallback sent flutter message first', () async {
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
iMobileDevice: IMobileDevice(
artifacts: artifacts,
processManager: FakeProcessManager.any(),
cache: fakeCache,
logger: logger,
),
usingCISystem: true,
majorSdkVersion: 16,
);
final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger();
iosDeployDebugger.logLines = debuggingLogs;
logReader.debuggerStream = iosDeployDebugger;
expect(logReader.useSyslogLogging, isTrue);
expect(logReader.useIOSDeployLogging, isTrue);
expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy);
expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.idevicesyslog);
final Future<List<String>> logLines = logReader.logLines.toList();
logReader.addToLinesController(
'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
IOSDeviceLogSource.idevicesyslog,
);
// Will be excluded because was already added by fallback.
logReader.addToLinesController(
'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
IOSDeviceLogSource.iosDeploy,
);
logReader.addToLinesController(
'A second non-flutter message',
IOSDeviceLogSource.iosDeploy,
);
logReader.addToLinesController(
'flutter: Another flutter message',
IOSDeviceLogSource.iosDeploy,
);
final List<String> lines = await logLines;
expect(lines, containsAllInOrder(<String>[
'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', // from idevicesyslog
'A second non-flutter message', // from iosDeploy
'flutter: Another flutter message', // from iosDeploy
]));
});
testWithoutContext('all primary messages are included when there is no fallback', () async {
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
iMobileDevice: IMobileDevice(
artifacts: artifacts,
processManager: FakeProcessManager.any(),
cache: fakeCache,
logger: logger,
),
majorSdkVersion: 12,
);
expect(logReader.useSyslogLogging, isTrue);
expect(logReader.logSources.primarySource, IOSDeviceLogSource.idevicesyslog);
expect(logReader.logSources.fallbackSource, isNull);
final Future<List<String>> logLines = logReader.logLines.toList();
logReader.addToLinesController(
'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
IOSDeviceLogSource.idevicesyslog,
);
logReader.addToLinesController(
'A non-flutter message',
IOSDeviceLogSource.idevicesyslog,
);
logReader.addToLinesController(
'A non-flutter message',
IOSDeviceLogSource.idevicesyslog,
);
logReader.addToLinesController(
'flutter: A flutter message',
IOSDeviceLogSource.idevicesyslog,
);
logReader.addToLinesController(
'flutter: A flutter message',
IOSDeviceLogSource.idevicesyslog,
);
final List<String> lines = await logLines;
expect(lines, containsAllInOrder(<String>[
'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
'A non-flutter message',
'A non-flutter message',
'flutter: A flutter message',
'flutter: A flutter message',
]));
});
testWithoutContext('primary messages are not added if fallback already added them, otherwise duplicates are allowed', () async {
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
iMobileDevice: IMobileDevice(
artifacts: artifacts,
processManager: FakeProcessManager.any(),
cache: fakeCache,
logger: logger,
),
usingCISystem: true,
majorSdkVersion: 16,
);
expect(logReader.useSyslogLogging, isTrue);
expect(logReader.useIOSDeployLogging, isTrue);
expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy);
expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.idevicesyslog);
final Future<List<String>> logLines = logReader.logLines.toList();
logReader.addToLinesController(
'flutter: A flutter message',
IOSDeviceLogSource.idevicesyslog,
);
logReader.addToLinesController(
'flutter: A flutter message',
IOSDeviceLogSource.idevicesyslog,
);
logReader.addToLinesController(
'A non-flutter message',
IOSDeviceLogSource.iosDeploy,
);
logReader.addToLinesController(
'A non-flutter message',
IOSDeviceLogSource.iosDeploy,
);
// Will be excluded because was already added by fallback.
logReader.addToLinesController(
'flutter: A flutter message',
IOSDeviceLogSource.iosDeploy,
);
// Will be excluded because was already added by fallback.
logReader.addToLinesController(
'flutter: A flutter message',
IOSDeviceLogSource.iosDeploy,
);
// Will be included because, although the message is the same, the
// fallback only added it twice so this third one is considered new.
logReader.addToLinesController(
'flutter: A flutter message',
IOSDeviceLogSource.iosDeploy,
);
final List<String> lines = await logLines;
expect(lines, containsAllInOrder(<String>[
'flutter: A flutter message', // from idevicesyslog
'flutter: A flutter message', // from idevicesyslog
'A non-flutter message', // from iosDeploy
'A non-flutter message', // from iosDeploy
'flutter: A flutter message', // from iosDeploy
]));
});
testWithoutContext('flutter fallback messages are included until a primary flutter message is received', () async {
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
iMobileDevice: IMobileDevice(
artifacts: artifacts,
processManager: FakeProcessManager.any(),
cache: fakeCache,
logger: logger,
),
usingCISystem: true,
majorSdkVersion: 16,
);
expect(logReader.useSyslogLogging, isTrue);
expect(logReader.useIOSDeployLogging, isTrue);
expect(logReader.usingMultipleLoggingSources, isTrue);
expect(processManager, hasNoRemainingExpectations);
expect(lines.length, 3);
expect(lines, containsAll(<String>[
'(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.',
expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy);
expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.idevicesyslog);
final Future<List<String>> logLines = logReader.logLines.toList();
logReader.addToLinesController(
'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
IOSDeviceLogSource.idevicesyslog,
);
logReader.addToLinesController(
'A second non-flutter message',
IOSDeviceLogSource.iosDeploy,
);
// Will be included because the first log from primary source wasn't a
// flutter log.
logReader.addToLinesController(
'flutter: A flutter message',
IOSDeviceLogSource.idevicesyslog,
);
// Will be excluded because was already added by fallback, however, it
// will be used to determine a flutter log was received by the primary source.
logReader.addToLinesController(
'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
'flutter: Check for duplicate',
IOSDeviceLogSource.iosDeploy,
);
// Will be excluded because flutter log from primary was received.
logReader.addToLinesController(
'flutter: A third flutter message',
IOSDeviceLogSource.idevicesyslog,
);
final List<String> lines = await logLines;
expect(lines, containsAllInOrder(<String>[
'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', // from idevicesyslog
'A second non-flutter message', // from iosDeploy
'flutter: A flutter message', // from idevicesyslog
]));
});
testWithoutContext('non-flutter fallback messages are not included', () async {
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
iMobileDevice: IMobileDevice(
artifacts: artifacts,
processManager: FakeProcessManager.any(),
cache: fakeCache,
logger: logger,
),
usingCISystem: true,
majorSdkVersion: 16,
);
expect(logReader.useSyslogLogging, isTrue);
expect(logReader.useIOSDeployLogging, isTrue);
expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy);
expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.idevicesyslog);
final Future<List<String>> logLines = logReader.logLines.toList();
logReader.addToLinesController(
'flutter: A flutter message',
IOSDeviceLogSource.idevicesyslog,
);
// Will be excluded because it's from fallback and not a flutter message.
logReader.addToLinesController(
'A non-flutter message',
IOSDeviceLogSource.idevicesyslog,
);
final List<String> lines = await logLines;
expect(lines, containsAllInOrder(<String>[
'flutter: A flutter message',
]));
});
});
});
}
......
......@@ -784,6 +784,69 @@ void main() {
expect(status, isTrue);
});
testWithoutContext('prints error message when deleting temporary directory that is nonexistant', () async {
final Xcode xcode = setupXcode(
fakeProcessManager: fakeProcessManager,
fileSystem: fileSystem,
flutterRoot: flutterRoot,
);
final XcodeDebugProject project = XcodeDebugProject(
scheme: 'Runner',
xcodeProject: xcodeproj,
xcodeWorkspace: xcworkspace,
isTemporaryProject: true,
);
final XcodeDebug xcodeDebug = XcodeDebug(
logger: logger,
processManager: fakeProcessManager,
xcode: xcode,
fileSystem: fileSystem,
);
fakeProcessManager.addCommands(<FakeCommand>[
FakeCommand(
command: <String>[
'xcrun',
'osascript',
'-l',
'JavaScript',
pathToXcodeAutomationScript,
'stop',
'--xcode-path',
pathToXcodeApp,
'--project-path',
project.xcodeProject.path,
'--workspace-path',
project.xcodeWorkspace.path,
'--close-window'
],
stdout: '''
{"status":true,"errorMessage":null,"debugResult":null}
''',
),
]);
xcodeDebug.startDebugActionProcess = FakeProcess();
xcodeDebug.currentDebuggingProject = project;
expect(xcodeDebug.startDebugActionProcess, isNotNull);
expect(xcodeDebug.currentDebuggingProject, isNotNull);
expect(projectDirectory.existsSync(), isFalse);
expect(xcodeproj.existsSync(), isFalse);
expect(xcworkspace.existsSync(), isFalse);
final bool status = await xcodeDebug.exit(skipDelay: true);
expect((xcodeDebug.startDebugActionProcess! as FakeProcess).killed, isTrue);
expect(xcodeDebug.currentDebuggingProject, isNull);
expect(projectDirectory.existsSync(), isFalse);
expect(xcodeproj.existsSync(), isFalse);
expect(xcworkspace.existsSync(), isFalse);
expect(logger.errorText, contains('Failed to delete temporary Xcode project'));
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(status, isTrue);
});
testWithoutContext('kill Xcode when force exit', () async {
final Xcode xcode = setupXcode(
fakeProcessManager: FakeProcessManager.any(),
......@@ -825,6 +888,46 @@ void main() {
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(exitStatus, isTrue);
});
testWithoutContext('does not crash when deleting temporary directory that is nonexistant when force exiting', () async {
final Xcode xcode = setupXcode(
fakeProcessManager: FakeProcessManager.any(),
fileSystem: fileSystem,
flutterRoot: flutterRoot,
);
final XcodeDebugProject project = XcodeDebugProject(
scheme: 'Runner',
xcodeProject: xcodeproj,
xcodeWorkspace: xcworkspace,
isTemporaryProject: true,
);
final XcodeDebug xcodeDebug = XcodeDebug(
logger: logger,
processManager:FakeProcessManager.any(),
xcode: xcode,
fileSystem: fileSystem,
);
xcodeDebug.startDebugActionProcess = FakeProcess();
xcodeDebug.currentDebuggingProject = project;
expect(xcodeDebug.startDebugActionProcess, isNotNull);
expect(xcodeDebug.currentDebuggingProject, isNotNull);
expect(projectDirectory.existsSync(), isFalse);
expect(xcodeproj.existsSync(), isFalse);
expect(xcworkspace.existsSync(), isFalse);
final bool status = await xcodeDebug.exit(force: true);
expect((xcodeDebug.startDebugActionProcess! as FakeProcess).killed, isTrue);
expect(xcodeDebug.currentDebuggingProject, isNull);
expect(projectDirectory.existsSync(), isFalse);
expect(xcodeproj.existsSync(), isFalse);
expect(xcworkspace.existsSync(), isFalse);
expect(logger.errorText, isEmpty);
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(status, isTrue);
});
});
group('stop app', () {
......
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