1*d6050574SRomain Jobredeaux"""Helper functions to perform checks.""" 2*d6050574SRomain Jobredeaux 3*d6050574SRomain Jobredeauxload("@bazel_skylib//lib:types.bzl", "types") 4*d6050574SRomain Jobredeauxload(":compare_util.bzl", "MatchResult", "compare_contains_exactly_predicates") 5*d6050574SRomain Jobredeauxload(":failure_messages.bzl", "format_failure_unexpected_values") 6*d6050574SRomain Jobredeauxload(":matching.bzl", "matching") 7*d6050574SRomain Jobredeauxload(":ordered.bzl", "IN_ORDER", "OrderedIncorrectly") 8*d6050574SRomain Jobredeauxload(":truth_common.bzl", "enumerate_list_as_lines", "maybe_sorted", "to_list") 9*d6050574SRomain Jobredeaux 10*d6050574SRomain Jobredeauxdef check_contains_exactly( 11*d6050574SRomain Jobredeaux *, 12*d6050574SRomain Jobredeaux expect_contains, 13*d6050574SRomain Jobredeaux actual_container, 14*d6050574SRomain Jobredeaux format_actual, 15*d6050574SRomain Jobredeaux format_expected, 16*d6050574SRomain Jobredeaux format_missing, 17*d6050574SRomain Jobredeaux format_unexpected, 18*d6050574SRomain Jobredeaux format_out_of_order, 19*d6050574SRomain Jobredeaux meta): 20*d6050574SRomain Jobredeaux """Check that a collection contains exactly the given values and no more. 21*d6050574SRomain Jobredeaux 22*d6050574SRomain Jobredeaux This checks that the collection contains exactly the given values. Extra 23*d6050574SRomain Jobredeaux values are not allowed. Multiplicity of the expected values is respected. 24*d6050574SRomain Jobredeaux Ordering is not checked; call `in_order()` to also check the order 25*d6050574SRomain Jobredeaux of the actual values matches the order of the expected values. 26*d6050574SRomain Jobredeaux 27*d6050574SRomain Jobredeaux Args: 28*d6050574SRomain Jobredeaux expect_contains: the values that must exist (and no more). 29*d6050574SRomain Jobredeaux actual_container: the values to check within. 30*d6050574SRomain Jobredeaux format_actual: (callable) accepts no args and returns [`str`] (the 31*d6050574SRomain Jobredeaux description of the actual values). 32*d6050574SRomain Jobredeaux format_expected: (callable) accepts no args and returns [`str`] ( 33*d6050574SRomain Jobredeaux description of the expected values). 34*d6050574SRomain Jobredeaux format_missing: (callable) accepts 1 position arg (list of values from 35*d6050574SRomain Jobredeaux `expect_contains` that were missing), and returns [`str`] (description of 36*d6050574SRomain Jobredeaux the missing values). 37*d6050574SRomain Jobredeaux format_unexpected: (callable) accepts 1 positional arg (list of values from 38*d6050574SRomain Jobredeaux `actual_container` that weren't expected), and returns [`str`] (description of 39*d6050574SRomain Jobredeaux the unexpected values). 40*d6050574SRomain Jobredeaux format_out_of_order: (callable) accepts 1 arg (a list of "MatchResult" 41*d6050574SRomain Jobredeaux structs, see above) and returns a string (the problem message 42*d6050574SRomain Jobredeaux reported on failure). The order of match results is the expected 43*d6050574SRomain Jobredeaux order. 44*d6050574SRomain Jobredeaux meta: ([`ExpectMeta`]) to record failures. 45*d6050574SRomain Jobredeaux 46*d6050574SRomain Jobredeaux Returns: 47*d6050574SRomain Jobredeaux [`Ordered`] object. 48*d6050574SRomain Jobredeaux """ 49*d6050574SRomain Jobredeaux result = compare_contains_exactly_predicates( 50*d6050574SRomain Jobredeaux expect_contains = [ 51*d6050574SRomain Jobredeaux matching.equals_wrapper(raw_expected) 52*d6050574SRomain Jobredeaux for raw_expected in expect_contains 53*d6050574SRomain Jobredeaux ], 54*d6050574SRomain Jobredeaux actual_container = actual_container, 55*d6050574SRomain Jobredeaux ) 56*d6050574SRomain Jobredeaux if not result.contains_exactly: 57*d6050574SRomain Jobredeaux problems = [] 58*d6050574SRomain Jobredeaux if result.missing: 59*d6050574SRomain Jobredeaux problems.append(format_missing([m.desc for m in result.missing])) 60*d6050574SRomain Jobredeaux if result.unexpected: 61*d6050574SRomain Jobredeaux problems.append(format_unexpected(result.unexpected)) 62*d6050574SRomain Jobredeaux problems.append(format_expected()) 63*d6050574SRomain Jobredeaux 64*d6050574SRomain Jobredeaux meta.add_failure("\n".join(problems), format_actual()) 65*d6050574SRomain Jobredeaux 66*d6050574SRomain Jobredeaux # We already recorded an error, so just pretend order is correct to 67*d6050574SRomain Jobredeaux # avoid spamming another error. 68*d6050574SRomain Jobredeaux return IN_ORDER 69*d6050574SRomain Jobredeaux elif result.is_in_order: 70*d6050574SRomain Jobredeaux return IN_ORDER 71*d6050574SRomain Jobredeaux else: 72*d6050574SRomain Jobredeaux return OrderedIncorrectly.new( 73*d6050574SRomain Jobredeaux format_problem = lambda: format_out_of_order(result.matches), 74*d6050574SRomain Jobredeaux format_actual = format_actual, 75*d6050574SRomain Jobredeaux meta = meta, 76*d6050574SRomain Jobredeaux ) 77*d6050574SRomain Jobredeaux 78*d6050574SRomain Jobredeauxdef check_contains_exactly_predicates( 79*d6050574SRomain Jobredeaux *, 80*d6050574SRomain Jobredeaux expect_contains, 81*d6050574SRomain Jobredeaux actual_container, 82*d6050574SRomain Jobredeaux format_actual, 83*d6050574SRomain Jobredeaux format_expected, 84*d6050574SRomain Jobredeaux format_missing, 85*d6050574SRomain Jobredeaux format_unexpected, 86*d6050574SRomain Jobredeaux format_out_of_order, 87*d6050574SRomain Jobredeaux meta): 88*d6050574SRomain Jobredeaux """Check that a collection contains values matching the given predicates and no more. 89*d6050574SRomain Jobredeaux 90*d6050574SRomain Jobredeaux todo doc to describe behavior 91*d6050574SRomain Jobredeaux This checks that the collection contains values that match the given exactly the given values. 92*d6050574SRomain Jobredeaux Extra values that do not match a predicate are not allowed. Multiplicity of 93*d6050574SRomain Jobredeaux the expected predicates is respected. Ordering is not checked; call 94*d6050574SRomain Jobredeaux `in_order()` to also check the order of the actual values matches the order 95*d6050574SRomain Jobredeaux of the expected predicates. 96*d6050574SRomain Jobredeaux 97*d6050574SRomain Jobredeaux Args: 98*d6050574SRomain Jobredeaux expect_contains: the predicates that must match (and no more). 99*d6050574SRomain Jobredeaux actual_container: the values to check within. 100*d6050574SRomain Jobredeaux format_actual: (callable) accepts no args and returns [`str`] (the 101*d6050574SRomain Jobredeaux description of the actual values). 102*d6050574SRomain Jobredeaux format_expected: (callable) accepts no args and returns [`str`] ( 103*d6050574SRomain Jobredeaux description of the expected values). 104*d6050574SRomain Jobredeaux format_missing: (callable) accepts 1 position arg (list of values from 105*d6050574SRomain Jobredeaux `expect_contains` that were missing), and returns [`str`] (description of 106*d6050574SRomain Jobredeaux the missing values). 107*d6050574SRomain Jobredeaux format_unexpected: (callable) accepts 1 positional arg (list of values from 108*d6050574SRomain Jobredeaux `actual_container` that weren't expected), and returns [`str`] (description of 109*d6050574SRomain Jobredeaux the unexpected values). 110*d6050574SRomain Jobredeaux format_out_of_order: (callable) accepts 1 arg (a list of "MatchResult" 111*d6050574SRomain Jobredeaux structs, see above) and returns a string (the problem message 112*d6050574SRomain Jobredeaux reported on failure). The order of match results is the expected 113*d6050574SRomain Jobredeaux order. 114*d6050574SRomain Jobredeaux meta: ([`ExpectMeta`]) to record failures. 115*d6050574SRomain Jobredeaux 116*d6050574SRomain Jobredeaux Returns: 117*d6050574SRomain Jobredeaux [`Ordered`] object. 118*d6050574SRomain Jobredeaux """ 119*d6050574SRomain Jobredeaux result = compare_contains_exactly_predicates( 120*d6050574SRomain Jobredeaux expect_contains = expect_contains, 121*d6050574SRomain Jobredeaux actual_container = actual_container, 122*d6050574SRomain Jobredeaux ) 123*d6050574SRomain Jobredeaux if not result.contains_exactly: 124*d6050574SRomain Jobredeaux problems = [] 125*d6050574SRomain Jobredeaux if result.missing: 126*d6050574SRomain Jobredeaux problems.append(format_missing(result.missing)) 127*d6050574SRomain Jobredeaux if result.unexpected: 128*d6050574SRomain Jobredeaux problems.append(format_unexpected(result.unexpected)) 129*d6050574SRomain Jobredeaux problems.append(format_expected()) 130*d6050574SRomain Jobredeaux 131*d6050574SRomain Jobredeaux meta.add_failure("\n".join(problems), format_actual()) 132*d6050574SRomain Jobredeaux 133*d6050574SRomain Jobredeaux # We already recorded an error, so just pretend order is correct to 134*d6050574SRomain Jobredeaux # avoid spamming another error. 135*d6050574SRomain Jobredeaux return IN_ORDER 136*d6050574SRomain Jobredeaux elif result.is_in_order: 137*d6050574SRomain Jobredeaux return IN_ORDER 138*d6050574SRomain Jobredeaux else: 139*d6050574SRomain Jobredeaux return OrderedIncorrectly.new( 140*d6050574SRomain Jobredeaux format_problem = lambda: format_out_of_order(result.matches), 141*d6050574SRomain Jobredeaux format_actual = format_actual, 142*d6050574SRomain Jobredeaux meta = meta, 143*d6050574SRomain Jobredeaux ) 144*d6050574SRomain Jobredeaux 145*d6050574SRomain Jobredeauxdef check_contains_predicate(collection, matcher, *, format_problem, format_actual, meta): 146*d6050574SRomain Jobredeaux """Check that `matcher` matches any value in `collection`, and record an error if not. 147*d6050574SRomain Jobredeaux 148*d6050574SRomain Jobredeaux Args: 149*d6050574SRomain Jobredeaux collection: ([`collection`]) the collection whose values are compared against. 150*d6050574SRomain Jobredeaux matcher: ([`Matcher`]) that must match. 151*d6050574SRomain Jobredeaux format_problem: ([`str`] | callable) If a string, then the problem message 152*d6050574SRomain Jobredeaux to use when failing. If a callable, a no-arg callable that returns 153*d6050574SRomain Jobredeaux the problem string; see `_format_problem_*` for existing helpers. 154*d6050574SRomain Jobredeaux format_actual: ([`str`] | callable) If a string, then the actual message 155*d6050574SRomain Jobredeaux to use when failing. If a callable, a no-arg callable that returns 156*d6050574SRomain Jobredeaux the actual string; see `_format_actual_*` for existing helpers. 157*d6050574SRomain Jobredeaux meta: ([`ExpectMeta`]) to record failures 158*d6050574SRomain Jobredeaux """ 159*d6050574SRomain Jobredeaux for value in collection: 160*d6050574SRomain Jobredeaux if matcher.match(value): 161*d6050574SRomain Jobredeaux return 162*d6050574SRomain Jobredeaux meta.add_failure( 163*d6050574SRomain Jobredeaux format_problem if types.is_string(format_problem) else format_problem(), 164*d6050574SRomain Jobredeaux format_actual if types.is_string(format_actual) else format_actual(), 165*d6050574SRomain Jobredeaux ) 166*d6050574SRomain Jobredeaux 167*d6050574SRomain Jobredeauxdef check_contains_at_least_predicates( 168*d6050574SRomain Jobredeaux collection, 169*d6050574SRomain Jobredeaux matchers, 170*d6050574SRomain Jobredeaux *, 171*d6050574SRomain Jobredeaux format_missing, 172*d6050574SRomain Jobredeaux format_out_of_order, 173*d6050574SRomain Jobredeaux format_actual, 174*d6050574SRomain Jobredeaux meta): 175*d6050574SRomain Jobredeaux """Check that the collection is a subset of the predicates. 176*d6050574SRomain Jobredeaux 177*d6050574SRomain Jobredeaux The collection must match all the predicates. It can contain extra elements. 178*d6050574SRomain Jobredeaux The multiplicity of matchers is respected. Checking that the relative order 179*d6050574SRomain Jobredeaux of matches is the same as the passed-in matchers order can done by calling 180*d6050574SRomain Jobredeaux `in_order()`. 181*d6050574SRomain Jobredeaux 182*d6050574SRomain Jobredeaux Args: 183*d6050574SRomain Jobredeaux collection: [`collection`] of values to check within. 184*d6050574SRomain Jobredeaux matchers: [`collection`] of [`Matcher`] objects to match (see `matchers` struct) 185*d6050574SRomain Jobredeaux format_missing: (callable) accepts 1 positional arg (a list of the 186*d6050574SRomain Jobredeaux `matchers` that did not match) and returns a string (the problem 187*d6050574SRomain Jobredeaux message reported on failure). 188*d6050574SRomain Jobredeaux format_out_of_order: (callable) accepts 1 arg (a list of `MatchResult`s) 189*d6050574SRomain Jobredeaux and returns a string (the problem message reported on failure). The 190*d6050574SRomain Jobredeaux order of match results is the expected order. 191*d6050574SRomain Jobredeaux format_actual: callable: accepts no args and returns a string (the 192*d6050574SRomain Jobredeaux text describing the actual value reported on failure). 193*d6050574SRomain Jobredeaux meta: ([`ExpectMeta`]) used for reporting errors. 194*d6050574SRomain Jobredeaux 195*d6050574SRomain Jobredeaux Returns: 196*d6050574SRomain Jobredeaux [`Ordered`] object to allow checking the order of matches. 197*d6050574SRomain Jobredeaux """ 198*d6050574SRomain Jobredeaux 199*d6050574SRomain Jobredeaux # We'll later update this list in-place with results. We keep the order 200*d6050574SRomain Jobredeaux # so that, on failure, the formatters receive the expected order of matches. 201*d6050574SRomain Jobredeaux matches = [None for _ in matchers] 202*d6050574SRomain Jobredeaux 203*d6050574SRomain Jobredeaux # A list of (original position, matcher) tuples. This allows 204*d6050574SRomain Jobredeaux # mapping a matcher back to its original order and respecting 205*d6050574SRomain Jobredeaux # the multiplicity of matchers. 206*d6050574SRomain Jobredeaux remaining_matchers = enumerate(matchers) 207*d6050574SRomain Jobredeaux ordered = True 208*d6050574SRomain Jobredeaux for absolute_pos, value in enumerate(collection): 209*d6050574SRomain Jobredeaux if not remaining_matchers: 210*d6050574SRomain Jobredeaux break 211*d6050574SRomain Jobredeaux found_i = -1 212*d6050574SRomain Jobredeaux for cur_i, (_, matcher) in enumerate(remaining_matchers): 213*d6050574SRomain Jobredeaux if matcher.match(value): 214*d6050574SRomain Jobredeaux found_i = cur_i 215*d6050574SRomain Jobredeaux break 216*d6050574SRomain Jobredeaux if found_i > -1: 217*d6050574SRomain Jobredeaux ordered = ordered and (found_i == 0) 218*d6050574SRomain Jobredeaux orig_matcher_pos, matcher = remaining_matchers.pop(found_i) 219*d6050574SRomain Jobredeaux matches[orig_matcher_pos] = MatchResult.new( 220*d6050574SRomain Jobredeaux matched_value = value, 221*d6050574SRomain Jobredeaux found_at = absolute_pos, 222*d6050574SRomain Jobredeaux matcher = matcher, 223*d6050574SRomain Jobredeaux ) 224*d6050574SRomain Jobredeaux 225*d6050574SRomain Jobredeaux if remaining_matchers: 226*d6050574SRomain Jobredeaux meta.add_failure( 227*d6050574SRomain Jobredeaux format_missing([v[1] for v in remaining_matchers]), 228*d6050574SRomain Jobredeaux format_actual if types.is_string(format_actual) else format_actual(), 229*d6050574SRomain Jobredeaux ) 230*d6050574SRomain Jobredeaux 231*d6050574SRomain Jobredeaux # We've added a failure, so no need to spam another error message, so 232*d6050574SRomain Jobredeaux # just pretend things are in order. 233*d6050574SRomain Jobredeaux return IN_ORDER 234*d6050574SRomain Jobredeaux elif ordered: 235*d6050574SRomain Jobredeaux return IN_ORDER 236*d6050574SRomain Jobredeaux else: 237*d6050574SRomain Jobredeaux return OrderedIncorrectly.new( 238*d6050574SRomain Jobredeaux format_problem = lambda: format_out_of_order(matches), 239*d6050574SRomain Jobredeaux format_actual = format_actual, 240*d6050574SRomain Jobredeaux meta = meta, 241*d6050574SRomain Jobredeaux ) 242*d6050574SRomain Jobredeaux 243*d6050574SRomain Jobredeauxdef check_contains_none_of(*, collection, none_of, meta, sort = True): 244*d6050574SRomain Jobredeaux """Check that a collection does not have any of the `none_of` values. 245*d6050574SRomain Jobredeaux 246*d6050574SRomain Jobredeaux Args: 247*d6050574SRomain Jobredeaux collection: ([`collection`]) the values to check within. 248*d6050574SRomain Jobredeaux none_of: the values that should not exist. 249*d6050574SRomain Jobredeaux meta: ([`ExpectMeta`]) to record failures. 250*d6050574SRomain Jobredeaux sort: ([`bool`]) If true, sort the values for display. 251*d6050574SRomain Jobredeaux """ 252*d6050574SRomain Jobredeaux unexpected = [] 253*d6050574SRomain Jobredeaux for value in none_of: 254*d6050574SRomain Jobredeaux if value in collection: 255*d6050574SRomain Jobredeaux unexpected.append(value) 256*d6050574SRomain Jobredeaux if not unexpected: 257*d6050574SRomain Jobredeaux return 258*d6050574SRomain Jobredeaux 259*d6050574SRomain Jobredeaux unexpected = maybe_sorted(unexpected, sort) 260*d6050574SRomain Jobredeaux problem, actual = format_failure_unexpected_values( 261*d6050574SRomain Jobredeaux none_of = "\n" + enumerate_list_as_lines(unexpected, prefix = " "), 262*d6050574SRomain Jobredeaux unexpected = unexpected, 263*d6050574SRomain Jobredeaux actual = collection, 264*d6050574SRomain Jobredeaux sort = sort, 265*d6050574SRomain Jobredeaux ) 266*d6050574SRomain Jobredeaux meta.add_failure(problem, actual) 267*d6050574SRomain Jobredeaux 268*d6050574SRomain Jobredeauxdef check_not_contains_predicate(collection, matcher, *, meta, sort = True): 269*d6050574SRomain Jobredeaux """Check that `matcher` matches no values in `collection`. 270*d6050574SRomain Jobredeaux 271*d6050574SRomain Jobredeaux Args: 272*d6050574SRomain Jobredeaux collection: ([`collection`]) the collection whose values are compared against. 273*d6050574SRomain Jobredeaux matcher: ([`Matcher`]) that must not match. 274*d6050574SRomain Jobredeaux meta: ([`ExpectMeta`]) to record failures 275*d6050574SRomain Jobredeaux sort: ([`bool`]) If `True`, the collection will be sorted for display. 276*d6050574SRomain Jobredeaux """ 277*d6050574SRomain Jobredeaux matches = maybe_sorted([v for v in collection if matcher.match(v)], sort) 278*d6050574SRomain Jobredeaux if not matches: 279*d6050574SRomain Jobredeaux return 280*d6050574SRomain Jobredeaux problem, actual = format_failure_unexpected_values( 281*d6050574SRomain Jobredeaux none_of = matcher.desc, 282*d6050574SRomain Jobredeaux unexpected = matches, 283*d6050574SRomain Jobredeaux actual = collection, 284*d6050574SRomain Jobredeaux sort = sort, 285*d6050574SRomain Jobredeaux ) 286*d6050574SRomain Jobredeaux meta.add_failure(problem, actual) 287*d6050574SRomain Jobredeaux 288*d6050574SRomain Jobredeauxdef common_subject_is_in(self, any_of): 289*d6050574SRomain Jobredeaux """Generic implementation of `Subject.is_in` 290*d6050574SRomain Jobredeaux 291*d6050574SRomain Jobredeaux Args: 292*d6050574SRomain Jobredeaux self: The subject object. It must provide `actual` and `meta` 293*d6050574SRomain Jobredeaux attributes. 294*d6050574SRomain Jobredeaux any_of: [`collection`] of values. 295*d6050574SRomain Jobredeaux """ 296*d6050574SRomain Jobredeaux return _check_is_in(self.actual, to_list(any_of), self.meta) 297*d6050574SRomain Jobredeaux 298*d6050574SRomain Jobredeauxdef _check_is_in(actual, any_of, meta): 299*d6050574SRomain Jobredeaux """Check that `actual` is one of the values in `any_of`. 300*d6050574SRomain Jobredeaux 301*d6050574SRomain Jobredeaux Args: 302*d6050574SRomain Jobredeaux actual: value to check for in `any_of` 303*d6050574SRomain Jobredeaux any_of: [`collection`] of values to check within. 304*d6050574SRomain Jobredeaux meta: ([`ExpectMeta`]) to record failures 305*d6050574SRomain Jobredeaux """ 306*d6050574SRomain Jobredeaux if actual in any_of: 307*d6050574SRomain Jobredeaux return 308*d6050574SRomain Jobredeaux meta.add_failure( 309*d6050574SRomain Jobredeaux "expected any of:\n{}".format( 310*d6050574SRomain Jobredeaux enumerate_list_as_lines(any_of, prefix = " "), 311*d6050574SRomain Jobredeaux ), 312*d6050574SRomain Jobredeaux "actual: {}".format(actual), 313*d6050574SRomain Jobredeaux ) 314*d6050574SRomain Jobredeaux 315*d6050574SRomain Jobredeauxdef check_not_equals(*, unexpected, actual, meta): 316*d6050574SRomain Jobredeaux """Check that the values are the same type and not equal (according to !=). 317*d6050574SRomain Jobredeaux 318*d6050574SRomain Jobredeaux NOTE: This requires the same type for both values. This is to prevent 319*d6050574SRomain Jobredeaux mistakes where different data types (usually) can never be equal. 320*d6050574SRomain Jobredeaux 321*d6050574SRomain Jobredeaux Args: 322*d6050574SRomain Jobredeaux unexpected: (object) the value that actual cannot equal 323*d6050574SRomain Jobredeaux actual: (object) the observed value 324*d6050574SRomain Jobredeaux meta: ([`ExpectMeta`]) to record failures 325*d6050574SRomain Jobredeaux """ 326*d6050574SRomain Jobredeaux same_type = type(actual) == type(unexpected) 327*d6050574SRomain Jobredeaux equal = not (actual != unexpected) # Use != to preserve semantics 328*d6050574SRomain Jobredeaux if same_type and not equal: 329*d6050574SRomain Jobredeaux return 330*d6050574SRomain Jobredeaux if not same_type: 331*d6050574SRomain Jobredeaux meta.add_failure( 332*d6050574SRomain Jobredeaux "expected not to be: {} (type: {})".format(unexpected, type(unexpected)), 333*d6050574SRomain Jobredeaux "actual: {} (type: {})".format(actual, type(actual)), 334*d6050574SRomain Jobredeaux ) 335*d6050574SRomain Jobredeaux else: 336*d6050574SRomain Jobredeaux meta.add_failure( 337*d6050574SRomain Jobredeaux "expected not to be: {}".format(unexpected), 338*d6050574SRomain Jobredeaux "actual: {}".format(actual), 339*d6050574SRomain Jobredeaux ) 340