mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Redo of https://github.com/flutter/flutter/pull/130728 - code is the same as before. That PR was stuck in Google testing and then I messed up the rebase so started over. ---- Starting in Xcode 15, the simulator is no longer included in Xcode and must be downloaded and installed separately. This adds a validation to `flutter doctor` to warn when the needed simulator runtime is missing. Validation message looks like: ``` [!] Xcode - develop for iOS and macOS (Xcode 15.0) ! iOS 17.0 Simulator not installed; this may be necessary for iOS and macOS development. To download and install the platform, open Xcode, select Xcode > Settings > Platforms, and click the GET button for the required platform. For more information, please visit: https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes ``` It may also show an error like this when something goes wrong when checking for the simulator: ``` [!] Xcode - develop for iOS and macOS (Xcode 15.0) â Unable to find the iPhone Simulator SDK. ``` Note: I'm unsure of in the future if the SDK and the simulator runtime will need to match the exact version or just the major. For now, it only checks against the major version. Part 3 of https://github.com/flutter/flutter/issues/129558.
410 lines
14 KiB
Dart
410 lines
14 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:flutter_tools/src/android/android_workflow.dart';
|
|
import 'package:flutter_tools/src/base/bot_detector.dart';
|
|
import 'package:flutter_tools/src/base/config.dart';
|
|
import 'package:flutter_tools/src/base/context.dart';
|
|
import 'package:flutter_tools/src/base/file_system.dart';
|
|
import 'package:flutter_tools/src/base/io.dart';
|
|
import 'package:flutter_tools/src/base/logger.dart';
|
|
import 'package:flutter_tools/src/base/os.dart';
|
|
import 'package:flutter_tools/src/base/process.dart';
|
|
import 'package:flutter_tools/src/base/signals.dart';
|
|
import 'package:flutter_tools/src/base/template.dart';
|
|
import 'package:flutter_tools/src/base/terminal.dart';
|
|
import 'package:flutter_tools/src/base/version.dart';
|
|
import 'package:flutter_tools/src/cache.dart';
|
|
import 'package:flutter_tools/src/context_runner.dart';
|
|
import 'package:flutter_tools/src/dart/pub.dart';
|
|
import 'package:flutter_tools/src/device.dart';
|
|
import 'package:flutter_tools/src/doctor.dart';
|
|
import 'package:flutter_tools/src/doctor_validator.dart';
|
|
import 'package:flutter_tools/src/globals.dart' as globals;
|
|
import 'package:flutter_tools/src/ios/plist_parser.dart';
|
|
import 'package:flutter_tools/src/ios/simulators.dart';
|
|
import 'package:flutter_tools/src/ios/xcodeproj.dart';
|
|
import 'package:flutter_tools/src/isolated/mustache_template.dart';
|
|
import 'package:flutter_tools/src/persistent_tool_state.dart';
|
|
import 'package:flutter_tools/src/project.dart';
|
|
import 'package:flutter_tools/src/reporting/crash_reporting.dart';
|
|
import 'package:flutter_tools/src/reporting/reporting.dart';
|
|
import 'package:flutter_tools/src/version.dart';
|
|
import 'package:meta/meta.dart';
|
|
import 'package:test/fake.dart';
|
|
|
|
import 'common.dart';
|
|
import 'fake_http_client.dart';
|
|
import 'fake_process_manager.dart';
|
|
import 'fakes.dart';
|
|
import 'throwing_pub.dart';
|
|
|
|
export 'package:flutter_tools/src/base/context.dart' show Generator;
|
|
|
|
export 'fake_process_manager.dart' show FakeCommand, FakeProcessManager, ProcessManager;
|
|
|
|
/// Return the test logger. This assumes that the current Logger is a BufferLogger.
|
|
BufferLogger get testLogger => context.get<Logger>()! as BufferLogger;
|
|
|
|
FakeDeviceManager get testDeviceManager => context.get<DeviceManager>()! as FakeDeviceManager;
|
|
|
|
@isTest
|
|
void testUsingContext(
|
|
String description,
|
|
dynamic Function() testMethod, {
|
|
Map<Type, Generator> overrides = const <Type, Generator>{},
|
|
bool initializeFlutterRoot = true,
|
|
String? testOn,
|
|
bool? skip, // should default to `false`, but https://github.com/dart-lang/test/issues/545 doesn't allow this
|
|
}) {
|
|
if (overrides[FileSystem] != null && overrides[ProcessManager] == null) {
|
|
throw StateError(
|
|
'If you override the FileSystem context you must also provide a ProcessManager, '
|
|
'otherwise the processes you launch will not be dealing with the same file system '
|
|
'that you are dealing with in your test.'
|
|
);
|
|
}
|
|
if (overrides.containsKey(ProcessUtils)) {
|
|
throw StateError('Do not inject ProcessUtils for testing, use ProcessManager instead.');
|
|
}
|
|
|
|
// Ensure we don't rely on the default [Config] constructor which will
|
|
// leak a sticky $HOME/.flutter_settings behind!
|
|
Directory? configDir;
|
|
tearDown(() {
|
|
if (configDir != null) {
|
|
tryToDelete(configDir!);
|
|
configDir = null;
|
|
}
|
|
});
|
|
Config buildConfig(FileSystem fs) {
|
|
configDir ??= globals.fs.systemTempDirectory.createTempSync('flutter_config_dir_test.');
|
|
return Config.test(
|
|
name: Config.kFlutterSettings,
|
|
directory: configDir,
|
|
logger: globals.logger,
|
|
);
|
|
}
|
|
PersistentToolState buildPersistentToolState(FileSystem fs) {
|
|
configDir ??= globals.fs.systemTempDirectory.createTempSync('flutter_config_dir_test.');
|
|
return PersistentToolState.test(
|
|
directory: configDir!,
|
|
logger: globals.logger,
|
|
);
|
|
}
|
|
|
|
test(description, () async {
|
|
await runInContext<dynamic>(() {
|
|
return context.run<dynamic>(
|
|
name: 'mocks',
|
|
overrides: <Type, Generator>{
|
|
AnsiTerminal: () => AnsiTerminal(platform: globals.platform, stdio: globals.stdio),
|
|
Config: () => buildConfig(globals.fs),
|
|
DeviceManager: () => FakeDeviceManager(),
|
|
Doctor: () => FakeDoctor(globals.logger),
|
|
FlutterVersion: () => FakeFlutterVersion(),
|
|
HttpClient: () => FakeHttpClient.any(),
|
|
IOSSimulatorUtils: () => const NoopIOSSimulatorUtils(),
|
|
OutputPreferences: () => OutputPreferences.test(),
|
|
Logger: () => BufferLogger.test(),
|
|
OperatingSystemUtils: () => FakeOperatingSystemUtils(),
|
|
PersistentToolState: () => buildPersistentToolState(globals.fs),
|
|
Usage: () => TestUsage(),
|
|
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(),
|
|
FileSystem: () => LocalFileSystemBlockingSetCurrentDirectory(),
|
|
PlistParser: () => FakePlistParser(),
|
|
Signals: () => FakeSignals(),
|
|
Pub: () => ThrowingPub(), // prevent accidentally using pub.
|
|
CrashReporter: () => const NoopCrashReporter(),
|
|
TemplateRenderer: () => const MustacheTemplateRenderer(),
|
|
},
|
|
body: () {
|
|
return runZonedGuarded<Future<dynamic>>(() {
|
|
try {
|
|
return context.run<dynamic>(
|
|
// Apply the overrides to the test context in the zone since their
|
|
// instantiation may reference items already stored on the context.
|
|
overrides: overrides,
|
|
name: 'test-specific overrides',
|
|
body: () async {
|
|
if (initializeFlutterRoot) {
|
|
// Provide a sane default for the flutterRoot directory. Individual
|
|
// tests can override this either in the test or during setup.
|
|
Cache.flutterRoot ??= getFlutterRoot();
|
|
}
|
|
return await testMethod();
|
|
},
|
|
);
|
|
// This catch rethrows, so doesn't need to catch only Exception.
|
|
} catch (error) { // ignore: avoid_catches_without_on_clauses
|
|
_printBufferedErrors(context);
|
|
rethrow;
|
|
}
|
|
}, (Object error, StackTrace stackTrace) {
|
|
// When things fail, it's ok to print to the console!
|
|
print(error); // ignore: avoid_print
|
|
print(stackTrace); // ignore: avoid_print
|
|
_printBufferedErrors(context);
|
|
throw error; //ignore: only_throw_errors
|
|
});
|
|
},
|
|
);
|
|
}, overrides: <Type, Generator>{
|
|
// This has to go here so that runInContext will pick it up when it tries
|
|
// to do bot detection before running the closure. This is important
|
|
// because the test may be giving us a fake HttpClientFactory, which may
|
|
// throw in unexpected/abnormal ways.
|
|
// If a test needs a BotDetector that does not always return true, it
|
|
// can provide the AlwaysFalseBotDetector in the overrides, or its own
|
|
// BotDetector implementation in the overrides.
|
|
BotDetector: overrides[BotDetector] ?? () => const FakeBotDetector(true),
|
|
});
|
|
}, testOn: testOn, skip: skip);
|
|
// We don't support "timeout"; see ../../dart_test.yaml which
|
|
// configures all tests to have a 15 minute timeout which should
|
|
// definitely be enough.
|
|
}
|
|
|
|
void _printBufferedErrors(AppContext testContext) {
|
|
if (testContext.get<Logger>() is BufferLogger) {
|
|
final BufferLogger bufferLogger = testContext.get<Logger>()! as BufferLogger;
|
|
if (bufferLogger.errorText.isNotEmpty) {
|
|
// This is where the logger outputting errors is implemented, so it has
|
|
// to use `print`.
|
|
print(bufferLogger.errorText); // ignore: avoid_print
|
|
}
|
|
bufferLogger.clear();
|
|
}
|
|
}
|
|
|
|
class FakeDeviceManager implements DeviceManager {
|
|
List<Device> attachedDevices = <Device>[];
|
|
List<Device> wirelessDevices = <Device>[];
|
|
|
|
String? _specifiedDeviceId;
|
|
|
|
@override
|
|
String? get specifiedDeviceId {
|
|
if (_specifiedDeviceId == null || _specifiedDeviceId == 'all') {
|
|
return null;
|
|
}
|
|
return _specifiedDeviceId;
|
|
}
|
|
|
|
@override
|
|
set specifiedDeviceId(String? id) {
|
|
_specifiedDeviceId = id;
|
|
}
|
|
|
|
@override
|
|
bool get hasSpecifiedDeviceId => specifiedDeviceId != null;
|
|
|
|
@override
|
|
bool get hasSpecifiedAllDevices {
|
|
return _specifiedDeviceId != null && _specifiedDeviceId == 'all';
|
|
}
|
|
|
|
@override
|
|
Future<List<Device>> getAllDevices({
|
|
DeviceDiscoveryFilter? filter,
|
|
}) async => filteredDevices(filter);
|
|
|
|
@override
|
|
Future<List<Device>> refreshAllDevices({
|
|
Duration? timeout,
|
|
DeviceDiscoveryFilter? filter,
|
|
}) async => filteredDevices(filter);
|
|
|
|
@override
|
|
Future<List<Device>> refreshExtendedWirelessDeviceDiscoverers({
|
|
Duration? timeout,
|
|
DeviceDiscoveryFilter? filter,
|
|
}) async => filteredDevices(filter);
|
|
|
|
@override
|
|
Future<List<Device>> getDevicesById(
|
|
String deviceId, {
|
|
DeviceDiscoveryFilter? filter,
|
|
bool waitForDeviceToConnect = false,
|
|
}) async {
|
|
return filteredDevices(filter).where((Device device) {
|
|
return device.id == deviceId || device.id.startsWith(deviceId);
|
|
}).toList();
|
|
}
|
|
|
|
@override
|
|
Future<List<Device>> getDevices({
|
|
DeviceDiscoveryFilter? filter,
|
|
bool waitForDeviceToConnect = false,
|
|
}) {
|
|
return hasSpecifiedDeviceId
|
|
? getDevicesById(specifiedDeviceId!, filter: filter)
|
|
: getAllDevices(filter: filter);
|
|
}
|
|
|
|
void addAttachedDevice(Device device) => attachedDevices.add(device);
|
|
void addWirelessDevice(Device device) => wirelessDevices.add(device);
|
|
|
|
@override
|
|
bool get canListAnything => true;
|
|
|
|
@override
|
|
Future<List<String>> getDeviceDiagnostics() async => <String>[];
|
|
|
|
@override
|
|
List<DeviceDiscovery> get deviceDiscoverers => <DeviceDiscovery>[];
|
|
|
|
@override
|
|
DeviceDiscoverySupportFilter deviceSupportFilter({
|
|
bool includeDevicesUnsupportedByProject = false,
|
|
FlutterProject? flutterProject,
|
|
}) {
|
|
return TestDeviceDiscoverySupportFilter();
|
|
}
|
|
|
|
@override
|
|
Device? getSingleEphemeralDevice(List<Device> devices) => null;
|
|
|
|
List<Device> filteredDevices(DeviceDiscoveryFilter? filter) {
|
|
if (filter?.deviceConnectionInterface == DeviceConnectionInterface.attached) {
|
|
return attachedDevices;
|
|
}
|
|
if (filter?.deviceConnectionInterface == DeviceConnectionInterface.wireless) {
|
|
return wirelessDevices;
|
|
}
|
|
return attachedDevices + wirelessDevices;
|
|
}
|
|
}
|
|
|
|
class TestDeviceDiscoverySupportFilter extends Fake implements DeviceDiscoverySupportFilter {
|
|
TestDeviceDiscoverySupportFilter();
|
|
}
|
|
|
|
class FakeAndroidLicenseValidator extends Fake implements AndroidLicenseValidator {
|
|
@override
|
|
Future<LicensesAccepted> get licensesAccepted async => LicensesAccepted.all;
|
|
}
|
|
|
|
class FakeDoctor extends Doctor {
|
|
FakeDoctor(Logger logger) : super(logger: logger);
|
|
|
|
// True for testing.
|
|
@override
|
|
bool get canListAnything => true;
|
|
|
|
// True for testing.
|
|
@override
|
|
bool get canLaunchAnything => true;
|
|
|
|
@override
|
|
/// Replaces the android workflow with a version that overrides licensesAccepted,
|
|
/// to prevent individual tests from having to mock out the process for
|
|
/// the Doctor.
|
|
List<DoctorValidator> get validators {
|
|
final List<DoctorValidator> superValidators = super.validators;
|
|
return superValidators.map<DoctorValidator>((DoctorValidator v) {
|
|
if (v is AndroidLicenseValidator) {
|
|
return FakeAndroidLicenseValidator();
|
|
}
|
|
return v;
|
|
}).toList();
|
|
}
|
|
}
|
|
|
|
class NoopIOSSimulatorUtils implements IOSSimulatorUtils {
|
|
const NoopIOSSimulatorUtils();
|
|
|
|
@override
|
|
Future<List<IOSSimulator>> getAttachedDevices() async => <IOSSimulator>[];
|
|
|
|
@override
|
|
Future<List<IOSSimulatorRuntime>> getAvailableIOSRuntimes() async => <IOSSimulatorRuntime>[];
|
|
}
|
|
|
|
class FakeXcodeProjectInterpreter implements XcodeProjectInterpreter {
|
|
@override
|
|
bool get isInstalled => true;
|
|
|
|
@override
|
|
String get versionText => 'Xcode 14';
|
|
|
|
@override
|
|
Version get version => Version(14, null, null);
|
|
|
|
@override
|
|
String get build => '14A309';
|
|
|
|
@override
|
|
Future<Map<String, String>> getBuildSettings(
|
|
String projectPath, {
|
|
XcodeProjectBuildContext? buildContext,
|
|
Duration timeout = const Duration(minutes: 1),
|
|
}) async {
|
|
return <String, String>{};
|
|
}
|
|
|
|
@override
|
|
Future<String> pluginsBuildSettingsOutput(
|
|
Directory podXcodeProject, {
|
|
Duration timeout = const Duration(minutes: 1),
|
|
}) async {
|
|
return '';
|
|
}
|
|
|
|
@override
|
|
Future<void> cleanWorkspace(String workspacePath, String scheme, { bool verbose = false }) async { }
|
|
|
|
@override
|
|
Future<XcodeProjectInfo> getInfo(String projectPath, {String? projectFilename}) async {
|
|
return XcodeProjectInfo(
|
|
<String>['Runner'],
|
|
<String>['Debug', 'Release'],
|
|
<String>['Runner'],
|
|
BufferLogger.test(),
|
|
);
|
|
}
|
|
|
|
@override
|
|
List<String> xcrunCommand() => <String>['xcrun'];
|
|
}
|
|
|
|
/// Prevent test crashest from being reported to the crash backend.
|
|
class NoopCrashReporter implements CrashReporter {
|
|
const NoopCrashReporter();
|
|
|
|
@override
|
|
Future<void> informUser(CrashDetails details, File crashFile) async { }
|
|
}
|
|
|
|
class LocalFileSystemBlockingSetCurrentDirectory extends LocalFileSystem {
|
|
LocalFileSystemBlockingSetCurrentDirectory() : super.test(
|
|
signals: LocalSignals.instance,
|
|
);
|
|
|
|
@override
|
|
set currentDirectory(dynamic value) {
|
|
throw Exception('globals.fs.currentDirectory should not be set on the local file system during '
|
|
'tests as this can cause race conditions with concurrent tests. '
|
|
'Consider using a MemoryFileSystem for testing if possible or refactor '
|
|
'code to not require setting globals.fs.currentDirectory.');
|
|
}
|
|
}
|
|
|
|
class FakeSignals implements Signals {
|
|
@override
|
|
Object addHandler(ProcessSignal signal, SignalHandler handler) {
|
|
return const Object();
|
|
}
|
|
|
|
@override
|
|
Future<bool> removeHandler(ProcessSignal signal, Object token) async {
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Stream<Object> get errors => const Stream<Object>.empty();
|
|
}
|