xref: /aosp_15_r20/external/bazelbuild-rules_testing/lib/private/dict_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"""# DictSubject"""
16
17load(":collection_subject.bzl", "CollectionSubject")
18load(":compare_util.bzl", "compare_dicts")
19load(
20    ":failure_messages.bzl",
21    "format_dict_as_lines",
22    "format_problem_dict_expected",
23)
24
25def _dict_subject_new(actual, meta, container_name = "dict", key_plural_name = "keys"):
26    """Creates a new `DictSubject`.
27
28    Method: DictSubject.new
29
30    Args:
31        actual: ([`dict`]) the dict to assert against.
32        meta: ([`ExpectMeta`]) of call chain information.
33        container_name: ([`str`]) conceptual name of the dict.
34        key_plural_name: ([`str`]) the plural word for the keys of the dict.
35
36    Returns:
37        New `DictSubject` struct.
38    """
39
40    # buildifier: disable=uninitialized
41    public = struct(
42        # keep sorted start
43        contains_exactly = lambda *a, **k: _dict_subject_contains_exactly(self, *a, **k),
44        contains_at_least = lambda *a, **k: _dict_subject_contains_at_least(self, *a, **k),
45        contains_none_of = lambda *a, **k: _dict_subject_contains_none_of(self, *a, **k),
46        get = lambda *a, **k: _dict_subject_get(self, *a, **k),
47        keys = lambda *a, **k: _dict_subject_keys(self, *a, **k),
48        # keep sorted end
49    )
50    self = struct(
51        actual = actual,
52        meta = meta,
53        container_name = container_name,
54        key_plural_name = key_plural_name,
55    )
56    return public
57
58def _dict_subject_contains_at_least(self, at_least):
59    """Assert the dict has at least the entries from `at_least`.
60
61    Method: DictSubject.contains_at_least
62
63    Args:
64        self: implicitly added.
65        at_least: ([`dict`]) the subset of keys/values that must exist. Extra
66            keys are allowed. Order is not checked.
67    """
68    result = compare_dicts(
69        expected = at_least,
70        actual = self.actual,
71    )
72    if not result.missing_keys and not result.incorrect_entries:
73        return
74
75    self.meta.add_failure(
76        problem = format_problem_dict_expected(
77            expected = at_least,
78            missing_keys = result.missing_keys,
79            unexpected_keys = [],
80            incorrect_entries = result.incorrect_entries,
81            container_name = self.container_name,
82            key_plural_name = self.key_plural_name,
83        ),
84        actual = "actual: {{\n{}\n}}".format(format_dict_as_lines(self.actual)),
85    )
86
87def _dict_subject_contains_exactly(self, expected):
88    """Assert the dict has exactly the provided values.
89
90    Method: DictSubject.contains_exactly
91
92    Args:
93        self: implicitly added
94        expected: ([`dict`]) the values that must exist. Missing values or
95            extra values are not allowed. Order is not checked.
96    """
97    result = compare_dicts(
98        expected = expected,
99        actual = self.actual,
100    )
101
102    if (not result.missing_keys and not result.unexpected_keys and
103        not result.incorrect_entries):
104        return
105
106    self.meta.add_failure(
107        problem = format_problem_dict_expected(
108            expected = expected,
109            missing_keys = result.missing_keys,
110            unexpected_keys = result.unexpected_keys,
111            incorrect_entries = result.incorrect_entries,
112            container_name = self.container_name,
113            key_plural_name = self.key_plural_name,
114        ),
115        actual = "actual: {{\n{}\n}}".format(format_dict_as_lines(self.actual)),
116    )
117
118def _dict_subject_contains_none_of(self, none_of):
119    """Assert the dict contains none of `none_of` keys/values.
120
121    Method: DictSubject.contains_none_of
122
123    Args:
124        self: implicitly added
125        none_of: ([`dict`]) the keys/values that must not exist. Order is not
126            checked.
127    """
128    result = compare_dicts(
129        expected = none_of,
130        actual = self.actual,
131    )
132    none_of_keys = sorted(none_of.keys())
133    if (sorted(result.missing_keys) == none_of_keys or
134        sorted(result.incorrect_entries.keys()) == none_of_keys):
135        return
136
137    incorrect_entries = {}
138    for key, not_expected in none_of.items():
139        actual = self.actual[key]
140        if actual == not_expected:
141            incorrect_entries[key] = struct(
142                actual = actual,
143                expected = "<not {}>".format(not_expected),
144            )
145
146    self.meta.add_failure(
147        problem = format_problem_dict_expected(
148            expected = none_of,
149            missing_keys = [],
150            unexpected_keys = [],
151            incorrect_entries = incorrect_entries,
152            container_name = self.container_name + " to be missing",
153            key_plural_name = self.key_plural_name,
154        ),
155        actual = "actual: {{\n{}\n}}".format(format_dict_as_lines(self.actual)),
156    )
157
158def _dict_subject_get(self, key, *, factory):
159    """Gets `key` from the actual dict wrapped in a subject.
160
161    Args:
162        self: implicitly added.
163        key: ([`object`]) the key to fetch.
164        factory: ([`callable`]) subject factory function, with the signature
165            of `def factory(value, *, meta)`, and returns the wrapped value.
166
167    Returns:
168        The return value of the `factory` arg.
169    """
170    if key not in self.actual:
171        fail("KeyError: '{key}' not found in {expr}".format(
172            key = key,
173            expr = self.meta.current_expr(),
174        ))
175    return factory(self.actual[key], meta = self.meta.derive("get({})".format(key)))
176
177def _dict_subject_keys(self):
178    """Returns a `CollectionSubject` for the dict's keys.
179
180    Method: DictSubject.keys
181
182    Args:
183        self: implicitly added
184
185    Returns:
186        [`CollectionSubject`] of the keys.
187    """
188    return CollectionSubject.new(
189        self.actual.keys(),
190        meta = self.meta.derive("keys()"),
191        container_name = "dict keys",
192        element_plural_name = "keys",
193    )
194
195# We use this name so it shows up nice in docs.
196# buildifier: disable=name-conventions
197DictSubject = struct(
198    new = _dict_subject_new,
199    contains_at_least = _dict_subject_contains_at_least,
200    contains_exactly = _dict_subject_contains_exactly,
201    contains_none_of = _dict_subject_contains_none_of,
202    keys = _dict_subject_keys,
203)
204