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"""# RunfilesSubject""" 16*d6050574SRomain Jobredeaux 17*d6050574SRomain Jobredeauxload( 18*d6050574SRomain Jobredeaux "//lib:util.bzl", 19*d6050574SRomain Jobredeaux "is_runfiles", 20*d6050574SRomain Jobredeaux "runfiles_paths", 21*d6050574SRomain Jobredeaux) 22*d6050574SRomain Jobredeauxload( 23*d6050574SRomain Jobredeaux ":check_util.bzl", 24*d6050574SRomain Jobredeaux "check_contains_exactly", 25*d6050574SRomain Jobredeaux "check_contains_predicate", 26*d6050574SRomain Jobredeaux "check_not_contains_predicate", 27*d6050574SRomain Jobredeaux) 28*d6050574SRomain Jobredeauxload(":collection_subject.bzl", "CollectionSubject") 29*d6050574SRomain Jobredeauxload( 30*d6050574SRomain Jobredeaux ":failure_messages.bzl", 31*d6050574SRomain Jobredeaux "format_actual_collection", 32*d6050574SRomain Jobredeaux "format_failure_unexpected_value", 33*d6050574SRomain Jobredeaux "format_problem_expected_exactly", 34*d6050574SRomain Jobredeaux "format_problem_missing_required_values", 35*d6050574SRomain Jobredeaux "format_problem_unexpected_values", 36*d6050574SRomain Jobredeaux) 37*d6050574SRomain Jobredeauxload(":matching.bzl", "matching") 38*d6050574SRomain Jobredeauxload(":truth_common.bzl", "to_list") 39*d6050574SRomain Jobredeaux 40*d6050574SRomain Jobredeauxdef _runfiles_subject_new(runfiles, meta, kind = None): 41*d6050574SRomain Jobredeaux """Creates a "RunfilesSubject" struct. 42*d6050574SRomain Jobredeaux 43*d6050574SRomain Jobredeaux Method: RunfilesSubject.new 44*d6050574SRomain Jobredeaux 45*d6050574SRomain Jobredeaux Args: 46*d6050574SRomain Jobredeaux runfiles: ([`runfiles`]) the runfiles to check against. 47*d6050574SRomain Jobredeaux meta: ([`ExpectMeta`]) the metadata about the call chain. 48*d6050574SRomain Jobredeaux kind: (optional [`str`]) what type of runfiles they are, usually "data" 49*d6050574SRomain Jobredeaux or "default". If not known or not applicable, use None. 50*d6050574SRomain Jobredeaux 51*d6050574SRomain Jobredeaux Returns: 52*d6050574SRomain Jobredeaux [`RunfilesSubject`] object. 53*d6050574SRomain Jobredeaux """ 54*d6050574SRomain Jobredeaux self = struct( 55*d6050574SRomain Jobredeaux runfiles = runfiles, 56*d6050574SRomain Jobredeaux meta = meta, 57*d6050574SRomain Jobredeaux kind = kind, 58*d6050574SRomain Jobredeaux actual_paths = sorted(runfiles_paths(meta.ctx.workspace_name, runfiles)), 59*d6050574SRomain Jobredeaux ) 60*d6050574SRomain Jobredeaux public = struct( 61*d6050574SRomain Jobredeaux # keep sorted start 62*d6050574SRomain Jobredeaux actual = runfiles, 63*d6050574SRomain Jobredeaux contains = lambda *a, **k: _runfiles_subject_contains(self, *a, **k), 64*d6050574SRomain Jobredeaux contains_at_least = lambda *a, **k: _runfiles_subject_contains_at_least(self, *a, **k), 65*d6050574SRomain Jobredeaux contains_exactly = lambda *a, **k: _runfiles_subject_contains_exactly(self, *a, **k), 66*d6050574SRomain Jobredeaux contains_none_of = lambda *a, **k: _runfiles_subject_contains_none_of(self, *a, **k), 67*d6050574SRomain Jobredeaux contains_predicate = lambda *a, **k: _runfiles_subject_contains_predicate(self, *a, **k), 68*d6050574SRomain Jobredeaux not_contains = lambda *a, **k: _runfiles_subject_not_contains(self, *a, **k), 69*d6050574SRomain Jobredeaux not_contains_predicate = lambda *a, **k: _runfiles_subject_not_contains_predicate(self, *a, **k), 70*d6050574SRomain Jobredeaux # keep sorted end 71*d6050574SRomain Jobredeaux ) 72*d6050574SRomain Jobredeaux return public 73*d6050574SRomain Jobredeaux 74*d6050574SRomain Jobredeauxdef _runfiles_subject_contains(self, expected): 75*d6050574SRomain Jobredeaux """Assert that the runfiles contains the provided path. 76*d6050574SRomain Jobredeaux 77*d6050574SRomain Jobredeaux Method: RunfilesSubject.contains 78*d6050574SRomain Jobredeaux 79*d6050574SRomain Jobredeaux Args: 80*d6050574SRomain Jobredeaux self: implicitly added. 81*d6050574SRomain Jobredeaux expected: ([`str`]) the path to check is present. This will be formatted 82*d6050574SRomain Jobredeaux using `ExpectMeta.format_str` and its current contextual 83*d6050574SRomain Jobredeaux keywords. Note that paths are runfiles-root relative (i.e. 84*d6050574SRomain Jobredeaux you likely need to include the workspace name.) 85*d6050574SRomain Jobredeaux """ 86*d6050574SRomain Jobredeaux expected = self.meta.format_str(expected) 87*d6050574SRomain Jobredeaux matcher = matching.equals_wrapper(expected) 88*d6050574SRomain Jobredeaux return _runfiles_subject_contains_predicate(self, matcher) 89*d6050574SRomain Jobredeaux 90*d6050574SRomain Jobredeauxdef _runfiles_subject_contains_at_least(self, paths): 91*d6050574SRomain Jobredeaux """Assert that the runfiles contains at least all of the provided paths. 92*d6050574SRomain Jobredeaux 93*d6050574SRomain Jobredeaux Method: RunfilesSubject.contains_at_least 94*d6050574SRomain Jobredeaux 95*d6050574SRomain Jobredeaux All the paths must exist, but extra paths are allowed. Order is not checked. 96*d6050574SRomain Jobredeaux Multiplicity is respected. 97*d6050574SRomain Jobredeaux 98*d6050574SRomain Jobredeaux Args: 99*d6050574SRomain Jobredeaux self: implicitly added. 100*d6050574SRomain Jobredeaux paths: ((collection of [`str`]) | [`runfiles`]) the paths that must 101*d6050574SRomain Jobredeaux exist. If a collection of strings is provided, they will be 102*d6050574SRomain Jobredeaux formatted using [`ExpectMeta.format_str`], so its template keywords 103*d6050574SRomain Jobredeaux can be directly passed. If a `runfiles` object is passed, it is 104*d6050574SRomain Jobredeaux converted to a set of path strings. 105*d6050574SRomain Jobredeaux """ 106*d6050574SRomain Jobredeaux if is_runfiles(paths): 107*d6050574SRomain Jobredeaux paths = runfiles_paths(self.meta.ctx.workspace_name, paths) 108*d6050574SRomain Jobredeaux 109*d6050574SRomain Jobredeaux paths = [self.meta.format_str(p) for p in to_list(paths)] 110*d6050574SRomain Jobredeaux 111*d6050574SRomain Jobredeaux # NOTE: We don't return Ordered because there isn't a well-defined order 112*d6050574SRomain Jobredeaux # between the different sub-objects within the runfiles. 113*d6050574SRomain Jobredeaux CollectionSubject.new( 114*d6050574SRomain Jobredeaux self.actual_paths, 115*d6050574SRomain Jobredeaux meta = self.meta, 116*d6050574SRomain Jobredeaux element_plural_name = "paths", 117*d6050574SRomain Jobredeaux container_name = "{}runfiles".format(self.kind + " " if self.kind else ""), 118*d6050574SRomain Jobredeaux ).contains_at_least(paths) 119*d6050574SRomain Jobredeaux 120*d6050574SRomain Jobredeauxdef _runfiles_subject_contains_predicate(self, matcher): 121*d6050574SRomain Jobredeaux """Asserts that `matcher` matches at least one value. 122*d6050574SRomain Jobredeaux 123*d6050574SRomain Jobredeaux Method: RunfilesSubject.contains_predicate 124*d6050574SRomain Jobredeaux 125*d6050574SRomain Jobredeaux Args: 126*d6050574SRomain Jobredeaux self: implicitly added. 127*d6050574SRomain Jobredeaux matcher: callable that takes 1 positional arg ([`str`] path) and returns 128*d6050574SRomain Jobredeaux boolean. 129*d6050574SRomain Jobredeaux """ 130*d6050574SRomain Jobredeaux check_contains_predicate( 131*d6050574SRomain Jobredeaux self.actual_paths, 132*d6050574SRomain Jobredeaux matcher = matcher, 133*d6050574SRomain Jobredeaux format_problem = "expected to contain: {}".format(matcher.desc), 134*d6050574SRomain Jobredeaux format_actual = lambda: format_actual_collection( 135*d6050574SRomain Jobredeaux self.actual_paths, 136*d6050574SRomain Jobredeaux name = "{}runfiles".format(self.kind + " " if self.kind else ""), 137*d6050574SRomain Jobredeaux ), 138*d6050574SRomain Jobredeaux meta = self.meta, 139*d6050574SRomain Jobredeaux ) 140*d6050574SRomain Jobredeaux 141*d6050574SRomain Jobredeauxdef _runfiles_subject_contains_exactly(self, paths): 142*d6050574SRomain Jobredeaux """Asserts that the runfiles contains_exactly the set of paths 143*d6050574SRomain Jobredeaux 144*d6050574SRomain Jobredeaux Method: RunfilesSubject.contains_exactly 145*d6050574SRomain Jobredeaux 146*d6050574SRomain Jobredeaux Args: 147*d6050574SRomain Jobredeaux self: implicitly added. 148*d6050574SRomain Jobredeaux paths: ([`collection`] of [`str`]) the paths to check. These will be 149*d6050574SRomain Jobredeaux formatted using `meta.format_str`, so its template keywords can 150*d6050574SRomain Jobredeaux be directly passed. All the paths must exist in the runfiles exactly 151*d6050574SRomain Jobredeaux as provided, and no extra paths may exist. 152*d6050574SRomain Jobredeaux """ 153*d6050574SRomain Jobredeaux paths = [self.meta.format_str(p) for p in to_list(paths)] 154*d6050574SRomain Jobredeaux runfiles_name = "{}runfiles".format(self.kind + " " if self.kind else "") 155*d6050574SRomain Jobredeaux 156*d6050574SRomain Jobredeaux check_contains_exactly( 157*d6050574SRomain Jobredeaux expect_contains = paths, 158*d6050574SRomain Jobredeaux actual_container = self.actual_paths, 159*d6050574SRomain Jobredeaux format_actual = lambda: format_actual_collection( 160*d6050574SRomain Jobredeaux self.actual_paths, 161*d6050574SRomain Jobredeaux name = runfiles_name, 162*d6050574SRomain Jobredeaux ), 163*d6050574SRomain Jobredeaux format_expected = lambda: format_problem_expected_exactly(paths, sort = True), 164*d6050574SRomain Jobredeaux format_missing = lambda missing: format_problem_missing_required_values( 165*d6050574SRomain Jobredeaux missing, 166*d6050574SRomain Jobredeaux sort = True, 167*d6050574SRomain Jobredeaux ), 168*d6050574SRomain Jobredeaux format_unexpected = lambda unexpected: format_problem_unexpected_values( 169*d6050574SRomain Jobredeaux unexpected, 170*d6050574SRomain Jobredeaux sort = True, 171*d6050574SRomain Jobredeaux ), 172*d6050574SRomain Jobredeaux format_out_of_order = lambda matches: fail("Should not be called"), 173*d6050574SRomain Jobredeaux meta = self.meta, 174*d6050574SRomain Jobredeaux ) 175*d6050574SRomain Jobredeaux 176*d6050574SRomain Jobredeauxdef _runfiles_subject_contains_none_of(self, paths, require_workspace_prefix = True): 177*d6050574SRomain Jobredeaux """Asserts the runfiles contain none of `paths`. 178*d6050574SRomain Jobredeaux 179*d6050574SRomain Jobredeaux Method: RunfilesSubject.contains_none_of 180*d6050574SRomain Jobredeaux 181*d6050574SRomain Jobredeaux Args: 182*d6050574SRomain Jobredeaux self: implicitly added. 183*d6050574SRomain Jobredeaux paths: ([`collection`] of [`str`]) the paths that should not exist. They should 184*d6050574SRomain Jobredeaux be runfiles root-relative paths (not workspace relative). The value 185*d6050574SRomain Jobredeaux is formatted using `ExpectMeta.format_str` and the current 186*d6050574SRomain Jobredeaux contextual keywords. 187*d6050574SRomain Jobredeaux require_workspace_prefix: ([`bool`]) True to check that the path includes the 188*d6050574SRomain Jobredeaux workspace prefix. This is to guard against accidentallly passing a 189*d6050574SRomain Jobredeaux workspace relative path, which will (almost) never exist, and cause 190*d6050574SRomain Jobredeaux the test to always pass. Specify False if the file being checked for 191*d6050574SRomain Jobredeaux is _actually_ a runfiles-root relative path that isn't under the 192*d6050574SRomain Jobredeaux workspace itself. 193*d6050574SRomain Jobredeaux """ 194*d6050574SRomain Jobredeaux formatted_paths = [] 195*d6050574SRomain Jobredeaux for path in paths: 196*d6050574SRomain Jobredeaux path = self.meta.format_str(path) 197*d6050574SRomain Jobredeaux formatted_paths.append(path) 198*d6050574SRomain Jobredeaux if require_workspace_prefix: 199*d6050574SRomain Jobredeaux _runfiles_subject_check_workspace_prefix(self, path) 200*d6050574SRomain Jobredeaux 201*d6050574SRomain Jobredeaux CollectionSubject.new( 202*d6050574SRomain Jobredeaux self.actual_paths, 203*d6050574SRomain Jobredeaux meta = self.meta, 204*d6050574SRomain Jobredeaux ).contains_none_of(formatted_paths) 205*d6050574SRomain Jobredeaux 206*d6050574SRomain Jobredeauxdef _runfiles_subject_not_contains(self, path, require_workspace_prefix = True): 207*d6050574SRomain Jobredeaux """Assert that the runfiles does not contain the given path. 208*d6050574SRomain Jobredeaux 209*d6050574SRomain Jobredeaux Method: RunfilesSubject.not_contains 210*d6050574SRomain Jobredeaux 211*d6050574SRomain Jobredeaux Args: 212*d6050574SRomain Jobredeaux self: implicitly added. 213*d6050574SRomain Jobredeaux path: ([`str`]) the path that should not exist. It should be a runfiles 214*d6050574SRomain Jobredeaux root-relative path (not workspace relative). The value is formatted 215*d6050574SRomain Jobredeaux using `format_str`, so its template keywords can be directly 216*d6050574SRomain Jobredeaux passed. 217*d6050574SRomain Jobredeaux require_workspace_prefix: ([`bool`]) True to check that the path includes the 218*d6050574SRomain Jobredeaux workspace prefix. This is to guard against accidentallly passing a 219*d6050574SRomain Jobredeaux workspace relative path, which will (almost) never exist, and cause 220*d6050574SRomain Jobredeaux the test to always pass. Specify False if the file being checked for 221*d6050574SRomain Jobredeaux is _actually_ a runfiles-root relative path that isn't under the 222*d6050574SRomain Jobredeaux workspace itself. 223*d6050574SRomain Jobredeaux """ 224*d6050574SRomain Jobredeaux path = self.meta.format_str(path) 225*d6050574SRomain Jobredeaux if require_workspace_prefix: 226*d6050574SRomain Jobredeaux _runfiles_subject_check_workspace_prefix(self, path) 227*d6050574SRomain Jobredeaux 228*d6050574SRomain Jobredeaux if path in self.actual_paths: 229*d6050574SRomain Jobredeaux problem, actual = format_failure_unexpected_value( 230*d6050574SRomain Jobredeaux container_name = "{}runfiles".format(self.kind + " " if self.kind else ""), 231*d6050574SRomain Jobredeaux unexpected = path, 232*d6050574SRomain Jobredeaux actual = self.actual_paths, 233*d6050574SRomain Jobredeaux ) 234*d6050574SRomain Jobredeaux self.meta.add_failure(problem, actual) 235*d6050574SRomain Jobredeaux 236*d6050574SRomain Jobredeauxdef _runfiles_subject_not_contains_predicate(self, matcher): 237*d6050574SRomain Jobredeaux """Asserts that none of the runfiles match `matcher`. 238*d6050574SRomain Jobredeaux 239*d6050574SRomain Jobredeaux Method: RunfilesSubject.not_contains_predicate 240*d6050574SRomain Jobredeaux 241*d6050574SRomain Jobredeaux Args: 242*d6050574SRomain Jobredeaux self: implicitly added. 243*d6050574SRomain Jobredeaux matcher: [`Matcher`] that accepts a string (runfiles root-relative path). 244*d6050574SRomain Jobredeaux """ 245*d6050574SRomain Jobredeaux check_not_contains_predicate(self.actual_paths, matcher, meta = self.meta) 246*d6050574SRomain Jobredeaux 247*d6050574SRomain Jobredeauxdef _runfiles_subject_check_workspace_prefix(self, path): 248*d6050574SRomain Jobredeaux if not path.startswith(self.meta.ctx.workspace_name + "/"): 249*d6050574SRomain Jobredeaux fail("Rejecting path lacking workspace prefix: this often indicates " + 250*d6050574SRomain Jobredeaux "a bug. Include the workspace name as part of the path, or pass " + 251*d6050574SRomain Jobredeaux "require_workspace_prefix=False if the path is truly " + 252*d6050574SRomain Jobredeaux "runfiles-root relative, not workspace relative.\npath=" + path) 253*d6050574SRomain Jobredeaux 254*d6050574SRomain Jobredeaux# We use this name so it shows up nice in docs. 255*d6050574SRomain Jobredeaux# buildifier: disable=name-conventions 256*d6050574SRomain JobredeauxRunfilesSubject = struct( 257*d6050574SRomain Jobredeaux new = _runfiles_subject_new, 258*d6050574SRomain Jobredeaux contains = _runfiles_subject_contains, 259*d6050574SRomain Jobredeaux contains_at_least = _runfiles_subject_contains_at_least, 260*d6050574SRomain Jobredeaux contains_predicate = _runfiles_subject_contains_predicate, 261*d6050574SRomain Jobredeaux contains_exactly = _runfiles_subject_contains_exactly, 262*d6050574SRomain Jobredeaux contains_none_of = _runfiles_subject_contains_none_of, 263*d6050574SRomain Jobredeaux not_contains = _runfiles_subject_not_contains, 264*d6050574SRomain Jobredeaux not_contains_predicate = _runfiles_subject_not_contains_predicate, 265*d6050574SRomain Jobredeaux check_workspace_prefix = _runfiles_subject_check_workspace_prefix, 266*d6050574SRomain Jobredeaux) 267