mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

Runs tests on Xcode 16.2 and iOS 18.2. Also updates engine scenario golden files to iOS 18.2 and removes non-impeller (Skia) test files that we no longer use. All framework tests passing: https://github.com/flutter/flutter/issues/148899#issuecomment-2701465612 All engine tests passing: https://github.com/flutter/flutter/issues/148906#issuecomment-2702112378 Fixes https://github.com/flutter/flutter/issues/148907 and https://github.com/flutter/flutter/issues/148957. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- 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
298 lines
10 KiB
Dart
298 lines
10 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:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:path/path.dart' as path;
|
|
|
|
import 'host_agent.dart';
|
|
import 'utils.dart';
|
|
|
|
typedef SimulatorFunction = Future<void> Function(String deviceId);
|
|
|
|
Future<String> fileType(String pathToBinary) {
|
|
return eval('file', <String>[pathToBinary]);
|
|
}
|
|
|
|
Future<String?> minPhoneOSVersion(String pathToBinary) async {
|
|
final String loadCommands = await eval('otool', <String>['-l', '-arch', 'arm64', pathToBinary]);
|
|
if (!loadCommands.contains('LC_VERSION_MIN_IPHONEOS')) {
|
|
return null;
|
|
}
|
|
|
|
String? minVersion;
|
|
// Load command 7
|
|
// cmd LC_VERSION_MIN_IPHONEOS
|
|
// cmdsize 16
|
|
// version 9.0
|
|
// sdk 15.2
|
|
// ...
|
|
final List<String> lines = LineSplitter.split(loadCommands).toList();
|
|
lines.asMap().forEach((int index, String line) {
|
|
if (line.contains('LC_VERSION_MIN_IPHONEOS') && lines.length - index - 1 > 3) {
|
|
final String versionLine = lines.skip(index - 1).take(4).last;
|
|
final RegExp versionRegex = RegExp(r'\s*version\s*(\S*)');
|
|
minVersion = versionRegex.firstMatch(versionLine)?.group(1);
|
|
}
|
|
});
|
|
return minVersion;
|
|
}
|
|
|
|
/// Creates and boots a new simulator, passes the new simulator's identifier to
|
|
/// `testFunction`.
|
|
///
|
|
/// Remember to call removeIOSSimulator in the test teardown.
|
|
Future<void> testWithNewIOSSimulator(
|
|
String deviceName,
|
|
SimulatorFunction testFunction, {
|
|
String deviceTypeId = 'com.apple.CoreSimulator.SimDeviceType.iPhone-11',
|
|
}) async {
|
|
final String availableRuntimes = await eval('xcrun', <String>[
|
|
'simctl',
|
|
'list',
|
|
'runtimes',
|
|
], workingDirectory: flutterDirectory.path);
|
|
|
|
final String runtimesForSelectedXcode = await eval('xcrun', <String>[
|
|
'simctl',
|
|
'runtime',
|
|
'match',
|
|
'list',
|
|
'--json',
|
|
], workingDirectory: flutterDirectory.path);
|
|
|
|
// First check for userOverriddenBuild, which may be set in CI by mac_toolchain.
|
|
// Next, get the preferred runtime build for the selected Xcode version. Preferred
|
|
// means the runtime was either bundled with Xcode, exactly matched your SDK
|
|
// version, or it's indicated a better match for your SDK.
|
|
final Map<String, Object?> decodeResult =
|
|
json.decode(runtimesForSelectedXcode) as Map<String, Object?>;
|
|
final String? iosKey =
|
|
decodeResult.keys.where((String key) => key.contains('iphoneos')).firstOrNull;
|
|
final String? runtimeBuildForSelectedXcode = switch (decodeResult[iosKey]) {
|
|
{'userOverriddenBuild': final String build} => build,
|
|
{'preferredBuild': final String build} => build,
|
|
_ => null,
|
|
};
|
|
|
|
String? iOSSimRuntime;
|
|
|
|
final RegExp iOSRuntimePattern = RegExp(r'iOS .*\) - (.*)');
|
|
|
|
// [availableRuntimes] may include runtime versions greater than the selected
|
|
// Xcode's greatest supported version. Use [runtimeBuildForSelectedXcode] when
|
|
// possible to pick which runtime to use.
|
|
// For example, iOS 17 (released with Xcode 15) may be available even if the
|
|
// selected Xcode version is 14.
|
|
for (final String runtime in LineSplitter.split(availableRuntimes)) {
|
|
if (runtimeBuildForSelectedXcode != null && !runtime.contains(runtimeBuildForSelectedXcode)) {
|
|
continue;
|
|
}
|
|
// These seem to be in order, so allow matching multiple lines so it grabs
|
|
// the last (hopefully latest) one.
|
|
final RegExpMatch? iOSRuntimeMatch = iOSRuntimePattern.firstMatch(runtime);
|
|
if (iOSRuntimeMatch != null) {
|
|
iOSSimRuntime = iOSRuntimeMatch.group(1)!.trim();
|
|
continue;
|
|
}
|
|
}
|
|
if (iOSSimRuntime == null) {
|
|
if (runtimeBuildForSelectedXcode != null) {
|
|
throw 'iOS simulator runtime $runtimeBuildForSelectedXcode not found. Available runtimes:\n$availableRuntimes';
|
|
} else {
|
|
throw 'No iOS simulator runtime found. Available runtimes:\n$availableRuntimes';
|
|
}
|
|
}
|
|
|
|
final String deviceId = await eval('xcrun', <String>[
|
|
'simctl',
|
|
'create',
|
|
deviceName,
|
|
deviceTypeId,
|
|
iOSSimRuntime,
|
|
], workingDirectory: flutterDirectory.path);
|
|
await eval('xcrun', <String>[
|
|
'simctl',
|
|
'boot',
|
|
deviceId,
|
|
], workingDirectory: flutterDirectory.path);
|
|
|
|
await testFunction(deviceId);
|
|
}
|
|
|
|
/// Shuts down and deletes simulator with deviceId.
|
|
Future<void> removeIOSSimulator(String? deviceId) async {
|
|
if (deviceId != null && deviceId != '') {
|
|
await eval(
|
|
'xcrun',
|
|
<String>['simctl', 'shutdown', deviceId],
|
|
canFail: true,
|
|
workingDirectory: flutterDirectory.path,
|
|
);
|
|
await eval(
|
|
'xcrun',
|
|
<String>['simctl', 'delete', deviceId],
|
|
canFail: true,
|
|
workingDirectory: flutterDirectory.path,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<bool> runXcodeTests({
|
|
required String platformDirectory,
|
|
required String destination,
|
|
required String testName,
|
|
List<String> actions = const <String>['test'],
|
|
String configuration = 'Release',
|
|
List<String> extraOptions = const <String>[],
|
|
String scheme = 'Runner',
|
|
bool skipCodesign = false,
|
|
}) {
|
|
return runXcodeBuild(
|
|
platformDirectory: platformDirectory,
|
|
destination: destination,
|
|
testName: testName,
|
|
actions: actions,
|
|
configuration: configuration,
|
|
extraOptions: extraOptions,
|
|
scheme: scheme,
|
|
skipCodesign: skipCodesign,
|
|
);
|
|
}
|
|
|
|
Future<bool> runXcodeBuild({
|
|
required String platformDirectory,
|
|
required String destination,
|
|
required String testName,
|
|
List<String> actions = const <String>['build'],
|
|
String configuration = 'Release',
|
|
List<String> extraOptions = const <String>[],
|
|
String scheme = 'Runner',
|
|
bool skipCodesign = false,
|
|
}) async {
|
|
final Map<String, String> environment = Platform.environment;
|
|
String? developmentTeam;
|
|
String? codeSignStyle;
|
|
String? provisioningProfile;
|
|
if (!skipCodesign) {
|
|
// If not running on CI, inject the Flutter team code signing properties.
|
|
developmentTeam = environment['FLUTTER_XCODE_DEVELOPMENT_TEAM'] ?? 'S8QB4VV633';
|
|
codeSignStyle = environment['FLUTTER_XCODE_CODE_SIGN_STYLE'];
|
|
provisioningProfile = environment['FLUTTER_XCODE_PROVISIONING_PROFILE_SPECIFIER'];
|
|
}
|
|
File? disabledSandboxEntitlementFile;
|
|
if (platformDirectory.endsWith('macos')) {
|
|
disabledSandboxEntitlementFile = _createDisabledSandboxEntitlementFile(
|
|
platformDirectory,
|
|
configuration,
|
|
);
|
|
}
|
|
final String resultBundleTemp = Directory.systemTemp.createTempSync('flutter_xcresult.').path;
|
|
final String resultBundlePath = path.join(resultBundleTemp, 'result');
|
|
final int testResultExit = await exec(
|
|
'xcodebuild',
|
|
<String>[
|
|
'-workspace',
|
|
'Runner.xcworkspace',
|
|
'-scheme',
|
|
scheme,
|
|
'-configuration',
|
|
configuration,
|
|
'-destination',
|
|
destination,
|
|
'-resultBundlePath',
|
|
resultBundlePath,
|
|
...actions,
|
|
...extraOptions,
|
|
'COMPILER_INDEX_STORE_ENABLE=NO',
|
|
if (developmentTeam != null) 'DEVELOPMENT_TEAM=$developmentTeam',
|
|
if (codeSignStyle != null) 'CODE_SIGN_STYLE=$codeSignStyle',
|
|
if (provisioningProfile != null) 'PROVISIONING_PROFILE_SPECIFIER=$provisioningProfile',
|
|
if (disabledSandboxEntitlementFile != null)
|
|
'CODE_SIGN_ENTITLEMENTS=${disabledSandboxEntitlementFile.path}',
|
|
],
|
|
workingDirectory: platformDirectory,
|
|
canFail: true,
|
|
);
|
|
|
|
if (testResultExit != 0) {
|
|
final Directory? dumpDirectory = hostAgent.dumpDirectory;
|
|
final Directory xcresultBundle = Directory(path.join(resultBundleTemp, 'result.xcresult'));
|
|
if (dumpDirectory != null) {
|
|
if (xcresultBundle.existsSync()) {
|
|
// Zip the test results to the artifacts directory for upload.
|
|
final String zipPath = path.join(
|
|
dumpDirectory.path,
|
|
'$testName-${DateTime.now().toLocal().toIso8601String()}.zip',
|
|
);
|
|
await exec(
|
|
'zip',
|
|
<String>['-r', '-9', '-q', zipPath, path.basename(xcresultBundle.path)],
|
|
workingDirectory: resultBundleTemp,
|
|
canFail: true, // Best effort to get the logs.
|
|
);
|
|
} else {
|
|
print('xcresult bundle ${xcresultBundle.path} does not exist, skipping upload');
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// Finds and copies macOS entitlements file. In the copy, disables sandboxing.
|
|
/// If entitlements file is not found, returns null.
|
|
///
|
|
/// As of macOS 14, testing a macOS sandbox app may prompt the user to grant
|
|
/// access to the app. To workaround this in CI, we create and use a entitlements
|
|
/// file with sandboxing disabled. See
|
|
/// https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox.
|
|
File? _createDisabledSandboxEntitlementFile(String platformDirectory, String configuration) {
|
|
String entitlementDefaultFileName;
|
|
if (configuration == 'Release') {
|
|
entitlementDefaultFileName = 'Release';
|
|
} else {
|
|
entitlementDefaultFileName = 'DebugProfile';
|
|
}
|
|
|
|
final String entitlementFilePath = path.join(
|
|
platformDirectory,
|
|
'Runner',
|
|
'$entitlementDefaultFileName.entitlements',
|
|
);
|
|
final File entitlementFile = File(entitlementFilePath);
|
|
|
|
if (!entitlementFile.existsSync()) {
|
|
print('Unable to find entitlements file at ${entitlementFile.path}');
|
|
return null;
|
|
}
|
|
|
|
final String originalEntitlementFileContents = entitlementFile.readAsStringSync();
|
|
final String tempEntitlementPath =
|
|
Directory.systemTemp.createTempSync('flutter_disable_sandbox_entitlement.').path;
|
|
final File disabledSandboxEntitlementFile = File(
|
|
path.join(
|
|
tempEntitlementPath,
|
|
'${entitlementDefaultFileName}WithDisabledSandboxing.entitlements',
|
|
),
|
|
);
|
|
disabledSandboxEntitlementFile.createSync(recursive: true);
|
|
disabledSandboxEntitlementFile.writeAsStringSync(
|
|
originalEntitlementFileContents.replaceAll(
|
|
RegExp(r'<key>com\.apple\.security\.app-sandbox<\/key>[\S\s]*?<true\/>'),
|
|
'''
|
|
<key>com.apple.security.app-sandbox</key>
|
|
<false/>''',
|
|
),
|
|
);
|
|
|
|
return disabledSandboxEntitlementFile;
|
|
}
|
|
|
|
/// Returns global (external) symbol table entries, delimited by new lines.
|
|
Future<String> dumpSymbolTable(String filePath) {
|
|
return eval('nm', <String>['--extern-only', '--just-symbol-name', filePath, '-arch', 'arm64']);
|
|
}
|