xref: /aosp_15_r20/external/bazelbuild-rules_testing/lib/private/target_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"""# 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