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"""# TargetSubject 16 17`TargetSubject` wraps a [`Target`] object and provides method for asserting 18its state. 19""" 20 21load( 22 "//lib:util.bzl", 23 "TestingAspectInfo", 24) 25load(":action_subject.bzl", "ActionSubject") 26load(":bool_subject.bzl", "BoolSubject") 27load(":collection_subject.bzl", "CollectionSubject") 28load(":depset_file_subject.bzl", "DepsetFileSubject") 29load(":execution_info_subject.bzl", "ExecutionInfoSubject") 30load(":file_subject.bzl", "FileSubject") 31load(":instrumented_files_info_subject.bzl", "InstrumentedFilesInfoSubject") 32load(":label_subject.bzl", "LabelSubject") 33load(":run_environment_info_subject.bzl", "RunEnvironmentInfoSubject") 34load(":runfiles_subject.bzl", "RunfilesSubject") 35load(":truth_common.bzl", "enumerate_list_as_lines") 36 37def _target_subject_new(target, meta): 38 """Creates a subject for asserting Targets. 39 40 Method: TargetSubject.new 41 42 **Public attributes**: 43 * `actual`: The wrapped [`Target`] object. 44 45 Args: 46 target: ([`Target`]) the target to check against. 47 meta: ([`ExpectMeta`]) metadata about the call chain. 48 49 Returns: 50 [`TargetSubject`] object 51 """ 52 self = struct(target = target, meta = meta) 53 public = struct( 54 # keep sorted start 55 action_generating = lambda *a, **k: _target_subject_action_generating(self, *a, **k), 56 action_named = lambda *a, **k: _target_subject_action_named(self, *a, **k), 57 actual = target, 58 attr = lambda *a, **k: _target_subject_attr(self, *a, **k), 59 data_runfiles = lambda *a, **k: _target_subject_data_runfiles(self, *a, **k), 60 default_outputs = lambda *a, **k: _target_subject_default_outputs(self, *a, **k), 61 executable = lambda *a, **k: _target_subject_executable(self, *a, **k), 62 failures = lambda *a, **k: _target_subject_failures(self, *a, **k), 63 has_provider = lambda *a, **k: _target_subject_has_provider(self, *a, **k), 64 label = lambda *a, **k: _target_subject_label(self, *a, **k), 65 meta = meta, 66 output_group = lambda *a, **k: _target_subject_output_group(self, *a, **k), 67 provider = lambda *a, **k: _target_subject_provider(self, *a, **k), 68 runfiles = lambda *a, **k: _target_subject_runfiles(self, *a, **k), 69 tags = lambda *a, **k: _target_subject_tags(self, *a, **k), 70 # keep sorted end 71 ) 72 return public 73 74def _target_subject_runfiles(self): 75 """Creates a subject asserting on the target's default runfiles. 76 77 Method: TargetSubject.runfiles 78 79 Args: 80 self: implicitly added. 81 82 Returns: 83 [`RunfilesSubject`] object. 84 """ 85 meta = self.meta.derive("runfiles()") 86 return RunfilesSubject.new(self.target[DefaultInfo].default_runfiles, meta, "default") 87 88def _target_subject_tags(self): 89 """Gets the target's tags as a `CollectionSubject` 90 91 Method: TargetSubject.tags 92 93 Args: 94 self: implicitly added 95 96 Returns: 97 [`CollectionSubject`] asserting the target's tags. 98 """ 99 return CollectionSubject.new( 100 _target_subject_get_attr(self, "tags"), 101 self.meta.derive("tags()"), 102 ) 103 104def _target_subject_get_attr(self, name): 105 if TestingAspectInfo not in self.target: 106 fail("TestingAspectInfo provider missing: if this is a second order or higher " + 107 "dependency, the recursing testing aspect must be enabled.") 108 109 attrs = self.target[TestingAspectInfo].attrs 110 if not hasattr(attrs, name): 111 fail("Attr '{}' not present for target {}".format(name, self.target.label)) 112 else: 113 return getattr(attrs, name) 114 115def _target_subject_data_runfiles(self): 116 """Creates a subject asserting on the target's data runfiles. 117 118 Method: TargetSubject.data_runfiles 119 120 Args: 121 self: implicitly added. 122 123 Returns: 124 [`RunfilesSubject`] object 125 """ 126 meta = self.meta.derive("data_runfiles()") 127 return RunfilesSubject.new(self.target[DefaultInfo].data_runfiles, meta, "data") 128 129def _target_subject_default_outputs(self): 130 """Creates a subject asserting on the target's default outputs. 131 132 Method: TargetSubject.default_outputs 133 134 Args: 135 self: implicitly added. 136 137 Returns: 138 [`DepsetFileSubject`] object. 139 """ 140 meta = self.meta.derive("default_outputs()") 141 return DepsetFileSubject.new(self.target[DefaultInfo].files, meta) 142 143def _target_subject_executable(self): 144 """Creates a subject asesrting on the target's executable File. 145 146 Method: TargetSubject.executable 147 148 Args: 149 self: implicitly added. 150 151 Returns: 152 [`FileSubject`] object. 153 """ 154 meta = self.meta.derive("executable()") 155 return FileSubject.new(self.target[DefaultInfo].files_to_run.executable, meta) 156 157def _target_subject_failures(self): 158 """Creates a subject asserting on the target's failure message strings. 159 160 Method: TargetSubject.failures 161 162 Args: 163 self: implicitly added 164 165 Returns: 166 [`CollectionSubject`] of [`str`]. 167 """ 168 meta = self.meta.derive("failures()") 169 if AnalysisFailureInfo in self.target: 170 failure_messages = sorted([ 171 f.message 172 for f in self.target[AnalysisFailureInfo].causes.to_list() 173 ]) 174 else: 175 failure_messages = [] 176 return CollectionSubject.new(failure_messages, meta, container_name = "failure messages") 177 178def _target_subject_has_provider(self, provider): 179 """Asserts that the target as provider `provider`. 180 181 Method: TargetSubject.has_provider 182 183 Args: 184 self: implicitly added. 185 provider: The provider object to check for. 186 """ 187 if self.meta.has_provider(self.target, provider): 188 return 189 self.meta.add_failure( 190 "expected to have provider: {}".format(_provider_name(provider)), 191 "but provider was not found", 192 ) 193 194def _target_subject_label(self): 195 """Returns a `LabelSubject` for the target's label value. 196 197 Method: TargetSubject.label 198 """ 199 return LabelSubject.new( 200 label = self.target.label, 201 meta = self.meta.derive(expr = "label()"), 202 ) 203 204def _target_subject_output_group(self, name): 205 """Returns a DepsetFileSubject of the files in the named output group. 206 207 Method: TargetSubject.output_group 208 209 Args: 210 self: implicitly added. 211 name: ([`str`]) an output group name. If it isn't present, an error is raised. 212 213 Returns: 214 DepsetFileSubject of the named output group. 215 """ 216 info = self.target[OutputGroupInfo] 217 if not hasattr(info, name): 218 fail("OutputGroupInfo.{} not present for target {}".format(name, self.target.label)) 219 return DepsetFileSubject.new( 220 getattr(info, name), 221 meta = self.meta.derive("output_group({})".format(name)), 222 ) 223 224def _target_subject_provider(self, provider_key, factory = None): 225 """Returns a subject for a provider in the target. 226 227 Method: TargetSubject.provider 228 229 Args: 230 self: implicitly added. 231 provider_key: The provider key to create a subject for 232 factory: optional callable. The factory function to use to create 233 the subject for the found provider. Required if the provider key is 234 not an inherently supported provider. It must have the following 235 signature: `def factory(value, /, *, meta)`. 236 237 Returns: 238 A subject wrapper of the provider value. 239 """ 240 if not factory: 241 for key, value in _PROVIDER_SUBJECT_FACTORIES: 242 if key == provider_key: 243 factory = value 244 break 245 246 if not factory: 247 fail("Unsupported provider: {}".format(provider_key)) 248 info = self.target[provider_key] 249 250 return factory( 251 info, 252 meta = self.meta.derive("provider({})".format(provider_key)), 253 ) 254 255def _target_subject_action_generating(self, short_path): 256 """Get the single action generating the given path. 257 258 Method: TargetSubject.action_generating 259 260 NOTE: in order to use this method, the target must have the `TestingAspectInfo` 261 provider (added by the `testing_aspect` aspect.) 262 263 Args: 264 self: implicitly added. 265 short_path: ([`str`]) the output's short_path to match. The value is 266 formatted using [`format_str`], so its template keywords can be 267 directly passed. 268 269 Returns: 270 [`ActionSubject`] for the matching action. If no action is found, or 271 more than one action matches, then an error is raised. 272 """ 273 274 if not self.meta.has_provider(self.target, TestingAspectInfo): 275 fail("TestingAspectInfo provider missing: if this is a second order or higher " + 276 "dependency, the recursing testing aspect must be enabled.") 277 278 short_path = self.meta.format_str(short_path) 279 actions = [] 280 for action in self.meta.get_provider(self.target, TestingAspectInfo).actions: 281 for output in action.outputs.to_list(): 282 if output.short_path == short_path: 283 actions.append(action) 284 break 285 if not actions: 286 fail("No action generating '{}'".format(short_path)) 287 elif len(actions) > 1: 288 fail("Expected 1 action to generate '{output}', found {count}: {actions}".format( 289 output = short_path, 290 count = len(actions), 291 actions = "\n".join([str(a) for a in actions]), 292 )) 293 action = actions[0] 294 meta = self.meta.derive( 295 expr = "action_generating({})".format(short_path), 296 details = ["action: [{}] {}".format(action.mnemonic, action)], 297 ) 298 return ActionSubject.new(action, meta) 299 300def _target_subject_action_named(self, mnemonic): 301 """Get the single action with the matching mnemonic. 302 303 Method: TargetSubject.action_named 304 305 NOTE: in order to use this method, the target must have the [`TestingAspectInfo`] 306 provider (added by the [`testing_aspect`] aspect.) 307 308 Args: 309 self: implicitly added. 310 mnemonic: ([`str`]) the mnemonic to match 311 312 Returns: 313 [`ActionSubject`]. If no action matches, or more than one action matches, an error 314 is raised. 315 """ 316 if TestingAspectInfo not in self.target: 317 fail("TestingAspectInfo provider missing: if this is a second order or higher " + 318 "dependency, the recursing testing aspect must be enabled.") 319 actions = [a for a in self.target[TestingAspectInfo].actions if a.mnemonic == mnemonic] 320 if not actions: 321 fail( 322 "No action named '{name}' for target {target}.\nFound: {found}".format( 323 name = mnemonic, 324 target = self.target.label, 325 found = enumerate_list_as_lines([ 326 a.mnemonic 327 for a in self.target[TestingAspectInfo].actions 328 ]), 329 ), 330 ) 331 elif len(actions) > 1: 332 fail("Expected 1 action to match '{name}', found {count}: {actions}".format( 333 name = mnemonic, 334 count = len(actions), 335 actions = "\n".join([str(a) for a in actions]), 336 )) 337 action = actions[0] 338 meta = self.meta.derive( 339 expr = "action_named({})".format(mnemonic), 340 details = ["action: [{}] {}".format(action.mnemonic, action)], 341 ) 342 return ActionSubject.new(action, meta) 343 344# NOTE: This map should only have attributes that are common to all target 345# types, otherwise we can't rely on an attribute having a specific type. 346_ATTR_NAME_TO_SUBJECT_FACTORY = { 347 "testonly": BoolSubject.new, 348} 349 350def _target_subject_attr(self, name, *, factory = None): 351 """Gets a subject-wrapped value for the named attribute. 352 353 Method: TargetSubject.attr 354 355 NOTE: in order to use this method, the target must have the `TestingAspectInfo` 356 provider (added by the `testing_aspect` aspect.) 357 358 Args: 359 self: implicitly added 360 name: ([`str`]) the attribute to get. If it's an unsupported attribute, and 361 no explicit factory was provided, an error will be raised. 362 factory: (callable) function to create the returned subject based on 363 the attribute value. If specified, it takes precedence over the 364 attributes that are inherently understood. It must have the 365 following signature: `def factory(value, *, meta)`, where `value` is 366 the value of the attribute, and `meta` is the call chain metadata. 367 368 Returns: 369 A Subject-like object for the given attribute. The particular subject 370 type returned depends on attribute and `factory` arg. If it isn't know 371 what type of subject to use for the attribute, an error is raised. 372 """ 373 if TestingAspectInfo not in self.target: 374 fail("TestingAspectInfo provider missing: if this is a second order or higher " + 375 "dependency, the recursing testing aspect must be enabled.") 376 377 attr_value = getattr(self.target[TestingAspectInfo].attrs, name) 378 if not factory: 379 if name not in _ATTR_NAME_TO_SUBJECT_FACTORY: 380 fail("Unsupported attr: {}".format(name)) 381 factory = _ATTR_NAME_TO_SUBJECT_FACTORY[name] 382 383 return factory( 384 attr_value, 385 meta = self.meta.derive("attr({})".format(name)), 386 ) 387 388# Providers aren't hashable, so we have to use a list of (key, value) 389_PROVIDER_SUBJECT_FACTORIES = [ 390 (InstrumentedFilesInfo, InstrumentedFilesInfoSubject.new), 391 (RunEnvironmentInfo, RunEnvironmentInfoSubject.new), 392 (testing.ExecutionInfo, ExecutionInfoSubject.new), 393] 394 395def _provider_name(provider): 396 # This relies on implementation details of how Starlark represents 397 # providers, and isn't entirely accurate, but works well enough 398 # for error messages. 399 return str(provider).split("<function ")[1].split(">")[0] 400 401# We use this name so it shows up nice in docs. 402# buildifier: disable=name-conventions 403TargetSubject = struct( 404 new = _target_subject_new, 405 runfiles = _target_subject_runfiles, 406 tags = _target_subject_tags, 407 get_attr = _target_subject_get_attr, 408 data_runfiles = _target_subject_data_runfiles, 409 default_outputs = _target_subject_default_outputs, 410 executable = _target_subject_executable, 411 failures = _target_subject_failures, 412 has_provider = _target_subject_has_provider, 413 label = _target_subject_label, 414 output_group = _target_subject_output_group, 415 provider = _target_subject_provider, 416 action_generating = _target_subject_action_generating, 417 action_named = _target_subject_action_named, 418 attr = _target_subject_attr, 419) 420