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