xref: /aosp_15_r20/external/bazelbuild-rules_testing/lib/util.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
15*d6050574SRomain Jobredeaux"""# Util
16*d6050574SRomain Jobredeaux
17*d6050574SRomain JobredeauxVarious utilities to aid with testing.
18*d6050574SRomain Jobredeaux"""
19*d6050574SRomain Jobredeaux
20*d6050574SRomain Jobredeauxload("@bazel_skylib//lib:paths.bzl", "paths")
21*d6050574SRomain Jobredeauxload("@bazel_skylib//lib:types.bzl", "types")
22*d6050574SRomain Jobredeaux
23*d6050574SRomain Jobredeaux# TODO(ilist): remove references to skylib analysistest
24*d6050574SRomain Jobredeauxload("@bazel_skylib//lib:unittest.bzl", "analysistest")
25*d6050574SRomain Jobredeauxload("@bazel_skylib//rules:write_file.bzl", "write_file")
26*d6050574SRomain Jobredeaux
27*d6050574SRomain Jobredeaux_SKIP_CI_TAGS = [
28*d6050574SRomain Jobredeaux    # copybara-marker: skip-ci-tag
29*d6050574SRomain Jobredeaux]
30*d6050574SRomain Jobredeaux
31*d6050574SRomain Jobredeaux# We add the manual tag to prevent implicitly building and running the subject
32*d6050574SRomain Jobredeaux# targets. When the rule-under-test is a test rule, it prevents trying to run
33*d6050574SRomain Jobredeaux# it. For binary rules, it prevents implicitly building it (and thus activating
34*d6050574SRomain Jobredeaux# more validation logic) when --build_tests_only is enabled.
35*d6050574SRomain JobredeauxPREVENT_IMPLICIT_BUILDING_TAGS = [
36*d6050574SRomain Jobredeaux    "manual",  # Prevent `bazel ...` from directly building them
37*d6050574SRomain Jobredeaux    # copybara-marker: skip-coverage-tag
38*d6050574SRomain Jobredeaux] + _SKIP_CI_TAGS
39*d6050574SRomain JobredeauxPREVENT_IMPLICIT_BUILDING = {"tags": PREVENT_IMPLICIT_BUILDING_TAGS}
40*d6050574SRomain Jobredeaux
41*d6050574SRomain Jobredeauxdef merge_kwargs(*kwargs):
42*d6050574SRomain Jobredeaux    """Merges multiple dicts of kwargs.
43*d6050574SRomain Jobredeaux
44*d6050574SRomain Jobredeaux    This is similar to dict.update except:
45*d6050574SRomain Jobredeaux        * If a key's value is a list, it'll be concatenated to any existing value.
46*d6050574SRomain Jobredeaux        * An error is raised when the same non-list key occurs more than once.
47*d6050574SRomain Jobredeaux
48*d6050574SRomain Jobredeaux    Args:
49*d6050574SRomain Jobredeaux        *kwargs: kwarg arg dicts to merge
50*d6050574SRomain Jobredeaux
51*d6050574SRomain Jobredeaux    Returns:
52*d6050574SRomain Jobredeaux        dict of the merged kwarg dics.
53*d6050574SRomain Jobredeaux    """
54*d6050574SRomain Jobredeaux    final = {}
55*d6050574SRomain Jobredeaux    for kwarg in kwargs:
56*d6050574SRomain Jobredeaux        for key, value in kwarg.items():
57*d6050574SRomain Jobredeaux            if types.is_list(value):
58*d6050574SRomain Jobredeaux                final[key] = final.get(key, []) + value
59*d6050574SRomain Jobredeaux            elif key in final:
60*d6050574SRomain Jobredeaux                fail("Key already exists: {}: {}".format(key, final[key]))
61*d6050574SRomain Jobredeaux            else:
62*d6050574SRomain Jobredeaux                final[key] = value
63*d6050574SRomain Jobredeaux    return final
64*d6050574SRomain Jobredeaux
65*d6050574SRomain Jobredeauxdef empty_file(name):
66*d6050574SRomain Jobredeaux    """Generates an empty file and returns the target name for it.
67*d6050574SRomain Jobredeaux
68*d6050574SRomain Jobredeaux    Args:
69*d6050574SRomain Jobredeaux        name: str, name of the generated output file.
70*d6050574SRomain Jobredeaux
71*d6050574SRomain Jobredeaux    Returns:
72*d6050574SRomain Jobredeaux        str, the name of the generated output.
73*d6050574SRomain Jobredeaux    """
74*d6050574SRomain Jobredeaux    write_file(
75*d6050574SRomain Jobredeaux        name = "write_" + name,
76*d6050574SRomain Jobredeaux        content = [],
77*d6050574SRomain Jobredeaux        out = name,
78*d6050574SRomain Jobredeaux    )
79*d6050574SRomain Jobredeaux    return name
80*d6050574SRomain Jobredeaux
81*d6050574SRomain Jobredeauxdef helper_target(rule, **kwargs):
82*d6050574SRomain Jobredeaux    """Define a target only used as a Starlark test input.
83*d6050574SRomain Jobredeaux
84*d6050574SRomain Jobredeaux    This is useful for e.g. analysis tests, which have to setup a small
85*d6050574SRomain Jobredeaux    graph of targets that should only be built via the test (e.g. they
86*d6050574SRomain Jobredeaux    may require config settings the test sets). Tags are added to
87*d6050574SRomain Jobredeaux    hide the target from `:all`, `/...`, TAP, etc.
88*d6050574SRomain Jobredeaux
89*d6050574SRomain Jobredeaux    Args:
90*d6050574SRomain Jobredeaux        rule: rule-like function.
91*d6050574SRomain Jobredeaux        **kwargs: Any kwargs to pass to `rule`. Additional tags will
92*d6050574SRomain Jobredeaux            be added to hide the target.
93*d6050574SRomain Jobredeaux    """
94*d6050574SRomain Jobredeaux    kwargs = merge_kwargs(kwargs, PREVENT_IMPLICIT_BUILDING)
95*d6050574SRomain Jobredeaux    rule(**kwargs)
96*d6050574SRomain Jobredeaux
97*d6050574SRomain Jobredeauxdef short_paths(files_depset):
98*d6050574SRomain Jobredeaux    """Returns the `short_path` paths for a depset of files."""
99*d6050574SRomain Jobredeaux    return [f.short_path for f in files_depset.to_list()]
100*d6050574SRomain Jobredeaux
101*d6050574SRomain Jobredeauxdef runfiles_paths(workspace_name, runfiles):
102*d6050574SRomain Jobredeaux    """Returns the root-relative short paths for the files in runfiles.
103*d6050574SRomain Jobredeaux
104*d6050574SRomain Jobredeaux    Args:
105*d6050574SRomain Jobredeaux        workspace_name: str, the workspace name (`ctx.workspace_name`).
106*d6050574SRomain Jobredeaux        runfiles: runfiles, the runfiles to convert to short paths.
107*d6050574SRomain Jobredeaux
108*d6050574SRomain Jobredeaux    Returns:
109*d6050574SRomain Jobredeaux        list of short paths but runfiles root-relative. e.g.
110*d6050574SRomain Jobredeaux        'myworkspace/foo/bar.py'.
111*d6050574SRomain Jobredeaux    """
112*d6050574SRomain Jobredeaux    paths = []
113*d6050574SRomain Jobredeaux    paths.extend(short_paths(runfiles.files))
114*d6050574SRomain Jobredeaux    paths.extend(runfiles.empty_filenames.to_list())
115*d6050574SRomain Jobredeaux    paths.extend(_runfiles_symlink_paths(runfiles.symlinks))
116*d6050574SRomain Jobredeaux    paths = _prepend_path(workspace_name, paths)
117*d6050574SRomain Jobredeaux
118*d6050574SRomain Jobredeaux    paths.extend(_runfiles_symlink_paths(runfiles.root_symlinks))
119*d6050574SRomain Jobredeaux    return paths
120*d6050574SRomain Jobredeaux
121*d6050574SRomain Jobredeauxdef runfiles_map(workspace_name, runfiles):
122*d6050574SRomain Jobredeaux    """Convert runfiles to a path->file mapping.
123*d6050574SRomain Jobredeaux
124*d6050574SRomain Jobredeaux    This approximates how Bazel materializes the runfiles on the file
125*d6050574SRomain Jobredeaux    system.
126*d6050574SRomain Jobredeaux
127*d6050574SRomain Jobredeaux    Args:
128*d6050574SRomain Jobredeaux        workspace_name: str; the workspace the runfiles belong to.
129*d6050574SRomain Jobredeaux        runfiles: runfiles; the runfiles to convert to a map.
130*d6050574SRomain Jobredeaux
131*d6050574SRomain Jobredeaux    Returns:
132*d6050574SRomain Jobredeaux        `dict[str, optional File]` that maps the path under the runfiles root
133*d6050574SRomain Jobredeaux        to it's backing file. The file may be None if the path came
134*d6050574SRomain Jobredeaux        from `runfiles.empty_filenames`.
135*d6050574SRomain Jobredeaux    """
136*d6050574SRomain Jobredeaux    path_map = {}
137*d6050574SRomain Jobredeaux    workspace_prefix = workspace_name + "/"
138*d6050574SRomain Jobredeaux    for file in runfiles.files.to_list():
139*d6050574SRomain Jobredeaux        path_map[workspace_prefix + file.short_path] = file
140*d6050574SRomain Jobredeaux    for path in runfiles.empty_filenames.to_list():
141*d6050574SRomain Jobredeaux        path_map[workspace_prefix + path] = None
142*d6050574SRomain Jobredeaux
143*d6050574SRomain Jobredeaux    # NOTE: What happens when different files have the same symlink isn't
144*d6050574SRomain Jobredeaux    # exactly clear. For lack of a better option, we'll just take the last seen
145*d6050574SRomain Jobredeaux    # value.
146*d6050574SRomain Jobredeaux    for entry in runfiles.symlinks.to_list():
147*d6050574SRomain Jobredeaux        path_map[workspace_prefix + entry.path] = entry.target_file
148*d6050574SRomain Jobredeaux    for entry in runfiles.root_symlinks.to_list():
149*d6050574SRomain Jobredeaux        path_map[entry.path] = entry.target_file
150*d6050574SRomain Jobredeaux    return path_map
151*d6050574SRomain Jobredeaux
152*d6050574SRomain Jobredeauxdef _prepend_path(prefix, path_strs):
153*d6050574SRomain Jobredeaux    return [paths.join(prefix, p) for p in path_strs]
154*d6050574SRomain Jobredeaux
155*d6050574SRomain Jobredeauxdef _runfiles_symlink_paths(symlinks_depset):
156*d6050574SRomain Jobredeaux    return [entry.path for entry in symlinks_depset.to_list()]
157*d6050574SRomain Jobredeaux
158*d6050574SRomain JobredeauxTestingAspectInfo = provider(
159*d6050574SRomain Jobredeaux    "Details about a target-under-test useful for testing.",
160*d6050574SRomain Jobredeaux    fields = {
161*d6050574SRomain Jobredeaux        "attrs": "The raw attributes of the target under test.",
162*d6050574SRomain Jobredeaux        "actions": "The actions registered for the target under test.",
163*d6050574SRomain Jobredeaux        "vars": "The var dict (ctx.var) for the target under text.",
164*d6050574SRomain Jobredeaux        "bin_path": "str; the ctx.bin_dir.path value (aka execroot).",
165*d6050574SRomain Jobredeaux    },
166*d6050574SRomain Jobredeaux)
167*d6050574SRomain Jobredeaux
168*d6050574SRomain Jobredeauxdef _testing_aspect_impl(target, ctx):
169*d6050574SRomain Jobredeaux    return [TestingAspectInfo(
170*d6050574SRomain Jobredeaux        attrs = ctx.rule.attr,
171*d6050574SRomain Jobredeaux        actions = target.actions,
172*d6050574SRomain Jobredeaux        vars = ctx.var,
173*d6050574SRomain Jobredeaux        bin_path = ctx.bin_dir.path,
174*d6050574SRomain Jobredeaux    )]
175*d6050574SRomain Jobredeaux
176*d6050574SRomain Jobredeaux# TODO(ilist): make private, after switching python tests to new testing framework
177*d6050574SRomain Jobredeauxtesting_aspect = aspect(
178*d6050574SRomain Jobredeaux    implementation = _testing_aspect_impl,
179*d6050574SRomain Jobredeaux)
180*d6050574SRomain Jobredeaux
181*d6050574SRomain Jobredeaux# The same as `testing_aspect`, but recurses through all attributes in the
182*d6050574SRomain Jobredeaux# whole graph. This is useful if you need to extract information about
183*d6050574SRomain Jobredeaux# targets that aren't direct dependencies of the target under test, or to
184*d6050574SRomain Jobredeaux# reconstruct a more complete graph of inputs/outputs/generating-target.
185*d6050574SRomain Jobredeaux# TODO(ilist): make private, after switching python tests to new testing framework
186*d6050574SRomain Jobredeauxrecursive_testing_aspect = aspect(
187*d6050574SRomain Jobredeaux    implementation = _testing_aspect_impl,
188*d6050574SRomain Jobredeaux    attr_aspects = ["*"],
189*d6050574SRomain Jobredeaux)
190*d6050574SRomain Jobredeaux
191*d6050574SRomain Jobredeauxdef get_target_attrs(env):
192*d6050574SRomain Jobredeaux    return analysistest.target_under_test(env)[TestingAspectInfo].attrs
193*d6050574SRomain Jobredeaux
194*d6050574SRomain Jobredeaux# TODO(b/203567235): Remove this after cl/382467002 lands and the regular
195*d6050574SRomain Jobredeaux# `analysistest.target_actions()` can be used.
196*d6050574SRomain Jobredeauxdef get_target_actions(env):
197*d6050574SRomain Jobredeaux    return analysistest.target_under_test(env)[TestingAspectInfo].actions
198*d6050574SRomain Jobredeaux
199*d6050574SRomain Jobredeauxdef is_runfiles(obj):
200*d6050574SRomain Jobredeaux    """Tells if an object is a runfiles object."""
201*d6050574SRomain Jobredeaux    return type(obj) == "runfiles"
202*d6050574SRomain Jobredeaux
203*d6050574SRomain Jobredeauxdef is_file(obj):
204*d6050574SRomain Jobredeaux    """Tells if an object is a File object."""
205*d6050574SRomain Jobredeaux    return type(obj) == "File"
206*d6050574SRomain Jobredeaux
207*d6050574SRomain Jobredeauxdef skip_test(name):
208*d6050574SRomain Jobredeaux    """Defines a test target that is always skipped.
209*d6050574SRomain Jobredeaux
210*d6050574SRomain Jobredeaux    This is useful for tests that should be skipped if some condition,
211*d6050574SRomain Jobredeaux    determinable during the loading phase, isn't met. The resulting target will
212*d6050574SRomain Jobredeaux    show up as "SKIPPED" in the output.
213*d6050574SRomain Jobredeaux
214*d6050574SRomain Jobredeaux    If possible, prefer to use `target_compatible_with` to mark tests as
215*d6050574SRomain Jobredeaux    incompatible. This avoids confusing behavior where the type of a target
216*d6050574SRomain Jobredeaux    varies depending on loading-phase behavior.
217*d6050574SRomain Jobredeaux
218*d6050574SRomain Jobredeaux    Args:
219*d6050574SRomain Jobredeaux      name: The name of the target.
220*d6050574SRomain Jobredeaux    """
221*d6050574SRomain Jobredeaux    _skip_test(
222*d6050574SRomain Jobredeaux        name = name,
223*d6050574SRomain Jobredeaux        target_compatible_with = ["@platforms//:incompatible"],
224*d6050574SRomain Jobredeaux        tags = _SKIP_CI_TAGS,
225*d6050574SRomain Jobredeaux    )
226*d6050574SRomain Jobredeaux
227*d6050574SRomain Jobredeauxdef _skip_test_impl(ctx):
228*d6050574SRomain Jobredeaux    _ = ctx  # @unused
229*d6050574SRomain Jobredeaux    fail("Should have been skipped")
230*d6050574SRomain Jobredeaux
231*d6050574SRomain Jobredeaux_skip_test = rule(
232*d6050574SRomain Jobredeaux    implementation = _skip_test_impl,
233*d6050574SRomain Jobredeaux    test = True,
234*d6050574SRomain Jobredeaux)
235*d6050574SRomain Jobredeaux
236*d6050574SRomain Jobredeauxdef _force_exec_config_impl(ctx):
237*d6050574SRomain Jobredeaux    return [DefaultInfo(
238*d6050574SRomain Jobredeaux        files = depset(ctx.files.tools),
239*d6050574SRomain Jobredeaux        default_runfiles = ctx.runfiles().merge_all([
240*d6050574SRomain Jobredeaux            t[DefaultInfo].default_runfiles
241*d6050574SRomain Jobredeaux            for t in ctx.attr.tools
242*d6050574SRomain Jobredeaux        ]),
243*d6050574SRomain Jobredeaux        data_runfiles = ctx.runfiles().merge_all([
244*d6050574SRomain Jobredeaux            t[DefaultInfo].data_runfiles
245*d6050574SRomain Jobredeaux            for t in ctx.attr.tools
246*d6050574SRomain Jobredeaux        ]),
247*d6050574SRomain Jobredeaux    )]
248*d6050574SRomain Jobredeaux
249*d6050574SRomain Jobredeauxforce_exec_config = rule(
250*d6050574SRomain Jobredeaux    implementation = _force_exec_config_impl,
251*d6050574SRomain Jobredeaux    doc = "Rule to force arbitrary targets to `cfg=exec` so they can be " +
252*d6050574SRomain Jobredeaux          "tested when used as tools.",
253*d6050574SRomain Jobredeaux    attrs = {
254*d6050574SRomain Jobredeaux        "tools": attr.label_list(
255*d6050574SRomain Jobredeaux            cfg = "exec",
256*d6050574SRomain Jobredeaux            allow_files = True,
257*d6050574SRomain Jobredeaux            doc = "A list of tools to force into the exec config",
258*d6050574SRomain Jobredeaux        ),
259*d6050574SRomain Jobredeaux    },
260*d6050574SRomain Jobredeaux)
261*d6050574SRomain Jobredeaux
262*d6050574SRomain Jobredeauxutil = struct(
263*d6050574SRomain Jobredeaux    # keep sorted start
264*d6050574SRomain Jobredeaux    empty_file = empty_file,
265*d6050574SRomain Jobredeaux    force_exec_config = force_exec_config,
266*d6050574SRomain Jobredeaux    helper_target = helper_target,
267*d6050574SRomain Jobredeaux    merge_kwargs = merge_kwargs,
268*d6050574SRomain Jobredeaux    recursive_testing_aspect = recursive_testing_aspect,
269*d6050574SRomain Jobredeaux    runfiles_map = runfiles_map,
270*d6050574SRomain Jobredeaux    runfiles_paths = runfiles_paths,
271*d6050574SRomain Jobredeaux    short_paths = short_paths,
272*d6050574SRomain Jobredeaux    skip_test = skip_test,
273*d6050574SRomain Jobredeaux    testing_aspect = testing_aspect,
274*d6050574SRomain Jobredeaux    # keep sorted end
275*d6050574SRomain Jobredeaux)
276