| # 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): |
| if value == None: |
| meta.add_failure("Wanted a %s but got" % name, value) |
| got_keys = sorted(structs.to_dict(value).keys()) |
| subjects.collection(got_keys, meta = meta.derive(details = [ |
| "Value %r was not a %s - it has a different set of fields" % (value, name), |
| ])).contains_exactly(want_keys).in_order() |
| |
| 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(), |
| ) |