Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

2014 lines
70 KiB
Dart

// 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:file/memory.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/io.dart' show ProcessException;
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/ios/core_devices.dart';
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/ios/iproxy.dart';
import 'package:flutter_tools/src/ios/xcode_debug.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/macos/xcdevice.dart';
import 'package:flutter_tools/src/macos/xcode.dart';
import 'package:test/fake.dart';
import 'package:unified_analytics/unified_analytics.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_process_manager.dart';
import '../../src/fakes.dart' hide FakeProcess;
void main() {
late BufferLogger logger;
setUp(() {
logger = BufferLogger.test();
});
group('FakeProcessManager', () {
late FakeProcessManager fakeProcessManager;
setUp(() {
fakeProcessManager = FakeProcessManager.empty();
});
group('Xcode', () {
late FakeXcodeProjectInterpreter xcodeProjectInterpreter;
setUp(() {
xcodeProjectInterpreter = FakeXcodeProjectInterpreter();
});
testWithoutContext('isInstalledAndMeetsVersionCheck is false when not macOS', () {
final xcode = Xcode.test(
platform: FakePlatform(operatingSystem: 'windows'),
processManager: fakeProcessManager,
xcodeProjectInterpreter: xcodeProjectInterpreter,
);
expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
});
testWithoutContext('isSimctlInstalled is true when simctl list succeeds', () {
fakeProcessManager.addCommand(
const FakeCommand(command: <String>['xcrun', 'simctl', 'list', 'devices', 'booted']),
);
final xcode = Xcode.test(
processManager: fakeProcessManager,
xcodeProjectInterpreter: xcodeProjectInterpreter,
);
expect(xcode.isSimctlInstalled, isTrue);
expect(fakeProcessManager, hasNoRemainingExpectations);
});
testWithoutContext('isSimctlInstalled is false when simctl list fails', () {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'simctl', 'list', 'devices', 'booted'],
stderr: 'failed to run',
exitCode: 1,
),
);
final xcode = Xcode.test(
processManager: fakeProcessManager,
xcodeProjectInterpreter: xcodeProjectInterpreter,
logger: logger,
);
expect(xcode.isSimctlInstalled, isFalse);
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
});
testWithoutContext(
'isSimctlInstalled is false when simctl list throws process exception',
() {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'simctl', 'list', 'devices', 'booted'],
exception: ProcessException('xcrun', <String>['simctl']),
),
);
final xcode = Xcode.test(
processManager: fakeProcessManager,
xcodeProjectInterpreter: xcodeProjectInterpreter,
logger: logger,
);
expect(xcode.isSimctlInstalled, isFalse);
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
expect(logger.traceText, contains('ProcessException'));
},
);
group('isDevicectlInstalled', () {
testWithoutContext('is true when Xcode is 15+ and devicectl succeeds', () {
fakeProcessManager.addCommand(
const FakeCommand(command: <String>['xcrun', 'devicectl', '--version']),
);
xcodeProjectInterpreter.version = Version(15, 0, 0);
final xcode = Xcode.test(
processManager: fakeProcessManager,
xcodeProjectInterpreter: xcodeProjectInterpreter,
);
expect(xcode.isDevicectlInstalled, isTrue);
expect(fakeProcessManager, hasNoRemainingExpectations);
});
testWithoutContext('is false when devicectl fails', () {
fakeProcessManager.addCommand(
const FakeCommand(command: <String>['xcrun', 'devicectl', '--version'], exitCode: 1),
);
xcodeProjectInterpreter.version = Version(15, 0, 0);
final xcode = Xcode.test(
processManager: fakeProcessManager,
xcodeProjectInterpreter: xcodeProjectInterpreter,
logger: logger,
);
expect(xcode.isDevicectlInstalled, isFalse);
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
});
testWithoutContext('is false when devicectl throws process exception', () {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'devicectl', '--version'],
exitCode: 1,
exception: ProcessException('xcrun', <String>['devicectl']),
),
);
xcodeProjectInterpreter.version = Version(15, 0, 0);
final xcode = Xcode.test(
processManager: fakeProcessManager,
xcodeProjectInterpreter: xcodeProjectInterpreter,
logger: logger,
);
expect(xcode.isDevicectlInstalled, isFalse);
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
expect(logger.traceText, contains('ProcessException'));
});
testWithoutContext('is false when Xcode is less than 15', () {
xcodeProjectInterpreter.version = Version(14, 0, 0);
final xcode = Xcode.test(
processManager: fakeProcessManager,
xcodeProjectInterpreter: xcodeProjectInterpreter,
);
expect(xcode.isDevicectlInstalled, isFalse);
expect(fakeProcessManager, hasNoRemainingExpectations);
});
});
group('pathToXcodeApp', () {
late UserMessages userMessages;
setUp(() {
userMessages = UserMessages();
});
testWithoutContext('parses correctly', () {
final xcode = Xcode.test(
processManager: fakeProcessManager,
xcodeProjectInterpreter: xcodeProjectInterpreter,
);
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['/usr/bin/xcode-select', '--print-path'],
stdout: '/Applications/Xcode.app/Contents/Developer',
),
);
expect(xcode.xcodeAppPath, '/Applications/Xcode.app');
expect(fakeProcessManager, hasNoRemainingExpectations);
});
testWithoutContext('throws error if not found', () {
final xcode = Xcode.test(
processManager: FakeProcessManager.any(),
xcodeProjectInterpreter: xcodeProjectInterpreter,
);
expect(() => xcode.xcodeAppPath, throwsToolExit(message: userMessages.xcodeMissing));
});
testWithoutContext('throws error with unexpected outcome', () {
final xcode = Xcode.test(
processManager: fakeProcessManager,
xcodeProjectInterpreter: xcodeProjectInterpreter,
);
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['/usr/bin/xcode-select', '--print-path'],
stdout: '/Library/Developer/CommandLineTools',
),
);
expect(() => xcode.xcodeAppPath, throwsToolExit(message: userMessages.xcodeMissing));
expect(fakeProcessManager, hasNoRemainingExpectations);
});
});
group('pathToXcodeAutomationScript', () {
const flutterRoot = '/path/to/flutter';
late MemoryFileSystem fileSystem;
setUp(() {
fileSystem = MemoryFileSystem.test();
});
testWithoutContext('returns path when file is found', () {
final xcode = Xcode.test(
processManager: fakeProcessManager,
xcodeProjectInterpreter: xcodeProjectInterpreter,
fileSystem: fileSystem,
flutterRoot: flutterRoot,
);
fileSystem
.file('$flutterRoot/packages/flutter_tools/bin/xcode_debug.js')
.createSync(recursive: true);
expect(
xcode.xcodeAutomationScriptPath,
'$flutterRoot/packages/flutter_tools/bin/xcode_debug.js',
);
});
testWithoutContext('throws error when not found', () {
final xcode = Xcode.test(
processManager: fakeProcessManager,
xcodeProjectInterpreter: xcodeProjectInterpreter,
fileSystem: fileSystem,
flutterRoot: flutterRoot,
);
expect(() => xcode.xcodeAutomationScriptPath, throwsToolExit());
});
});
group('macOS', () {
late Xcode xcode;
late BufferLogger logger;
setUp(() {
xcodeProjectInterpreter = FakeXcodeProjectInterpreter();
logger = BufferLogger.test();
xcode = Xcode.test(
processManager: fakeProcessManager,
xcodeProjectInterpreter: xcodeProjectInterpreter,
logger: logger,
);
});
testWithoutContext('xcodeSelectPath returns path when xcode-select is installed', () {
const xcodePath = '/Applications/Xcode8.0.app/Contents/Developer';
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['/usr/bin/xcode-select', '--print-path'],
stdout: xcodePath,
),
);
expect(xcode.xcodeSelectPath, xcodePath);
expect(fakeProcessManager, hasNoRemainingExpectations);
});
testWithoutContext('xcodeSelectPath returns null when xcode-select is not installed', () {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['/usr/bin/xcode-select', '--print-path'],
exception: ProcessException('/usr/bin/xcode-select', <String>['--print-path']),
),
);
expect(xcode.xcodeSelectPath, isNull);
expect(fakeProcessManager, hasNoRemainingExpectations);
fakeProcessManager.addCommand(
FakeCommand(
command: const <String>['/usr/bin/xcode-select', '--print-path'],
exception: ArgumentError(
'Invalid argument(s): Cannot find executable for /usr/bin/xcode-select',
),
),
);
expect(xcode.xcodeSelectPath, isNull);
expect(fakeProcessManager, hasNoRemainingExpectations);
});
testWithoutContext('version checks fail when version is less than minimum', () {
xcodeProjectInterpreter.isInstalled = true;
xcodeProjectInterpreter.version = Version(9, null, null);
expect(xcode.isRequiredVersionSatisfactory, isFalse);
expect(xcode.isRecommendedVersionSatisfactory, isFalse);
});
testWithoutContext('version checks fail when xcodebuild tools are not installed', () {
xcodeProjectInterpreter.isInstalled = false;
expect(xcode.isRequiredVersionSatisfactory, isFalse);
expect(xcode.isRecommendedVersionSatisfactory, isFalse);
});
testWithoutContext(
'version checks pass when version meets minimum but not recommended',
() {
xcodeProjectInterpreter.isInstalled = true;
xcodeProjectInterpreter.version = Version(14, null, null);
expect(xcode.isRequiredVersionSatisfactory, isTrue);
expect(xcode.isRecommendedVersionSatisfactory, isFalse);
},
);
testWithoutContext('version checks pass when major version exceeds minimum', () {
xcodeProjectInterpreter.isInstalled = true;
xcodeProjectInterpreter.version = Version(15, 0, 0);
expect(xcode.isRequiredVersionSatisfactory, isTrue);
});
testWithoutContext('version checks pass when minor version exceeds minimum', () {
xcodeProjectInterpreter.isInstalled = true;
xcodeProjectInterpreter.version = Version(14, 3, 0);
expect(xcode.isRequiredVersionSatisfactory, isTrue);
});
testWithoutContext('version checks pass when patch version exceeds minimum', () {
xcodeProjectInterpreter.isInstalled = true;
xcodeProjectInterpreter.version = Version(14, 0, 2);
expect(xcode.isRequiredVersionSatisfactory, isTrue);
});
testWithoutContext('version checks pass when major version exceeds recommendation', () {
xcodeProjectInterpreter.isInstalled = true;
xcodeProjectInterpreter.version = Version(16, 0, 0);
expect(xcode.isRequiredVersionSatisfactory, isTrue);
expect(xcode.isRecommendedVersionSatisfactory, isTrue);
});
testWithoutContext('version checks pass when minor version exceeds recommendation', () {
xcodeProjectInterpreter.isInstalled = true;
xcodeProjectInterpreter.version = Version(15, 3, 0);
expect(xcode.isRequiredVersionSatisfactory, isTrue);
expect(xcode.isRecommendedVersionSatisfactory, isTrue);
});
testWithoutContext('version checks pass when patch version exceeds recommendation', () {
xcodeProjectInterpreter.isInstalled = true;
xcodeProjectInterpreter.version = Version(15, 0, 2);
expect(xcode.isRequiredVersionSatisfactory, isTrue);
expect(xcode.isRecommendedVersionSatisfactory, isTrue);
});
testWithoutContext('isInstalledAndMeetsVersionCheck is false when not installed', () {
xcodeProjectInterpreter.isInstalled = false;
expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
expect(fakeProcessManager, hasNoRemainingExpectations);
});
testWithoutContext(
'isInstalledAndMeetsVersionCheck is false when version not satisfied',
() {
xcodeProjectInterpreter.isInstalled = true;
xcodeProjectInterpreter.version = Version(10, 2, 0);
expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
expect(fakeProcessManager, hasNoRemainingExpectations);
},
);
testWithoutContext(
'isInstalledAndMeetsVersionCheck is true when macOS and installed and version is satisfied',
() {
xcodeProjectInterpreter.isInstalled = true;
xcodeProjectInterpreter.version = Version(14, null, null);
expect(xcode.isInstalledAndMeetsVersionCheck, isTrue);
expect(fakeProcessManager, hasNoRemainingExpectations);
},
);
testWithoutContext(
'eulaSigned is false when clang output indicates EULA not yet accepted',
() {
fakeProcessManager.addCommands(const <FakeCommand>[
FakeCommand(
command: <String>['xcrun', 'clang'],
exitCode: 1,
stderr: 'Xcode EULA has not been accepted.\nLaunch Xcode and accept the license.',
),
]);
expect(xcode.eulaSigned, isFalse);
expect(fakeProcessManager, hasNoRemainingExpectations);
},
);
testWithoutContext('eulaSigned is false when clang is not installed', () {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'clang'],
exception: ProcessException('xcrun', <String>['clang']),
),
);
expect(xcode.eulaSigned, isFalse);
});
testWithoutContext(
'eulaSigned is true when clang output indicates EULA has been accepted',
() {
fakeProcessManager.addCommands(const <FakeCommand>[
FakeCommand(
command: <String>['xcrun', 'clang'],
exitCode: 1,
stderr: 'clang: error: no input files',
),
]);
expect(xcode.eulaSigned, isTrue);
expect(fakeProcessManager, hasNoRemainingExpectations);
},
);
testWithoutContext('SDK name', () {
expect(getSDKNameForIOSEnvironmentType(EnvironmentType.physical), 'iphoneos');
expect(getSDKNameForIOSEnvironmentType(EnvironmentType.simulator), 'iphonesimulator');
});
group('SDK location', () {
const sdkroot =
'Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.2.sdk';
testWithoutContext('--show-sdk-path iphoneos', () async {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', '--sdk', 'iphoneos', '--show-sdk-path'],
stdout: sdkroot,
),
);
expect(await xcode.sdkLocation(EnvironmentType.physical), sdkroot);
expect(fakeProcessManager, hasNoRemainingExpectations);
});
testWithoutContext('--show-sdk-path fails', () async {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', '--sdk', 'iphoneos', '--show-sdk-path'],
exitCode: 1,
stderr: 'xcrun: error:',
),
);
expect(
() async => xcode.sdkLocation(EnvironmentType.physical),
throwsToolExit(message: 'Could not find SDK location'),
);
expect(fakeProcessManager, hasNoRemainingExpectations);
});
});
group('SDK Platform Version', () {
testWithoutContext('--show-sdk-platform-version iphonesimulator', () async {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>[
'xcrun',
'--sdk',
'iphonesimulator',
'--show-sdk-platform-version',
],
stdout: '16.4',
),
);
expect(await xcode.sdkPlatformVersion(EnvironmentType.simulator), Version(16, 4, null));
expect(fakeProcessManager, hasNoRemainingExpectations);
});
testWithoutContext(
'--show-sdk-platform-version iphonesimulator with leading and trailing new line',
() async {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>[
'xcrun',
'--sdk',
'iphonesimulator',
'--show-sdk-platform-version',
],
stdout: '\n16.4\n',
),
);
expect(
await xcode.sdkPlatformVersion(EnvironmentType.simulator),
Version(16, 4, null),
);
expect(fakeProcessManager, hasNoRemainingExpectations);
},
);
testWithoutContext(
'--show-sdk-platform-version returns version followed by text',
() async {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>[
'xcrun',
'--sdk',
'iphonesimulator',
'--show-sdk-platform-version',
],
stdout: '13.2 (a) 12344',
),
);
expect(
await xcode.sdkPlatformVersion(EnvironmentType.simulator),
Version(13, 2, null, text: '13.2 (a) 12344'),
);
expect(fakeProcessManager, hasNoRemainingExpectations);
},
);
testWithoutContext('--show-sdk-platform-version returns something unexpected', () async {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>[
'xcrun',
'--sdk',
'iphonesimulator',
'--show-sdk-platform-version',
],
stdout: 'bogus',
),
);
expect(await xcode.sdkPlatformVersion(EnvironmentType.simulator), null);
expect(fakeProcessManager, hasNoRemainingExpectations);
});
testWithoutContext('--show-sdk-platform-version fails', () async {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>[
'xcrun',
'--sdk',
'iphonesimulator',
'--show-sdk-platform-version',
],
exitCode: 1,
stderr: 'xcrun: error:',
),
);
expect(await xcode.sdkPlatformVersion(EnvironmentType.simulator), null);
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(logger.errorText, contains('Could not find SDK Platform Version'));
});
});
});
});
group('xcdevice not installed', () {
late XCDevice xcdevice;
late Xcode xcode;
late MemoryFileSystem fileSystem;
setUp(() {
xcode = Xcode.test(
processManager: FakeProcessManager.any(),
xcodeProjectInterpreter: XcodeProjectInterpreter.test(
processManager: FakeProcessManager.any(),
version: null, // Not installed.
),
);
fileSystem = MemoryFileSystem.test();
xcdevice = XCDevice(
processManager: fakeProcessManager,
logger: logger,
xcode: xcode,
platform: FakePlatform(operatingSystem: 'macos'),
artifacts: Artifacts.test(),
cache: Cache.test(processManager: FakeProcessManager.any()),
iproxy: IProxy.test(logger: logger, processManager: fakeProcessManager),
fileSystem: fileSystem,
coreDeviceControl: FakeIOSCoreDeviceControl(),
xcodeDebug: FakeXcodeDebug(),
analytics: const NoOpAnalytics(),
shutdownHooks: FakeShutdownHooks(),
);
});
testWithoutContext('Xcode not installed', () async {
expect(xcode.isInstalled, false);
expect(xcdevice.isInstalled, false);
expect(xcdevice.observedDeviceEvents(), isNull);
expect(
logger.traceText,
contains("Xcode not found. Run 'flutter doctor' for more information."),
);
expect(await xcdevice.getAvailableIOSDevices(), isEmpty);
expect(await xcdevice.getDiagnostics(), isEmpty);
});
});
testWithoutContext('shutdown hooks disposes xcdevice observers', () async {
final shutdownHooks = ShutdownHooks();
final xcdevice = XCDevice(
processManager: FakeProcessManager.any(),
logger: logger,
xcode: Xcode.test(processManager: FakeProcessManager.any()),
platform: FakePlatform(operatingSystem: 'macos'),
artifacts: Artifacts.test(),
cache: Cache.test(processManager: FakeProcessManager.any()),
iproxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
fileSystem: MemoryFileSystem.test(),
coreDeviceControl: FakeIOSCoreDeviceControl(),
xcodeDebug: FakeXcodeDebug(),
analytics: const NoOpAnalytics(),
shutdownHooks: shutdownHooks,
);
expect(shutdownHooks.registeredHooks, hasLength(1));
final doneCompleter = Completer<void>();
xcdevice.observedDeviceEvents()!.listen(
null,
onDone: () {
doneCompleter.complete();
},
);
await doneCompleter.future;
xcdevice.dispose();
expect(logger.traceText, contains('xcdevice observe --usb exited with code 0'));
expect(logger.traceText, contains('xcdevice observe --wifi exited with code 0'));
});
group('xcdevice', () {
late XCDevice xcdevice;
late Xcode xcode;
late MemoryFileSystem fileSystem;
late FakeAnalytics fakeAnalytics;
late FakeIOSCoreDeviceControl coreDeviceControl;
setUp(() {
xcode = Xcode.test(processManager: FakeProcessManager.any());
fileSystem = MemoryFileSystem.test();
coreDeviceControl = FakeIOSCoreDeviceControl();
fakeAnalytics = getInitializedFakeAnalyticsInstance(
fs: fileSystem,
fakeFlutterVersion: FakeFlutterVersion(),
);
xcdevice = XCDevice(
processManager: fakeProcessManager,
logger: logger,
xcode: xcode,
platform: FakePlatform(operatingSystem: 'macos'),
artifacts: Artifacts.test(),
cache: Cache.test(processManager: FakeProcessManager.any()),
iproxy: IProxy.test(logger: logger, processManager: fakeProcessManager),
fileSystem: fileSystem,
coreDeviceControl: coreDeviceControl,
xcodeDebug: FakeXcodeDebug(),
analytics: fakeAnalytics,
shutdownHooks: FakeShutdownHooks(),
);
});
group('observe device events', () {
testUsingContext('relays events', () async {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'observe',
'--usb',
],
stdout:
'Listening for all devices, on USB.\n'
'Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418\n'
'Attach: 00008027-00192736010F802E\n'
'Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418',
stderr: 'Some usb error',
),
);
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'observe',
'--wifi',
],
stdout:
'Listening for all devices, on WiFi.\n'
'Attach: 00000001-0000000000000000\n'
'Detach: 00000001-0000000000000000',
stderr: 'Some wifi error',
),
);
final attach1 = Completer<void>();
final attach2 = Completer<void>();
final detach1 = Completer<void>();
final attach3 = Completer<void>();
final detach2 = Completer<void>();
// Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
// Attach: 00008027-00192736010F802E
// Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
xcdevice.observedDeviceEvents()!.listen((XCDeviceEventNotification event) {
if (event.eventType == XCDeviceEvent.attach) {
if (event.deviceIdentifier == 'd83d5bc53967baa0ee18626ba87b6254b2ab5418') {
attach1.complete();
} else if (event.deviceIdentifier == '00008027-00192736010F802E') {
attach2.complete();
}
if (event.deviceIdentifier == '00000001-0000000000000000') {
attach3.complete();
}
} else if (event.eventType == XCDeviceEvent.detach) {
if (event.deviceIdentifier == 'd83d5bc53967baa0ee18626ba87b6254b2ab5418') {
detach1.complete();
}
if (event.deviceIdentifier == '00000001-0000000000000000') {
detach2.complete();
}
} else {
fail('Unexpected event');
}
});
await attach1.future;
await attach2.future;
await detach1.future;
await attach3.future;
await detach2.future;
expect(logger.errorText, contains('xcdevice observe --usb: Some usb error'));
expect(logger.errorText, contains('xcdevice observe --wifi: Some wifi error'));
});
testUsingContext('handles exit code', () async {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'observe',
'--usb',
],
),
);
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'observe',
'--wifi',
],
exitCode: 1,
),
);
final doneCompleter = Completer<void>();
xcdevice.observedDeviceEvents()!.listen(
null,
onDone: () {
doneCompleter.complete();
},
);
await doneCompleter.future;
expect(logger.traceText, contains('xcdevice observe --usb exited with code 0'));
expect(logger.traceText, contains('xcdevice observe --wifi exited with code 0'));
});
});
group('wait device events', () {
testUsingContext('relays events', () async {
const deviceId = '00000001-0000000000000000';
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'wait',
'--usb',
deviceId,
],
stdout: 'Waiting for $deviceId to appear, on USB.\n',
),
);
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'wait',
'--wifi',
deviceId,
],
stdout:
'Waiting for $deviceId to appear, on WiFi.\n'
'Attach: 00000001-0000000000000000\n',
),
);
// Attach: 00000001-0000000000000000
final XCDeviceEventNotification? event = await xcdevice.waitForDeviceToConnect(deviceId);
expect(event?.deviceIdentifier, deviceId);
expect(event?.eventInterface, XCDeviceEventInterface.wifi);
expect(event?.eventType, XCDeviceEvent.attach);
});
testUsingContext('handles exit code', () async {
const deviceId = '00000001-0000000000000000';
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'wait',
'--usb',
deviceId,
],
exitCode: 1,
stderr: 'Some error',
),
);
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'wait',
'--wifi',
deviceId,
],
),
);
final XCDeviceEventNotification? event = await xcdevice.waitForDeviceToConnect(deviceId);
expect(event, isNull);
expect(logger.errorText, contains('xcdevice wait --usb: Some error'));
expect(logger.traceText, contains('xcdevice wait --usb exited with code 0'));
expect(logger.traceText, contains('xcdevice wait --wifi exited with code 0'));
expect(xcdevice.waitStreamController?.isClosed, isTrue);
});
testUsingContext('handles cancel', () async {
const deviceId = '00000001-0000000000000000';
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'wait',
'--usb',
deviceId,
],
),
);
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'wait',
'--wifi',
deviceId,
],
),
);
final Future<XCDeviceEventNotification?> futureEvent = xcdevice.waitForDeviceToConnect(
deviceId,
);
xcdevice.cancelWaitForDeviceToConnect();
final XCDeviceEventNotification? event = await futureEvent;
expect(event, isNull);
expect(logger.traceText, contains('xcdevice wait --usb exited with code 0'));
expect(logger.traceText, contains('xcdevice wait --wifi exited with code 0'));
expect(xcdevice.waitStreamController?.isClosed, isTrue);
});
});
group('available devices', () {
final macPlatform = FakePlatform(operatingSystem: 'macos');
testUsingContext(
'returns devices',
() async {
const devicesOutput = '''
[
{
"simulator" : true,
"operatingSystemVersion" : "13.3 (17K446)",
"available" : true,
"platform" : "com.apple.platform.appletvsimulator",
"modelCode" : "AppleTV5,3",
"identifier" : "CBB5E1ED-2172-446E-B4E7-F2B5823DBBA6",
"architecture" : "x86_64",
"modelName" : "Apple TV",
"name" : "Apple TV"
},
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : true,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "00008027-00192736010F802E",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "An iPhone (Space Gray)"
},
{
"simulator" : false,
"operatingSystemVersion" : "10.1 (14C54)",
"interface" : "usb",
"available" : true,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPad11,4",
"identifier" : "98206e7a4afd4aedaff06e687594e089dede3c44",
"architecture" : "armv7",
"modelName" : "iPad Air 3rd Gen",
"name" : "iPad 1"
},
{
"simulator" : false,
"operatingSystemVersion" : "10.1 (14C54)",
"interface" : "network",
"available" : true,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPad11,4",
"identifier" : "234234234234234234345445687594e089dede3c44",
"architecture" : "arm64",
"modelName" : "iPad Air 3rd Gen",
"name" : "A networked iPad"
},
{
"simulator" : false,
"operatingSystemVersion" : "10.1 (14C54)",
"interface" : "usb",
"available" : true,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPad11,4",
"identifier" : "f577a7903cc54959be2e34bc4f7f80b7009efcf4",
"architecture" : "BOGUS",
"modelName" : "iPad Air 3rd Gen",
"name" : "iPad 2"
},
{
"simulator" : true,
"operatingSystemVersion" : "6.1.1 (17S445)",
"available" : true,
"platform" : "com.apple.platform.watchsimulator",
"modelCode" : "Watch5,4",
"identifier" : "2D74FB11-88A0-44D0-B81E-C0C142B1C94A",
"architecture" : "i386",
"modelName" : "Apple Watch Series 5 - 44mm",
"name" : "Apple Watch Series 5 - 44mm"
},
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "c4ca6f7a53027d1b7e4972e28478e7a28e2faee2",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "iPhone",
"error" : {
"code" : -9,
"failureReason" : "",
"description" : "iPhone is not paired with your computer.",
"domain" : "com.apple.platform.iphoneos"
}
}
]
''';
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: devicesOutput,
),
);
final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
expect(devices, hasLength(5));
expect(devices[0].id, '00008027-00192736010F802E');
expect(devices[0].name, 'An iPhone (Space Gray)');
expect(await devices[0].sdkNameAndVersion, 'iOS 13.3 17C54');
expect(devices[0].cpuArchitecture, DarwinArch.arm64);
expect(devices[0].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[0].isConnected, true);
expect(devices[1].id, '98206e7a4afd4aedaff06e687594e089dede3c44');
expect(devices[1].name, 'iPad 1');
expect(await devices[1].sdkNameAndVersion, 'iOS 10.1 14C54');
expect(devices[1].cpuArchitecture, DarwinArch.armv7);
expect(devices[1].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[1].isConnected, true);
expect(devices[2].id, '234234234234234234345445687594e089dede3c44');
expect(devices[2].name, 'A networked iPad');
expect(await devices[2].sdkNameAndVersion, 'iOS 10.1 14C54');
expect(
devices[2].cpuArchitecture,
DarwinArch.arm64,
); // Defaults to arm64 for unknown architecture.
expect(devices[2].connectionInterface, DeviceConnectionInterface.wireless);
expect(devices[2].isConnected, true);
expect(devices[3].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4');
expect(devices[3].name, 'iPad 2');
expect(await devices[3].sdkNameAndVersion, 'iOS 10.1 14C54');
expect(
devices[3].cpuArchitecture,
DarwinArch.arm64,
); // Defaults to arm64 for unknown architecture.
expect(devices[3].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[3].isConnected, true);
expect(devices[4].id, 'c4ca6f7a53027d1b7e4972e28478e7a28e2faee2');
expect(devices[4].name, 'iPhone');
expect(await devices[4].sdkNameAndVersion, 'iOS 13.3 17C54');
expect(devices[4].cpuArchitecture, DarwinArch.arm64);
expect(devices[4].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[4].isConnected, false);
expect(fakeProcessManager, hasNoRemainingExpectations);
},
overrides: <Type, Generator>{
Platform: () => macPlatform,
Artifacts: () => Artifacts.test(),
},
);
testWithoutContext('available devices xcdevice fails', () async {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
exception: ProcessException('xcrun', <String>['xcdevice', 'list', '--timeout', '2']),
),
);
expect(await xcdevice.getAvailableIOSDevices(), isEmpty);
});
testWithoutContext('uses timeout', () async {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '20'],
stdout: '[]',
),
);
await xcdevice.getAvailableIOSDevices(timeout: const Duration(seconds: 20));
expect(fakeProcessManager, hasNoRemainingExpectations);
});
testUsingContext(
'ignores "Preparing debugger support for iPhone" error',
() async {
const devicesOutput = '''
[
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "43ad2fda7991b34fe1acbda82f9e2fd3d6ddc9f7",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "iPhone",
"error" : {
"code" : -10,
"failureReason" : "",
"description" : "iPhone is busy: Preparing debugger support for iPhone",
"recoverySuggestion" : "Xcode will continue when iPhone is finished.",
"domain" : "com.apple.platform.iphoneos"
}
}
]
''';
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: devicesOutput,
),
);
final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
expect(devices, hasLength(1));
expect(devices[0].id, '43ad2fda7991b34fe1acbda82f9e2fd3d6ddc9f7');
expect(fakeProcessManager, hasNoRemainingExpectations);
},
overrides: <Type, Generator>{
Platform: () => macPlatform,
Artifacts: () => Artifacts.test(),
},
);
testUsingContext(
'handles unknown architectures',
() async {
const devicesOutput = '''
[
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : true,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "d83d5bc53967baa0ee18626ba87b6254b2ab5418",
"architecture" : "armv7x",
"modelName" : "iPad 3 BOGUS",
"name" : "iPad"
},
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : true,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "43ad2fda7991b34fe1acbda82f9e2fd3d6ddc9f7",
"architecture" : "BOGUS",
"modelName" : "Future iPad",
"name" : "iPad"
}
]
''';
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: devicesOutput,
),
);
final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
expect(devices[0].cpuArchitecture, DarwinArch.armv7);
expect(devices[1].cpuArchitecture, DarwinArch.arm64);
expect(fakeProcessManager, hasNoRemainingExpectations);
},
overrides: <Type, Generator>{
Platform: () => macPlatform,
Artifacts: () => Artifacts.test(),
},
);
testUsingContext('Sdk Version is parsed correctly', () async {
const devicesOutput = '''
[
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : true,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "00008027-00192736010F802E",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "An iPhone (Space Gray)"
},
{
"simulator" : false,
"operatingSystemVersion" : "10.1",
"interface" : "usb",
"available" : true,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPad11,4",
"identifier" : "234234234234234234345445687594e089dede3c44",
"architecture" : "arm64",
"modelName" : "iPad Air 3rd Gen",
"name" : "A networked iPad"
},
{
"simulator" : false,
"interface" : "usb",
"available" : true,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPad11,4",
"identifier" : "f577a7903cc54959be2e34bc4f7f80b7009efcf4",
"architecture" : "BOGUS",
"modelName" : "iPad Air 3rd Gen",
"name" : "iPad 2"
}
]
''';
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: devicesOutput,
),
);
final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
expect(await devices[0].sdkNameAndVersion, 'iOS 13.3 17C54');
expect(await devices[1].sdkNameAndVersion, 'iOS 10.1');
expect(await devices[2].sdkNameAndVersion, 'iOS unknown version');
}, overrides: <Type, Generator>{Platform: () => macPlatform});
testUsingContext(
'use connected entry when filtering out duplicates',
() async {
const devicesOutput = '''
[
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "c4ca6f7a53027d1b7e4972e28478e7a28e2faee2",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "iPhone"
},
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "c4ca6f7a53027d1b7e4972e28478e7a28e2faee2",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "iPhone",
"error" : {
"code" : -13,
"failureReason" : "",
"description" : "iPhone iPad is not connected",
"recoverySuggestion" : "Xcode will continue when iPhone is connected and unlocked.",
"domain" : "com.apple.platform.iphoneos"
}
}
]
''';
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: devicesOutput,
),
);
final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
expect(devices, hasLength(1));
expect(devices[0].id, 'c4ca6f7a53027d1b7e4972e28478e7a28e2faee2');
expect(devices[0].name, 'iPhone');
expect(await devices[0].sdkNameAndVersion, 'iOS 13.3 17C54');
expect(devices[0].cpuArchitecture, DarwinArch.arm64);
expect(devices[0].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[0].isConnected, true);
expect(fakeProcessManager, hasNoRemainingExpectations);
},
overrides: <Type, Generator>{
Platform: () => macPlatform,
Artifacts: () => Artifacts.test(),
},
);
testUsingContext(
'use entry with sdk when filtering out duplicates',
() async {
const devicesOutput = '''
[
{
"simulator" : false,
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "c4ca6f7a53027d1b7e4972e28478e7a28e2faee2",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "iPhone_1",
"error" : {
"code" : -13,
"failureReason" : "",
"description" : "iPhone iPad is not connected",
"recoverySuggestion" : "Xcode will continue when iPhone is connected and unlocked.",
"domain" : "com.apple.platform.iphoneos"
}
},
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "c4ca6f7a53027d1b7e4972e28478e7a28e2faee2",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "iPhone_2",
"error" : {
"code" : -13,
"failureReason" : "",
"description" : "iPhone iPad is not connected",
"recoverySuggestion" : "Xcode will continue when iPhone is connected and unlocked.",
"domain" : "com.apple.platform.iphoneos"
}
}
]
''';
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: devicesOutput,
),
);
final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
expect(devices, hasLength(1));
expect(devices[0].id, 'c4ca6f7a53027d1b7e4972e28478e7a28e2faee2');
expect(devices[0].name, 'iPhone_2');
expect(await devices[0].sdkNameAndVersion, 'iOS 13.3 17C54');
expect(devices[0].cpuArchitecture, DarwinArch.arm64);
expect(devices[0].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[0].isConnected, false);
expect(fakeProcessManager, hasNoRemainingExpectations);
},
overrides: <Type, Generator>{
Platform: () => macPlatform,
Artifacts: () => Artifacts.test(),
},
);
testUsingContext(
'use entry with higher sdk when filtering out duplicates',
() async {
const devicesOutput = '''
[
{
"simulator" : false,
"operatingSystemVersion" : "14.3 (17C54)",
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "c4ca6f7a53027d1b7e4972e28478e7a28e2faee2",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "iPhone_1",
"error" : {
"code" : -13,
"failureReason" : "",
"description" : "iPhone iPad is not connected",
"recoverySuggestion" : "Xcode will continue when iPhone is connected and unlocked.",
"domain" : "com.apple.platform.iphoneos"
}
},
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "c4ca6f7a53027d1b7e4972e28478e7a28e2faee2",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "iPhone_2",
"error" : {
"code" : -13,
"failureReason" : "",
"description" : "iPhone iPad is not connected",
"recoverySuggestion" : "Xcode will continue when iPhone is connected and unlocked.",
"domain" : "com.apple.platform.iphoneos"
}
}
]
''';
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: devicesOutput,
),
);
final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
expect(devices, hasLength(1));
expect(devices[0].id, 'c4ca6f7a53027d1b7e4972e28478e7a28e2faee2');
expect(devices[0].name, 'iPhone_1');
expect(await devices[0].sdkNameAndVersion, 'iOS 14.3 17C54');
expect(devices[0].cpuArchitecture, DarwinArch.arm64);
expect(devices[0].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[0].isConnected, false);
expect(fakeProcessManager, hasNoRemainingExpectations);
},
overrides: <Type, Generator>{
Platform: () => macPlatform,
Artifacts: () => Artifacts.test(),
},
);
testUsingContext('handles bad output', () async {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: 'Something bad happened, not JSON',
),
);
final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
expect(devices, isEmpty);
expect(logger.errorText, contains('xcdevice returned non-JSON response'));
}, overrides: <Type, Generator>{Platform: () => macPlatform});
group('with CoreDevices', () {
testUsingContext('wireless discovery is cancelled', () async {
final getCoreDevicesCompleter = Completer<void>();
final coreDeviceControl = FakeIOSCoreDeviceControl(
getCoreDevicesCompleter: getCoreDevicesCompleter,
);
coreDeviceControl.devices.add(
FakeIOSCoreDevice(
udid: '00008110-00062D2E2632801E',
connectionInterface: DeviceConnectionInterface.wireless,
developerModeStatus: 'enabled',
),
);
xcdevice = XCDevice(
processManager: fakeProcessManager,
logger: logger,
xcode: xcode,
platform: FakePlatform(operatingSystem: 'macos'),
artifacts: Artifacts.test(),
cache: Cache.test(processManager: FakeProcessManager.any()),
iproxy: IProxy.test(logger: logger, processManager: fakeProcessManager),
fileSystem: fileSystem,
coreDeviceControl: coreDeviceControl,
xcodeDebug: FakeXcodeDebug(),
analytics: fakeAnalytics,
shutdownHooks: FakeShutdownHooks(),
);
const devicesOutput = '''
[
{
"simulator" : false,
"operatingSystemVersion" : "17.0 (17C54)",
"interface" : "network",
"available" : true,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone15,1",
"identifier" : "234234234234234234345445687594e089dede3c44",
"architecture" : "arm64",
"modelName" : "iPhone 14",
"name" : "A networked iPhone"
}
]
''';
fakeProcessManager.addCommands(<FakeCommand>[
const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: devicesOutput,
),
]);
final Future<List<IOSDevice>> futureDevices = xcdevice
.getAvailableIOSDevicesForWirelessDiscovery();
await pumpEventQueue();
xcdevice.cancelWirelessDiscovery();
getCoreDevicesCompleter.complete();
final List<IOSDevice> devices = await futureDevices;
expect(devices, isEmpty);
expect(fakeProcessManager, hasNoRemainingExpectations);
});
testUsingContext(
'returns devices with corresponding CoreDevices',
() async {
const devicesOutput = '''
[
{
"simulator" : true,
"operatingSystemVersion" : "13.3 (17K446)",
"available" : true,
"platform" : "com.apple.platform.appletvsimulator",
"modelCode" : "AppleTV5,3",
"identifier" : "CBB5E1ED-2172-446E-B4E7-F2B5823DBBA6",
"architecture" : "x86_64",
"modelName" : "Apple TV",
"name" : "Apple TV"
},
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : true,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "00008027-00192736010F802E",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "An iPhone (Space Gray)"
},
{
"simulator" : false,
"operatingSystemVersion" : "10.1 (14C54)",
"interface" : "usb",
"available" : true,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPad11,4",
"identifier" : "98206e7a4afd4aedaff06e687594e089dede3c44",
"architecture" : "armv7",
"modelName" : "iPad Air 3rd Gen",
"name" : "iPad 1"
},
{
"simulator" : false,
"operatingSystemVersion" : "10.1 (14C54)",
"interface" : "network",
"available" : true,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPad11,4",
"identifier" : "234234234234234234345445687594e089dede3c44",
"architecture" : "arm64",
"modelName" : "iPad Air 3rd Gen",
"name" : "A networked iPad"
},
{
"simulator" : false,
"operatingSystemVersion" : "10.1 (14C54)",
"interface" : "usb",
"available" : true,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPad11,4",
"identifier" : "f577a7903cc54959be2e34bc4f7f80b7009efcf4",
"architecture" : "BOGUS",
"modelName" : "iPad Air 3rd Gen",
"name" : "iPad 2"
},
{
"simulator" : true,
"operatingSystemVersion" : "6.1.1 (17S445)",
"available" : true,
"platform" : "com.apple.platform.watchsimulator",
"modelCode" : "Watch5,4",
"identifier" : "2D74FB11-88A0-44D0-B81E-C0C142B1C94A",
"architecture" : "i386",
"modelName" : "Apple Watch Series 5 - 44mm",
"name" : "Apple Watch Series 5 - 44mm"
},
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "c4ca6f7a53027d1b7e4972e28478e7a28e2faee2",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "iPhone",
"error" : {
"code" : -9,
"failureReason" : "",
"description" : "iPhone is not paired with your computer.",
"domain" : "com.apple.platform.iphoneos"
}
}
]
''';
coreDeviceControl.devices.addAll(<FakeIOSCoreDevice>[
FakeIOSCoreDevice(
udid: '00008027-00192736010F802E',
connectionInterface: DeviceConnectionInterface.wireless,
developerModeStatus: 'enabled',
),
FakeIOSCoreDevice(
connectionInterface: DeviceConnectionInterface.wireless,
developerModeStatus: 'enabled',
),
FakeIOSCoreDevice(
udid: '234234234234234234345445687594e089dede3c44',
connectionInterface: DeviceConnectionInterface.attached,
),
FakeIOSCoreDevice(
udid: 'f577a7903cc54959be2e34bc4f7f80b7009efcf4',
connectionInterface: DeviceConnectionInterface.attached,
developerModeStatus: 'disabled',
),
]);
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: devicesOutput,
),
);
final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
expect(devices, hasLength(5));
expect(devices[0].id, '00008027-00192736010F802E');
expect(devices[0].name, 'An iPhone (Space Gray)');
expect(await devices[0].sdkNameAndVersion, 'iOS 13.3 17C54');
expect(devices[0].cpuArchitecture, DarwinArch.arm64);
expect(devices[0].connectionInterface, DeviceConnectionInterface.wireless);
expect(devices[0].isConnected, true);
expect(devices[0].devModeEnabled, true);
expect(devices[1].id, '98206e7a4afd4aedaff06e687594e089dede3c44');
expect(devices[1].name, 'iPad 1');
expect(await devices[1].sdkNameAndVersion, 'iOS 10.1 14C54');
expect(devices[1].cpuArchitecture, DarwinArch.armv7);
expect(devices[1].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[1].isConnected, true);
expect(devices[1].devModeEnabled, true);
expect(devices[2].id, '234234234234234234345445687594e089dede3c44');
expect(devices[2].name, 'A networked iPad');
expect(await devices[2].sdkNameAndVersion, 'iOS 10.1 14C54');
expect(
devices[2].cpuArchitecture,
DarwinArch.arm64,
); // Defaults to arm64 for unknown architecture.
expect(devices[2].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[2].isConnected, true);
expect(devices[2].devModeEnabled, false);
expect(devices[3].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4');
expect(devices[3].name, 'iPad 2');
expect(await devices[3].sdkNameAndVersion, 'iOS 10.1 14C54');
expect(
devices[3].cpuArchitecture,
DarwinArch.arm64,
); // Defaults to arm64 for unknown architecture.
expect(devices[3].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[3].isConnected, true);
expect(devices[3].devModeEnabled, false);
expect(devices[4].id, 'c4ca6f7a53027d1b7e4972e28478e7a28e2faee2');
expect(devices[4].name, 'iPhone');
expect(await devices[4].sdkNameAndVersion, 'iOS 13.3 17C54');
expect(devices[4].cpuArchitecture, DarwinArch.arm64);
expect(devices[4].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[4].isConnected, false);
expect(devices[4].devModeEnabled, true);
expect(fakeProcessManager, hasNoRemainingExpectations);
expect(
fakeAnalytics.sentEvents,
contains(Event.appleUsageEvent(workflow: 'device', parameter: 'ios-trust-failure')),
);
},
overrides: <Type, Generator>{
Platform: () => macPlatform,
Artifacts: () => Artifacts.test(),
},
);
});
});
group('diagnostics', () {
final macPlatform = FakePlatform(operatingSystem: 'macos');
testUsingContext('uses cache', () async {
const devicesOutput = '''
[
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "network",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "d83d5bc53967baa0ee18626ba87b6254b2ab5418",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"error" : {
"code" : -13,
"failureReason" : "",
"domain" : "com.apple.platform.iphoneos"
}
}
]
''';
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: devicesOutput,
),
);
await xcdevice.getAvailableIOSDevices();
final List<String> errors = await xcdevice.getDiagnostics();
expect(errors, hasLength(1));
expect(fakeProcessManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{Platform: () => macPlatform});
testWithoutContext('diagnostics xcdevice fails', () async {
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
exception: ProcessException('xcrun', <String>['xcdevice', 'list', '--timeout', '2']),
),
);
expect(await xcdevice.getDiagnostics(), isEmpty);
});
testUsingContext('returns error message', () async {
const devicesOutput = '''
[
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "98206e7a4afd4aedaff06e687594e089dede3c44",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "An iPhone (Space Gray)",
"error" : {
"code" : -9,
"failureReason" : "",
"underlyingErrors" : [
{
"code" : 5,
"failureReason" : "allowsSecureServices: 1. isConnected: 0. Platform: <DVTPlatform:0x7f804ce32880:'com.apple.platform.iphoneos':<DVTFilePath:0x7f804ce32800:'/Users/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform'>>. DTDKDeviceIdentifierIsIDID: 0",
"description" : "📱<DVTiOSDevice (0x7f801f190450), iPhone, iPhone, 13.3 (17C54), d83d5bc53967baa0ee18626ba87b6254b2ab5418> -- Failed _shouldMakeReadyForDevelopment check even though device is not locked by passcode.",
"recoverySuggestion" : "",
"domain" : "com.apple.platform.iphoneos"
}
],
"description" : "iPhone is not paired with your computer.",
"recoverySuggestion" : "To use iPhone with Xcode, unlock it and choose to trust this computer when prompted.",
"domain" : "com.apple.platform.iphoneos"
}
},
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "d83d5bc53967baa0ee18626ba87b6254b2ab5418",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "iPhone",
"error" : {
"failureReason" : "",
"description" : "iPhone is not paired with your computer",
"domain" : "com.apple.platform.iphoneos"
}
},
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "network",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "d83d5bc53967baa0ee18626ba87b6254b2ab5418",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"error" : {
"code" : -13,
"failureReason" : "",
"domain" : "com.apple.platform.iphoneos"
}
},
{
"simulator" : false,
"operatingSystemVersion" : "13.3 (17C54)",
"interface" : "usb",
"available" : false,
"platform" : "com.apple.platform.iphoneos",
"modelCode" : "iPhone8,1",
"identifier" : "43ad2fda7991b34fe1acbda82f9e2fd3d6ddc9f7",
"architecture" : "arm64",
"modelName" : "iPhone 6s",
"name" : "iPhone",
"error" : {
"code" : -10,
"failureReason" : "",
"description" : "iPhone is busy: Preparing debugger support for iPhone",
"recoverySuggestion" : "Xcode will continue when iPhone is finished.",
"domain" : "com.apple.platform.iphoneos"
}
},
{
"modelCode" : "iPad8,5",
"simulator" : false,
"modelName" : "iPad Pro (12.9-inch) (3rd generation)",
"error" : {
"code" : -13,
"failureReason" : "",
"underlyingErrors" : [
{
"code" : 4,
"failureReason" : "",
"description" : "iPad is locked.",
"recoverySuggestion" : "To use iPad with Xcode, unlock it.",
"domain" : "DVTDeviceIneligibilityErrorDomain"
}
],
"description" : "iPad is not connected",
"recoverySuggestion" : "Xcode will continue when iPad is connected.",
"domain" : "com.apple.platform.iphoneos"
},
"operatingSystemVersion" : "15.6 (19G5027e)",
"identifier" : "00008027-0019253601068123",
"platform" : "com.apple.platform.iphoneos",
"architecture" : "arm64e",
"interface" : "usb",
"available" : false,
"name" : "iPad",
"modelUTI" : "com.apple.ipad-pro-12point9-1"
}
]
''';
fakeProcessManager.addCommand(
const FakeCommand(
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
stdout: devicesOutput,
),
);
final List<String> errors = await xcdevice.getDiagnostics();
expect(errors, hasLength(4));
expect(
errors[0],
'Error: iPhone is not paired with your computer. To use iPhone with Xcode, unlock it and choose to trust this computer when prompted. (code -9)',
);
expect(errors[1], 'Error: iPhone is not paired with your computer.');
expect(errors[2], 'Error: Xcode pairing error. (code -13)');
expect(
errors[3],
'Error: iPhone is busy: Preparing debugger support for iPhone. Xcode will continue when iPhone is finished. (code -10)',
);
expect(errors, isNot(contains('Xcode will continue')));
expect(fakeProcessManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{Platform: () => macPlatform});
});
});
});
}
class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
@override
Version version = Version(0, 0, 0);
@override
bool isInstalled = false;
@override
List<String> xcrunCommand() => <String>['xcrun'];
}
class FakeXcodeDebug extends Fake implements XcodeDebug {}
class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {
FakeIOSCoreDeviceControl({this.getCoreDevicesCompleter});
final Completer<void>? getCoreDevicesCompleter;
List<FakeIOSCoreDevice> devices = <FakeIOSCoreDevice>[];
@override
Future<List<IOSCoreDevice>> getCoreDevices({
Duration timeout = Duration.zero,
Completer<void>? cancelCompleter,
}) async {
if (getCoreDevicesCompleter != null) {
await getCoreDevicesCompleter!.future;
}
return devices;
}
}
class FakeIOSCoreDevice extends Fake implements IOSCoreDevice {
FakeIOSCoreDevice({this.udid, this.connectionInterface, this.developerModeStatus});
final String? developerModeStatus;
@override
final String? udid;
@override
final DeviceConnectionInterface? connectionInterface;
@override
IOSCoreDeviceProperties? get deviceProperties =>
FakeIOSCoreDeviceProperties(developerModeStatus: developerModeStatus);
}
class FakeIOSCoreDeviceProperties extends Fake implements IOSCoreDeviceProperties {
FakeIOSCoreDeviceProperties({required this.developerModeStatus});
@override
final String? developerModeStatus;
}