// 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. // @dart = 2.8 import 'dart:async'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/ios/devices.dart'; import 'package:flutter_tools/src/ios/ios_deploy.dart'; import 'package:flutter_tools/src/ios/mac.dart'; import 'package:mockito/mockito.dart'; import 'package:vm_service/vm_service.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/testbed.dart'; void main() { FakeProcessManager processManager; Artifacts artifacts; FakeCache fakeCache; BufferLogger logger; String ideviceSyslogPath; setUp(() { processManager = FakeProcessManager.list(<FakeCommand>[]); fakeCache = FakeCache(); artifacts = Artifacts.test(); logger = BufferLogger.test(); ideviceSyslogPath = artifacts.getArtifactPath(Artifact.idevicesyslog, platform: TargetPlatform.ios); }); group('syslog stream', () { testWithoutContext('decodeSyslog decodes a syslog-encoded line', () { final String decoded = decodeSyslog( r'I \M-b\M^]\M-$\M-o\M-8\M^O syslog \M-B\M-/\' r'134_(\M-c\M^C\M^D)_/\M-B\M-/ \M-l\M^F\240!'); 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 MockVmService vmService = MockVmService(); final DeviceLogReader 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 ')), )); // Wait for stream listeners to fire. await expectLater(logReader.logLines, emitsInAnyOrder(<Matcher>[ equals(' This is a message '), equals(' And this is an error '), ])); 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 MockVmService extends Mock implements VmService {} class MockIOSDeployDebugger extends Mock implements IOSDeployDebugger {}