blob: 07ea50234695378f98b5fa01d5c26f203efe49f6 [file] [log] [blame]
// 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;
}
}