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