xref: /aosp_15_r20/external/bazelbuild-rules_testing/lib/private/struct_subject.bzl (revision d605057434dcabba796c020773aab68d9790ff9f)
1*d6050574SRomain Jobredeaux# Copyright 2023 The Bazel Authors. All rights reserved.
2*d6050574SRomain Jobredeaux#
3*d6050574SRomain Jobredeaux# Licensed under the Apache License, Version 2.0 (the "License");
4*d6050574SRomain Jobredeaux# you may not use this file except in compliance with the License.
5*d6050574SRomain Jobredeaux# You may obtain a copy of the License at
6*d6050574SRomain Jobredeaux#
7*d6050574SRomain Jobredeaux#     http://www.apache.org/licenses/LICENSE-2.0
8*d6050574SRomain Jobredeaux#
9*d6050574SRomain Jobredeaux# Unless required by applicable law or agreed to in writing, software
10*d6050574SRomain Jobredeaux# distributed under the License is distributed on an "AS IS" BASIS,
11*d6050574SRomain Jobredeaux# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*d6050574SRomain Jobredeaux# See the License for the specific language governing permissions and
13*d6050574SRomain Jobredeaux# limitations under the License.
14*d6050574SRomain Jobredeaux"""# StructSubject
15*d6050574SRomain Jobredeaux
16*d6050574SRomain JobredeauxA subject for arbitrary structs. This is most useful when wrapping an ad-hoc
17*d6050574SRomain Jobredeauxstruct (e.g. a struct specific to a particular function). Such ad-hoc structs
18*d6050574SRomain Jobredeauxare usually just plain data objects, so they don't need special functionality
19*d6050574SRomain Jobredeauxthat writing a full custom subject allows. If a struct would benefit from
20*d6050574SRomain Jobredeauxcustom accessors or asserts, write a custom subject instead.
21*d6050574SRomain Jobredeaux
22*d6050574SRomain JobredeauxThis subject is usually used as a helper to a more formally defined subject that
23*d6050574SRomain Jobredeauxknows the shape of the struct it needs to wrap. For example, a `FooInfoSubject`
24*d6050574SRomain Jobredeauximplementation might use it to handle `FooInfo.struct_with_a_couple_fields`.
25*d6050574SRomain Jobredeaux
26*d6050574SRomain JobredeauxNote the resulting subject object is not a direct replacement for the struct
27*d6050574SRomain Jobredeauxbeing wrapped:
28*d6050574SRomain Jobredeaux    * Structs wrapped by this subject have the attributes exposed as functions,
29*d6050574SRomain Jobredeaux      not as plain attributes. This matches the other subject classes and defers
30*d6050574SRomain Jobredeaux      converting an attribute to a subject unless necessary.
31*d6050574SRomain Jobredeaux    * The attribute name `actual` is reserved.
32*d6050574SRomain Jobredeaux
33*d6050574SRomain Jobredeaux
34*d6050574SRomain Jobredeaux## Example usages
35*d6050574SRomain Jobredeaux
36*d6050574SRomain JobredeauxTo use it as part of a custom subject returning a sub-value, construct it using
37*d6050574SRomain Jobredeaux`subjects.struct()` like so:
38*d6050574SRomain Jobredeaux
39*d6050574SRomain Jobredeaux```starlark
40*d6050574SRomain Jobredeauxload("@rules_testing//lib:truth.bzl", "subjects")
41*d6050574SRomain Jobredeaux
42*d6050574SRomain Jobredeauxdef _my_subject_foo(self):
43*d6050574SRomain Jobredeaux    return subjects.struct(
44*d6050574SRomain Jobredeaux        self.actual.foo,
45*d6050574SRomain Jobredeaux        meta = self.meta.derive("foo()"),
46*d6050574SRomain Jobredeaux        attrs = dict(a=subjects.int, b=subjects.str),
47*d6050574SRomain Jobredeaux    )
48*d6050574SRomain Jobredeaux```
49*d6050574SRomain Jobredeaux
50*d6050574SRomain JobredeauxIf you're checking a struct directly in a test, then you can use
51*d6050574SRomain Jobredeaux`Expect.that_struct`. You'll still have to pass the `attrs` arg so it knows how
52*d6050574SRomain Jobredeauxto map the attributes to the matching subject factories.
53*d6050574SRomain Jobredeaux
54*d6050574SRomain Jobredeaux```starlark
55*d6050574SRomain Jobredeauxdef _foo_test(env):
56*d6050574SRomain Jobredeaux    actual = env.expect.that_struct(
57*d6050574SRomain Jobredeaux        struct(a=1, b="x"),
58*d6050574SRomain Jobredeaux        attrs = dict(a=subjects.int, b=subjects.str)
59*d6050574SRomain Jobredeaux    )
60*d6050574SRomain Jobredeaux    actual.a().equals(1)
61*d6050574SRomain Jobredeaux    actual.b().equals("x")
62*d6050574SRomain Jobredeaux```
63*d6050574SRomain Jobredeaux"""
64*d6050574SRomain Jobredeaux
65*d6050574SRomain Jobredeauxdef _struct_subject_new(actual, *, meta, attrs):
66*d6050574SRomain Jobredeaux    """Creates a `StructSubject`, which is a thin wrapper around a [`struct`].
67*d6050574SRomain Jobredeaux
68*d6050574SRomain Jobredeaux    Args:
69*d6050574SRomain Jobredeaux        actual: ([`struct`]) the struct to wrap.
70*d6050574SRomain Jobredeaux        meta: ([`ExpectMeta`]) object of call context information.
71*d6050574SRomain Jobredeaux        attrs: ([`dict`] of [`str`] to [`callable`]) the functions to convert
72*d6050574SRomain Jobredeaux            attributes to subjects. The keys are attribute names that must
73*d6050574SRomain Jobredeaux            exist on `actual`. The values are functions with the signature
74*d6050574SRomain Jobredeaux            `def factory(value, *, meta)`, where `value` is the actual attribute
75*d6050574SRomain Jobredeaux            value of the struct, and `meta` is an [`ExpectMeta`] object.
76*d6050574SRomain Jobredeaux
77*d6050574SRomain Jobredeaux    Returns:
78*d6050574SRomain Jobredeaux        [`StructSubject`] object, which is a struct with the following shape:
79*d6050574SRomain Jobredeaux          * `actual` attribute, the underlying struct that was wrapped.
80*d6050574SRomain Jobredeaux          * A callable attribute for each `attrs` entry; it takes no args
81*d6050574SRomain Jobredeaux            and returns what the corresponding factory from `attrs` returns.
82*d6050574SRomain Jobredeaux    """
83*d6050574SRomain Jobredeaux    attr_accessors = {}
84*d6050574SRomain Jobredeaux    for name, factory in attrs.items():
85*d6050574SRomain Jobredeaux        if not hasattr(actual, name):
86*d6050574SRomain Jobredeaux            fail("Struct missing attribute: '{}' (from expression {})".format(
87*d6050574SRomain Jobredeaux                name,
88*d6050574SRomain Jobredeaux                meta.current_expr(),
89*d6050574SRomain Jobredeaux            ))
90*d6050574SRomain Jobredeaux        attr_accessors[name] = _make_attr_accessor(actual, name, factory, meta)
91*d6050574SRomain Jobredeaux
92*d6050574SRomain Jobredeaux    public = struct(actual = actual, **attr_accessors)
93*d6050574SRomain Jobredeaux    return public
94*d6050574SRomain Jobredeaux
95*d6050574SRomain Jobredeauxdef _make_attr_accessor(actual, name, factory, meta):
96*d6050574SRomain Jobredeaux    # A named function is used instead of a lambda so stack traces are easier to
97*d6050574SRomain Jobredeaux    # grok.
98*d6050574SRomain Jobredeaux    def attr_accessor():
99*d6050574SRomain Jobredeaux        return factory(getattr(actual, name), meta = meta.derive(name + "()"))
100*d6050574SRomain Jobredeaux
101*d6050574SRomain Jobredeaux    return attr_accessor
102*d6050574SRomain Jobredeaux
103*d6050574SRomain Jobredeaux# buildifier: disable=name-conventions
104*d6050574SRomain JobredeauxStructSubject = struct(
105*d6050574SRomain Jobredeaux    # keep sorted start
106*d6050574SRomain Jobredeaux    new = _struct_subject_new,
107*d6050574SRomain Jobredeaux    # keep sorted end
108*d6050574SRomain Jobredeaux)
109