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"""# ActionSubject""" 16 17load(":collection_subject.bzl", "CollectionSubject") 18load(":depset_file_subject.bzl", "DepsetFileSubject") 19load(":dict_subject.bzl", "DictSubject") 20load( 21 ":failure_messages.bzl", 22 "format_failure_missing_all_values", 23 "format_failure_unexpected_value", 24 "format_failure_unexpected_values", 25) 26load(":str_subject.bzl", "StrSubject") 27load(":truth_common.bzl", "enumerate_list_as_lines", "mkmethod") 28 29def _action_subject_new(action, meta): 30 """Creates an "ActionSubject" struct. 31 32 Method: ActionSubject.new 33 34 Example usage: 35 36 expect(env).that_action(action).not_contains_arg("foo") 37 38 Args: 39 action: ([`Action`]) value to check against. 40 meta: ([`ExpectMeta`]) of call chain information. 41 42 Returns: 43 [`ActionSubject`] object. 44 """ 45 46 # buildifier: disable=uninitialized 47 self = struct( 48 action = action, 49 meta = meta, 50 # Dict[str, list[str]] of flags. The keys must be in the same order 51 # as found in argv to allow ordering asserts of them. 52 parsed_flags = _action_subject_parse_flags(action.argv), 53 ) 54 public = struct( 55 # keep sorted start 56 actual = action, 57 argv = mkmethod(self, _action_subject_argv), 58 contains_at_least_args = mkmethod(self, _action_subject_contains_at_least_args), 59 contains_at_least_inputs = mkmethod(self, _action_subject_contains_at_least_inputs), 60 contains_flag_values = mkmethod(self, _action_subject_contains_flag_values), 61 contains_none_of_flag_values = mkmethod(self, _action_subject_contains_none_of_flag_values), 62 content = mkmethod(self, _action_subject_content), 63 env = mkmethod(self, _action_subject_env), 64 has_flags_specified = mkmethod(self, _action_subject_has_flags_specified), 65 inputs = mkmethod(self, _action_subject_inputs), 66 mnemonic = mkmethod(self, _action_subject_mnemonic), 67 not_contains_arg = mkmethod(self, _action_subject_not_contains_arg), 68 substitutions = mkmethod(self, _action_subject_substitutions), 69 # keep sorted end 70 ) 71 return public 72 73def _action_subject_parse_flags(argv): 74 parsed_flags = {} 75 76 # argv might be none for e.g. builtin actions 77 if argv == None: 78 return parsed_flags 79 for i, arg in enumerate(argv): 80 if not arg.startswith("--"): 81 continue 82 if "=" in arg: 83 name, value = arg.split("=", 1) 84 else: 85 name = arg 86 87 # Handle a flag being the last arg in argv 88 if (i + 1) < len(argv): 89 value = argv[i + 1] 90 else: 91 value = None 92 parsed_flags.setdefault(name, []).append(value) 93 return parsed_flags 94 95def _action_subject_argv(self): 96 """Returns a CollectionSubject for the action's argv. 97 98 Method: ActionSubject.argv 99 100 Returns: 101 [`CollectionSubject`] object. 102 """ 103 meta = self.meta.derive("argv()") 104 return CollectionSubject.new( 105 self.action.argv, 106 meta, 107 container_name = "argv", 108 sortable = False, 109 ) 110 111def _action_subject_contains_at_least_args(self, args): 112 """Assert that an action contains at least the provided args. 113 114 Method: ActionSubject.contains_at_least_args 115 116 Example usage: 117 expect(env).that_action(action).contains_at_least_args(["foo", "bar"]). 118 119 Args: 120 self: implicitly added. 121 args: ([`list`] of [`str`]) all the args must be in the argv exactly 122 as provided. Multiplicity is respected. 123 124 Returns: 125 [`Ordered`] (see `_ordered_incorrectly_new`). 126 """ 127 return CollectionSubject.new( 128 self.action.argv, 129 self.meta, 130 container_name = "argv", 131 element_plural_name = "args", 132 sortable = False, # Preserve argv ordering 133 ).contains_at_least(args) 134 135def _action_subject_not_contains_arg(self, arg): 136 """Assert that an action does not contain an arg. 137 138 Example usage: 139 expect(env).that_action(action).not_contains_arg("should-not-exist") 140 141 Args: 142 self: implicitly added. 143 arg: ([`str`]) the arg that cannot be present in the argv. 144 """ 145 if arg in self.action.argv: 146 problem, actual = format_failure_unexpected_value( 147 container_name = "argv", 148 unexpected = arg, 149 actual = self.action.argv, 150 sort = False, # Preserve argv ordering 151 ) 152 self.meta.add_failure(problem, actual) 153 154def _action_subject_substitutions(self): 155 """Creates a `DictSubject` to assert on the substitutions dict. 156 157 Method: ActionSubject.substitutions. 158 159 Args: 160 self: implicitly added 161 162 Returns: 163 `DictSubject` struct. 164 """ 165 return DictSubject.new( 166 actual = self.action.substitutions, 167 meta = self.meta.derive("substitutions()"), 168 ) 169 170def _action_subject_has_flags_specified(self, flags): 171 """Assert that an action has the given flags present (but ignore any value). 172 173 Method: ActionSubject.has_flags_specified 174 175 This parses the argv, assuming the typical formats (`--flag=value`, 176 `--flag value`, and `--flag`). Any of the formats will be matched. 177 178 Example usage, given `argv = ["--a", "--b=1", "--c", "2"]`: 179 expect(env).that_action(action).has_flags_specified([ 180 "--a", "--b", "--c"]) 181 182 Args: 183 self: implicitly added. 184 flags: ([`list`] of [`str`]) The flags to check for. Include the leading "--". 185 Multiplicity is respected. A flag is considered present if any of 186 these forms are detected: `--flag=value`, `--flag value`, or a lone 187 `--flag`. 188 189 Returns: 190 [`Ordered`] (see `_ordered_incorrectly_new`). 191 """ 192 return CollectionSubject.new( 193 # Starlark dict keys maintain insertion order, so it's OK to 194 # pass keys directly and return Ordered. 195 self.parsed_flags.keys(), 196 meta = self.meta, 197 container_name = "argv", 198 element_plural_name = "specified flags", 199 sortable = False, # Preserve argv ordering 200 ).contains_at_least(flags) 201 202def _action_subject_mnemonic(self): 203 """Returns a `StrSubject` for the action's mnemonic. 204 205 Method: ActionSubject.mnemonic 206 207 Returns: 208 [`StrSubject`] object. 209 """ 210 return StrSubject.new( 211 self.action.mnemonic, 212 meta = self.meta.derive("mnemonic()"), 213 ) 214 215def _action_subject_inputs(self): 216 """Returns a DepsetFileSubject for the action's inputs. 217 218 Method: ActionSubject.inputs 219 220 Returns: 221 `DepsetFileSubject` of the action's inputs. 222 """ 223 meta = self.meta.derive("inputs()") 224 return DepsetFileSubject.new(self.action.inputs, meta) 225 226def _action_subject_contains_flag_values(self, flag_values): 227 """Assert that an action's argv has the given ("--flag", "value") entries. 228 229 Method: ActionSubject.contains_flag_values 230 231 This parses the argv, assuming the typical formats (`--flag=value`, 232 `--flag value`, and `--flag`). Note, however, that for the `--flag value` 233 and `--flag` forms, the parsing can't know how many args, if any, a flag 234 actually consumes, so it simply takes the first following arg, if any, as 235 the matching value. 236 237 NOTE: This function can give misleading results checking flags that don't 238 consume any args (e.g. boolean flags). Use `has_flags_specified()` to test 239 for such flags. Such cases will either show the subsequent arg as the value, 240 or None if the flag was the last arg in argv. 241 242 Example usage, given `argv = ["--b=1", "--c", "2"]`: 243 expect(env).that_action(action).contains_flag_values([ 244 ("--b", "1"), 245 ("--c", "2") 246 ]) 247 248 Args: 249 self: implicitly added. 250 flag_values: ([`list`] of ([`str`] name, [`str`]) tuples) Include the 251 leading "--" in the flag name. Order and duplicates aren't checked. 252 Flags without a value found use `None` as their value. 253 """ 254 missing = [] 255 for flag, value in sorted(flag_values): 256 if flag not in self.parsed_flags: 257 missing.append("'{}' (not specified)".format(flag)) 258 elif value not in self.parsed_flags[flag]: 259 missing.append("'{}' with value '{}'".format(flag, value)) 260 if not missing: 261 return 262 problem, actual = format_failure_missing_all_values( 263 element_plural_name = "flags with values", 264 container_name = "argv", 265 missing = missing, 266 actual = self.action.argv, 267 sort = False, # Preserve argv ordering 268 ) 269 self.meta.add_failure(problem, actual) 270 271def _action_subject_contains_none_of_flag_values(self, flag_values): 272 """Assert that an action's argv has none of the given ("--flag", "value") entries. 273 274 Method: ActionSubject.contains_none_of_flag_values 275 276 This parses the argv, assuming the typical formats (`--flag=value`, 277 `--flag value`, and `--flag`). Note, however, that for the `--flag value` 278 and `--flag` forms, the parsing can't know how many args, if any, a flag 279 actually consumes, so it simply takes the first following arg, if any, as 280 the matching value. 281 282 NOTE: This function can give misleading results checking flags that don't 283 consume any args (e.g. boolean flags). Use `has_flags_specified()` to test 284 for such flags. 285 286 Args: 287 self: implicitly added. 288 flag_values: ([`list`] of ([`str`] name, [`str`] value) tuples) Include 289 the leading "--" in the flag name. Order and duplicates aren't 290 checked. 291 """ 292 unexpected = [] 293 for flag, value in sorted(flag_values): 294 if flag not in self.parsed_flags: 295 continue 296 elif value in self.parsed_flags[flag]: 297 unexpected.append("'{}' with value '{}'".format(flag, value)) 298 if not unexpected: 299 return 300 301 problem, actual = format_failure_unexpected_values( 302 none_of = "\n" + enumerate_list_as_lines(sorted(unexpected), prefix = " "), 303 unexpected = unexpected, 304 actual = self.action.argv, 305 sort = False, # Preserve argv ordering 306 ) 307 self.meta.add_failure(problem, actual) 308 309def _action_subject_contains_at_least_inputs(self, inputs): 310 """Assert the action's inputs contains at least all of `inputs`. 311 312 Method: ActionSubject.contains_at_least_inputs 313 314 Example usage: 315 expect(env).that_action(action).contains_at_least_inputs([<some file>]) 316 317 Args: 318 self: implicitly added. 319 inputs: (collection of [`File`]) All must be present. Multiplicity 320 is respected. 321 322 Returns: 323 [`Ordered`] (see `_ordered_incorrectly_new`). 324 """ 325 return DepsetFileSubject.new( 326 self.action.inputs, 327 meta = self.meta, 328 container_name = "action inputs", 329 element_plural_name = "inputs", 330 ).contains_at_least(inputs) 331 332def _action_subject_content(self): 333 """Returns a `StrSubject` for `Action.content`. 334 335 Method: ActionSubject.content 336 337 Returns: 338 [`StrSubject`] object. 339 """ 340 return StrSubject.new( 341 self.action.content, 342 self.meta.derive("content()"), 343 ) 344 345def _action_subject_env(self): 346 """Returns a `DictSubject` for `Action.env`. 347 348 Method: ActionSubject.env 349 350 Args: 351 self: implicitly added. 352 """ 353 return DictSubject.new( 354 self.action.env, 355 self.meta.derive("env()"), 356 container_name = "environment", 357 key_plural_name = "envvars", 358 ) 359 360# We use this name so it shows up nice in docs. 361# buildifier: disable=name-conventions 362ActionSubject = struct( 363 new = _action_subject_new, 364 parse_flags = _action_subject_parse_flags, 365 argv = _action_subject_argv, 366 contains_at_least_args = _action_subject_contains_at_least_args, 367 not_contains_arg = _action_subject_not_contains_arg, 368 substitutions = _action_subject_substitutions, 369 has_flags_specified = _action_subject_has_flags_specified, 370 mnemonic = _action_subject_mnemonic, 371 inputs = _action_subject_inputs, 372 contains_flag_values = _action_subject_contains_flag_values, 373 contains_none_of_flag_values = _action_subject_contains_none_of_flag_values, 374 contains_at_least_inputs = _action_subject_contains_at_least_inputs, 375 content = _action_subject_content, 376 env = _action_subject_env, 377) 378