From 2e780ceda91ae65c7ed3835bc912c145fc6fd21c Mon Sep 17 00:00:00 2001 From: Googler Date: Tue, 20 Feb 2024 15:53:42 -0800 Subject: [PATCH] BEGIN_PUBLIC Add support for testing rules_cc's new toolchains with rules_testing. END_PUBLIC PiperOrigin-RevId: 608769646 Change-Id: I1a698355e5e977cc86eedc7cf6e8e0f888593cb8 --- MODULE.bazel | 1 + WORKSPACE | 7 + cc/toolchains/cc_toolchain_info.bzl | 12 +- tests/rule_based_toolchain/BUILD | 0 tests/rule_based_toolchain/actions/BUILD | 34 ++++ .../actions/actions_test.bzl | 43 ++++ .../analysis_test_suite.bzl | 50 +++++ .../rule_based_toolchain/generate_factory.bzl | 127 ++++++++++++ tests/rule_based_toolchain/subjects.bzl | 185 ++++++++++++++++++ 9 files changed, 454 insertions(+), 5 deletions(-) create mode 100644 tests/rule_based_toolchain/BUILD create mode 100644 tests/rule_based_toolchain/actions/BUILD create mode 100644 tests/rule_based_toolchain/actions/actions_test.bzl create mode 100644 tests/rule_based_toolchain/analysis_test_suite.bzl create mode 100644 tests/rule_based_toolchain/generate_factory.bzl create mode 100644 tests/rule_based_toolchain/subjects.bzl diff --git a/MODULE.bazel b/MODULE.bazel index f6beb62..f664840 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -12,3 +12,4 @@ use_repo(cc_configure, "local_config_cc_toolchains") 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 @@ load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_ 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 @@ ActionConfigInfo = provider( # @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 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, +]