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"""Utilities for performing comparisons for Truth.""" 16*d6050574SRomain Jobredeaux 17*d6050574SRomain Jobredeauxload(":truth_common.bzl", "to_list") 18*d6050574SRomain Jobredeaux 19*d6050574SRomain Jobredeauxdef _match_result_new(*, found_at, matched_value, matcher): 20*d6050574SRomain Jobredeaux """Creates a "MatchResult" struct. 21*d6050574SRomain Jobredeaux 22*d6050574SRomain Jobredeaux A `MatchResult` struct is information about how an expected value 23*d6050574SRomain Jobredeaux matched to an actual value. 24*d6050574SRomain Jobredeaux 25*d6050574SRomain Jobredeaux Args: 26*d6050574SRomain Jobredeaux found_at: ([`int`]) the position in the actual container the match 27*d6050574SRomain Jobredeaux occurred at. 28*d6050574SRomain Jobredeaux matched_value: the actual value that caused the match 29*d6050574SRomain Jobredeaux matcher: ([`Matcher`] | value) the value that matched 30*d6050574SRomain Jobredeaux """ 31*d6050574SRomain Jobredeaux return struct( 32*d6050574SRomain Jobredeaux found_at = found_at, 33*d6050574SRomain Jobredeaux matched_value = matched_value, 34*d6050574SRomain Jobredeaux matcher = matcher, 35*d6050574SRomain Jobredeaux ) 36*d6050574SRomain Jobredeaux 37*d6050574SRomain Jobredeaux# We use this name so it shows up nice in docs. 38*d6050574SRomain Jobredeaux# buildifier: disable=name-conventions 39*d6050574SRomain JobredeauxMatchResult = struct( 40*d6050574SRomain Jobredeaux new = _match_result_new, 41*d6050574SRomain Jobredeaux) 42*d6050574SRomain Jobredeaux 43*d6050574SRomain Jobredeauxdef compare_contains_exactly_predicates(*, expect_contains, actual_container): 44*d6050574SRomain Jobredeaux """Tells how and if values and predicates correspond 1:1 in the specified order. 45*d6050574SRomain Jobredeaux 46*d6050574SRomain Jobredeaux * There must be a 1:1 correspondence between the container values and the 47*d6050574SRomain Jobredeaux predicates. 48*d6050574SRomain Jobredeaux * Multiplicity is respected (i.e., if the same predicate occurs twice, then 49*d6050574SRomain Jobredeaux two distinct elements must match). 50*d6050574SRomain Jobredeaux * Matching occurs in first-seen order. That is, a predicate will "consume" 51*d6050574SRomain Jobredeaux the first value in `actual_container` it matches. 52*d6050574SRomain Jobredeaux * The collection must match all the predicates, no more or less. 53*d6050574SRomain Jobredeaux * Checking that the order of matches is the same as the passed-in matchers 54*d6050574SRomain Jobredeaux order can be done by call `in_order()`. 55*d6050574SRomain Jobredeaux 56*d6050574SRomain Jobredeaux Note that confusing results may occur if predicates with overlapping 57*d6050574SRomain Jobredeaux match conditions are used. For example, given: 58*d6050574SRomain Jobredeaux actual=["a", "ab", "abc"], 59*d6050574SRomain Jobredeaux predicates=[<contains a>, <contains b>, <equals a>] 60*d6050574SRomain Jobredeaux 61*d6050574SRomain Jobredeaux Then the result will be they aren't equal: the first two predicates 62*d6050574SRomain Jobredeaux consume "a" and "ab", leaving only "abc" for the <equals a> predicate 63*d6050574SRomain Jobredeaux to match against, which fails. 64*d6050574SRomain Jobredeaux 65*d6050574SRomain Jobredeaux Args: 66*d6050574SRomain Jobredeaux expect_contains: ([`collection`] of `Matcher`s) the predicates that must match. 67*d6050574SRomain Jobredeaux To perform simple equalty, use `matching.equals_wrapper()`. 68*d6050574SRomain Jobredeaux actual_container: ([`collection`]) The container to check within. 69*d6050574SRomain Jobredeaux 70*d6050574SRomain Jobredeaux Returns: 71*d6050574SRomain Jobredeaux struct with the following attributes: 72*d6050574SRomain Jobredeaux * contains_exactly: ([`bool`]) True if all the predicates (and no others) 73*d6050574SRomain Jobredeaux matched a distinct element; does not consider order. 74*d6050574SRomain Jobredeaux * is_in_order: ([`bool`]) True if the actuals values matched in the same 75*d6050574SRomain Jobredeaux order as the expected predicates. False if they were out of order. 76*d6050574SRomain Jobredeaux If `contains_exactly=False`, this attribute is undefined. 77*d6050574SRomain Jobredeaux * missing: [`list`] of [`Matcher`]s from `expect_contains` that did not find a 78*d6050574SRomain Jobredeaux corresponding element in `actual_container`. 79*d6050574SRomain Jobredeaux * unexpected: ([`list`]) values from `actual_container` that were not 80*d6050574SRomain Jobredeaux present in `expect_contains`. 81*d6050574SRomain Jobredeaux * matches: ([`list`] of [`MatchResult`]) information about which elements 82*d6050574SRomain Jobredeaux in the two lists that matched each other. If 83*d6050574SRomain Jobredeaux `contains_exactly=False`, this attribute is undefined. 84*d6050574SRomain Jobredeaux """ 85*d6050574SRomain Jobredeaux 86*d6050574SRomain Jobredeaux # The basic idea is treating the expected and actual lists as queues of 87*d6050574SRomain Jobredeaux # remaining values to search for. This allows the multiplicity of values 88*d6050574SRomain Jobredeaux # to be respected and ordering correctness to be computed. 89*d6050574SRomain Jobredeaux # 90*d6050574SRomain Jobredeaux # Each iteration, we "pop" an element off each queue and... 91*d6050574SRomain Jobredeaux # * If the elements are equal, then all is good: ordering is still 92*d6050574SRomain Jobredeaux # possible, and the required element is present. Start a new iteration. 93*d6050574SRomain Jobredeaux # * Otherwise, we know ordering isn't possible anymore and need to 94*d6050574SRomain Jobredeaux # answer two questions: 95*d6050574SRomain Jobredeaux # 1. Is the actual value extra, or elsewhere in the expected values? 96*d6050574SRomain Jobredeaux # 2. Is the expected value missing, or elsewhere in the actual values? 97*d6050574SRomain Jobredeaux # If a value exists elsewhere in the other queue, then we have to 98*d6050574SRomain Jobredeaux # remove it to prevent it from being searched for again in a later 99*d6050574SRomain Jobredeaux # iteration. 100*d6050574SRomain Jobredeaux # As we go along, we keep track of where expected values matched; this 101*d6050574SRomain Jobredeaux # allows for better error reporting. 102*d6050574SRomain Jobredeaux expect_contains = to_list(expect_contains) 103*d6050574SRomain Jobredeaux actual_container = to_list(actual_container) 104*d6050574SRomain Jobredeaux 105*d6050574SRomain Jobredeaux actual_queue = [] # List of (original pos, value) 106*d6050574SRomain Jobredeaux for i, value in enumerate(actual_container): 107*d6050574SRomain Jobredeaux actual_queue.append([i, value]) 108*d6050574SRomain Jobredeaux 109*d6050574SRomain Jobredeaux expected_queue = [] # List of (original pos, value) 110*d6050574SRomain Jobredeaux matches = [] # List of "MatchResult" structs 111*d6050574SRomain Jobredeaux for i, value in enumerate(expect_contains): 112*d6050574SRomain Jobredeaux expected_queue.append([i, value]) 113*d6050574SRomain Jobredeaux matches.append(None) 114*d6050574SRomain Jobredeaux 115*d6050574SRomain Jobredeaux missing = [] # List of expected values missing 116*d6050574SRomain Jobredeaux unexpected = [] # List of actual values that weren't expected 117*d6050574SRomain Jobredeaux ordered = True 118*d6050574SRomain Jobredeaux 119*d6050574SRomain Jobredeaux # Start at -1 because the first iteration adds 1 120*d6050574SRomain Jobredeaux pos = -1 121*d6050574SRomain Jobredeaux loop = range(max(len(actual_queue), len(expected_queue))) 122*d6050574SRomain Jobredeaux for _ in loop: 123*d6050574SRomain Jobredeaux # Advancing the position is equivalent to removing the queues's head 124*d6050574SRomain Jobredeaux pos += 1 125*d6050574SRomain Jobredeaux if pos >= len(actual_queue) and pos >= len(expected_queue): 126*d6050574SRomain Jobredeaux # Can occur when e.g. actual=[A, B], expected=[B] 127*d6050574SRomain Jobredeaux break 128*d6050574SRomain Jobredeaux if pos >= len(actual_queue): 129*d6050574SRomain Jobredeaux # Fewer actual values than expected, so the rest are missing 130*d6050574SRomain Jobredeaux missing.extend([v[1] for v in expected_queue[pos:]]) 131*d6050574SRomain Jobredeaux break 132*d6050574SRomain Jobredeaux if pos >= len(expected_queue): 133*d6050574SRomain Jobredeaux # More actual values than expected, so the rest are unexpected 134*d6050574SRomain Jobredeaux unexpected.extend([v[1] for v in actual_queue[pos:]]) 135*d6050574SRomain Jobredeaux break 136*d6050574SRomain Jobredeaux 137*d6050574SRomain Jobredeaux actual_entry = actual_queue[pos] 138*d6050574SRomain Jobredeaux expected_entry = expected_queue[pos] 139*d6050574SRomain Jobredeaux 140*d6050574SRomain Jobredeaux if expected_entry[1].match(actual_entry[1]): 141*d6050574SRomain Jobredeaux # Happy path: both are equal and order is maintained. 142*d6050574SRomain Jobredeaux matches[expected_entry[0]] = MatchResult.new( 143*d6050574SRomain Jobredeaux found_at = actual_entry[0], 144*d6050574SRomain Jobredeaux matched_value = actual_entry[1], 145*d6050574SRomain Jobredeaux matcher = expected_entry[1], 146*d6050574SRomain Jobredeaux ) 147*d6050574SRomain Jobredeaux continue 148*d6050574SRomain Jobredeaux ordered = False 149*d6050574SRomain Jobredeaux found_at, found_entry = _list_find( 150*d6050574SRomain Jobredeaux actual_queue, 151*d6050574SRomain Jobredeaux lambda v: expected_entry[1].match(v[1]), 152*d6050574SRomain Jobredeaux start = pos, 153*d6050574SRomain Jobredeaux end = len(actual_queue), 154*d6050574SRomain Jobredeaux ) 155*d6050574SRomain Jobredeaux if found_at == -1: 156*d6050574SRomain Jobredeaux missing.append(expected_entry[1]) 157*d6050574SRomain Jobredeaux else: 158*d6050574SRomain Jobredeaux # Remove it from the queue so a subsequent iteration doesn't 159*d6050574SRomain Jobredeaux # try to search for it again. 160*d6050574SRomain Jobredeaux actual_queue.pop(found_at) 161*d6050574SRomain Jobredeaux matches[expected_entry[0]] = MatchResult.new( 162*d6050574SRomain Jobredeaux found_at = found_entry[0], 163*d6050574SRomain Jobredeaux matched_value = found_entry[1], 164*d6050574SRomain Jobredeaux matcher = expected_entry[1], 165*d6050574SRomain Jobredeaux ) 166*d6050574SRomain Jobredeaux 167*d6050574SRomain Jobredeaux found_at, found_entry = _list_find( 168*d6050574SRomain Jobredeaux expected_queue, 169*d6050574SRomain Jobredeaux lambda entry: entry[1].match(actual_entry[1]), 170*d6050574SRomain Jobredeaux start = pos, 171*d6050574SRomain Jobredeaux end = len(expected_queue), 172*d6050574SRomain Jobredeaux ) 173*d6050574SRomain Jobredeaux if found_at == -1: 174*d6050574SRomain Jobredeaux unexpected.append(actual_entry[1]) 175*d6050574SRomain Jobredeaux else: 176*d6050574SRomain Jobredeaux # Remove it from the queue so a subsequent iteration doesn't 177*d6050574SRomain Jobredeaux # try to search for it again. 178*d6050574SRomain Jobredeaux expected_queue.pop(found_at) 179*d6050574SRomain Jobredeaux matches[found_entry[0]] = MatchResult.new( 180*d6050574SRomain Jobredeaux found_at = actual_entry[0], 181*d6050574SRomain Jobredeaux matched_value = actual_entry[1], 182*d6050574SRomain Jobredeaux matcher = found_entry[1], 183*d6050574SRomain Jobredeaux ) 184*d6050574SRomain Jobredeaux 185*d6050574SRomain Jobredeaux return struct( 186*d6050574SRomain Jobredeaux contains_exactly = not (missing or unexpected), 187*d6050574SRomain Jobredeaux is_in_order = ordered, 188*d6050574SRomain Jobredeaux missing = missing, 189*d6050574SRomain Jobredeaux unexpected = unexpected, 190*d6050574SRomain Jobredeaux matches = matches, 191*d6050574SRomain Jobredeaux ) 192*d6050574SRomain Jobredeaux 193*d6050574SRomain Jobredeauxdef _list_find(search_in, search_for, *, start = 0, end = None): 194*d6050574SRomain Jobredeaux """Linear search a list for a value matching a predicate. 195*d6050574SRomain Jobredeaux 196*d6050574SRomain Jobredeaux Args: 197*d6050574SRomain Jobredeaux search_in: ([`list`]) the list to search within. 198*d6050574SRomain Jobredeaux search_for: (callable) accepts 1 positional arg (the current value) 199*d6050574SRomain Jobredeaux and returns `bool` (`True` if matched, `False` if not). 200*d6050574SRomain Jobredeaux start: ([`int`]) the position within `search_in` to start at. Defaults 201*d6050574SRomain Jobredeaux to `0` (start of list) 202*d6050574SRomain Jobredeaux end: (optional [`int`]) the position within `search_in` to stop before 203*d6050574SRomain Jobredeaux (i.e. the value is exclusive; given a list of length 5, specifying 204*d6050574SRomain Jobredeaux `end=5` means it will search the whole list). Defaults to the length 205*d6050574SRomain Jobredeaux of `search_in`. 206*d6050574SRomain Jobredeaux Returns: 207*d6050574SRomain Jobredeaux [`tuple`] of ([`int`] found_at, value). 208*d6050574SRomain Jobredeaux * If the value was found, then `found_at` is the offset in `search_in` 209*d6050574SRomain Jobredeaux it was found at, and matched value is the element at that offset. 210*d6050574SRomain Jobredeaux * If the value was not found, then `found_at=-1`, and the matched 211*d6050574SRomain Jobredeaux value is `None`. 212*d6050574SRomain Jobredeaux """ 213*d6050574SRomain Jobredeaux end = len(search_in) if end == None else end 214*d6050574SRomain Jobredeaux pos = start 215*d6050574SRomain Jobredeaux for _ in search_in: 216*d6050574SRomain Jobredeaux if pos >= end: 217*d6050574SRomain Jobredeaux return -1, None 218*d6050574SRomain Jobredeaux value = search_in[pos] 219*d6050574SRomain Jobredeaux if search_for(value): 220*d6050574SRomain Jobredeaux return pos, value 221*d6050574SRomain Jobredeaux pos += 1 222*d6050574SRomain Jobredeaux return -1, None 223*d6050574SRomain Jobredeaux 224*d6050574SRomain Jobredeauxdef compare_dicts(*, expected, actual): 225*d6050574SRomain Jobredeaux """Compares two dicts, reporting differences. 226*d6050574SRomain Jobredeaux 227*d6050574SRomain Jobredeaux Args: 228*d6050574SRomain Jobredeaux expected: ([`dict`]) the desired state of `actual` 229*d6050574SRomain Jobredeaux actual: ([`dict`]) the observed dict 230*d6050574SRomain Jobredeaux Returns: 231*d6050574SRomain Jobredeaux Struct with the following attributes: 232*d6050574SRomain Jobredeaux * missing_keys: [`list`] of keys that were missing in `actual`, but present 233*d6050574SRomain Jobredeaux in `expected` 234*d6050574SRomain Jobredeaux * unexpected_keys: [`list`] of keys that were present in `actual`, but not 235*d6050574SRomain Jobredeaux present in `expected` 236*d6050574SRomain Jobredeaux * incorrect_entries: ([`dict`] of key -> [`DictEntryMismatch`]) of keys that 237*d6050574SRomain Jobredeaux were in both dicts, but whose values were not equal. The value is 238*d6050574SRomain Jobredeaux a "DictEntryMismatch" struct, which is defined as a struct with 239*d6050574SRomain Jobredeaux attributes: 240*d6050574SRomain Jobredeaux * `actual`: the value from `actual[key]` 241*d6050574SRomain Jobredeaux * `expected`: the value from `expected[key]` 242*d6050574SRomain Jobredeaux """ 243*d6050574SRomain Jobredeaux all_keys = {key: None for key in actual.keys()} 244*d6050574SRomain Jobredeaux all_keys.update({key: None for key in expected.keys()}) 245*d6050574SRomain Jobredeaux missing_keys = [] 246*d6050574SRomain Jobredeaux unexpected_keys = [] 247*d6050574SRomain Jobredeaux incorrect_entries = {} 248*d6050574SRomain Jobredeaux 249*d6050574SRomain Jobredeaux for key in sorted(all_keys): 250*d6050574SRomain Jobredeaux if key not in actual: 251*d6050574SRomain Jobredeaux missing_keys.append(key) 252*d6050574SRomain Jobredeaux elif key not in expected: 253*d6050574SRomain Jobredeaux unexpected_keys.append(key) 254*d6050574SRomain Jobredeaux else: 255*d6050574SRomain Jobredeaux actual_value = actual[key] 256*d6050574SRomain Jobredeaux expected_value = expected[key] 257*d6050574SRomain Jobredeaux if actual_value != expected_value: 258*d6050574SRomain Jobredeaux incorrect_entries[key] = struct( 259*d6050574SRomain Jobredeaux actual = actual_value, 260*d6050574SRomain Jobredeaux expected = expected_value, 261*d6050574SRomain Jobredeaux ) 262*d6050574SRomain Jobredeaux 263*d6050574SRomain Jobredeaux return struct( 264*d6050574SRomain Jobredeaux missing_keys = missing_keys, 265*d6050574SRomain Jobredeaux unexpected_keys = unexpected_keys, 266*d6050574SRomain Jobredeaux incorrect_entries = incorrect_entries, 267*d6050574SRomain Jobredeaux ) 268