1*d6050574SRomain Jobredeaux# Copyright 2023 The Bazel Authors. All rights reserved. 2*d6050574SRomain Jobredeaux# 3*d6050574SRomain Jobredeaux# Licensed under the Apache License, Version 2.0 (the "License"); 4*d6050574SRomain Jobredeaux# you may not use this file except in compliance with the License. 5*d6050574SRomain Jobredeaux# You may obtain a copy of the License at 6*d6050574SRomain Jobredeaux# 7*d6050574SRomain Jobredeaux# http://www.apache.org/licenses/LICENSE-2.0 8*d6050574SRomain Jobredeaux# 9*d6050574SRomain Jobredeaux# Unless required by applicable law or agreed to in writing, software 10*d6050574SRomain Jobredeaux# distributed under the License is distributed on an "AS IS" BASIS, 11*d6050574SRomain Jobredeaux# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*d6050574SRomain Jobredeaux# See the License for the specific language governing permissions and 13*d6050574SRomain Jobredeaux# limitations under the License. 14*d6050574SRomain Jobredeaux 15*d6050574SRomain Jobredeaux"""# DictSubject""" 16*d6050574SRomain Jobredeaux 17*d6050574SRomain Jobredeauxload(":collection_subject.bzl", "CollectionSubject") 18*d6050574SRomain Jobredeauxload(":compare_util.bzl", "compare_dicts") 19*d6050574SRomain Jobredeauxload( 20*d6050574SRomain Jobredeaux ":failure_messages.bzl", 21*d6050574SRomain Jobredeaux "format_dict_as_lines", 22*d6050574SRomain Jobredeaux "format_problem_dict_expected", 23*d6050574SRomain Jobredeaux) 24*d6050574SRomain Jobredeaux 25*d6050574SRomain Jobredeauxdef _dict_subject_new(actual, meta, container_name = "dict", key_plural_name = "keys"): 26*d6050574SRomain Jobredeaux """Creates a new `DictSubject`. 27*d6050574SRomain Jobredeaux 28*d6050574SRomain Jobredeaux Method: DictSubject.new 29*d6050574SRomain Jobredeaux 30*d6050574SRomain Jobredeaux Args: 31*d6050574SRomain Jobredeaux actual: ([`dict`]) the dict to assert against. 32*d6050574SRomain Jobredeaux meta: ([`ExpectMeta`]) of call chain information. 33*d6050574SRomain Jobredeaux container_name: ([`str`]) conceptual name of the dict. 34*d6050574SRomain Jobredeaux key_plural_name: ([`str`]) the plural word for the keys of the dict. 35*d6050574SRomain Jobredeaux 36*d6050574SRomain Jobredeaux Returns: 37*d6050574SRomain Jobredeaux New `DictSubject` struct. 38*d6050574SRomain Jobredeaux """ 39*d6050574SRomain Jobredeaux 40*d6050574SRomain Jobredeaux # buildifier: disable=uninitialized 41*d6050574SRomain Jobredeaux public = struct( 42*d6050574SRomain Jobredeaux # keep sorted start 43*d6050574SRomain Jobredeaux contains_exactly = lambda *a, **k: _dict_subject_contains_exactly(self, *a, **k), 44*d6050574SRomain Jobredeaux contains_at_least = lambda *a, **k: _dict_subject_contains_at_least(self, *a, **k), 45*d6050574SRomain Jobredeaux contains_none_of = lambda *a, **k: _dict_subject_contains_none_of(self, *a, **k), 46*d6050574SRomain Jobredeaux get = lambda *a, **k: _dict_subject_get(self, *a, **k), 47*d6050574SRomain Jobredeaux keys = lambda *a, **k: _dict_subject_keys(self, *a, **k), 48*d6050574SRomain Jobredeaux # keep sorted end 49*d6050574SRomain Jobredeaux ) 50*d6050574SRomain Jobredeaux self = struct( 51*d6050574SRomain Jobredeaux actual = actual, 52*d6050574SRomain Jobredeaux meta = meta, 53*d6050574SRomain Jobredeaux container_name = container_name, 54*d6050574SRomain Jobredeaux key_plural_name = key_plural_name, 55*d6050574SRomain Jobredeaux ) 56*d6050574SRomain Jobredeaux return public 57*d6050574SRomain Jobredeaux 58*d6050574SRomain Jobredeauxdef _dict_subject_contains_at_least(self, at_least): 59*d6050574SRomain Jobredeaux """Assert the dict has at least the entries from `at_least`. 60*d6050574SRomain Jobredeaux 61*d6050574SRomain Jobredeaux Method: DictSubject.contains_at_least 62*d6050574SRomain Jobredeaux 63*d6050574SRomain Jobredeaux Args: 64*d6050574SRomain Jobredeaux self: implicitly added. 65*d6050574SRomain Jobredeaux at_least: ([`dict`]) the subset of keys/values that must exist. Extra 66*d6050574SRomain Jobredeaux keys are allowed. Order is not checked. 67*d6050574SRomain Jobredeaux """ 68*d6050574SRomain Jobredeaux result = compare_dicts( 69*d6050574SRomain Jobredeaux expected = at_least, 70*d6050574SRomain Jobredeaux actual = self.actual, 71*d6050574SRomain Jobredeaux ) 72*d6050574SRomain Jobredeaux if not result.missing_keys and not result.incorrect_entries: 73*d6050574SRomain Jobredeaux return 74*d6050574SRomain Jobredeaux 75*d6050574SRomain Jobredeaux self.meta.add_failure( 76*d6050574SRomain Jobredeaux problem = format_problem_dict_expected( 77*d6050574SRomain Jobredeaux expected = at_least, 78*d6050574SRomain Jobredeaux missing_keys = result.missing_keys, 79*d6050574SRomain Jobredeaux unexpected_keys = [], 80*d6050574SRomain Jobredeaux incorrect_entries = result.incorrect_entries, 81*d6050574SRomain Jobredeaux container_name = self.container_name, 82*d6050574SRomain Jobredeaux key_plural_name = self.key_plural_name, 83*d6050574SRomain Jobredeaux ), 84*d6050574SRomain Jobredeaux actual = "actual: {{\n{}\n}}".format(format_dict_as_lines(self.actual)), 85*d6050574SRomain Jobredeaux ) 86*d6050574SRomain Jobredeaux 87*d6050574SRomain Jobredeauxdef _dict_subject_contains_exactly(self, expected): 88*d6050574SRomain Jobredeaux """Assert the dict has exactly the provided values. 89*d6050574SRomain Jobredeaux 90*d6050574SRomain Jobredeaux Method: DictSubject.contains_exactly 91*d6050574SRomain Jobredeaux 92*d6050574SRomain Jobredeaux Args: 93*d6050574SRomain Jobredeaux self: implicitly added 94*d6050574SRomain Jobredeaux expected: ([`dict`]) the values that must exist. Missing values or 95*d6050574SRomain Jobredeaux extra values are not allowed. Order is not checked. 96*d6050574SRomain Jobredeaux """ 97*d6050574SRomain Jobredeaux result = compare_dicts( 98*d6050574SRomain Jobredeaux expected = expected, 99*d6050574SRomain Jobredeaux actual = self.actual, 100*d6050574SRomain Jobredeaux ) 101*d6050574SRomain Jobredeaux 102*d6050574SRomain Jobredeaux if (not result.missing_keys and not result.unexpected_keys and 103*d6050574SRomain Jobredeaux not result.incorrect_entries): 104*d6050574SRomain Jobredeaux return 105*d6050574SRomain Jobredeaux 106*d6050574SRomain Jobredeaux self.meta.add_failure( 107*d6050574SRomain Jobredeaux problem = format_problem_dict_expected( 108*d6050574SRomain Jobredeaux expected = expected, 109*d6050574SRomain Jobredeaux missing_keys = result.missing_keys, 110*d6050574SRomain Jobredeaux unexpected_keys = result.unexpected_keys, 111*d6050574SRomain Jobredeaux incorrect_entries = result.incorrect_entries, 112*d6050574SRomain Jobredeaux container_name = self.container_name, 113*d6050574SRomain Jobredeaux key_plural_name = self.key_plural_name, 114*d6050574SRomain Jobredeaux ), 115*d6050574SRomain Jobredeaux actual = "actual: {{\n{}\n}}".format(format_dict_as_lines(self.actual)), 116*d6050574SRomain Jobredeaux ) 117*d6050574SRomain Jobredeaux 118*d6050574SRomain Jobredeauxdef _dict_subject_contains_none_of(self, none_of): 119*d6050574SRomain Jobredeaux """Assert the dict contains none of `none_of` keys/values. 120*d6050574SRomain Jobredeaux 121*d6050574SRomain Jobredeaux Method: DictSubject.contains_none_of 122*d6050574SRomain Jobredeaux 123*d6050574SRomain Jobredeaux Args: 124*d6050574SRomain Jobredeaux self: implicitly added 125*d6050574SRomain Jobredeaux none_of: ([`dict`]) the keys/values that must not exist. Order is not 126*d6050574SRomain Jobredeaux checked. 127*d6050574SRomain Jobredeaux """ 128*d6050574SRomain Jobredeaux result = compare_dicts( 129*d6050574SRomain Jobredeaux expected = none_of, 130*d6050574SRomain Jobredeaux actual = self.actual, 131*d6050574SRomain Jobredeaux ) 132*d6050574SRomain Jobredeaux none_of_keys = sorted(none_of.keys()) 133*d6050574SRomain Jobredeaux if (sorted(result.missing_keys) == none_of_keys or 134*d6050574SRomain Jobredeaux sorted(result.incorrect_entries.keys()) == none_of_keys): 135*d6050574SRomain Jobredeaux return 136*d6050574SRomain Jobredeaux 137*d6050574SRomain Jobredeaux incorrect_entries = {} 138*d6050574SRomain Jobredeaux for key, not_expected in none_of.items(): 139*d6050574SRomain Jobredeaux actual = self.actual[key] 140*d6050574SRomain Jobredeaux if actual == not_expected: 141*d6050574SRomain Jobredeaux incorrect_entries[key] = struct( 142*d6050574SRomain Jobredeaux actual = actual, 143*d6050574SRomain Jobredeaux expected = "<not {}>".format(not_expected), 144*d6050574SRomain Jobredeaux ) 145*d6050574SRomain Jobredeaux 146*d6050574SRomain Jobredeaux self.meta.add_failure( 147*d6050574SRomain Jobredeaux problem = format_problem_dict_expected( 148*d6050574SRomain Jobredeaux expected = none_of, 149*d6050574SRomain Jobredeaux missing_keys = [], 150*d6050574SRomain Jobredeaux unexpected_keys = [], 151*d6050574SRomain Jobredeaux incorrect_entries = incorrect_entries, 152*d6050574SRomain Jobredeaux container_name = self.container_name + " to be missing", 153*d6050574SRomain Jobredeaux key_plural_name = self.key_plural_name, 154*d6050574SRomain Jobredeaux ), 155*d6050574SRomain Jobredeaux actual = "actual: {{\n{}\n}}".format(format_dict_as_lines(self.actual)), 156*d6050574SRomain Jobredeaux ) 157*d6050574SRomain Jobredeaux 158*d6050574SRomain Jobredeauxdef _dict_subject_get(self, key, *, factory): 159*d6050574SRomain Jobredeaux """Gets `key` from the actual dict wrapped in a subject. 160*d6050574SRomain Jobredeaux 161*d6050574SRomain Jobredeaux Args: 162*d6050574SRomain Jobredeaux self: implicitly added. 163*d6050574SRomain Jobredeaux key: ([`object`]) the key to fetch. 164*d6050574SRomain Jobredeaux factory: ([`callable`]) subject factory function, with the signature 165*d6050574SRomain Jobredeaux of `def factory(value, *, meta)`, and returns the wrapped value. 166*d6050574SRomain Jobredeaux 167*d6050574SRomain Jobredeaux Returns: 168*d6050574SRomain Jobredeaux The return value of the `factory` arg. 169*d6050574SRomain Jobredeaux """ 170*d6050574SRomain Jobredeaux if key not in self.actual: 171*d6050574SRomain Jobredeaux fail("KeyError: '{key}' not found in {expr}".format( 172*d6050574SRomain Jobredeaux key = key, 173*d6050574SRomain Jobredeaux expr = self.meta.current_expr(), 174*d6050574SRomain Jobredeaux )) 175*d6050574SRomain Jobredeaux return factory(self.actual[key], meta = self.meta.derive("get({})".format(key))) 176*d6050574SRomain Jobredeaux 177*d6050574SRomain Jobredeauxdef _dict_subject_keys(self): 178*d6050574SRomain Jobredeaux """Returns a `CollectionSubject` for the dict's keys. 179*d6050574SRomain Jobredeaux 180*d6050574SRomain Jobredeaux Method: DictSubject.keys 181*d6050574SRomain Jobredeaux 182*d6050574SRomain Jobredeaux Args: 183*d6050574SRomain Jobredeaux self: implicitly added 184*d6050574SRomain Jobredeaux 185*d6050574SRomain Jobredeaux Returns: 186*d6050574SRomain Jobredeaux [`CollectionSubject`] of the keys. 187*d6050574SRomain Jobredeaux """ 188*d6050574SRomain Jobredeaux return CollectionSubject.new( 189*d6050574SRomain Jobredeaux self.actual.keys(), 190*d6050574SRomain Jobredeaux meta = self.meta.derive("keys()"), 191*d6050574SRomain Jobredeaux container_name = "dict keys", 192*d6050574SRomain Jobredeaux element_plural_name = "keys", 193*d6050574SRomain Jobredeaux ) 194*d6050574SRomain Jobredeaux 195*d6050574SRomain Jobredeaux# We use this name so it shows up nice in docs. 196*d6050574SRomain Jobredeaux# buildifier: disable=name-conventions 197*d6050574SRomain JobredeauxDictSubject = struct( 198*d6050574SRomain Jobredeaux new = _dict_subject_new, 199*d6050574SRomain Jobredeaux contains_at_least = _dict_subject_contains_at_least, 200*d6050574SRomain Jobredeaux contains_exactly = _dict_subject_contains_exactly, 201*d6050574SRomain Jobredeaux contains_none_of = _dict_subject_contains_none_of, 202*d6050574SRomain Jobredeaux keys = _dict_subject_keys, 203*d6050574SRomain Jobredeaux) 204