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