xref: /aosp_15_r20/external/bazelbuild-rules_testing/lib/private/compare_util.bzl (revision d605057434dcabba796c020773aab68d9790ff9f)
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