// 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 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>[ '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'), ])); }); }); 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(), ); }); }); } class FakeIOSDeployDebugger extends Fake implements IOSDeployDebugger { bool detached = false; @override bool debuggerAttached = false; @override Stream<String> logLines = const Stream<String>.empty(); @override void detach() { detached = true; } }