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