Skip to content

Commit d6fd5b5

Browse files
authored
Fix SDK->simruntime mapping assumptions in test runners (#2889)
Redux of #2675. Refactors simulator_creator.py to discover a recent simruntime when none is explicitly declared, and removes the SDK version fallback from the test runner rule implementations. This assumption is problematic in recent times whenever Apple pushes a new simruntime OTA for an existing Xcode, or when the simruntime identifier does not align with the simruntime version and consequently the SDK version (such as OS patch releases). --------- Signed-off-by: Aaron Sky <asky@dropbox.com>
1 parent e0eab89 commit d6fd5b5

File tree

6 files changed

+102
-46
lines changed

6 files changed

+102
-46
lines changed

apple/testing/default_runner/ios_test_runner.bzl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
"""iOS test runner rule."""
1616

17+
load("@build_bazel_apple_support//xcode:providers.bzl", "XcodeVersionPropertiesInfo")
1718
load(
1819
"//apple:providers.bzl",
1920
"AppleDeviceTestRunnerInfo",
@@ -28,6 +29,7 @@ def _get_template_substitutions(
2829
post_action_binary,
2930
post_action_determines_exit_code,
3031
pre_action_binary,
32+
sdk_version,
3133
testrunner):
3234
"""Returns the template substitutions for this runner."""
3335
subs = {
@@ -37,6 +39,7 @@ def _get_template_substitutions(
3739
"post_action_binary": post_action_binary,
3840
"post_action_determines_exit_code": post_action_determines_exit_code,
3941
"pre_action_binary": pre_action_binary,
42+
"sdk_version": sdk_version,
4043
"testrunner_binary": testrunner,
4144
}
4245
return {"%(" + k + ")s": subs[k] for k in subs}
@@ -53,6 +56,8 @@ def _get_execution_environment(*, xcode_config):
5356
def _ios_test_runner_impl(ctx):
5457
"""Implementation for the ios_test_runner rule."""
5558

59+
xcode_properties_attr = getattr(apple_common, "XcodeProperties", None) or XcodeVersionPropertiesInfo
60+
sdk_version = ctx.attr._xcode_config[xcode_properties_attr].default_ios_sdk_version
5661
os_version = str(ctx.attr.os_version or ctx.fragments.objc.ios_simulator_version or "")
5762
device_type = ctx.attr.device_type or ctx.fragments.objc.ios_simulator_device or ""
5863

@@ -84,6 +89,7 @@ def _ios_test_runner_impl(ctx):
8489
post_action_binary = post_action_binary,
8590
post_action_determines_exit_code = "true" if post_action_determines_exit_code else "false",
8691
pre_action_binary = pre_action_binary,
92+
sdk_version = sdk_version,
8793
testrunner = ctx.executable._testrunner.short_path,
8894
),
8995
)
@@ -118,6 +124,7 @@ When executed, the binary will have the following environment variables availabl
118124
<ul>
119125
<li>`SIMULATOR_DEVICE_TYPE`: The device type of the simulator to create. The supported types correspond to the output of `xcrun simctl list devicetypes`. E.g., iPhone 6, iPad Air. The value will either be the value of the `device_type` attribute, the `--ios_simulator_device` command-line flag, or an empty string that should imply a default device.</li>
120126
<li>`SIMULATOR_OS_VERSION`: The os version of the simulator to create. The supported os versions correspond to the output of `xcrun simctl list runtimes`. ' 'E.g., 11.2, 9.3. The value will either be the value of the `os_version` attribute, the `--ios_simulator_version` command-line flag, or an empty string that should imply a default OS version for the selected simulator runtime.</li>
127+
<li>`SIMULATOR_SDK_VERSION`: The SDK version of the simulator to create. The supported SDK builds correspond to the output of `xcrun simctl runtime match list`. E.g., 11.2, 9.3. The value will be derived from the `default_ios_sdk_version` for the current Xcode version.</li>
121128
</ul>
122129
""",
123130
),

apple/testing/default_runner/ios_test_runner.template.sh

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -243,17 +243,12 @@ if [[ -n "${REUSE_GLOBAL_SIMULATOR:-}" ]]; then
243243
exit 1
244244
fi
245245

246-
if [[ -z "%(os_version)s" ]]; then
247-
echo "error: to create a re-useable simulator the OS version must always be set on the test runner or with '--ios_simulator_version'" >&2
248-
exit 1
249-
fi
250-
251246
if [[ -z "%(device_type)s" ]]; then
252-
echo "error: to create a re-useable simulator the device type must always be set on the test runner or with '--ios_simulator_device'" >&2
247+
echo "error: to create a re-useable simulator; the device type must always be set on the test runner or with '--ios_simulator_device'" >&2
253248
exit 1
254249
fi
255250

256-
id="$(SIMULATOR_DEVICE_TYPE="%(device_type)s" SIMULATOR_OS_VERSION="%(os_version)s" "%(create_simulator_action_binary)s")"
251+
id="$(SIMULATOR_DEVICE_TYPE="%(device_type)s" SIMULATOR_OS_VERSION="%(os_version)s" SIMULATOR_SDK_VERSION="%(sdk_version)s" "%(create_simulator_action_binary)s")"
257252
target_flags=(
258253
"test"
259254
"--platform=ios_simulator"

apple/testing/default_runner/ios_xctestrun_runner.bzl

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def _get_template_substitutions(
2929
pre_action_binary,
3030
random,
3131
reuse_simulator,
32+
sdk_version,
3233
xcodebuild_args,
3334
xctestrun_template,
3435
xctrunner_entitlements_template):
@@ -45,6 +46,7 @@ def _get_template_substitutions(
4546
"post_action_determines_exit_code": post_action_determines_exit_code,
4647
"pre_action_binary": pre_action_binary,
4748
"reuse_simulator": reuse_simulator,
49+
"sdk_version": sdk_version,
4850
# "ordered" isn't a special string, but anything besides "random" for this field runs in order
4951
"test_order": "random" if random else "ordered",
5052
"xcodebuild_args": xcodebuild_args,
@@ -64,16 +66,9 @@ def _get_execution_environment(ctx):
6466
def _ios_xctestrun_runner_impl(ctx):
6567
# TODO: Remove this getattr when we drop Bazel 8
6668
xcode_properties_attr = getattr(apple_common, "XcodeProperties", None) or XcodeVersionPropertiesInfo
67-
os_version = str(ctx.attr.os_version or ctx.fragments.objc.ios_simulator_version or
68-
ctx.attr._xcode_config[xcode_properties_attr].default_ios_sdk_version)
69-
70-
# TODO: Ideally we would be smarter about picking a device, but we don't know what the current version of Xcode supports
71-
device_type = ctx.attr.device_type or ctx.fragments.objc.ios_simulator_device or "iPhone 15"
72-
73-
if not os_version:
74-
fail("error: os_version must be set on ios_xctestrun_runner, or passed with --ios_simulator_version")
75-
if not device_type:
76-
fail("error: device_type must be set on ios_xctestrun_runner, or passed with --ios_simulator_device")
69+
sdk_version = ctx.attr._xcode_config[xcode_properties_attr].default_ios_sdk_version
70+
os_version = str(ctx.attr.os_version or ctx.fragments.objc.ios_simulator_version or "")
71+
device_type = ctx.attr.device_type or ctx.fragments.objc.ios_simulator_device or ""
7772

7873
runfiles = ctx.runfiles(files = [
7974
ctx.file._xctestrun_template,
@@ -114,6 +109,7 @@ def _ios_xctestrun_runner_impl(ctx):
114109
pre_action_binary = pre_action_binary,
115110
random = ctx.attr.random,
116111
reuse_simulator = "true" if ctx.attr.reuse_simulator else "false",
112+
sdk_version = sdk_version,
117113
xcodebuild_args = " ".join(ctx.attr.xcodebuild_args) if ctx.attr.xcodebuild_args else "",
118114
xctestrun_template = ctx.file._xctestrun_template.short_path,
119115
xctrunner_entitlements_template = ctx.file._xctrunner_entitlements_template.short_path,
@@ -176,8 +172,9 @@ When executed, the binary will have the following environment variables availabl
176172
177173
<ul>
178174
<li>`SIMULATOR_DEVICE_TYPE`: The device type of the simulator to create. The supported types correspond to the output of `xcrun simctl list devicetypes`. E.g., iPhone 6, iPad Air. The value will either be the value of the `device_type` attribute, or the `--ios_simulator_device` command-line flag.</li>
179-
<li>`SIMULATOR_OS_VERSION`: The os version of the simulator to create. The supported os versions correspond to the output of `xcrun simctl list runtimes`. ' 'E.g., 11.2, 9.3. The value will either be the value of the `os_version` attribute, or the `--ios_simulator_version` command-line flag.</li>
175+
<li>`SIMULATOR_OS_VERSION`: The OS version of the simulator to create. The supported OS versions correspond to the output of `xcrun simctl list runtimes`. E.g., 11.2, 9.3. The value will either be the value of the `os_version` attribute, or the `--ios_simulator_version` command-line flag.</li>
180176
<li>`SIMULATOR_REUSE_SIMULATOR`: Whether to reuse an existing simulator or create a new one. The value will be set to "1" if the `reuse_simulator` attribute is true, and unset otherwise. Whether or not this variable is respected should be treated as an implementation detail of the simulator creator tool.</li>
177+
<li>`SIMULATOR_SDK_VERSION`: The SDK version of the simulator to create. The supported SDK builds correspond to the output of `xcrun simctl runtime match list`. E.g., 11.2, 9.3. The value will be derived from the `default_ios_sdk_version` for the current Xcode version.</li>
181178
</ul>
182179
""",
183180
),

apple/testing/default_runner/ios_xctestrun_runner.template.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,12 @@ fi
399399

400400
simulator_id="unused"
401401
if [[ "$build_for_device" == false ]]; then
402-
simulator_id="$(SIMULATOR_DEVICE_TYPE="%(device_type)s" SIMULATOR_OS_VERSION="%(os_version)s" SIMULATOR_REUSE_SIMULATOR="${reuse_simulator:-}" "%(create_simulator_action_binary)s")"
402+
if [[ -z "%(device_type)s" ]]; then
403+
echo "error: to create a simulator; the device type must always be set on the test runner or with '--ios_simulator_device'" >&2
404+
exit 1
405+
fi
406+
407+
simulator_id="$(SIMULATOR_DEVICE_TYPE="%(device_type)s" SIMULATOR_OS_VERSION="%(os_version)s" SIMULATOR_REUSE_SIMULATOR="${reuse_simulator:-}" SIMULATOR_SDK_VERSION="%(sdk_version)s" "%(create_simulator_action_binary)s")"
403408
fi
404409

405410
test_exit_code=0

apple/testing/default_runner/simulator_creator.py

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@
2121
import subprocess
2222
import sys
2323
import time
24+
from dataclasses import dataclass
2425
from typing import List, Optional
2526

2627

2728
def _simctl(extra_args: List[str]) -> str:
28-
return subprocess.check_output(["xcrun", "simctl"] + extra_args).decode()
29+
return subprocess.check_output(["xcrun", "simctl", *extra_args], text=True)
2930

3031

3132
def _boot_simulator(simulator_id: str) -> None:
@@ -82,23 +83,64 @@ def _boot_simulator(simulator_id: str) -> None:
8283
time.sleep(3)
8384

8485

85-
def _device_name(device_type: str, os_version: str) -> str:
86+
@dataclass
87+
class _Runtime:
88+
identifier: str
89+
platform: str
90+
version: str
91+
92+
93+
def _selected_simulator_runtime(
94+
os_version: Optional[str], sdk_version: Optional[str]
95+
) -> _Runtime:
96+
runtimes = json.loads(_simctl(["list", "runtimes", "-j"]))["runtimes"]
97+
available_runtimes = [
98+
runtime
99+
for runtime in runtimes
100+
if runtime["platform"] == "iOS" and runtime["isAvailable"]
101+
]
102+
if not available_runtimes:
103+
raise RuntimeError("no available runtimes found")
104+
sdk_runtime_match = None
105+
if sdk_version:
106+
sdk_name = f"iphoneos{sdk_version}"
107+
sdk_runtime_matches = json.loads(_simctl(["runtime", "match", "list", "-j"]))
108+
if sdk_name not in sdk_runtime_matches:
109+
raise RuntimeError(
110+
f"no sdk build to runtime mapping found matching {sdk_name}"
111+
)
112+
sdk_runtime_match = sdk_runtime_matches[sdk_name]
113+
for runtime in available_runtimes:
114+
if os_version and runtime["version"] == os_version:
115+
return _Runtime(
116+
identifier=runtime["identifier"],
117+
platform=runtime["platform"],
118+
version=runtime["version"],
119+
)
120+
elif (
121+
sdk_runtime_match
122+
and runtime["buildversion"] == sdk_runtime_match["chosenRuntimeBuild"]
123+
):
124+
return _Runtime(
125+
identifier=runtime["identifier"],
126+
platform=runtime["platform"],
127+
version=runtime["version"],
128+
)
129+
raise RuntimeError("no matching runtimes found")
130+
131+
132+
def _default_device_name(device_type: str, os_version: str) -> str:
86133
return f"BAZEL_TEST_{device_type}_{os_version}"
87134

88135

89136
def _create_and_boot_simulator(
90-
os_version: str,
137+
device_name: str,
91138
device_type: str,
92-
name: Optional[str],
139+
runtime_identifier: str,
93140
reuse_simulator: bool,
94141
) -> str:
95142
devices = json.loads(_simctl(["list", "devices", "-j"]))["devices"]
96-
device_name = name or _device_name(device_type, os_version)
97-
runtime_identifier = "com.apple.CoreSimulator.SimRuntime.iOS-{}".format(
98-
os_version.replace(".", "-")
99-
)
100-
101-
existing_device=None
143+
existing_device = None
102144

103145
if reuse_simulator:
104146
devices_for_os = devices.get(runtime_identifier) or []
@@ -134,6 +176,7 @@ def _create_and_boot_simulator(
134176

135177
class Namespace(argparse.Namespace):
136178
os_version: Optional[str]
179+
sdk_version: Optional[str]
137180
device_type: Optional[str]
138181
simulator_name: Optional[str]
139182
reuse_simulator: bool
@@ -142,16 +185,22 @@ class Namespace(argparse.Namespace):
142185
def _build_parser() -> argparse.ArgumentParser:
143186
parser = argparse.ArgumentParser()
144187
parser.add_argument(
145-
"--os_version",
188+
"--os-version",
146189
required=False,
147190
default=None,
148-
help="The iOS version to run the tests on, ex: 12.1",
191+
help="The OS version to run the tests on, ex: 12.1",
149192
)
150193
parser.add_argument(
151-
"--device_type",
194+
"--sdk-version",
152195
required=False,
153196
default=None,
154-
help="The iOS device to run the tests on, ex: iPhone X",
197+
help="The SDK version the tests were built for, ex: 12.1",
198+
)
199+
parser.add_argument(
200+
"--device-type",
201+
required=False,
202+
default=None,
203+
help="The iOS device to run the tests on, ex: iPhone 15",
155204
)
156205
parser.add_argument(
157206
"--name",
@@ -173,24 +222,27 @@ def _main() -> None:
173222

174223
args = parser.parse_args(namespace=Namespace())
175224

176-
os_version = args.os_version or os.getenv("SIMULATOR_OS_VERSION")
177225
device_type = args.device_type or os.getenv("SIMULATOR_DEVICE_TYPE")
178-
simulator_name = args.name
179-
reuse_simulator: bool = args.reuse_simulator or (
180-
os.getenv("SIMULATOR_REUSE_SIMULATOR") is not None
181-
)
182-
183-
if not os_version:
184-
parser.error(
185-
"OS version must be provided either as an argument or through the SIMULATOR_OS_VERSION environment variable"
186-
)
187226
if not device_type:
188227
parser.error(
189228
"Device type must be provided either as an argument or through the SIMULATOR_DEVICE_TYPE environment variable"
190229
)
191230

231+
os_version = args.os_version or os.getenv("SIMULATOR_OS_VERSION")
232+
sdk_version = args.sdk_version or os.getenv("SIMULATOR_SDK_VERSION")
233+
reuse_simulator: bool = args.reuse_simulator or (
234+
os.getenv("SIMULATOR_REUSE_SIMULATOR") is not None
235+
)
236+
237+
selected_runtime = _selected_simulator_runtime(os_version, sdk_version)
238+
device_name = args.name or _default_device_name(
239+
device_type, selected_runtime.version
240+
)
241+
242+
print("Selected simulator runtime", selected_runtime.identifier, file=sys.stderr)
243+
192244
simulator_id = _create_and_boot_simulator(
193-
os_version, device_type, simulator_name, reuse_simulator
245+
device_name, device_type, selected_runtime.identifier, reuse_simulator
194246
)
195247

196248
print(simulator_id.strip())

0 commit comments

Comments
 (0)