BEGIN_PUBLIC
Add support for testing rules_cc's new toolchains with rules_testing.
END_PUBLIC
PiperOrigin-RevId: 608769646
Change-Id: I1a698355e5e977cc86eedc7cf6e8e0f888593cb8
diff --git a/MODULE.bazel b/MODULE.bazel
index f6beb62..f664840 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -12,3 +12,4 @@
register_toolchains("@local_config_cc_toolchains//:all")
bazel_dep(name = "bazel_skylib", version = "1.3.0", dev_dependency = True)
+bazel_dep(name = "rules_testing", version = "0.6.0", dev_dependency = True)
diff --git a/WORKSPACE b/WORKSPACE
index 1bbf0e2..875888e 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -90,3 +90,10 @@
rules_proto_dependencies()
rules_proto_toolchains()
+
+http_archive(
+ name = "rules_testing",
+ sha256 = "02c62574631876a4e3b02a1820cb51167bb9cdcdea2381b2fa9d9b8b11c407c4",
+ strip_prefix = "rules_testing-0.6.0",
+ url = "https://github.com/bazelbuild/rules_testing/releases/download/v0.6.0/rules_testing-v0.6.0.tar.gz",
+)
diff --git a/cc/toolchains/cc_toolchain_info.bzl b/cc/toolchains/cc_toolchain_info.bzl
index 4c4d8a1..3f1b9e6 100644
--- a/cc/toolchains/cc_toolchain_info.bzl
+++ b/cc/toolchains/cc_toolchain_info.bzl
@@ -17,7 +17,10 @@
# that can access the providers directly.
# Once it's stabilized, we *may* consider opening up parts of the API, or we may
# decide to just require users to use the public user-facing rules.
-visibility("//third_party/bazel_rules/rules_cc/toolchains/...")
+visibility([
+ "//cc/toolchains/...",
+ "//tests/...",
+])
# Note that throughout this file, we never use a list. This is because mutable
# types cannot be stored in depsets. Thus, we type them as a sequence in the
@@ -120,12 +123,11 @@
# @unsorted-dict-items
fields = {
"label": "(Label) The label defining this provider. Place in error messages to simplify debugging",
- "action_name": "(str) The name of the action",
+ "action": "(ActionTypeInfo) The name of the action",
"enabled": "(bool) If True, this action is enabled unless a rule type explicitly marks it as unsupported",
"tools": "(Sequence[ToolInfo]) The tool applied to the action will be the first tool in the sequence with a feature set that matches the feature configuration",
- "flag_sets": "(depset[FlagSetInfo]) Set of flag sets the action sets",
- "implies_features": "(depset[FeatureInfo]) Set of features implied by this action config",
- "implies_action_configs": "(depset[ActionConfigInfo]) Set of action configs enabled by this action config",
+ "flag_sets": "(Sequence[FlagSetInfo]) Set of flag sets the action sets",
+ "implies": "(depset[FeatureInfo]) Set of features implied by this action config",
"files": "(depset[File]) The files required to run these actions",
},
)
diff --git a/tests/rule_based_toolchain/BUILD b/tests/rule_based_toolchain/BUILD
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/rule_based_toolchain/BUILD
diff --git a/tests/rule_based_toolchain/actions/BUILD b/tests/rule_based_toolchain/actions/BUILD
new file mode 100644
index 0000000..4e45f7e
--- /dev/null
+++ b/tests/rule_based_toolchain/actions/BUILD
@@ -0,0 +1,34 @@
+load("@rules_testing//lib:util.bzl", "util")
+load("//cc/toolchains:actions.bzl", "cc_action_type", "cc_action_type_set")
+load("//tests/rule_based_toolchain:analysis_test_suite.bzl", "analysis_test_suite")
+load(":actions_test.bzl", "TARGETS", "TESTS")
+
+util.helper_target(
+ cc_action_type,
+ name = "c_compile",
+ action_name = "c_compile",
+ visibility = ["//tests/rule_based_toolchain:__subpackages__"],
+)
+
+util.helper_target(
+ cc_action_type,
+ name = "cpp_compile",
+ action_name = "cpp_compile",
+ visibility = ["//tests/rule_based_toolchain:__subpackages__"],
+)
+
+util.helper_target(
+ cc_action_type_set,
+ name = "all_compile",
+ actions = [
+ ":c_compile",
+ ":cpp_compile",
+ ],
+ visibility = ["//tests/rule_based_toolchain:__subpackages__"],
+)
+
+analysis_test_suite(
+ name = "test_suite",
+ targets = TARGETS,
+ tests = TESTS,
+)
diff --git a/tests/rule_based_toolchain/actions/actions_test.bzl b/tests/rule_based_toolchain/actions/actions_test.bzl
new file mode 100644
index 0000000..284ed9a
--- /dev/null
+++ b/tests/rule_based_toolchain/actions/actions_test.bzl
@@ -0,0 +1,43 @@
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for actions for the rule based toolchain."""
+
+load(
+ "//cc/toolchains:cc_toolchain_info.bzl",
+ "ActionTypeInfo",
+ "ActionTypeSetInfo",
+)
+
+visibility("private")
+
+def _test_action_types_impl(env, targets):
+ env.expect.that_target(targets.c_compile).provider(ActionTypeInfo) \
+ .name().equals("c_compile")
+ env.expect.that_target(targets.cpp_compile).provider(ActionTypeSetInfo) \
+ .actions().contains_exactly([targets.cpp_compile.label])
+ env.expect.that_target(targets.all_compile).provider(ActionTypeSetInfo) \
+ .actions().contains_exactly([
+ targets.c_compile.label,
+ targets.cpp_compile.label,
+ ])
+
+TARGETS = [
+ ":c_compile",
+ ":cpp_compile",
+ ":all_compile",
+]
+
+TESTS = {
+ "actions_test": _test_action_types_impl,
+}
diff --git a/tests/rule_based_toolchain/analysis_test_suite.bzl b/tests/rule_based_toolchain/analysis_test_suite.bzl
new file mode 100644
index 0000000..01ba4e2
--- /dev/null
+++ b/tests/rule_based_toolchain/analysis_test_suite.bzl
@@ -0,0 +1,50 @@
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Test suites for the rule based toolchain."""
+
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
+load(":subjects.bzl", "FACTORIES")
+
+visibility("//tests/rule_based_toolchain/...")
+
+_DEFAULT_TARGET = "//tests/rule_based_toolchain/actions:c_compile"
+
+# Tests of internal starlark functions will often not require any targets,
+# but analysis_test requires at least one, so we pick an arbitrary one.
+def analysis_test_suite(name, tests, targets = [_DEFAULT_TARGET]):
+ """A test suite for the internals of the toolchain.
+
+ Args:
+ name: (str) The name of the test suite.
+ tests: (dict[str, fn]) A mapping from test name to implementations.
+ targets: (List[Label|str]) List of targets accessible to the test.
+ """
+ targets = [native.package_relative_label(target) for target in targets]
+
+ test_case_names = []
+ for test_name, impl in tests.items():
+ if not test_name.endswith("_test"):
+ fail("Expected test keys to end with '_test', got test case %r" % test_name)
+ test_case_names.append(":" + test_name)
+ analysis_test(
+ name = test_name,
+ impl = impl,
+ provider_subject_factories = FACTORIES,
+ targets = {label.name: label for label in targets},
+ )
+
+ native.test_suite(
+ name = name,
+ tests = test_case_names,
+ )
diff --git a/tests/rule_based_toolchain/generate_factory.bzl b/tests/rule_based_toolchain/generate_factory.bzl
new file mode 100644
index 0000000..72c9680
--- /dev/null
+++ b/tests/rule_based_toolchain/generate_factory.bzl
@@ -0,0 +1,127 @@
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Generates provider factories."""
+
+load("@bazel_skylib//lib:structs.bzl", "structs")
+load("@rules_testing//lib:truth.bzl", "subjects")
+
+visibility("private")
+
+def generate_factory(type, name, attrs):
+ """Generates a factory for a custom struct.
+
+ There are three reasons we need to do so:
+ 1. It's very difficult to read providers printed by these types.
+ eg. If you have a 10 layer deep diamond dependency graph, and try to
+ print the top value, the bottom value will be printed 2^10 times.
+ 2. Collections of subjects are not well supported by rules_testing
+ eg. `FeatureInfo(flag_sets = [FlagSetInfo(...)])`
+ (You can do it, but the inner values are just regular bazel structs and
+ you can't do fluent assertions on them).
+ 3. Recursive types are not supported at all
+ eg. `FeatureInfo(implies = depset([FeatureInfo(...)]))`
+
+ To solve this, we create a factory that:
+ * Validates that the types of the children are correct.
+ * Inlines providers to their labels when unambiguous.
+
+ For example, given:
+
+ ```
+ foo = FeatureInfo(name = "foo", label = Label("//:foo"))
+ bar = FeatureInfo(..., implies = depset([foo]))
+ ```
+
+ It would convert itself a subject for the following struct:
+ `FeatureInfo(..., implies = depset([Label("//:foo")]))`
+
+ Args:
+ type: (type) The type to create a factory for (eg. FooInfo)
+ name: (str) The name of the type (eg. "FooInfo")
+ attrs: (dict[str, Factory]) The attributes associated with this type.
+
+ Returns:
+ A struct `FooFactory` suitable for use with
+ * `analysis_test(provider_subject_factories=[FooFactory])`
+ * `generate_factory(..., attrs=dict(foo = FooFactory))`
+ * `ProviderSequence(FooFactory)`
+ * `DepsetSequence(FooFactory)`
+ """
+ attrs["label"] = subjects.label
+
+ want_keys = sorted(attrs.keys())
+
+ def validate(*, value, meta):
+ got_keys = sorted(structs.to_dict(value).keys())
+ if got_keys != want_keys:
+ meta.add_failure("Wanted a %s with keys %r, got %r" % (name, want_keys, got_keys), "")
+
+ def type_factory(value, *, meta):
+ validate(value = value, meta = meta)
+
+ transformed_value = {}
+ transformed_factories = {}
+ for field, factory in attrs.items():
+ field_value = getattr(value, field)
+
+ # If it's a type generated by generate_factory, inline it.
+ if hasattr(factory, "factory"):
+ factory.validate(value = field_value, meta = meta.derive(field))
+ transformed_value[field] = field_value.label
+ transformed_factories[field] = subjects.label
+ else:
+ transformed_value[field] = field_value
+ transformed_factories[field] = factory
+
+ return subjects.struct(
+ struct(**transformed_value),
+ meta = meta,
+ attrs = transformed_factories,
+ )
+
+ return struct(
+ type = type,
+ name = name,
+ factory = type_factory,
+ validate = validate,
+ )
+
+def _provider_collection(element_factory, fn):
+ def factory(value, *, meta):
+ value = fn(value)
+
+ # Validate that it really is the correct type
+ for i in range(len(value)):
+ element_factory.validate(
+ value = value[i],
+ meta = meta.derive("offset({})".format(i)),
+ )
+
+ # Inline the providers to just labels.
+ return subjects.collection([v.label for v in value], meta = meta)
+
+ return factory
+
+# This acts like a class, so we name it like one.
+# buildifier: disable=name-conventions
+ProviderSequence = lambda element_factory: _provider_collection(
+ element_factory,
+ fn = lambda x: list(x),
+)
+
+# buildifier: disable=name-conventions
+ProviderDepset = lambda element_factory: _provider_collection(
+ element_factory,
+ fn = lambda x: x.to_list(),
+)
diff --git a/tests/rule_based_toolchain/subjects.bzl b/tests/rule_based_toolchain/subjects.bzl
new file mode 100644
index 0000000..b330093
--- /dev/null
+++ b/tests/rule_based_toolchain/subjects.bzl
@@ -0,0 +1,185 @@
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Test subjects for cc_toolchain_info providers."""
+
+load("@rules_testing//lib:truth.bzl", "subjects")
+load(
+ "//cc/toolchains:cc_toolchain_info.bzl",
+ "ActionConfigInfo",
+ "ActionConfigSetInfo",
+ "ActionTypeInfo",
+ "ActionTypeSetInfo",
+ "FeatureConstraintInfo",
+ "FeatureInfo",
+ "FeatureSetInfo",
+ "FlagGroupInfo",
+ "FlagSetInfo",
+ "MutuallyExclusiveCategoryInfo",
+ "ToolInfo",
+)
+load(":generate_factory.bzl", "ProviderDepset", "ProviderSequence", "generate_factory")
+
+visibility("private")
+
+# buildifier: disable=name-conventions
+_ActionTypeFactory = generate_factory(
+ ActionTypeInfo,
+ "ActionTypeInfo",
+ dict(
+ name = subjects.str,
+ ),
+)
+
+# buildifier: disable=name-conventions
+_ActionTypeSetFactory = generate_factory(
+ ActionTypeSetInfo,
+ "ActionTypeInfo",
+ dict(
+ actions = ProviderDepset(_ActionTypeFactory),
+ ),
+)
+
+# buildifier: disable=name-conventions
+_MutuallyExclusiveCategoryFactory = generate_factory(
+ MutuallyExclusiveCategoryInfo,
+ "MutuallyExclusiveCategoryInfo",
+ dict(name = subjects.str),
+)
+
+_FEATURE_FLAGS = dict(
+ name = subjects.str,
+ enabled = subjects.bool,
+ flag_sets = None,
+ implies = None,
+ requires_any_of = None,
+ provides = ProviderSequence(_MutuallyExclusiveCategoryFactory),
+ known = subjects.bool,
+ overrides = None,
+)
+
+# Break the dependency loop.
+# buildifier: disable=name-conventions
+_FakeFeatureFactory = generate_factory(
+ FeatureInfo,
+ "FeatureInfo",
+ _FEATURE_FLAGS,
+)
+
+# buildifier: disable=name-conventions
+_FeatureSetFactory = generate_factory(
+ FeatureSetInfo,
+ "FeatureSetInfo",
+ dict(features = _FakeFeatureFactory),
+)
+
+# buildifier: disable=name-conventions
+_FeatureConstraintFactory = generate_factory(
+ FeatureConstraintInfo,
+ "FeatureConstraintInfo",
+ dict(
+ all_of = ProviderDepset(_FakeFeatureFactory),
+ none_of = ProviderDepset(_FakeFeatureFactory),
+ ),
+)
+
+# buildifier: disable=name-conventions
+_FlagGroupFactory = generate_factory(
+ FlagGroupInfo,
+ "FlagGroupInfo",
+ dict(
+ flags = subjects.collection,
+ ),
+)
+
+# buildifier: disable=name-conventions
+_FlagSetFactory = generate_factory(
+ FlagSetInfo,
+ "FlagSetInfo",
+ dict(
+ actions = ProviderDepset(_ActionTypeFactory),
+ # Use a collection here because we don't want to
+ flag_groups = subjects.collection,
+ requires_any_of = ProviderSequence(_FeatureConstraintFactory),
+ ),
+)
+
+# buildifier: disable=name-conventions
+_FeatureFactory = generate_factory(
+ FeatureInfo,
+ "FeatureInfo",
+ _FEATURE_FLAGS | dict(
+ implies = ProviderDepset(_FakeFeatureFactory),
+ requires_any_of = ProviderSequence(_FeatureSetFactory),
+ overrides = _FakeFeatureFactory,
+ ),
+)
+
+# buildifier: disable=name-conventions
+_ToolFactory = generate_factory(
+ ToolInfo,
+ "ToolInfo",
+ dict(
+ exe = subjects.file,
+ runifles = subjects.depset_file,
+ requires_any_of = ProviderSequence(_FeatureConstraintFactory),
+ ),
+)
+
+# buildifier: disable=name-conventions
+_ActionConfigFactory = generate_factory(
+ ActionConfigInfo,
+ "ActionConfigInfo",
+ dict(
+ action = _ActionTypeFactory,
+ enabled = subjects.bool,
+ tools = ProviderSequence(_ToolFactory),
+ flag_sets = ProviderSequence(_FlagSetFactory),
+ implies = ProviderDepset(_FeatureFactory),
+ files = subjects.depset_file,
+ ),
+)
+
+def _action_config_set_factory_impl(value, *, meta):
+ # We can't use the usual strategy of "inline the labels" since all labels
+ # are the same.
+ transformed = {}
+ for ac in value.action_configs.to_list():
+ key = ac.action.label
+ if key in transformed:
+ meta.add_failure("Action declared twice in action config", key)
+ transformed[key] = _ActionConfigFactory.factory(
+ value = ac,
+ meta = meta.derive(".get({})".format(key)),
+ )
+ return transformed
+
+# buildifier: disable=name-conventions
+_ActionConfigSetFactory = struct(
+ type = ActionConfigSetInfo,
+ name = "ActionConfigSetInfo",
+ factory = _action_config_set_factory_impl,
+)
+
+FACTORIES = [
+ _ActionTypeFactory,
+ _ActionTypeSetFactory,
+ _FlagGroupFactory,
+ _FlagSetFactory,
+ _MutuallyExclusiveCategoryFactory,
+ _FeatureFactory,
+ _FeatureConstraintFactory,
+ _FeatureSetFactory,
+ _ToolFactory,
+ _ActionConfigSetFactory,
+]