xref: /aosp_15_r20/external/bazelbuild-rules_testing/lib/private/expect_meta.bzl (revision d605057434dcabba796c020773aab68d9790ff9f)
1# Copyright 2023 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""# ExpectMeta
15
16ExpectMeta object implementation.
17"""
18
19load("@bazel_skylib//lib:unittest.bzl", ut_asserts = "asserts")
20
21def _expect_meta_new(env, exprs = [], details = [], format_str_kwargs = None):
22    """Creates a new "ExpectMeta" struct".
23
24    Method: ExpectMeta.new
25
26    ExpectMeta objects are internal helpers for the Expect object and Subject
27    objects. They are used for Subjects to store and communicate state through a
28    series of call chains and asserts.
29
30    This constructor should only be directly called by `Expect` objects. When a
31    parent Subject is creating a child-Subject, then [`derive()`] should be
32    used.
33
34    ### Env objects
35
36    The `env` object basically provides a way to interact with things outside
37    of the truth assertions framework. This allows easier testing of the
38    framework itself and decouples it from a particular test framework (which
39    makes it usable by by rules_testing's analysis_test and skylib's
40    analysistest)
41
42    The `env` object requires the following attribute:
43      * ctx: The test's ctx.
44
45    The `env` object allows the following attributes to customize behavior:
46      * fail: A callable that accepts a single string, which is the failure
47        message. Its return value is ignored. This is called when an assertion
48        fails. It's generally expected that it records a failure instead of
49        immediately failing.
50      * has_provider: (callable) it accepts two positional args, target and
51        provider and returns [`bool`]. This is used to implement `Provider in
52        target` operations.
53      * get_provider: (callable) it accepts two positional args, target and
54        provider and returns the provider value. This is used to implement
55        `target[Provider]`.
56
57    Args:
58        env: unittest env struct or some approximation.
59        exprs: ([`list`] of [`str`]) the expression strings of the call chain for
60            the subject.
61        details: ([`list`] of [`str`]) additional details to print on error. These
62            are usually informative details of the objects under test.
63        format_str_kwargs: optional dict of format() kwargs. These kwargs
64            are propagated through `derive()` calls and used when
65            `ExpectMeta.format_str()` is called.
66
67    Returns:
68        [`ExpectMeta`] object.
69    """
70    if format_str_kwargs == None:
71        format_str_kwargs = {}
72    format_str_kwargs.setdefault("workspace", env.ctx.workspace_name)
73    format_str_kwargs.setdefault("test_name", env.ctx.label.name)
74
75    # buildifier: disable=uninitialized
76    self = struct(
77        ctx = env.ctx,
78        env = env,
79        add_failure = lambda *a, **k: _expect_meta_add_failure(self, *a, **k),
80        current_expr = lambda *a, **k: _expect_meta_current_expr(self, *a, **k),
81        derive = lambda *a, **k: _expect_meta_derive(self, *a, **k),
82        format_str = lambda *a, **k: _expect_meta_format_str(self, *a, **k),
83        get_provider = lambda *a, **k: _expect_meta_get_provider(self, *a, **k),
84        has_provider = lambda *a, **k: _expect_meta_has_provider(self, *a, **k),
85        _exprs = exprs,
86        _details = details,
87        _format_str_kwargs = format_str_kwargs,
88    )
89    return self
90
91def _expect_meta_derive(self, expr = None, details = None, format_str_kwargs = {}):
92    """Create a derivation of the current meta object for a child-Subject.
93
94    Method: ExpectMeta.derive
95
96    When a Subject needs to create a child-Subject, it derives a new meta
97    object to pass to the child. This separates the parent's state from
98    the child's state and allows any failures generated by the child to
99    include the context of the parent creator.
100
101    Example usage:
102
103        def _foo_subject_action_named(self, name):
104            meta = self.meta.derive("action_named({})".format(name),
105                                    "action: {}".format(...))
106            return ActionSubject(..., meta)
107        def _foo_subject_name(self):
108            # No extra detail to include)
109            meta self.meta.derive("name()", None)
110
111
112    Args:
113        self: implicitly added.
114        expr: ([`str`]) human-friendly description of the call chain expression.
115            e.g., if `foo_subject.bar_named("baz")` returns a child-subject,
116            then "bar_named("bar")" would be the expression.
117        details: (optional [`list`] of [`str`]) human-friendly descriptions of additional
118            detail to include in errors. This is usually additional information
119            the child Subject wouldn't include itself. e.g. if
120            `foo.first_action_argv().contains(1)`, returned a ListSubject, then
121            including "first action: Action FooCompile" helps add context to the
122            error message. If there is no additional detail to include, pass
123            None.
124        format_str_kwargs: ([`dict`] of format()-kwargs) additional kwargs to
125            make available to [`format_str`] calls.
126
127    Returns:
128        [`ExpectMeta`] object.
129    """
130    if not details:
131        details = []
132    if expr:
133        exprs = [expr]
134    else:
135        exprs = []
136
137    if format_str_kwargs:
138        final_format_kwargs = {k: v for k, v in self._format_str_kwargs.items()}
139        final_format_kwargs.update(format_str_kwargs)
140    else:
141        final_format_kwargs = self._format_str_kwargs
142
143    return _expect_meta_new(
144        env = self.env,
145        exprs = self._exprs + exprs,
146        details = self._details + details,
147        format_str_kwargs = final_format_kwargs,
148    )
149
150def _expect_meta_format_str(self, template):
151    """Interpolate contextual keywords into a string.
152
153    This uses the normal `format()` style (i.e. using `{}`). Keywords
154    refer to parts of the call chain.
155
156    The particular keywords supported depend on the call chain. The following
157    are always present:
158      {workspace}: The name of the workspace, e.g. "rules_proto".
159      {test_name}: The base name of the current test.
160
161    Args:
162        self: implicitly added.
163        template: ([`str`]) the format template string to use.
164
165    Returns:
166        [`str`]; the template with parameters replaced.
167    """
168    return template.format(**self._format_str_kwargs)
169
170def _expect_meta_get_provider(self, target, provider):
171    """Get a provider from a target.
172
173    This is equivalent to `target[provider]`; the extra level of indirection
174    is to aid testing.
175
176    Args:
177        self: implicitly added.
178        target: ([`Target`]) the target to get the provider from.
179        provider: The provider type to get.
180
181    Returns:
182        The found provider, or fails if not present.
183    """
184    if hasattr(self.env, "get_provider"):
185        return self.env.get_provider(target, provider)
186    else:
187        return target[provider]
188
189def _expect_meta_has_provider(self, target, provider):
190    """Tells if a target has a provider.
191
192    This is equivalent to `provider in target`; the extra level of indirection
193    is to aid testing.
194
195    Args:
196        self: implicitly added.
197        target: ([`Target`]) the target to check for the provider.
198        provider: the provider type to check for.
199
200    Returns:
201        True if the target has the provider, False if not.
202    """
203    if hasattr(self.env, "has_provider"):
204        return self.env.has_provider(target, provider)
205    else:
206        return provider in target
207
208def _expect_meta_add_failure(self, problem, actual):
209    """Adds a failure with context.
210
211    Method: ExpectMeta.add_failure
212
213    Adds the given error message. Context from the subject and prior call chains
214    is automatically added.
215
216    Args:
217        self: implicitly added.
218        problem: ([`str`]) a string describing the expected value or problem
219            detected, and the expected values that weren't satisfied. A colon
220            should be used to separate the description from the values.
221            The description should be brief and include the word "expected",
222            e.g. "expected: foo", or "expected values missing: <list of missing>",
223            the key point being the reader can easily take the values shown
224            and look for it in the actual values displayed below it.
225        actual: ([`str`]) a string describing the values observed. A colon should
226            be used to separate the description from the observed values.
227            The description should be brief and include the word "actual", e.g.,
228            "actual: bar". The values should include the actual, observed,
229            values and pertinent information about them.
230    """
231    details = "\n".join([
232        "  {}".format(detail)
233        for detail in self._details
234        if detail
235    ])
236    if details:
237        details = "where... (most recent context last)\n" + details
238    msg = """\
239in test: {test}
240value of: {expr}
241{problem}
242{actual}
243{details}
244""".format(
245        test = self.ctx.label,
246        expr = _expect_meta_current_expr(self),
247        problem = problem,
248        actual = actual,
249        details = details,
250    )
251    _expect_meta_call_fail(self, msg)
252
253def _expect_meta_current_expr(self):
254    """Get a string representing the current expression.
255
256    Args:
257        self: implicitly added.
258
259    Returns:
260        [`str`] A string representing the current expression, e.g.
261        "foo.bar(something).baz()"
262    """
263    return ".".join(self._exprs)
264
265def _expect_meta_call_fail(self, msg):
266    """Adds a failure to the test run.
267
268    Args:
269        self: implicitly added.
270        msg: ([`str`]) the failure message.
271    """
272    fail_func = getattr(self.env, "fail", None)
273    if fail_func != None:
274        fail_func(msg)
275    else:
276        # Add a leading newline because unittest prepends the repr() of the
277        # function under test, which is often long and uninformative, making
278        # the first line of our message hard to see.
279        ut_asserts.true(self.env, False, "\n" + msg)
280
281# We use this name so it shows up nice in docs.
282# buildifier: disable=name-conventions
283ExpectMeta = struct(
284    new = _expect_meta_new,
285    derive = _expect_meta_derive,
286    format_str = _expect_meta_format_str,
287    get_provider = _expect_meta_get_provider,
288    has_provider = _expect_meta_has_provider,
289    add_failure = _expect_meta_add_failure,
290    call_fail = _expect_meta_call_fail,
291)
292