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