blob: 0d1a95e9687addc234c3bff21a410e937fbe9613 [file] [log] [blame]
// Copyright 2020 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 'dart:io';
import 'package:args/command_runner.dart';
import 'package:device_doctor/src/ios_debug_symbol_doctor.dart';
import 'package:fake_async/fake_async.dart';
import 'package:file/memory.dart';
import 'package:logging/logging.dart';
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:test/test.dart';
import 'utils.dart';
Future<void> main() async {
for (final String deviceName in const <String>['iPhone', 'iPhone 11', "Flutter's iOS Phone"]) {
test('ios_debug_symbol_doctor surfaces "Fetching debug symbols" error messages for "$deviceName"', () {
final Iterable<XCDevice> devices = XCDevice.parseJson(_jsonWithErrors(deviceName));
final Iterable<XCDevice> erroredDevices = devices.where((XCDevice device) {
return device.hasError;
});
expect(erroredDevices, hasLength(1));
final XCDevice erroredDevice = erroredDevices.single;
expect(erroredDevice.error!['code'], -10);
expect(erroredDevice.error!['failureReason'], isEmpty);
expect(erroredDevice.error!['description'], '$deviceName is busy: Fetching debug symbols for $deviceName');
expect(erroredDevice.error!['recoverySuggestion'], 'Xcode will continue when $deviceName is finished.');
expect(erroredDevice.error!['domain'], 'com.apple.platform.iphoneos');
});
test('ios_debug_symbol_doctor surfaces "Preparing $deviceName for development" error message', () {
final Iterable<XCDevice> devices = XCDevice.parseJson(_jsonWithPreparingErrors(deviceName));
final Iterable<XCDevice> erroredDevices = devices.where((XCDevice device) {
return device.hasError;
});
expect(erroredDevices, hasLength(1), reason: devices.toString());
final XCDevice erroredDevice = erroredDevices.single;
expect(erroredDevice.error!['code'], -10);
expect(erroredDevice.error!['failureReason'], isEmpty);
expect(
erroredDevice.error!['description'],
contains('$deviceName is busy: Preparing $deviceName for development'),
);
expect(erroredDevice.error!['recoverySuggestion'], 'Xcode will continue when $deviceName is finished.');
expect(erroredDevice.error!['domain'], 'com.apple.platform.iphoneos');
});
}
test('XCDevice ignores "phone is locked" errors', () {
final Iterable<XCDevice> devices = XCDevice.parseJson(_jsonWithNonFatalErrors);
final Iterable<XCDevice> erroredDevices = devices.where((XCDevice device) {
return device.hasError;
});
expect(erroredDevices, isEmpty);
});
CommandRunner<bool> _createTestRunner() {
return CommandRunner(
'ios-debug-symbol-doctor',
'for testing',
);
}
group('commands', () {
late MockProcessManager processManager;
late TestLogger logger;
const String cocoonPath = '/path/to/cocoon';
const String xcworkspacePath = '$cocoonPath/dashboard/ios/Runner.xcodeproj/project.xcworkspace';
late MemoryFileSystem fs;
late Platform platform;
const String deviceSupportDirectory = '/User/username/Library/Developer/Xcode/iOS DeviceSupport';
setUp(() {
processManager = MockProcessManager();
logger = TestLogger();
fs = MemoryFileSystem();
fs.directory(xcworkspacePath).createSync(recursive: true);
platform = MockPlatform();
platform.environment['HOME'] = '/User/username';
});
test('diagnose logs output of xcdevice list', () async {
when(
processManager.run(<String>['xcrun', 'xcdevice', 'list']),
).thenAnswer((_) async {
return ProcessResult(0, 0, _jsonWithNonFatalErrors, '');
});
final CommandRunner<bool> runner = _createTestRunner();
final command = DiagnoseCommand(
processManager: processManager,
loggerOverride: logger,
);
runner.addCommand(command);
await runner.run(<String>['diagnose']);
expect(logger.logs[Level.INFO], contains(_jsonWithNonFatalErrors));
});
test('recover returns early if xcodebuild -runFirstLaunch exits non-zero', () async {
final Directory symbolsDirectory = fs.directory('/User/username/Library/Developer/Xcode/iOS Temp');
symbolsDirectory.createSync(recursive: true);
when(
processManager.run(<String>['xcrun', 'xcodebuild', '-runFirstLaunch']),
).thenAnswer((_) async {
return ProcessResult(
0,
1,
'',
'xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun',
);
});
fakeAsync<void>((FakeAsync time) {
final CommandRunner<bool> runner = _createTestRunner();
final command = RecoverCommand(
processManager: processManager,
loggerOverride: logger,
fs: fs,
platform: platform,
);
runner.addCommand(command);
bool? result;
runner.run(<String>['recover', '--cocoon-root=$cocoonPath']).then((bool? value) => result = value);
time.elapse(const Duration(microseconds: 1));
expect(result, isNotNull);
});
});
test('recover deletes symbols, opens Xcode, waits, then kills it', () async {
final Directory symbolsDirectory = fs.directory(deviceSupportDirectory);
symbolsDirectory.createSync(recursive: true);
when(
processManager.run(<String>['xcrun', 'xcodebuild', '-runFirstLaunch']),
).thenAnswer((_) async {
return ProcessResult(0, 0, '', '');
});
when(
processManager.run(<String>['open', '-n', '-F', '-W', xcworkspacePath]),
).thenAnswer((_) async {
return ProcessResult(1, 0, '', '');
});
bool killedXcode = false;
when(
processManager.run(<String>['killall', '-9', 'Xcode']),
).thenAnswer((_) async {
killedXcode = true;
return ProcessResult(2, 0, '', '');
});
final bool result = fakeAsync<bool>((FakeAsync time) {
final CommandRunner<bool> runner = _createTestRunner();
final command = RecoverCommand(
processManager: processManager,
loggerOverride: logger,
fs: fs,
platform: platform,
);
runner.addCommand(command);
bool? result;
runner.run(<String>['recover', '--cocoon-root=$cocoonPath']).then((bool? value) => result = value);
time.elapse(const Duration(seconds: 299));
// We have not yet reached the timeout, so Xcode should still be open
expect(result, isNull);
expect(killedXcode, isFalse);
time.elapse(const Duration(seconds: 2));
expect(result, isNotNull);
expect(killedXcode, isTrue);
expect(logger.logs[Level.WARNING], isNull);
expect(symbolsDirectory.existsSync(), false);
return result!;
});
expect(result, isTrue);
expect(
logger.logs[Level.INFO],
containsAllInOrder(<String>[
'Running Xcode first launch...',
'Launching Xcode...',
'Waiting for 300 seconds',
'Waited for 300 seconds, now killing Xcode',
]),
);
});
test('recover cannot find symbols but still opens Xcode, waits, then kills it', () async {
when(
processManager.run(<String>['xcrun', 'xcodebuild', '-runFirstLaunch']),
).thenAnswer((_) async {
return ProcessResult(0, 0, '', '');
});
when(
processManager.run(<String>['open', '-n', '-F', '-W', xcworkspacePath]),
).thenAnswer((_) async {
return ProcessResult(1, 0, '', '');
});
bool killedXcode = false;
when(
processManager.run(<String>['killall', '-9', 'Xcode']),
).thenAnswer((_) async {
killedXcode = true;
return ProcessResult(2, 0, '', '');
});
final bool result = fakeAsync<bool>((FakeAsync time) {
final CommandRunner<bool> runner = _createTestRunner();
final command = RecoverCommand(
processManager: processManager,
loggerOverride: logger,
fs: fs,
platform: platform,
);
runner.addCommand(command);
bool? result;
runner.run(<String>['recover', '--cocoon-root=$cocoonPath']).then((bool? value) => result = value);
time.elapse(const Duration(seconds: 299));
// We have not yet reached the timeout, so Xcode should still be open
expect(result, isNull);
expect(killedXcode, isFalse);
time.elapse(const Duration(seconds: 2));
expect(result, isNotNull);
expect(killedXcode, isTrue);
expect(
logger.logs[Level.WARNING],
contains('iOS Device Support directory was not found at $deviceSupportDirectory'),
);
return result!;
});
expect(result, isTrue);
expect(
logger.logs[Level.INFO],
containsAllInOrder(<String>[
'Running Xcode first launch...',
'Launching Xcode...',
'Waiting for 300 seconds',
'Waited for 300 seconds, now killing Xcode',
]),
);
});
});
}
const String _jsonWithNonFatalErrors = '''
[
{
"modelCode" : "iPhone11,8",
"simulator" : false,
"modelName" : "iPhone XR",
"error" : {
"code" : -13,
"failureReason" : "",
"underlyingErrors" : [
{
"code" : 4,
"failureReason" : "",
"description" : "Flutter’s iPhone is locked.",
"recoverySuggestion" : "To use Flutter’s iPhone with Xcode, unlock it.",
"domain" : "DVTDeviceIneligibilityErrorDomain"
}
],
"description" : "Flutter’s iPhone is not connected",
"recoverySuggestion" : "Xcode will continue when Flutter’s iPhone is connected.",
"domain" : "com.apple.platform.iphoneos"
},
"operatingSystemVersion" : "15.4.1 (19E258)",
"identifier" : "00008120-00017DA80CC1002E",
"platform" : "com.apple.platform.iphoneos",
"architecture" : "arm64e",
"interface" : "usb",
"available" : false,
"name" : "Flutter’s iPhone",
"modelUTI" : "com.apple.iphone-xr-9"
}
]''';
String _jsonWithErrors(String name) => '''
[
{
"modelCode" : "iPhone8,1",
"simulator" : false,
"modelName" : "iPhone 6s",
"error" : {
"code" : -10,
"failureReason" : "",
"description" : "$name is busy: Fetching debug symbols for $name",
"recoverySuggestion" : "Xcode will continue when $name is finished.",
"domain" : "com.apple.platform.iphoneos"
},
"operatingSystemVersion" : "15.1 (19B74)",
"identifier" : "e3f3a0cf8005b8b34f14d16fa224b19017648353",
"platform" : "com.apple.platform.iphoneos",
"architecture" : "arm64",
"interface" : "usb",
"available" : false,
"name" : "$name",
"modelUTI" : "com.apple.iphone-6s-e1ccb7"
}
]
''';
String _jsonWithPreparingErrors(String name) => '''
[
{
"modelCode" : "iPhone8,1",
"simulator" : false,
"modelName" : "iPhone 6s",
"error" : {
"code" : -10,
"failureReason" : "",
"description" : "$name is busy: Preparing $name for development. Xcode will continue when $name is finished. (code -10)",
"recoverySuggestion" : "Xcode will continue when $name is finished.",
"domain" : "com.apple.platform.iphoneos"
},
"operatingSystemVersion" : "15.1 (19B74)",
"identifier" : "e3f3a0cf8005b8b34f14d16fa224b19017648353",
"platform" : "com.apple.platform.iphoneos",
"architecture" : "arm64",
"interface" : "usb",
"available" : false,
"name" : "$name",
"modelUTI" : "com.apple.iphone-6s-e1ccb7"
}
]
''';