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"""# DepsetFileSubject""" 16 17load("//lib:util.bzl", "is_file") 18load( 19 ":check_util.bzl", 20 "check_contains_at_least_predicates", 21 "check_contains_exactly", 22 "check_contains_predicate", 23 "check_not_contains_predicate", 24) 25load(":collection_subject.bzl", "CollectionSubject") 26load( 27 ":failure_messages.bzl", 28 "format_actual_collection", 29 "format_problem_expected_exactly", 30 "format_problem_matched_out_of_order", 31 "format_problem_missing_any_values", 32 "format_problem_missing_required_values", 33 "format_problem_predicates_did_not_match", 34 "format_problem_unexpected_values", 35) 36load(":matching.bzl", "matching") 37load(":truth_common.bzl", "to_list") 38 39def _depset_file_subject_new(files, meta, container_name = "depset", element_plural_name = "files"): 40 """Creates a DepsetFileSubject asserting on `files`. 41 42 Method: DepsetFileSubject.new 43 44 Args: 45 files: ([`depset`] of [`File`]) the values to assert on. 46 meta: ([`ExpectMeta`]) of call chain information. 47 container_name: ([`str`]) conceptual name of the container. 48 element_plural_name: ([`str`]) the plural word for the values in the container. 49 50 Returns: 51 [`DepsetFileSubject`] object. 52 """ 53 54 # buildifier: disable=uninitialized 55 public = struct( 56 # keep sorted start 57 contains = lambda *a, **k: _depset_file_subject_contains(self, *a, **k), 58 contains_any_in = lambda *a, **k: _depset_file_subject_contains_any_in(self, *a, **k), 59 contains_at_least = lambda *a, **k: _depset_file_subject_contains_at_least(self, *a, **k), 60 contains_at_least_predicates = lambda *a, **k: _depset_file_subject_contains_at_least_predicates(self, *a, **k), 61 contains_exactly = lambda *a, **k: _depset_file_subject_contains_exactly(self, *a, **k), 62 contains_predicate = lambda *a, **k: _depset_file_subject_contains_predicate(self, *a, **k), 63 not_contains = lambda *a, **k: _depset_file_subject_not_contains(self, *a, **k), 64 not_contains_predicate = lambda *a, **k: _depset_file_subject_not_contains_predicate(self, *a, **k), 65 # keep sorted end 66 ) 67 self = struct( 68 files = to_list(files), 69 meta = meta, 70 public = public, 71 actual_paths = sorted([f.short_path for f in to_list(files)]), 72 container_name = container_name, 73 element_plural_name = element_plural_name, 74 ) 75 return public 76 77def _depset_file_subject_contains(self, expected): 78 """Asserts that the depset of files contains the provided path/file. 79 80 Method: DepsetFileSubject.contains 81 82 Args: 83 self: implicitly added 84 expected: ([`str`] | [`File`]) If a string path is provided, it is 85 compared to the short path of the files and are formatted using 86 [`ExpectMeta.format_str`] and its current contextual keywords. Note 87 that, when using `File` objects, two files' configurations must be 88 the same for them to be considered equal. 89 """ 90 if is_file(expected): 91 actual = self.files 92 else: 93 expected = self.meta.format_str(expected) 94 actual = self.actual_paths 95 96 CollectionSubject.new( 97 actual, 98 meta = self.meta, 99 container_name = self.container_name, 100 element_plural_name = self.element_plural_name, 101 ).contains(expected) 102 103def _depset_file_subject_contains_at_least(self, expected): 104 """Asserts that the depset of files contains at least the provided paths. 105 106 Method: DepsetFileSubject.contains_at_least 107 108 Args: 109 self: implicitly added 110 expected: ([`collection`] of [`str`] | collection of [`File`]) multiplicity 111 is respected. If string paths are provided, they are compared to the 112 short path of the files and are formatted using 113 `ExpectMeta.format_str` and its current contextual keywords. Note 114 that, when using `File` objects, two files' configurations must be the 115 same for them to be considered equal. 116 117 Returns: 118 [`Ordered`] (see `_ordered_incorrectly_new`). 119 """ 120 expected = to_list(expected) 121 if len(expected) < 1 or is_file(expected[0]): 122 actual = self.files 123 else: 124 expected = [self.meta.format_str(v) for v in expected] 125 actual = self.actual_paths 126 127 return CollectionSubject.new( 128 actual, 129 meta = self.meta, 130 container_name = self.container_name, 131 element_plural_name = self.element_plural_name, 132 ).contains_at_least(expected) 133 134def _depset_file_subject_contains_any_in(self, expected): 135 """Asserts that any of the values in `expected` exist. 136 137 Method: DepsetFileSubject.contains_any_in 138 139 Args: 140 self: implicitly added. 141 expected: ([`collection`] of [`str`] paths | [`collection`] of [`File`]) 142 at least one of the values must exist. Note that, when using `File` 143 objects, two files' configurations must be the same for them to be 144 considered equal. When string paths are provided, they are compared 145 to `File.short_path`. 146 """ 147 expected = to_list(expected) 148 if len(expected) < 1 or is_file(expected[0]): 149 actual = self.files 150 else: 151 actual = self.actual_paths 152 153 expected_map = {value: None for value in expected} 154 155 check_contains_predicate( 156 actual, 157 matcher = matching.is_in(expected_map), 158 format_problem = lambda: format_problem_missing_any_values(expected), 159 format_actual = lambda: format_actual_collection( 160 actual, 161 container_name = self.container_name, 162 ), 163 meta = self.meta, 164 ) 165 166def _depset_file_subject_contains_at_least_predicates(self, matchers): 167 """Assert that the depset is a subset of the given predicates. 168 169 Method: DepsetFileSubject.contains_at_least_predicates 170 171 The depset must match all the predicates. It can contain extra elements. 172 The multiplicity of matchers is respected. Checking that the relative order 173 of matches is the same as the passed-in matchers order can done by calling 174 `in_order()`. 175 176 Args: 177 self: implicitly added. 178 matchers: ([`list`] of [`Matcher`]) (see `matchers` struct) that 179 accept [`File`] objects. 180 181 Returns: 182 [`Ordered`] (see `_ordered_incorrectly_new`). 183 """ 184 ordered = check_contains_at_least_predicates( 185 self.files, 186 matchers, 187 format_missing = lambda missing: format_problem_predicates_did_not_match( 188 missing, 189 element_plural_name = self.element_plural_name, 190 container_name = self.container_name, 191 ), 192 format_out_of_order = format_problem_matched_out_of_order, 193 format_actual = lambda: format_actual_collection( 194 self.files, 195 name = self.container_name, 196 ), 197 meta = self.meta, 198 ) 199 return ordered 200 201def _depset_file_subject_contains_predicate(self, matcher): 202 """Asserts that `matcher` matches at least one value. 203 204 Method: DepsetFileSubject.contains_predicate 205 206 Args: 207 self: implicitly added. 208 matcher: [`Matcher`] (see `matching` struct) that accepts `File` objects. 209 """ 210 check_contains_predicate( 211 self.files, 212 matcher = matcher, 213 format_problem = matcher.desc, 214 format_actual = lambda: format_actual_collection( 215 self.files, 216 name = self.container_name, 217 ), 218 meta = self.meta, 219 ) 220 221def _depset_file_subject_contains_exactly(self, paths): 222 """Asserts the depset of files contains exactly the given paths. 223 224 Method: DepsetFileSubject.contains_exactly 225 226 Args: 227 self: implicitly added. 228 paths: ([`collection`] of [`str`]) the paths that must exist. These are 229 compared to the `short_path` values of the files in the depset. 230 All the paths, and no more, must exist. 231 """ 232 paths = [self.meta.format_str(p) for p in to_list(paths)] 233 check_contains_exactly( 234 expect_contains = paths, 235 actual_container = self.actual_paths, 236 format_actual = lambda: format_actual_collection( 237 self.actual_paths, 238 name = self.container_name, 239 ), 240 format_expected = lambda: format_problem_expected_exactly( 241 paths, 242 sort = True, 243 ), 244 format_missing = lambda missing: format_problem_missing_required_values( 245 missing, 246 sort = True, 247 ), 248 format_unexpected = lambda unexpected: format_problem_unexpected_values( 249 unexpected, 250 sort = True, 251 ), 252 format_out_of_order = lambda matches: fail("Should not be called"), 253 meta = self.meta, 254 ) 255 256def _depset_file_subject_not_contains(self, short_path): 257 """Asserts that `short_path` is not in the depset. 258 259 Method: DepsetFileSubject.not_contains_predicate 260 261 Args: 262 self: implicitly added. 263 short_path: ([`str`]) the short path that should not be present. 264 """ 265 short_path = self.meta.format_str(short_path) 266 matcher = matching.custom(short_path, lambda f: f.short_path == short_path) 267 check_not_contains_predicate(self.files, matcher, meta = self.meta) 268 269def _depset_file_subject_not_contains_predicate(self, matcher): 270 """Asserts that nothing in the depset matches `matcher`. 271 272 Method: DepsetFileSubject.not_contains_predicate 273 274 Args: 275 self: implicitly added. 276 matcher: ([`Matcher`]) that must match. It operates on [`File`] objects. 277 """ 278 check_not_contains_predicate(self.files, matcher, meta = self.meta) 279 280# We use this name so it shows up nice in docs. 281# buildifier: disable=name-conventions 282DepsetFileSubject = struct( 283 new = _depset_file_subject_new, 284 contains = _depset_file_subject_contains, 285 contains_at_least = _depset_file_subject_contains_at_least, 286 contains_any_in = _depset_file_subject_contains_any_in, 287 contains_at_least_predicates = _depset_file_subject_contains_at_least_predicates, 288 contains_predicate = _depset_file_subject_contains_predicate, 289 contains_exactly = _depset_file_subject_contains_exactly, 290 not_contains = _depset_file_subject_not_contains, 291 not_contains_predicate = _depset_file_subject_not_contains_predicate, 292) 293