blob: 6e8785e6a99258df3da92c0f5f63ec7c44520223 [file]
"""Test framework for diffing cc_toolchain config against checked-in golden files."""
load("@bazel_features//private:util.bzl", bazel_version_ge = "ge")
load(
"@rules_cc//cc/toolchains:cc_toolchain_config_info.bzl",
"CcToolchainConfigInfo",
)
load("//configs:platforms.bzl", "APPLE_PLATFORMS_CONSTRAINTS")
def _platform_transition_impl(_settings, attr):
return {"//command_line_option:platforms": [attr.platform]}
_platform_transition = transition(
implementation = _platform_transition_impl,
inputs = [],
outputs = ["//command_line_option:platforms"],
)
def _sanitize_path(path):
if not path:
return path
# /Users/<username>/foo -> /USER_HOME/foo
parts = path.split("/")
if len(parts) >= 3 and parts[0] == "" and parts[1] == "Users":
return "/USER_HOME/" + "/".join(parts[3:])
return path
_SANITIZED_ENV_KEYS = ["APPLE_SDK_VERSION_OVERRIDE", "APPLE_SUPPORT_MODULEMAP", "XCODE_VERSION_OVERRIDE"]
def _strip_triple_version(triple):
"""Replace OS version in a target triple with VERSION.
Split on '-', strip digits and dots from the 3rd component, rejoin.
e.g. arm64-apple-macosx26.2 -> arm64-apple-macosxVERSION
arm64-apple-ios26.2-simulator -> arm64-apple-iosVERSION-simulator
"""
parts = triple.split("-")
os_part = parts[2]
stripped = ""
for c in os_part.elems():
if c.isdigit() or c == ".":
break
stripped += c
parts[2] = stripped + "VERSION"
return "-".join(parts)
def _sanitize_env_entries(features):
"""Replace machine-specific env values with placeholders."""
replacements = {}
for feat in features:
for es in feat.env_sets:
for entry in es.env_entries:
if entry.key in _SANITIZED_ENV_KEYS:
replacements[entry.value] = entry.key + "_PLACEHOLDER"
return replacements
def _config_to_json(config_info):
# convert struct to dict
output = json.decode(json.encode(config_info))
output["cxx_builtin_include_directories"] = [
_sanitize_path(x)
for x in config_info.cxx_builtin_include_directories
]
result = json.encode_indent(output, indent = " ")
for old, new in _sanitize_env_entries(config_info._features_DO_NOT_USE).items():
result = result.replace(json.encode(old), json.encode(new))
# Replace the versioned triple with a VERSION placeholder
triple = config_info.target_system_name
sanitized_triple = _strip_triple_version(triple)
result = result.replace(triple, sanitized_triple)
return result
def _toolchain_config_test_impl(ctx):
config_info = ctx.attr.toolchain_config[0][CcToolchainConfigInfo]
actual_json = _config_to_json(config_info)
golden = ctx.file.golden
test_script = ctx.actions.declare_file(ctx.label.name + ".sh")
actual_file = ctx.actions.declare_file(ctx.label.name + ".actual.json")
ctx.actions.write(
output = actual_file,
content = actual_json,
)
ctx.actions.write(
output = test_script,
content = """\
#!/usr/bin/env bash
set -euo pipefail
if ! diff -u "$GOLDEN" "$ACTUAL"; then
echo ""
echo "ERROR: Toolchain config does not match golden file."
echo "To update, run: bazel run {update_target}"
exit 1
fi
""".format(update_target = str(ctx.label).rsplit("_test", 1)[0] + "_update"),
is_executable = True,
)
return [DefaultInfo(
executable = test_script,
runfiles = ctx.runfiles(files = [actual_file, golden]),
), RunEnvironmentInfo(environment = {
"ACTUAL": actual_file.short_path,
"GOLDEN": golden.short_path,
})]
_toolchain_config_test = rule(
implementation = _toolchain_config_test_impl,
test = True,
attrs = {
"toolchain_config": attr.label(
mandatory = True,
providers = [CcToolchainConfigInfo],
cfg = _platform_transition,
),
"golden": attr.label(
mandatory = True,
allow_single_file = True,
),
"platform": attr.string(mandatory = True),
"_allowlist_function_transition": attr.label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
),
},
)
def _toolchain_config_update_impl(ctx):
config_info = ctx.attr.toolchain_config[0][CcToolchainConfigInfo]
actual_json = _config_to_json(config_info)
actual_file = ctx.actions.declare_file(ctx.label.name + ".json")
ctx.actions.write(
output = actual_file,
content = actual_json,
)
update_script = ctx.actions.declare_file(ctx.label.name + ".sh")
ctx.actions.write(
output = update_script,
content = """\
#!/usr/bin/env bash
set -euo pipefail
actual="${{RUNFILES_DIR:-$0.runfiles}}/{workspace}/{actual}"
golden="{golden_path}"
cp "$actual" "${{BUILD_WORKSPACE_DIRECTORY}}/${{golden}}"
echo "Updated $golden"
""".format(
workspace = ctx.workspace_name,
actual = actual_file.short_path,
golden_path = ctx.file.golden.short_path,
),
is_executable = True,
)
return [DefaultInfo(
executable = update_script,
runfiles = ctx.runfiles(files = [actual_file]),
), _UpdateInfo(
actual = actual_file,
golden_path = ctx.file.golden.short_path,
)]
_UpdateInfo = provider(
fields = ["actual", "golden_path"],
doc = "",
)
_toolchain_config_update = rule(
implementation = _toolchain_config_update_impl,
executable = True,
attrs = {
"toolchain_config": attr.label(
mandatory = True,
providers = [CcToolchainConfigInfo],
cfg = _platform_transition,
),
"golden": attr.label(
mandatory = True,
allow_single_file = True,
),
"platform": attr.string(mandatory = True),
"_allowlist_function_transition": attr.label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
),
},
)
_is_bazel_9_or_later = bazel_version_ge("9.0.0")
def toolchain_config_test_suite(name):
"""Create toolchain config tests for all platforms.
Args:
name: Name for the test suite.
"""
if not _is_bazel_9_or_later:
# Bazel 7.x/8.x use proto encoding for CcToolchainConfigInfo
# instead of JSON, so these tests don't work there.
native.test_suite(name = name, tests = [])
return
platforms = APPLE_PLATFORMS_CONSTRAINTS.keys()
toolchain_config = "//toolchain"
tests = []
updates = []
for platform in platforms:
golden = "//test/test_data/toolchain_configs:" + platform + ".json"
platform_label = "//platforms:" + platform
test_name = name + "_" + platform + "_test"
_toolchain_config_test(
name = test_name,
toolchain_config = toolchain_config,
golden = golden,
platform = platform_label,
)
tests.append(test_name)
update_name = name + "_" + platform + "_update"
_toolchain_config_update(
name = update_name,
toolchain_config = toolchain_config,
golden = golden,
platform = platform_label,
)
updates.append(update_name)
native.test_suite(
name = name,
tests = tests,
)
_update_all(
name = name + "_update_all",
updates = updates,
)
def _update_all_impl(ctx):
script = ctx.actions.declare_file(ctx.label.name + ".sh")
lines = ["#!/usr/bin/env bash", "set -euo pipefail"]
all_files = []
for update in ctx.attr.updates:
info = update[_UpdateInfo]
all_files.append(info.actual)
lines.append('cp "${{RUNFILES_DIR:-$0.runfiles}}/{workspace}/{actual}" "${{BUILD_WORKSPACE_DIRECTORY}}/{golden}"'.format(
workspace = ctx.workspace_name,
actual = info.actual.short_path,
golden = info.golden_path,
))
lines.append('echo "Updated {golden}"'.format(golden = info.golden_path))
ctx.actions.write(
output = script,
content = "\n".join(lines) + "\n",
is_executable = True,
)
return [DefaultInfo(
executable = script,
runfiles = ctx.runfiles(files = all_files),
)]
_update_all = rule(
implementation = _update_all_impl,
executable = True,
attrs = {
"updates": attr.label_list(providers = [_UpdateInfo]),
},
)