xref: /aosp_15_r20/external/bazelbuild-rules_testing/lib/private/analysis_test.bzl (revision d605057434dcabba796c020773aab68d9790ff9f)
1*d6050574SRomain Jobredeaux# Copyright 2022 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
15*d6050574SRomain Jobredeaux"""# Analysis test
16*d6050574SRomain Jobredeaux
17*d6050574SRomain JobredeauxSupport for testing analysis phase logic, such as rules.
18*d6050574SRomain Jobredeaux"""
19*d6050574SRomain Jobredeaux
20*d6050574SRomain Jobredeauxload("@bazel_skylib//lib:dicts.bzl", "dicts")
21*d6050574SRomain Jobredeauxload("//lib:truth.bzl", "truth")
22*d6050574SRomain Jobredeauxload("//lib:util.bzl", "recursive_testing_aspect", "testing_aspect")
23*d6050574SRomain Jobredeauxload("//lib/private:util.bzl", "get_test_name_from_function")
24*d6050574SRomain Jobredeaux
25*d6050574SRomain Jobredeauxdef _fail(env, msg):
26*d6050574SRomain Jobredeaux    """Unconditionally causes the current test to fail.
27*d6050574SRomain Jobredeaux
28*d6050574SRomain Jobredeaux    Args:
29*d6050574SRomain Jobredeaux      env: The test environment returned by `unittest.begin`.
30*d6050574SRomain Jobredeaux      msg: The message to log describing the failure.
31*d6050574SRomain Jobredeaux    """
32*d6050574SRomain Jobredeaux    full_msg = "In test %s: %s" % (env.ctx.attr._impl_name, msg)
33*d6050574SRomain Jobredeaux
34*d6050574SRomain Jobredeaux    # There isn't a better way to output the message in Starlark, so use print.
35*d6050574SRomain Jobredeaux    # buildifier: disable=print
36*d6050574SRomain Jobredeaux    print(full_msg)
37*d6050574SRomain Jobredeaux    env.failures.append(full_msg)
38*d6050574SRomain Jobredeaux
39*d6050574SRomain Jobredeauxdef _begin_analysis_test(ctx):
40*d6050574SRomain Jobredeaux    """Begins a unit test.
41*d6050574SRomain Jobredeaux
42*d6050574SRomain Jobredeaux    This should be the first function called in a unit test implementation
43*d6050574SRomain Jobredeaux    function. It initializes a "test environment" that is used to collect
44*d6050574SRomain Jobredeaux    assertion failures so that they can be reported and logged at the end of the
45*d6050574SRomain Jobredeaux    test.
46*d6050574SRomain Jobredeaux
47*d6050574SRomain Jobredeaux    Args:
48*d6050574SRomain Jobredeaux      ctx: The Starlark context. Pass the implementation function's `ctx` argument
49*d6050574SRomain Jobredeaux          in verbatim.
50*d6050574SRomain Jobredeaux
51*d6050574SRomain Jobredeaux    Returns:
52*d6050574SRomain Jobredeaux        An analysis_test "environment" struct. The following fields are public:
53*d6050574SRomain Jobredeaux          * ctx: the underlying rule ctx
54*d6050574SRomain Jobredeaux          * expect: a truth Expect object (see truth.bzl).
55*d6050574SRomain Jobredeaux          * fail: A function to register failures for later reporting.
56*d6050574SRomain Jobredeaux
57*d6050574SRomain Jobredeaux        Other attributes are private, internal details and may change at any time. Do not rely
58*d6050574SRomain Jobredeaux        on internal details.
59*d6050574SRomain Jobredeaux    """
60*d6050574SRomain Jobredeaux    target = getattr(ctx.attr, "target")
61*d6050574SRomain Jobredeaux    target = target[0] if type(target) == type([]) else target
62*d6050574SRomain Jobredeaux    failures = []
63*d6050574SRomain Jobredeaux    failures_env = struct(
64*d6050574SRomain Jobredeaux        ctx = ctx,
65*d6050574SRomain Jobredeaux        failures = failures,
66*d6050574SRomain Jobredeaux    )
67*d6050574SRomain Jobredeaux    truth_env = struct(
68*d6050574SRomain Jobredeaux        ctx = ctx,
69*d6050574SRomain Jobredeaux        fail = lambda msg: _fail(failures_env, msg),
70*d6050574SRomain Jobredeaux    )
71*d6050574SRomain Jobredeaux    analysis_test_env = struct(
72*d6050574SRomain Jobredeaux        ctx = ctx,
73*d6050574SRomain Jobredeaux        # Visibility: package; only exposed so that our own tests can verify
74*d6050574SRomain Jobredeaux        # failure behavior.
75*d6050574SRomain Jobredeaux        _failures = failures,
76*d6050574SRomain Jobredeaux        fail = truth_env.fail,
77*d6050574SRomain Jobredeaux        expect = truth.expect(truth_env),
78*d6050574SRomain Jobredeaux    )
79*d6050574SRomain Jobredeaux    return analysis_test_env, target
80*d6050574SRomain Jobredeaux
81*d6050574SRomain Jobredeauxdef _end_analysis_test(env):
82*d6050574SRomain Jobredeaux    """Ends an analysis test and logs the results.
83*d6050574SRomain Jobredeaux
84*d6050574SRomain Jobredeaux    This must be called and returned at the end of an analysis test implementation function so
85*d6050574SRomain Jobredeaux    that the results are reported.
86*d6050574SRomain Jobredeaux
87*d6050574SRomain Jobredeaux    Args:
88*d6050574SRomain Jobredeaux      env: The test environment returned by `analysistest.begin`.
89*d6050574SRomain Jobredeaux
90*d6050574SRomain Jobredeaux    Returns:
91*d6050574SRomain Jobredeaux      A list of providers needed to automatically register the analysis test result.
92*d6050574SRomain Jobredeaux    """
93*d6050574SRomain Jobredeaux    return [AnalysisTestResultInfo(
94*d6050574SRomain Jobredeaux        success = (len(env._failures) == 0),
95*d6050574SRomain Jobredeaux        message = "\n".join(env._failures),
96*d6050574SRomain Jobredeaux    )]
97*d6050574SRomain Jobredeaux
98*d6050574SRomain Jobredeauxdef analysis_test(
99*d6050574SRomain Jobredeaux        name,
100*d6050574SRomain Jobredeaux        target,
101*d6050574SRomain Jobredeaux        impl,
102*d6050574SRomain Jobredeaux        expect_failure = False,
103*d6050574SRomain Jobredeaux        attrs = {},
104*d6050574SRomain Jobredeaux        attr_values = {},
105*d6050574SRomain Jobredeaux        fragments = [],
106*d6050574SRomain Jobredeaux        config_settings = {},
107*d6050574SRomain Jobredeaux        extra_target_under_test_aspects = [],
108*d6050574SRomain Jobredeaux        collect_actions_recursively = False):
109*d6050574SRomain Jobredeaux    """Creates an analysis test from its implementation function.
110*d6050574SRomain Jobredeaux
111*d6050574SRomain Jobredeaux    An analysis test verifies the behavior of a "real" rule target by examining
112*d6050574SRomain Jobredeaux    and asserting on the providers given by the real target.
113*d6050574SRomain Jobredeaux
114*d6050574SRomain Jobredeaux    Each analysis test is defined in an implementation function. This function handles
115*d6050574SRomain Jobredeaux    the boilerplate to create and return a test target and captures the
116*d6050574SRomain Jobredeaux    implementation function's name so that it can be printed in test feedback.
117*d6050574SRomain Jobredeaux
118*d6050574SRomain Jobredeaux    An example of an analysis test:
119*d6050574SRomain Jobredeaux
120*d6050574SRomain Jobredeaux    ```
121*d6050574SRomain Jobredeaux    def basic_test(name):
122*d6050574SRomain Jobredeaux        my_rule(name = name + "_subject", ...)
123*d6050574SRomain Jobredeaux
124*d6050574SRomain Jobredeaux        analysistest(name = name, target = name + "_subject", impl = _your_test)
125*d6050574SRomain Jobredeaux
126*d6050574SRomain Jobredeaux    def _your_test(env, target, actions):
127*d6050574SRomain Jobredeaux        env.assert_that(target).runfiles().contains_at_least("foo.txt")
128*d6050574SRomain Jobredeaux        env.assert_that(find_action(actions, generating="foo.txt")).argv().contains("--a")
129*d6050574SRomain Jobredeaux    ```
130*d6050574SRomain Jobredeaux
131*d6050574SRomain Jobredeaux    Args:
132*d6050574SRomain Jobredeaux      name: Name of the target. It should be a Starlark identifier, matching pattern
133*d6050574SRomain Jobredeaux          '[A-Za-z_][A-Za-z0-9_]*'.
134*d6050574SRomain Jobredeaux      target: The target to test.
135*d6050574SRomain Jobredeaux      impl: The implementation function of the analysis test.
136*d6050574SRomain Jobredeaux      expect_failure: If true, the analysis test will expect the target
137*d6050574SRomain Jobredeaux          to fail. Assertions can be made on the underlying failure using truth.expect_failure
138*d6050574SRomain Jobredeaux      attrs: An optional dictionary to supplement the attrs passed to the
139*d6050574SRomain Jobredeaux          unit test's `rule()` constructor.
140*d6050574SRomain Jobredeaux      attr_values: An optional dictionary of kwargs to pass onto the
141*d6050574SRomain Jobredeaux          analysis test target itself (e.g. common attributes like `tags`,
142*d6050574SRomain Jobredeaux          `target_compatible_with`, or attributes from `attrs`). Note that these
143*d6050574SRomain Jobredeaux          are for the analysis test target itself, not the target under test.
144*d6050574SRomain Jobredeaux      fragments: An optional list of fragment names that can be used to give rules access to
145*d6050574SRomain Jobredeaux          language-specific parts of configuration.
146*d6050574SRomain Jobredeaux      config_settings: A dictionary of configuration settings to change for the target under
147*d6050574SRomain Jobredeaux          test and its dependencies. This may be used to essentially change 'build flags' for
148*d6050574SRomain Jobredeaux          the target under test, and may thus be utilized to test multiple targets with different
149*d6050574SRomain Jobredeaux          flags in a single build. NOTE: When values that are labels (e.g. for the
150*d6050574SRomain Jobredeaux          --platforms flag), it's suggested to always explicitly call `Label()`
151*d6050574SRomain Jobredeaux          on the value before passing it in. This ensures the label is resolved
152*d6050574SRomain Jobredeaux          in your repository's context, not rule_testing's.
153*d6050574SRomain Jobredeaux      extra_target_under_test_aspects: An optional list of aspects to apply to the target_under_test
154*d6050574SRomain Jobredeaux          in addition to those set up by default for the test harness itself.
155*d6050574SRomain Jobredeaux      collect_actions_recursively: If true, runs testing_aspect over all attributes, otherwise
156*d6050574SRomain Jobredeaux          it is only applied to the target under test.
157*d6050574SRomain Jobredeaux
158*d6050574SRomain Jobredeaux    Returns:
159*d6050574SRomain Jobredeaux        (None)
160*d6050574SRomain Jobredeaux    """
161*d6050574SRomain Jobredeaux
162*d6050574SRomain Jobredeaux    attrs = dict(attrs)
163*d6050574SRomain Jobredeaux    attrs["_impl_name"] = attr.string(default = get_test_name_from_function(impl))
164*d6050574SRomain Jobredeaux
165*d6050574SRomain Jobredeaux    changed_settings = dict(config_settings)
166*d6050574SRomain Jobredeaux    if expect_failure:
167*d6050574SRomain Jobredeaux        changed_settings["//command_line_option:allow_analysis_failures"] = "True"
168*d6050574SRomain Jobredeaux
169*d6050574SRomain Jobredeaux    target_attr_kwargs = {}
170*d6050574SRomain Jobredeaux    if changed_settings:
171*d6050574SRomain Jobredeaux        test_transition = analysis_test_transition(
172*d6050574SRomain Jobredeaux            settings = changed_settings,
173*d6050574SRomain Jobredeaux        )
174*d6050574SRomain Jobredeaux        target_attr_kwargs["cfg"] = test_transition
175*d6050574SRomain Jobredeaux
176*d6050574SRomain Jobredeaux    attrs["target"] = attr.label(
177*d6050574SRomain Jobredeaux        aspects = [recursive_testing_aspect if collect_actions_recursively else testing_aspect] + extra_target_under_test_aspects,
178*d6050574SRomain Jobredeaux        mandatory = True,
179*d6050574SRomain Jobredeaux        **target_attr_kwargs
180*d6050574SRomain Jobredeaux    )
181*d6050574SRomain Jobredeaux
182*d6050574SRomain Jobredeaux    def wrapped_impl(ctx):
183*d6050574SRomain Jobredeaux        env, target = _begin_analysis_test(ctx)
184*d6050574SRomain Jobredeaux        impl(env, target)
185*d6050574SRomain Jobredeaux        return _end_analysis_test(env)
186*d6050574SRomain Jobredeaux
187*d6050574SRomain Jobredeaux    return testing.analysis_test(
188*d6050574SRomain Jobredeaux        name,
189*d6050574SRomain Jobredeaux        wrapped_impl,
190*d6050574SRomain Jobredeaux        attrs = attrs,
191*d6050574SRomain Jobredeaux        fragments = fragments,
192*d6050574SRomain Jobredeaux        attr_values = dicts.add(attr_values, {"target": target}),
193*d6050574SRomain Jobredeaux    )
194