xref: /aosp_15_r20/external/bazelbuild-rules_testing/lib/private/runfiles_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
15*d6050574SRomain Jobredeaux"""# RunfilesSubject"""
16*d6050574SRomain Jobredeaux
17*d6050574SRomain Jobredeauxload(
18*d6050574SRomain Jobredeaux    "//lib:util.bzl",
19*d6050574SRomain Jobredeaux    "is_runfiles",
20*d6050574SRomain Jobredeaux    "runfiles_paths",
21*d6050574SRomain Jobredeaux)
22*d6050574SRomain Jobredeauxload(
23*d6050574SRomain Jobredeaux    ":check_util.bzl",
24*d6050574SRomain Jobredeaux    "check_contains_exactly",
25*d6050574SRomain Jobredeaux    "check_contains_predicate",
26*d6050574SRomain Jobredeaux    "check_not_contains_predicate",
27*d6050574SRomain Jobredeaux)
28*d6050574SRomain Jobredeauxload(":collection_subject.bzl", "CollectionSubject")
29*d6050574SRomain Jobredeauxload(
30*d6050574SRomain Jobredeaux    ":failure_messages.bzl",
31*d6050574SRomain Jobredeaux    "format_actual_collection",
32*d6050574SRomain Jobredeaux    "format_failure_unexpected_value",
33*d6050574SRomain Jobredeaux    "format_problem_expected_exactly",
34*d6050574SRomain Jobredeaux    "format_problem_missing_required_values",
35*d6050574SRomain Jobredeaux    "format_problem_unexpected_values",
36*d6050574SRomain Jobredeaux)
37*d6050574SRomain Jobredeauxload(":matching.bzl", "matching")
38*d6050574SRomain Jobredeauxload(":truth_common.bzl", "to_list")
39*d6050574SRomain Jobredeaux
40*d6050574SRomain Jobredeauxdef _runfiles_subject_new(runfiles, meta, kind = None):
41*d6050574SRomain Jobredeaux    """Creates a "RunfilesSubject" struct.
42*d6050574SRomain Jobredeaux
43*d6050574SRomain Jobredeaux    Method: RunfilesSubject.new
44*d6050574SRomain Jobredeaux
45*d6050574SRomain Jobredeaux    Args:
46*d6050574SRomain Jobredeaux        runfiles: ([`runfiles`]) the runfiles to check against.
47*d6050574SRomain Jobredeaux        meta: ([`ExpectMeta`]) the metadata about the call chain.
48*d6050574SRomain Jobredeaux        kind: (optional [`str`]) what type of runfiles they are, usually "data"
49*d6050574SRomain Jobredeaux            or "default". If not known or not applicable, use None.
50*d6050574SRomain Jobredeaux
51*d6050574SRomain Jobredeaux    Returns:
52*d6050574SRomain Jobredeaux        [`RunfilesSubject`] object.
53*d6050574SRomain Jobredeaux    """
54*d6050574SRomain Jobredeaux    self = struct(
55*d6050574SRomain Jobredeaux        runfiles = runfiles,
56*d6050574SRomain Jobredeaux        meta = meta,
57*d6050574SRomain Jobredeaux        kind = kind,
58*d6050574SRomain Jobredeaux        actual_paths = sorted(runfiles_paths(meta.ctx.workspace_name, runfiles)),
59*d6050574SRomain Jobredeaux    )
60*d6050574SRomain Jobredeaux    public = struct(
61*d6050574SRomain Jobredeaux        # keep sorted start
62*d6050574SRomain Jobredeaux        actual = runfiles,
63*d6050574SRomain Jobredeaux        contains = lambda *a, **k: _runfiles_subject_contains(self, *a, **k),
64*d6050574SRomain Jobredeaux        contains_at_least = lambda *a, **k: _runfiles_subject_contains_at_least(self, *a, **k),
65*d6050574SRomain Jobredeaux        contains_exactly = lambda *a, **k: _runfiles_subject_contains_exactly(self, *a, **k),
66*d6050574SRomain Jobredeaux        contains_none_of = lambda *a, **k: _runfiles_subject_contains_none_of(self, *a, **k),
67*d6050574SRomain Jobredeaux        contains_predicate = lambda *a, **k: _runfiles_subject_contains_predicate(self, *a, **k),
68*d6050574SRomain Jobredeaux        not_contains = lambda *a, **k: _runfiles_subject_not_contains(self, *a, **k),
69*d6050574SRomain Jobredeaux        not_contains_predicate = lambda *a, **k: _runfiles_subject_not_contains_predicate(self, *a, **k),
70*d6050574SRomain Jobredeaux        # keep sorted end
71*d6050574SRomain Jobredeaux    )
72*d6050574SRomain Jobredeaux    return public
73*d6050574SRomain Jobredeaux
74*d6050574SRomain Jobredeauxdef _runfiles_subject_contains(self, expected):
75*d6050574SRomain Jobredeaux    """Assert that the runfiles contains the provided path.
76*d6050574SRomain Jobredeaux
77*d6050574SRomain Jobredeaux    Method: RunfilesSubject.contains
78*d6050574SRomain Jobredeaux
79*d6050574SRomain Jobredeaux    Args:
80*d6050574SRomain Jobredeaux        self: implicitly added.
81*d6050574SRomain Jobredeaux        expected: ([`str`]) the path to check is present. This will be formatted
82*d6050574SRomain Jobredeaux            using `ExpectMeta.format_str` and its current contextual
83*d6050574SRomain Jobredeaux            keywords. Note that paths are runfiles-root relative (i.e.
84*d6050574SRomain Jobredeaux            you likely need to include the workspace name.)
85*d6050574SRomain Jobredeaux    """
86*d6050574SRomain Jobredeaux    expected = self.meta.format_str(expected)
87*d6050574SRomain Jobredeaux    matcher = matching.equals_wrapper(expected)
88*d6050574SRomain Jobredeaux    return _runfiles_subject_contains_predicate(self, matcher)
89*d6050574SRomain Jobredeaux
90*d6050574SRomain Jobredeauxdef _runfiles_subject_contains_at_least(self, paths):
91*d6050574SRomain Jobredeaux    """Assert that the runfiles contains at least all of the provided paths.
92*d6050574SRomain Jobredeaux
93*d6050574SRomain Jobredeaux    Method: RunfilesSubject.contains_at_least
94*d6050574SRomain Jobredeaux
95*d6050574SRomain Jobredeaux    All the paths must exist, but extra paths are allowed. Order is not checked.
96*d6050574SRomain Jobredeaux    Multiplicity is respected.
97*d6050574SRomain Jobredeaux
98*d6050574SRomain Jobredeaux    Args:
99*d6050574SRomain Jobredeaux        self: implicitly added.
100*d6050574SRomain Jobredeaux        paths: ((collection of [`str`]) | [`runfiles`]) the paths that must
101*d6050574SRomain Jobredeaux            exist. If a collection of strings is provided, they will be
102*d6050574SRomain Jobredeaux            formatted using [`ExpectMeta.format_str`], so its template keywords
103*d6050574SRomain Jobredeaux            can be directly passed. If a `runfiles` object is passed, it is
104*d6050574SRomain Jobredeaux            converted to a set of path strings.
105*d6050574SRomain Jobredeaux    """
106*d6050574SRomain Jobredeaux    if is_runfiles(paths):
107*d6050574SRomain Jobredeaux        paths = runfiles_paths(self.meta.ctx.workspace_name, paths)
108*d6050574SRomain Jobredeaux
109*d6050574SRomain Jobredeaux    paths = [self.meta.format_str(p) for p in to_list(paths)]
110*d6050574SRomain Jobredeaux
111*d6050574SRomain Jobredeaux    # NOTE: We don't return Ordered because there isn't a well-defined order
112*d6050574SRomain Jobredeaux    # between the different sub-objects within the runfiles.
113*d6050574SRomain Jobredeaux    CollectionSubject.new(
114*d6050574SRomain Jobredeaux        self.actual_paths,
115*d6050574SRomain Jobredeaux        meta = self.meta,
116*d6050574SRomain Jobredeaux        element_plural_name = "paths",
117*d6050574SRomain Jobredeaux        container_name = "{}runfiles".format(self.kind + " " if self.kind else ""),
118*d6050574SRomain Jobredeaux    ).contains_at_least(paths)
119*d6050574SRomain Jobredeaux
120*d6050574SRomain Jobredeauxdef _runfiles_subject_contains_predicate(self, matcher):
121*d6050574SRomain Jobredeaux    """Asserts that `matcher` matches at least one value.
122*d6050574SRomain Jobredeaux
123*d6050574SRomain Jobredeaux    Method: RunfilesSubject.contains_predicate
124*d6050574SRomain Jobredeaux
125*d6050574SRomain Jobredeaux    Args:
126*d6050574SRomain Jobredeaux        self: implicitly added.
127*d6050574SRomain Jobredeaux        matcher: callable that takes 1 positional arg ([`str`] path) and returns
128*d6050574SRomain Jobredeaux            boolean.
129*d6050574SRomain Jobredeaux    """
130*d6050574SRomain Jobredeaux    check_contains_predicate(
131*d6050574SRomain Jobredeaux        self.actual_paths,
132*d6050574SRomain Jobredeaux        matcher = matcher,
133*d6050574SRomain Jobredeaux        format_problem = "expected to contain: {}".format(matcher.desc),
134*d6050574SRomain Jobredeaux        format_actual = lambda: format_actual_collection(
135*d6050574SRomain Jobredeaux            self.actual_paths,
136*d6050574SRomain Jobredeaux            name = "{}runfiles".format(self.kind + " " if self.kind else ""),
137*d6050574SRomain Jobredeaux        ),
138*d6050574SRomain Jobredeaux        meta = self.meta,
139*d6050574SRomain Jobredeaux    )
140*d6050574SRomain Jobredeaux
141*d6050574SRomain Jobredeauxdef _runfiles_subject_contains_exactly(self, paths):
142*d6050574SRomain Jobredeaux    """Asserts that the runfiles contains_exactly the set of paths
143*d6050574SRomain Jobredeaux
144*d6050574SRomain Jobredeaux    Method: RunfilesSubject.contains_exactly
145*d6050574SRomain Jobredeaux
146*d6050574SRomain Jobredeaux    Args:
147*d6050574SRomain Jobredeaux        self: implicitly added.
148*d6050574SRomain Jobredeaux        paths: ([`collection`] of [`str`]) the paths to check. These will be
149*d6050574SRomain Jobredeaux            formatted using `meta.format_str`, so its template keywords can
150*d6050574SRomain Jobredeaux            be directly passed. All the paths must exist in the runfiles exactly
151*d6050574SRomain Jobredeaux            as provided, and no extra paths may exist.
152*d6050574SRomain Jobredeaux    """
153*d6050574SRomain Jobredeaux    paths = [self.meta.format_str(p) for p in to_list(paths)]
154*d6050574SRomain Jobredeaux    runfiles_name = "{}runfiles".format(self.kind + " " if self.kind else "")
155*d6050574SRomain Jobredeaux
156*d6050574SRomain Jobredeaux    check_contains_exactly(
157*d6050574SRomain Jobredeaux        expect_contains = paths,
158*d6050574SRomain Jobredeaux        actual_container = self.actual_paths,
159*d6050574SRomain Jobredeaux        format_actual = lambda: format_actual_collection(
160*d6050574SRomain Jobredeaux            self.actual_paths,
161*d6050574SRomain Jobredeaux            name = runfiles_name,
162*d6050574SRomain Jobredeaux        ),
163*d6050574SRomain Jobredeaux        format_expected = lambda: format_problem_expected_exactly(paths, sort = True),
164*d6050574SRomain Jobredeaux        format_missing = lambda missing: format_problem_missing_required_values(
165*d6050574SRomain Jobredeaux            missing,
166*d6050574SRomain Jobredeaux            sort = True,
167*d6050574SRomain Jobredeaux        ),
168*d6050574SRomain Jobredeaux        format_unexpected = lambda unexpected: format_problem_unexpected_values(
169*d6050574SRomain Jobredeaux            unexpected,
170*d6050574SRomain Jobredeaux            sort = True,
171*d6050574SRomain Jobredeaux        ),
172*d6050574SRomain Jobredeaux        format_out_of_order = lambda matches: fail("Should not be called"),
173*d6050574SRomain Jobredeaux        meta = self.meta,
174*d6050574SRomain Jobredeaux    )
175*d6050574SRomain Jobredeaux
176*d6050574SRomain Jobredeauxdef _runfiles_subject_contains_none_of(self, paths, require_workspace_prefix = True):
177*d6050574SRomain Jobredeaux    """Asserts the runfiles contain none of `paths`.
178*d6050574SRomain Jobredeaux
179*d6050574SRomain Jobredeaux    Method: RunfilesSubject.contains_none_of
180*d6050574SRomain Jobredeaux
181*d6050574SRomain Jobredeaux    Args:
182*d6050574SRomain Jobredeaux        self: implicitly added.
183*d6050574SRomain Jobredeaux        paths: ([`collection`] of [`str`]) the paths that should not exist. They should
184*d6050574SRomain Jobredeaux            be runfiles root-relative paths (not workspace relative). The value
185*d6050574SRomain Jobredeaux            is formatted using `ExpectMeta.format_str` and the current
186*d6050574SRomain Jobredeaux            contextual keywords.
187*d6050574SRomain Jobredeaux        require_workspace_prefix: ([`bool`]) True to check that the path includes the
188*d6050574SRomain Jobredeaux            workspace prefix. This is to guard against accidentallly passing a
189*d6050574SRomain Jobredeaux            workspace relative path, which will (almost) never exist, and cause
190*d6050574SRomain Jobredeaux            the test to always pass. Specify False if the file being checked for
191*d6050574SRomain Jobredeaux            is _actually_ a runfiles-root relative path that isn't under the
192*d6050574SRomain Jobredeaux            workspace itself.
193*d6050574SRomain Jobredeaux    """
194*d6050574SRomain Jobredeaux    formatted_paths = []
195*d6050574SRomain Jobredeaux    for path in paths:
196*d6050574SRomain Jobredeaux        path = self.meta.format_str(path)
197*d6050574SRomain Jobredeaux        formatted_paths.append(path)
198*d6050574SRomain Jobredeaux        if require_workspace_prefix:
199*d6050574SRomain Jobredeaux            _runfiles_subject_check_workspace_prefix(self, path)
200*d6050574SRomain Jobredeaux
201*d6050574SRomain Jobredeaux    CollectionSubject.new(
202*d6050574SRomain Jobredeaux        self.actual_paths,
203*d6050574SRomain Jobredeaux        meta = self.meta,
204*d6050574SRomain Jobredeaux    ).contains_none_of(formatted_paths)
205*d6050574SRomain Jobredeaux
206*d6050574SRomain Jobredeauxdef _runfiles_subject_not_contains(self, path, require_workspace_prefix = True):
207*d6050574SRomain Jobredeaux    """Assert that the runfiles does not contain the given path.
208*d6050574SRomain Jobredeaux
209*d6050574SRomain Jobredeaux    Method: RunfilesSubject.not_contains
210*d6050574SRomain Jobredeaux
211*d6050574SRomain Jobredeaux    Args:
212*d6050574SRomain Jobredeaux        self: implicitly added.
213*d6050574SRomain Jobredeaux        path: ([`str`]) the path that should not exist. It should be a runfiles
214*d6050574SRomain Jobredeaux            root-relative path (not workspace relative). The value is formatted
215*d6050574SRomain Jobredeaux            using `format_str`, so its template keywords can be directly
216*d6050574SRomain Jobredeaux            passed.
217*d6050574SRomain Jobredeaux        require_workspace_prefix: ([`bool`]) True to check that the path includes the
218*d6050574SRomain Jobredeaux            workspace prefix. This is to guard against accidentallly passing a
219*d6050574SRomain Jobredeaux            workspace relative path, which will (almost) never exist, and cause
220*d6050574SRomain Jobredeaux            the test to always pass. Specify False if the file being checked for
221*d6050574SRomain Jobredeaux            is _actually_ a runfiles-root relative path that isn't under the
222*d6050574SRomain Jobredeaux            workspace itself.
223*d6050574SRomain Jobredeaux    """
224*d6050574SRomain Jobredeaux    path = self.meta.format_str(path)
225*d6050574SRomain Jobredeaux    if require_workspace_prefix:
226*d6050574SRomain Jobredeaux        _runfiles_subject_check_workspace_prefix(self, path)
227*d6050574SRomain Jobredeaux
228*d6050574SRomain Jobredeaux    if path in self.actual_paths:
229*d6050574SRomain Jobredeaux        problem, actual = format_failure_unexpected_value(
230*d6050574SRomain Jobredeaux            container_name = "{}runfiles".format(self.kind + " " if self.kind else ""),
231*d6050574SRomain Jobredeaux            unexpected = path,
232*d6050574SRomain Jobredeaux            actual = self.actual_paths,
233*d6050574SRomain Jobredeaux        )
234*d6050574SRomain Jobredeaux        self.meta.add_failure(problem, actual)
235*d6050574SRomain Jobredeaux
236*d6050574SRomain Jobredeauxdef _runfiles_subject_not_contains_predicate(self, matcher):
237*d6050574SRomain Jobredeaux    """Asserts that none of the runfiles match `matcher`.
238*d6050574SRomain Jobredeaux
239*d6050574SRomain Jobredeaux    Method: RunfilesSubject.not_contains_predicate
240*d6050574SRomain Jobredeaux
241*d6050574SRomain Jobredeaux    Args:
242*d6050574SRomain Jobredeaux        self: implicitly added.
243*d6050574SRomain Jobredeaux        matcher: [`Matcher`] that accepts a string (runfiles root-relative path).
244*d6050574SRomain Jobredeaux    """
245*d6050574SRomain Jobredeaux    check_not_contains_predicate(self.actual_paths, matcher, meta = self.meta)
246*d6050574SRomain Jobredeaux
247*d6050574SRomain Jobredeauxdef _runfiles_subject_check_workspace_prefix(self, path):
248*d6050574SRomain Jobredeaux    if not path.startswith(self.meta.ctx.workspace_name + "/"):
249*d6050574SRomain Jobredeaux        fail("Rejecting path lacking workspace prefix: this often indicates " +
250*d6050574SRomain Jobredeaux             "a bug. Include the workspace name as part of the path, or pass " +
251*d6050574SRomain Jobredeaux             "require_workspace_prefix=False if the path is truly " +
252*d6050574SRomain Jobredeaux             "runfiles-root relative, not workspace relative.\npath=" + path)
253*d6050574SRomain Jobredeaux
254*d6050574SRomain Jobredeaux# We use this name so it shows up nice in docs.
255*d6050574SRomain Jobredeaux# buildifier: disable=name-conventions
256*d6050574SRomain JobredeauxRunfilesSubject = struct(
257*d6050574SRomain Jobredeaux    new = _runfiles_subject_new,
258*d6050574SRomain Jobredeaux    contains = _runfiles_subject_contains,
259*d6050574SRomain Jobredeaux    contains_at_least = _runfiles_subject_contains_at_least,
260*d6050574SRomain Jobredeaux    contains_predicate = _runfiles_subject_contains_predicate,
261*d6050574SRomain Jobredeaux    contains_exactly = _runfiles_subject_contains_exactly,
262*d6050574SRomain Jobredeaux    contains_none_of = _runfiles_subject_contains_none_of,
263*d6050574SRomain Jobredeaux    not_contains = _runfiles_subject_not_contains,
264*d6050574SRomain Jobredeaux    not_contains_predicate = _runfiles_subject_not_contains_predicate,
265*d6050574SRomain Jobredeaux    check_workspace_prefix = _runfiles_subject_check_workspace_prefix,
266*d6050574SRomain Jobredeaux)
267