// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/async_guard.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/cache.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:flutter_tools/src/vmservice.dart'; import 'package:test/fake.dart'; import 'package:vm_service/vm_service.dart'; import '../../src/common.dart'; import '../../src/fake_process_manager.dart'; import '../../src/fake_vm_services.dart'; void main() { late FakeProcessManager processManager; late Artifacts artifacts; late Cache fakeCache; late BufferLogger logger; late String ideviceSyslogPath; setUp(() { processManager = FakeProcessManager.empty(); fakeCache = Cache.test(processManager: FakeProcessManager.any()); artifacts = Artifacts.test(); logger = BufferLogger.test(); ideviceSyslogPath = artifacts.getHostArtifact(HostArtifact.idevicesyslog).path; }); 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 ' r'\M-B\M-/\134_(\M-c\M^C\M^D)_/\M-B\M-/ \M-l\M^F\240!'); 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!'); expect(decoded, r'I \M-b\M^O syslog!'); }); testWithoutContext('IOSDeviceLogReader suppresses non-Flutter lines from output with syslog', () async { processManager.addCommand( FakeCommand( command: <String>[ ideviceSyslogPath, '-u', '1234', ], stdout: ''' Runner(Flutter)[297] <Notice>: A is for ari Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt MobileGestaltSupport.m:153: pid 123 (Runner) does not have sandbox access for frZQaeyWLUvLjeuEK43hmg and IS NOT appropriately entitled Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt MobileGestalt.c:550: no access to InverseDeviceID (see <rdar://problem/11744455>) Runner(Flutter)[297] <Notice>: I is for ichigo Runner(UIKit)[297] <Notice>: E is for enpitsu" ''' ), ); final DeviceLogReader logReader = IOSDeviceLogReader.test( iMobileDevice: IMobileDevice( artifacts: artifacts, processManager: processManager, cache: fakeCache, logger: logger, ), ); final List<String> lines = await logReader.logLines.toList(); expect(lines, <String>['A is for ari', 'I is for ichigo']); }); testWithoutContext('IOSDeviceLogReader includes multi-line Flutter logs in the output with syslog', () async { processManager.addCommand( FakeCommand( command: <String>[ ideviceSyslogPath, '-u', '1234', ], stdout: ''' Runner(Flutter)[297] <Notice>: This is a multi-line message, with another Flutter message following it. Runner(Flutter)[297] <Notice>: This is a multi-line message, with a non-Flutter log message following it. Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt ''' ), ); final DeviceLogReader logReader = IOSDeviceLogReader.test( iMobileDevice: IMobileDevice( artifacts: artifacts, processManager: processManager, cache: fakeCache, logger: logger, ), ); final List<String> lines = await logReader.logLines.toList(); expect(lines, <String>[ '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( FakeCommand( command: <String>[ ideviceSyslogPath, '-u', '1234', ], stdout: ''' Runner(Flutter)[297] <Notice>: This is a multi-line message, with another Flutter message following it. Runner(Flutter)[297] <Notice>: This is a multi-line message, with a non-Flutter log message following it. Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt ''', ), ); final DeviceLogReader logReader = IOSDeviceLogReader.test( iMobileDevice: IMobileDevice( artifacts: artifacts, processManager: processManager, cache: fakeCache, logger: logger, ), ); final List<String> lines = await logReader.logLines.toList(); expect(lines, <String>[ '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 Event stdoutEvent = Event( kind: 'Stdout', timestamp: 0, bytes: base64.encode(utf8.encode(' This is a message ')), ); final Event stderrEvent = Event( kind: 'Stderr', timestamp: 0, bytes: base64.encode(utf8.encode(' And this is an error ')), ); 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 DeviceLogReader logReader = IOSDeviceLogReader.test( useSyslog: false, iMobileDevice: IMobileDevice( artifacts: artifacts, processManager: processManager, cache: fakeCache, logger: logger, ), ); logReader.connectedVMService = vmService; // Wait for stream listeners to fire. await expectLater(logReader.logLines, emitsInAnyOrder(<Matcher>[ equals(' This is a message '), equals(' And this is an error '), ])); }); testWithoutContext('IOSDeviceLogReader ignores VM Service logs when attached to and received flutter logs from debugger', () async { final Event stdoutEvent = Event( kind: 'Stdout', timestamp: 0, bytes: base64.encode(utf8.encode(' This is a message ')), ); final Event stderrEvent = Event( kind: 'Stderr', timestamp: 0, bytes: base64.encode(utf8.encode(' And this is an error ')), ); 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; final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger(); iosDeployDebugger.debuggerAttached = true; final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[ 'flutter: Message from debugger', ]); iosDeployDebugger.logLines = debuggingLogs; logReader.debuggerStream = iosDeployDebugger; // Wait for stream listeners to fire. await expectLater(logReader.logLines, emitsInAnyOrder(<Matcher>[ equals('flutter: 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 FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger(); iosDeployDebugger.logLines = 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 FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger(); iosDeployDebugger.logLines = 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 FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger(); logReader.debuggerStream = iosDeployDebugger; logReader.dispose(); expect(iosDeployDebugger.detached, true); }); testWithoutContext('Does not throw if debuggerStream set after logReader closed', () 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, ); Object? exception; StackTrace? trace; await asyncGuard( () async { await logReader.linesController.close(); final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger(); iosDeployDebugger.logLines = debuggingLogs; logReader.debuggerStream = iosDeployDebugger; await logReader.logLines.drain<void>(); }, onError: (Object err, StackTrace stackTrace) { exception = err; trace = stackTrace; } ); expect( exception, isNull, reason: trace.toString(), ); }); }); group('Determine which loggers to use', () { testWithoutContext('for physically attached CoreDevice', () { final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( iMobileDevice: IMobileDevice( artifacts: artifacts, processManager: processManager, cache: fakeCache, logger: logger, ), majorSdkVersion: 17, isCoreDevice: true, ); expect(logReader.useSyslogLogging, isTrue); expect(logReader.useUnifiedLogging, isTrue); expect(logReader.useIOSDeployLogging, isFalse); expect(logReader.logSources.primarySource, IOSDeviceLogSource.idevicesyslog); expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.unifiedLogging); }); testWithoutContext('for wirelessly attached CoreDevice', () { final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( iMobileDevice: IMobileDevice( artifacts: artifacts, processManager: processManager, cache: fakeCache, logger: logger, ), majorSdkVersion: 17, isCoreDevice: true, isWirelesslyConnected: true, ); expect(logReader.useSyslogLogging, isFalse); expect(logReader.useUnifiedLogging, isTrue); expect(logReader.useIOSDeployLogging, isFalse); expect(logReader.logSources.primarySource, IOSDeviceLogSource.unifiedLogging); expect(logReader.logSources.fallbackSource, isNull); }); testWithoutContext('for iOS 12 or less device', () { final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( iMobileDevice: IMobileDevice( artifacts: artifacts, processManager: processManager, cache: fakeCache, logger: logger, ), majorSdkVersion: 12, ); expect(logReader.useSyslogLogging, isTrue); expect(logReader.useUnifiedLogging, isFalse); expect(logReader.useIOSDeployLogging, isFalse); expect(logReader.logSources.primarySource, IOSDeviceLogSource.idevicesyslog); expect(logReader.logSources.fallbackSource, isNull); }); testWithoutContext('for iOS 13 or greater non-CoreDevice and _iosDeployDebugger not attached', () { final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( iMobileDevice: IMobileDevice( artifacts: artifacts, processManager: processManager, cache: fakeCache, logger: logger, ), majorSdkVersion: 13, ); expect(logReader.useSyslogLogging, isFalse); expect(logReader.useUnifiedLogging, isTrue); expect(logReader.useIOSDeployLogging, 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', () { final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( iMobileDevice: IMobileDevice( artifacts: artifacts, processManager: processManager, cache: fakeCache, logger: logger, ), majorSdkVersion: 13, ); final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger(); 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, isTrue); expect(logReader.useIOSDeployLogging, isTrue); expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy); expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.unifiedLogging); }); testWithoutContext('for iOS 16 or greater non-CoreDevice', () { final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( iMobileDevice: IMobileDevice( artifacts: artifacts, processManager: processManager, cache: fakeCache, logger: logger, ), 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.logSources.primarySource, IOSDeviceLogSource.iosDeploy); expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.unifiedLogging); }); testWithoutContext('for iOS 16 or greater non-CoreDevice in CI', () { final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( iMobileDevice: IMobileDevice( artifacts: artifacts, processManager: processManager, cache: fakeCache, logger: logger, ), usingCISystem: true, majorSdkVersion: 16, ); expect(logReader.useSyslogLogging, isTrue); expect(logReader.useUnifiedLogging, isFalse); expect(logReader.useIOSDeployLogging, isTrue); expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy); expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.idevicesyslog); }); 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. ''' ), ); final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( iMobileDevice: IMobileDevice( artifacts: artifacts, processManager: processManager, cache: fakeCache, logger: logger, ), usingCISystem: true, majorSdkVersion: 16, ); final List<String> lines = await logReader.logLines.toList(); expect(logReader.useSyslogLogging, isTrue); expect(processManager, hasNoRemainingExpectations); expect(lines, <String>[ 'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', 'flutter: This is a test' ]); }); testWithoutContext('is false syslog does not send flutter messages to stream', () async { final IOSDeviceLogReader logReader = IOSDeviceLogReader.test( iMobileDevice: IMobileDevice( artifacts: artifacts, processManager: processManager, cache: fakeCache, logger: logger, ), majorSdkVersion: 16, ); final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger(); iosDeployDebugger.logLines = Stream<String>.fromIterable(<String>[]); logReader.debuggerStream = iosDeployDebugger; final List<String> lines = await logReader.logLines.toList(); expect(logReader.useSyslogLogging, isFalse); expect(processManager, hasNoRemainingExpectations); 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, ), 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('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>[ '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, ); 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.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=/', 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', ])); }); }); }); } class FakeIOSDeployDebugger extends Fake implements IOSDeployDebugger { bool detached = false; @override bool debuggerAttached = false; @override Stream<String> logLines = const Stream<String>.empty(); @override Future<void> detach() async { detached = true; } }