Unverified Commit 2e18cd34 authored by Christopher Fujino's avatar Christopher Fujino Committed by GitHub

Fix simctl process exceptions not being caught (#51916)

parent a42d38a2
......@@ -137,7 +137,10 @@ Future<T> runInContext<T>(
platform: globals.platform,
processManager: globals.processManager,
),
IOSSimulatorUtils: () => IOSSimulatorUtils(),
IOSSimulatorUtils: () => IOSSimulatorUtils(
simControl: globals.simControl,
xcode: globals.xcode,
),
IOSWorkflow: () => const IOSWorkflow(),
KernelCompilerFactory: () => const KernelCompilerFactory(),
Logger: () => globals.platform.isWindows
......@@ -174,7 +177,10 @@ Future<T> runInContext<T>(
Pub: () => const Pub(),
ShutdownHooks: () => ShutdownHooks(logger: globals.logger),
Signals: () => Signals(),
SimControl: () => SimControl(),
SimControl: () => SimControl(
logger: globals.logger,
processManager: globals.processManager,
),
Stdio: () => Stdio(),
SystemClock: () => const SystemClock(),
TimeoutConfiguration: () => const TimeoutConfiguration(),
......
......@@ -71,7 +71,7 @@ class DeviceManager {
final List<DeviceDiscovery> _deviceDiscoverers = List<DeviceDiscovery>.unmodifiable(<DeviceDiscovery>[
AndroidDevices(),
IOSDevices(),
IOSSimulators(),
IOSSimulators(iosSimulatorUtils: globals.iosSimulatorUtils),
FuchsiaDevices(),
FlutterTesterDevices(),
MacOSDevices(),
......
......@@ -24,6 +24,7 @@ import 'cache.dart';
import 'ios/ios_deploy.dart';
import 'ios/mac.dart';
import 'ios/plist_parser.dart';
import 'ios/simulators.dart';
import 'macos/xcode.dart';
import 'persistent_tool_state.dart';
import 'reporting/reporting.dart';
......@@ -69,6 +70,8 @@ AndroidSdk get androidSdk => context.get<AndroidSdk>();
FlutterVersion get flutterVersion => context.get<FlutterVersion>();
IMobileDevice get iMobileDevice => context.get<IMobileDevice>();
IOSDeploy get iosDeploy => context.get<IOSDeploy>();
IOSSimulatorUtils get iosSimulatorUtils => context.get<IOSSimulatorUtils>();
SimControl get simControl => context.get<SimControl>();
UserMessages get userMessages => context.get<UserMessages>();
Xcode get xcode => context.get<Xcode>();
......
......@@ -6,12 +6,13 @@ import 'dart:async';
import 'dart:math' as math;
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import '../application_package.dart';
import '../base/common.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../base/utils.dart';
import '../build_info.dart';
......@@ -19,6 +20,7 @@ import '../bundle.dart';
import '../convert.dart';
import '../device.dart';
import '../globals.dart' as globals;
import '../macos/xcode.dart';
import '../project.dart';
import '../protocol_discovery.dart';
import 'ios_workflow.dart';
......@@ -29,7 +31,12 @@ const String _xcrunPath = '/usr/bin/xcrun';
const String iosSimulatorId = 'apple_ios_simulator';
class IOSSimulators extends PollingDeviceDiscovery {
IOSSimulators() : super('iOS simulators');
IOSSimulators({
@required IOSSimulatorUtils iosSimulatorUtils,
}) : _iosSimulatorUtils = iosSimulatorUtils,
super('iOS simulators');
final IOSSimulatorUtils _iosSimulatorUtils;
@override
bool get supportsPlatform => globals.platform.isMacOS;
......@@ -38,29 +45,47 @@ class IOSSimulators extends PollingDeviceDiscovery {
bool get canListAnything => iosWorkflow.canListDevices;
@override
Future<List<Device>> pollingGetDevices() async => IOSSimulatorUtils.instance.getAttachedDevices();
Future<List<Device>> pollingGetDevices() async => _iosSimulatorUtils.getAttachedDevices();
}
class IOSSimulatorUtils {
/// Returns [IOSSimulatorUtils] active in the current app context (i.e. zone).
static IOSSimulatorUtils get instance => context.get<IOSSimulatorUtils>();
IOSSimulatorUtils({
@required SimControl simControl,
@required Xcode xcode,
}) : _simControl = simControl,
_xcode = xcode;
final SimControl _simControl;
final Xcode _xcode;
Future<List<IOSSimulator>> getAttachedDevices() async {
if (!globals.xcode.isInstalledAndMeetsVersionCheck) {
if (!_xcode.isInstalledAndMeetsVersionCheck) {
return <IOSSimulator>[];
}
final List<SimDevice> connected = await SimControl.instance.getConnectedDevices();
final List<SimDevice> connected = await _simControl.getConnectedDevices();
return connected.map<IOSSimulator>((SimDevice device) {
return IOSSimulator(device.udid, name: device.name, simulatorCategory: device.category);
return IOSSimulator(
device.udid,
name: device.name,
simControl: _simControl,
simulatorCategory: device.category,
xcode: _xcode,
);
}).toList();
}
}
/// A wrapper around the `simctl` command line tool.
class SimControl {
/// Returns [SimControl] active in the current app context (i.e. zone).
static SimControl get instance => context.get<SimControl>();
SimControl({
@required Logger logger,
@required ProcessManager processManager,
}) : _logger = logger,
_processUtils = ProcessUtils(processManager: processManager, logger: logger);
final Logger _logger;
final ProcessUtils _processUtils;
/// Runs `simctl list --json` and returns the JSON of the corresponding
/// [section].
......@@ -83,10 +108,10 @@ class SimControl {
// "pairs": { ... },
final List<String> command = <String>[_xcrunPath, 'simctl', 'list', '--json', section.name];
globals.printTrace(command.join(' '));
final ProcessResult results = await globals.processManager.run(command);
_logger.printTrace(command.join(' '));
final RunResult results = await _processUtils.run(command);
if (results.exitCode != 0) {
globals.printError('Error executing simctl: ${results.exitCode}\n${results.stderr}');
_logger.printError('Error executing simctl: ${results.exitCode}\n${results.stderr}');
return <String, Map<String, dynamic>>{};
}
try {
......@@ -94,13 +119,13 @@ class SimControl {
if (decodeResult is Map<String, dynamic>) {
return decodeResult;
}
globals.printError('simctl returned unexpected JSON response: ${results.stdout}');
_logger.printError('simctl returned unexpected JSON response: ${results.stdout}');
return <String, dynamic>{};
} on FormatException {
// We failed to parse the simctl output, or it returned junk.
// One known message is "Install Started" isn't valid JSON but is
// returned sometimes.
globals.printError('simctl returned non-JSON response: ${results.stdout}');
_logger.printError('simctl returned non-JSON response: ${results.stdout}');
return <String, dynamic>{};
}
}
......@@ -130,7 +155,7 @@ class SimControl {
}
Future<bool> isInstalled(String deviceId, String appId) {
return processUtils.exitsHappy(<String>[
return _processUtils.exitsHappy(<String>[
_xcrunPath,
'simctl',
'get_app_container',
......@@ -139,23 +164,23 @@ class SimControl {
]);
}
Future<RunResult> install(String deviceId, String appPath) {
Future<RunResult> result;
Future<RunResult> install(String deviceId, String appPath) async {
RunResult result;
try {
result = processUtils.run(
result = await _processUtils.run(
<String>[_xcrunPath, 'simctl', 'install', deviceId, appPath],
throwOnError: true,
);
} on ProcessException catch (exception) {
throwToolExit('Unable to install $appPath on $deviceId:\n$exception');
throwToolExit('Unable to install $appPath on $deviceId. This is sometimes caused by a malformed plist file:\n$exception');
}
return result;
}
Future<RunResult> uninstall(String deviceId, String appId) {
Future<RunResult> result;
Future<RunResult> uninstall(String deviceId, String appId) async {
RunResult result;
try {
result = processUtils.run(
result = await _processUtils.run(
<String>[_xcrunPath, 'simctl', 'uninstall', deviceId, appId],
throwOnError: true,
);
......@@ -165,10 +190,10 @@ class SimControl {
return result;
}
Future<RunResult> launch(String deviceId, String appIdentifier, [ List<String> launchArgs ]) {
Future<RunResult> result;
Future<RunResult> launch(String deviceId, String appIdentifier, [ List<String> launchArgs ]) async {
RunResult result;
try {
result = processUtils.run(
result = await _processUtils.run(
<String>[
_xcrunPath,
'simctl',
......@@ -187,12 +212,12 @@ class SimControl {
Future<void> takeScreenshot(String deviceId, String outputPath) async {
try {
await processUtils.run(
await _processUtils.run(
<String>[_xcrunPath, 'simctl', 'io', deviceId, 'screenshot', outputPath],
throwOnError: true,
);
} on ProcessException catch (exception) {
throwToolExit('Unable to take screenshot of $deviceId:\n$exception');
_logger.printError('Unable to take screenshot of $deviceId:\n$exception');
}
}
}
......@@ -248,18 +273,29 @@ class SimDevice {
}
class IOSSimulator extends Device {
IOSSimulator(String id, { this.name, this.simulatorCategory }) : super(
id,
category: Category.mobile,
platformType: PlatformType.ios,
ephemeral: true,
);
IOSSimulator(
String id, {
this.name,
this.simulatorCategory,
@required SimControl simControl,
@required Xcode xcode,
}) : _simControl = simControl,
_xcode = xcode,
super(
id,
category: Category.mobile,
platformType: PlatformType.ios,
ephemeral: true,
);
@override
final String name;
final String simulatorCategory;
final SimControl _simControl;
final Xcode _xcode;
@override
Future<bool> get isLocalEmulator async => true;
......@@ -279,7 +315,7 @@ class IOSSimulator extends Device {
@override
Future<bool> isAppInstalled(ApplicationPackage app) {
return SimControl.instance.isInstalled(id, app.id);
return _simControl.isInstalled(id, app.id);
}
@override
......@@ -289,7 +325,7 @@ class IOSSimulator extends Device {
Future<bool> installApp(covariant IOSApp app) async {
try {
final IOSApp iosApp = app;
await SimControl.instance.install(id, iosApp.simulatorBundlePath);
await _simControl.install(id, iosApp.simulatorBundlePath);
return true;
} on Exception {
return false;
......@@ -299,7 +335,7 @@ class IOSSimulator extends Device {
@override
Future<bool> uninstallApp(ApplicationPackage app) async {
try {
await SimControl.instance.uninstall(id, app.id);
await _simControl.uninstall(id, app.id);
return true;
} on Exception {
return false;
......@@ -394,7 +430,7 @@ class IOSSimulator extends Device {
final String plistPath = globals.fs.path.join(package.simulatorBundlePath, 'Info.plist');
final String bundleIdentifier = globals.plistParser.getValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey);
await SimControl.instance.launch(id, bundleIdentifier, args);
await _simControl.launch(id, bundleIdentifier, args);
} on Exception catch (error) {
globals.printError('$error');
return LaunchResult.failed();
......@@ -449,7 +485,7 @@ class IOSSimulator extends Device {
}
// Step 3: Install the updated bundle to the simulator.
await SimControl.instance.install(id, globals.fs.path.absolute(bundle.path));
await _simControl.install(id, globals.fs.path.absolute(bundle.path));
}
@visibleForTesting
......@@ -527,7 +563,7 @@ class IOSSimulator extends Device {
}
bool get _xcodeVersionSupportsScreenshot {
return globals.xcode.majorVersion > 8 || (globals.xcode.majorVersion == 8 && globals.xcode.minorVersion >= 2);
return _xcode.majorVersion > 8 || (_xcode.majorVersion == 8 && _xcode.minorVersion >= 2);
}
@override
......@@ -535,7 +571,7 @@ class IOSSimulator extends Device {
@override
Future<void> takeScreenshot(File outputFile) {
return SimControl.instance.takeScreenshot(id, outputFile.path);
return _simControl.takeScreenshot(id, outputFile.path);
}
@override
......
......@@ -8,6 +8,8 @@ import 'dart:io' show ProcessResult, Process;
import 'package:file/file.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/dart.dart';
import 'package:flutter_tools/src/build_system/targets/icon_tree_shaker.dart';
......@@ -31,6 +33,7 @@ import '../../src/mocks.dart';
class MockFile extends Mock implements File {}
class MockIMobileDevice extends Mock implements IMobileDevice {}
class MockLogger extends Mock implements Logger {}
class MockProcess extends Mock implements Process {}
class MockProcessManager extends Mock implements ProcessManager {}
class MockXcode extends Mock implements Xcode {}
......@@ -50,8 +53,20 @@ void main() {
});
group('_IOSSimulatorDevicePortForwarder', () {
MockSimControl mockSimControl;
MockXcode mockXcode;
setUp(() {
mockSimControl = MockSimControl();
mockXcode = MockXcode();
});
testUsingContext('dispose() does not throw an exception', () async {
final IOSSimulator simulator = IOSSimulator('123');
final IOSSimulator simulator = IOSSimulator(
'123',
simControl: mockSimControl,
xcode: mockXcode,
);
final DevicePortForwarder portForwarder = simulator.portForwarder;
await portForwarder.forward(123);
await portForwarder.forward(124);
......@@ -70,9 +85,22 @@ void main() {
});
group('logFilePath', () {
MockSimControl mockSimControl;
MockXcode mockXcode;
setUp(() {
mockSimControl = MockSimControl();
mockXcode = MockXcode();
});
testUsingContext('defaults to rooted from HOME', () {
osx.environment['HOME'] = '/foo/bar';
expect(IOSSimulator('123').logFilePath, '/foo/bar/Library/Logs/CoreSimulator/123/system.log');
final IOSSimulator simulator = IOSSimulator(
'123',
simControl: mockSimControl,
xcode: mockXcode,
);
expect(simulator.logFilePath, '/foo/bar/Library/Logs/CoreSimulator/123/system.log');
}, overrides: <Type, Generator>{
Platform: () => osx,
FileSystemUtils: () => fsUtils,
......@@ -83,7 +111,12 @@ void main() {
testUsingContext('respects IOS_SIMULATOR_LOG_FILE_PATH', () {
osx.environment['HOME'] = '/foo/bar';
osx.environment['IOS_SIMULATOR_LOG_FILE_PATH'] = '/baz/qux/%{id}/system.log';
expect(IOSSimulator('456').logFilePath, '/baz/qux/456/system.log');
final IOSSimulator simulator = IOSSimulator(
'456',
simControl: mockSimControl,
xcode: mockXcode,
);
expect(simulator.logFilePath, '/baz/qux/456/system.log');
}, overrides: <Type, Generator>{
Platform: () => osx,
FileSystemUtils: () => fsUtils,
......@@ -93,7 +126,7 @@ void main() {
});
group('compareIosVersions', () {
test('compares correctly', () {
testWithoutContext('compares correctly', () {
// This list must be sorted in ascending preference order
final List<String> testList = <String>[
'8', '8.0', '8.1', '8.2',
......@@ -115,7 +148,7 @@ void main() {
});
group('compareIphoneVersions', () {
test('compares correctly', () {
testWithoutContext('compares correctly', () {
// This list must be sorted in ascending preference order
final List<String> testList = <String>[
'com.apple.CoreSimulator.SimDeviceType.iPhone-4s',
......@@ -142,29 +175,69 @@ void main() {
});
group('sdkMajorVersion', () {
MockSimControl mockSimControl;
MockXcode mockXcode;
setUp(() {
mockSimControl = MockSimControl();
mockXcode = MockXcode();
});
// This new version string appears in SimulatorApp-850 CoreSimulator-518.16 beta.
test('can be parsed from iOS-11-3', () async {
final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3');
testWithoutContext('can be parsed from iOS-11-3', () async {
final IOSSimulator device = IOSSimulator(
'x',
name: 'iPhone SE',
simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3',
simControl: mockSimControl,
xcode: mockXcode,
);
expect(await device.sdkMajorVersion, 11);
});
test('can be parsed from iOS 11.2', () async {
final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.2');
testWithoutContext('can be parsed from iOS 11.2', () async {
final IOSSimulator device = IOSSimulator(
'x',
name: 'iPhone SE',
simulatorCategory: 'iOS 11.2',
simControl: mockSimControl,
xcode: mockXcode,
);
expect(await device.sdkMajorVersion, 11);
});
test('Has a simulator category', () async {
final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.2');
testWithoutContext('Has a simulator category', () async {
final IOSSimulator device = IOSSimulator(
'x',
name: 'iPhone SE',
simulatorCategory: 'iOS 11.2',
simControl: mockSimControl,
xcode: mockXcode,
);
expect(device.category, Category.mobile);
});
});
group('IOSSimulator.isSupported', () {
MockSimControl mockSimControl;
MockXcode mockXcode;
setUp(() {
mockSimControl = MockSimControl();
mockXcode = MockXcode();
});
testUsingContext('Apple TV is unsupported', () {
expect(IOSSimulator('x', name: 'Apple TV').isSupported(), false);
final IOSSimulator simulator = IOSSimulator(
'x',
name: 'Apple TV',
simControl: mockSimControl,
xcode: mockXcode,
);
expect(simulator.isSupported(), false);
}, overrides: <Type, Generator>{
Platform: () => osx,
FileSystem: () => fileSystem,
......@@ -172,7 +245,12 @@ void main() {
});
testUsingContext('Apple Watch is unsupported', () {
expect(IOSSimulator('x', name: 'Apple Watch').isSupported(), false);
expect(IOSSimulator(
'x',
name: 'Apple Watch',
simControl: mockSimControl,
xcode: mockXcode,
).isSupported(), false);
}, overrides: <Type, Generator>{
Platform: () => osx,
FileSystem: () => fileSystem,
......@@ -180,7 +258,12 @@ void main() {
});
testUsingContext('iPad 2 is supported', () {
expect(IOSSimulator('x', name: 'iPad 2').isSupported(), true);
expect(IOSSimulator(
'x',
name: 'iPad 2',
simControl: mockSimControl,
xcode: mockXcode,
).isSupported(), true);
}, overrides: <Type, Generator>{
Platform: () => osx,
FileSystem: () => fileSystem,
......@@ -188,7 +271,12 @@ void main() {
});
testUsingContext('iPad Retina is supported', () {
expect(IOSSimulator('x', name: 'iPad Retina').isSupported(), true);
expect(IOSSimulator(
'x',
name: 'iPad Retina',
simControl: mockSimControl,
xcode: mockXcode,
).isSupported(), true);
}, overrides: <Type, Generator>{
Platform: () => osx,
FileSystem: () => fileSystem,
......@@ -196,7 +284,12 @@ void main() {
});
testUsingContext('iPhone 5 is supported', () {
expect(IOSSimulator('x', name: 'iPhone 5').isSupported(), true);
expect(IOSSimulator(
'x',
name: 'iPhone 5',
simControl: mockSimControl,
xcode: mockXcode,
).isSupported(), true);
}, overrides: <Type, Generator>{
Platform: () => osx,
FileSystem: () => fileSystem,
......@@ -204,7 +297,12 @@ void main() {
});
testUsingContext('iPhone 5s is supported', () {
expect(IOSSimulator('x', name: 'iPhone 5s').isSupported(), true);
expect(IOSSimulator(
'x',
name: 'iPhone 5s',
simControl: mockSimControl,
xcode: mockXcode,
).isSupported(), true);
}, overrides: <Type, Generator>{
Platform: () => osx,
FileSystem: () => fileSystem,
......@@ -212,7 +310,12 @@ void main() {
});
testUsingContext('iPhone SE is supported', () {
expect(IOSSimulator('x', name: 'iPhone SE').isSupported(), true);
expect(IOSSimulator(
'x',
name: 'iPhone SE',
simControl: mockSimControl,
xcode: mockXcode,
).isSupported(), true);
}, overrides: <Type, Generator>{
Platform: () => osx,
FileSystem: () => fileSystem,
......@@ -220,7 +323,12 @@ void main() {
});
testUsingContext('iPhone 7 Plus is supported', () {
expect(IOSSimulator('x', name: 'iPhone 7 Plus').isSupported(), true);
expect(IOSSimulator(
'x',
name: 'iPhone 7 Plus',
simControl: mockSimControl,
xcode: mockXcode,
).isSupported(), true);
}, overrides: <Type, Generator>{
Platform: () => osx,
FileSystem: () => fileSystem,
......@@ -228,7 +336,12 @@ void main() {
});
testUsingContext('iPhone X is supported', () {
expect(IOSSimulator('x', name: 'iPhone X').isSupported(), true);
expect(IOSSimulator(
'x',
name: 'iPhone X',
simControl: mockSimControl,
xcode: mockXcode,
).isSupported(), true);
}, overrides: <Type, Generator>{
Platform: () => osx,
FileSystem: () => fileSystem,
......@@ -236,35 +349,56 @@ void main() {
});
});
testUsingContext('builds with targetPlatform', () async {
final IOSSimulator simulator = IOSSimulator('x', name: 'iPhone X');
when(buildSystem.build(any, any)).thenAnswer((Invocation invocation) async {
return BuildResult(success: true);
});
await simulator.sideloadUpdatedAssetsForInstalledApplicationBundle(BuildInfo.debug, 'lib/main.dart');
final VerificationResult result = verify(buildSystem.build(any, captureAny));
final Environment environment = result.captured.single as Environment;
expect(environment.defines, <String, String>{
kTargetFile: 'lib/main.dart',
kTargetPlatform: 'ios',
kBuildMode: 'debug',
kTrackWidgetCreation: 'false',
kIconTreeShakerFlag: null,
});
}, overrides: <Type, Generator>{
BuildSystem: () => MockBuildSystem(),
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
group('buildSystem', () {
MockSimControl mockSimControl;
MockXcode mockXcode;
MockBuildSystem mockBuildSystem;
setUp(() {
mockBuildSystem = MockBuildSystem();
mockSimControl = MockSimControl();
mockXcode = MockXcode();
});
testUsingContext('builds with targetPlatform', () async {
final IOSSimulator simulator = IOSSimulator(
'x',
name: 'iPhone X',
simControl: mockSimControl,
xcode: mockXcode,
);
when(mockBuildSystem.build(any, any)).thenAnswer((Invocation invocation) async {
return BuildResult(success: true);
});
await simulator.sideloadUpdatedAssetsForInstalledApplicationBundle(BuildInfo.debug, 'lib/main.dart');
final VerificationResult result = verify(mockBuildSystem.build(any, captureAny));
final Environment environment = result.captured.single as Environment;
expect(environment.defines, <String, String>{
kTargetFile: 'lib/main.dart',
kTargetPlatform: 'ios',
kBuildMode: 'debug',
kTrackWidgetCreation: 'false',
kIconTreeShakerFlag: null,
});
}, overrides: <Type, Generator>{
BuildSystem: () => mockBuildSystem,
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
});
group('Simulator screenshot', () {
MockXcode mockXcode;
MockLogger mockLogger;
MockProcessManager mockProcessManager;
IOSSimulator deviceUnderTest;
// only used for fs.path.join()
final FileSystem fs = globals.fs;
setUp(() {
mockXcode = MockXcode();
mockLogger = MockLogger();
mockProcessManager = MockProcessManager();
// Let everything else return exit code 0 so process.dart doesn't crash.
when(
......@@ -272,32 +406,34 @@ void main() {
).thenAnswer((Invocation invocation) =>
Future<ProcessResult>.value(ProcessResult(2, 0, '', ''))
);
// Test a real one. Screenshot doesn't require instance states.
final SimControl simControl = SimControl(processManager: mockProcessManager, logger: mockLogger);
// Doesn't matter what the device is.
deviceUnderTest = IOSSimulator('x', name: 'iPhone SE');
deviceUnderTest = IOSSimulator(
'x',
name: 'iPhone SE',
simControl: simControl,
xcode: mockXcode,
);
});
testUsingContext(
testWithoutContext(
"old Xcode doesn't support screenshot",
() {
when(mockXcode.majorVersion).thenReturn(7);
when(mockXcode.minorVersion).thenReturn(1);
expect(deviceUnderTest.supportsScreenshot, false);
},
overrides: <Type, Generator>{
Xcode: () => mockXcode,
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
},
);
testUsingContext(
testWithoutContext(
'Xcode 8.2+ supports screenshots',
() async {
when(mockXcode.majorVersion).thenReturn(8);
when(mockXcode.minorVersion).thenReturn(2);
expect(deviceUnderTest.supportsScreenshot, true);
final MockFile mockFile = MockFile();
when(mockFile.path).thenReturn(globals.fs.path.join('some', 'path', 'to', 'screenshot.png'));
when(mockFile.path).thenReturn(fs.path.join('some', 'path', 'to', 'screenshot.png'));
await deviceUnderTest.takeScreenshot(mockFile);
verify(mockProcessManager.run(
<String>[
......@@ -306,33 +442,36 @@ void main() {
'io',
'x',
'screenshot',
globals.fs.path.join('some', 'path', 'to', 'screenshot.png'),
fs.path.join('some', 'path', 'to', 'screenshot.png'),
],
environment: null,
workingDirectory: null,
));
},
overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
// Test a real one. Screenshot doesn't require instance states.
SimControl: () => SimControl(),
Xcode: () => mockXcode,
FileSystem: () => fileSystem,
},
);
});
group('launchDeviceLogTool', () {
MockProcessManager mockProcessManager;
MockXcode mockXcode;
MockSimControl mockSimControl;
setUp(() {
mockProcessManager = MockProcessManager();
when(mockProcessManager.start(any, environment: null, workingDirectory: null))
.thenAnswer((Invocation invocation) => Future<Process>.value(MockProcess()));
mockSimControl = MockSimControl();
mockXcode = MockXcode();
});
testUsingContext('uses tail on iOS versions prior to iOS 11', () async {
final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 9.3');
final IOSSimulator device = IOSSimulator(
'x',
name: 'iPhone SE',
simulatorCategory: 'iOS 9.3',
simControl: mockSimControl,
xcode: mockXcode,
);
await launchDeviceLogTool(device);
expect(
verify(mockProcessManager.start(captureAny, environment: null, workingDirectory: null)).captured.single,
......@@ -345,7 +484,13 @@ void main() {
});
testUsingContext('uses /usr/bin/log on iOS 11 and above', () async {
final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.0');
final IOSSimulator device = IOSSimulator(
'x',
name: 'iPhone SE',
simulatorCategory: 'iOS 11.0',
simControl: mockSimControl,
xcode: mockXcode,
);
await launchDeviceLogTool(device);
expect(
verify(mockProcessManager.start(captureAny, environment: null, workingDirectory: null)).captured.single,
......@@ -361,14 +506,25 @@ void main() {
group('launchSystemLogTool', () {
MockProcessManager mockProcessManager;
MockSimControl mockSimControl;
MockXcode mockXcode;
setUp(() {
mockSimControl = MockSimControl();
mockXcode = MockXcode();
mockProcessManager = MockProcessManager();
when(mockProcessManager.start(any, environment: null, workingDirectory: null))
.thenAnswer((Invocation invocation) => Future<Process>.value(MockProcess()));
});
testUsingContext('uses tail on iOS versions prior to iOS 11', () async {
final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 9.3');
final IOSSimulator device = IOSSimulator(
'x',
name: 'iPhone SE',
simulatorCategory: 'iOS 9.3',
simControl: mockSimControl,
xcode: mockXcode,
);
await launchSystemLogTool(device);
expect(
verify(mockProcessManager.start(captureAny, environment: null, workingDirectory: null)).captured.single,
......@@ -381,7 +537,13 @@ void main() {
});
testUsingContext('uses /usr/bin/log on iOS 11 and above', () async {
final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.0');
final IOSSimulator device = IOSSimulator(
'x',
name: 'iPhone SE',
simulatorCategory: 'iOS 11.0',
simControl: mockSimControl,
xcode: mockXcode,
);
await launchSystemLogTool(device);
verifyNever(mockProcessManager.start(any, environment: null, workingDirectory: null));
},
......@@ -394,10 +556,14 @@ void main() {
group('log reader', () {
MockProcessManager mockProcessManager;
MockIosProject mockIosProject;
MockSimControl mockSimControl;
MockXcode mockXcode;
setUp(() {
mockProcessManager = MockProcessManager();
mockIosProject = MockIosProject();
mockSimControl = MockSimControl();
mockXcode = MockXcode();
});
testUsingContext('simulator can output `)`', () async {
......@@ -420,7 +586,12 @@ void main() {
return Future<Process>.value(mockProcess);
});
final IOSSimulator device = IOSSimulator('123456', simulatorCategory: 'iOS 11.0');
final IOSSimulator device = IOSSimulator(
'123456',
simulatorCategory: 'iOS 11.0',
simControl: mockSimControl,
xcode: mockXcode,
);
final DeviceLogReader logReader = device.getLogReader(
app: await BuildableIOSApp.fromProject(mockIosProject),
);
......@@ -470,19 +641,28 @@ void main() {
}
''';
MockLogger mockLogger;
MockProcessManager mockProcessManager;
MockXcode mockXcode;
SimControl simControl;
const String deviceId = 'smart-phone';
const String appId = 'flutterApp';
setUp(() {
mockLogger = MockLogger();
mockProcessManager = MockProcessManager();
when(mockProcessManager.run(any)).thenAnswer((Invocation _) async {
return ProcessResult(mockPid, 0, validSimControlOutput, '');
});
simControl = SimControl();
simControl = SimControl(
logger: mockLogger,
processManager: mockProcessManager,
);
mockXcode = MockXcode();
});
testUsingContext('getDevices succeeds', () async {
testWithoutContext('getDevices succeeds', () async {
final List<SimDevice> devices = await simControl.getDevices();
final SimDevice watch = devices[0];
......@@ -508,40 +688,82 @@ void main() {
expect(tv.name, 'Apple TV');
expect(tv.udid, 'TEST-TV-UDID');
expect(tv.isBooted, isFalse);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
SimControl: () => simControl,
FileSystem: () => fileSystem,
});
testUsingContext('getDevices handles bad simctl output', () async {
testWithoutContext('getDevices handles bad simctl output', () async {
when(mockProcessManager.run(any))
.thenAnswer((Invocation _) async => ProcessResult(mockPid, 0, 'Install Started', ''));
final List<SimDevice> devices = await simControl.getDevices();
expect(devices, isEmpty);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
SimControl: () => simControl,
FileSystem: () => fileSystem,
});
testUsingContext('sdkMajorVersion defaults to 11 when sdkNameAndVersion is junk', () async {
final IOSSimulator iosSimulatorA = IOSSimulator('x', name: 'Testo', simulatorCategory: 'NaN');
testWithoutContext('sdkMajorVersion defaults to 11 when sdkNameAndVersion is junk', () async {
final IOSSimulator iosSimulatorA = IOSSimulator(
'x',
name: 'Testo',
simulatorCategory: 'NaN',
simControl: simControl,
xcode: mockXcode,
);
expect(await iosSimulatorA.sdkMajorVersion, 11);
});
testWithoutContext('.install() handles exceptions', () async {
when(mockProcessManager.run(
<String>['/usr/bin/xcrun', 'simctl', 'install', deviceId, appId],
environment: anyNamed('environment'),
workingDirectory: anyNamed('workingDirectory'),
)).thenThrow(const ProcessException('xcrun', <String>[]));
expect(
() async => await simControl.install(deviceId, appId),
throwsToolExit(message: r'Unable to install'),
);
});
testWithoutContext('.uninstall() handles exceptions', () async {
when(mockProcessManager.run(
<String>['/usr/bin/xcrun', 'simctl', 'uninstall', deviceId, appId],
environment: anyNamed('environment'),
workingDirectory: anyNamed('workingDirectory'),
)).thenThrow(const ProcessException('xcrun', <String>[]));
expect(
() async => await simControl.uninstall(deviceId, appId),
throwsToolExit(message: r'Unable to uninstall'),
);
});
testWithoutContext('.launch() handles exceptions', () async {
when(mockProcessManager.run(
<String>['/usr/bin/xcrun', 'simctl', 'launch', deviceId, appId],
environment: anyNamed('environment'),
workingDirectory: anyNamed('workingDirectory'),
)).thenThrow(const ProcessException('xcrun', <String>[]));
expect(
() async => await simControl.launch(deviceId, appId),
throwsToolExit(message: r'Unable to launch'),
);
});
});
group('startApp', () {
SimControl simControl;
MockXcode mockXcode;
setUp(() {
simControl = MockSimControl();
mockXcode = MockXcode();
});
testUsingContext("startApp uses compiled app's Info.plist to find CFBundleIdentifier", () async {
final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.2');
final IOSSimulator device = IOSSimulator(
'x',
name: 'iPhone SE',
simulatorCategory: 'iOS 11.2',
simControl: simControl,
xcode: mockXcode,
);
when(globals.plistParser.getValueFromFile(any, any)).thenReturn('correct');
final Directory mockDir = globals.fs.currentDirectory;
......@@ -553,52 +775,77 @@ void main() {
verify(simControl.launch(any, 'correct', any));
}, overrides: <Type, Generator>{
SimControl: () => simControl,
PlistParser: () => MockPlistUtils(),
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
});
});
testUsingContext('IOSDevice.isSupportedForProject is true on module project', () async {
globals.fs.file('pubspec.yaml')
..createSync()
..writeAsStringSync(r'''
group('IOSDevice.isSupportedForProject', () {
MockSimControl mockSimControl;
MockXcode mockXcode;
setUp(() {
mockSimControl = MockSimControl();
mockXcode = MockXcode();
});
testUsingContext('is true on module project', () async {
globals.fs.file('pubspec.yaml')
..createSync()
..writeAsStringSync(r'''
name: example
flutter:
module: {}
''');
globals.fs.file('.packages').createSync();
final FlutterProject flutterProject = FlutterProject.current();
globals.fs.file('.packages').createSync();
final FlutterProject flutterProject = FlutterProject.current();
expect(IOSSimulator('test').isSupportedForProject(flutterProject), true);
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
ProcessManager: () => FakeProcessManager.any(),
});
final IOSSimulator simulator = IOSSimulator(
'test',
simControl: mockSimControl,
xcode: mockXcode,
);
expect(simulator.isSupportedForProject(flutterProject), true);
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('IOSDevice.isSupportedForProject is true with editable host app', () async {
globals.fs.file('pubspec.yaml').createSync();
globals.fs.file('.packages').createSync();
globals.fs.directory('ios').createSync();
final FlutterProject flutterProject = FlutterProject.current();
expect(IOSSimulator('test').isSupportedForProject(flutterProject), true);
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FileSystem: () => fileSystem,
});
testUsingContext('is true with editable host app', () async {
globals.fs.file('pubspec.yaml').createSync();
globals.fs.file('.packages').createSync();
globals.fs.directory('ios').createSync();
final FlutterProject flutterProject = FlutterProject.current();
testUsingContext('IOSDevice.isSupportedForProject is false with no host app and no module', () async {
globals.fs.file('pubspec.yaml').createSync();
globals.fs.file('.packages').createSync();
final FlutterProject flutterProject = FlutterProject.current();
final IOSSimulator simulator = IOSSimulator(
'test',
simControl: mockSimControl,
xcode: mockXcode,
);
expect(simulator.isSupportedForProject(flutterProject), true);
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('is false with no host app and no module', () async {
globals.fs.file('pubspec.yaml').createSync();
globals.fs.file('.packages').createSync();
final FlutterProject flutterProject = FlutterProject.current();
expect(IOSSimulator('test').isSupportedForProject(flutterProject), false);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
final IOSSimulator simulator = IOSSimulator(
'test',
simControl: mockSimControl,
xcode: mockXcode,
);
expect(simulator.isSupportedForProject(flutterProject), false);
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
ProcessManager: () => FakeProcessManager.any(),
});
});
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment