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"""# ExpectMeta 15*d6050574SRomain Jobredeaux 16*d6050574SRomain JobredeauxExpectMeta object implementation. 17*d6050574SRomain Jobredeaux""" 18*d6050574SRomain Jobredeaux 19*d6050574SRomain Jobredeauxload("@bazel_skylib//lib:unittest.bzl", ut_asserts = "asserts") 20*d6050574SRomain Jobredeaux 21*d6050574SRomain Jobredeauxdef _expect_meta_new(env, exprs = [], details = [], format_str_kwargs = None): 22*d6050574SRomain Jobredeaux """Creates a new "ExpectMeta" struct". 23*d6050574SRomain Jobredeaux 24*d6050574SRomain Jobredeaux Method: ExpectMeta.new 25*d6050574SRomain Jobredeaux 26*d6050574SRomain Jobredeaux ExpectMeta objects are internal helpers for the Expect object and Subject 27*d6050574SRomain Jobredeaux objects. They are used for Subjects to store and communicate state through a 28*d6050574SRomain Jobredeaux series of call chains and asserts. 29*d6050574SRomain Jobredeaux 30*d6050574SRomain Jobredeaux This constructor should only be directly called by `Expect` objects. When a 31*d6050574SRomain Jobredeaux parent Subject is creating a child-Subject, then [`derive()`] should be 32*d6050574SRomain Jobredeaux used. 33*d6050574SRomain Jobredeaux 34*d6050574SRomain Jobredeaux ### Env objects 35*d6050574SRomain Jobredeaux 36*d6050574SRomain Jobredeaux The `env` object basically provides a way to interact with things outside 37*d6050574SRomain Jobredeaux of the truth assertions framework. This allows easier testing of the 38*d6050574SRomain Jobredeaux framework itself and decouples it from a particular test framework (which 39*d6050574SRomain Jobredeaux makes it usable by by rules_testing's analysis_test and skylib's 40*d6050574SRomain Jobredeaux analysistest) 41*d6050574SRomain Jobredeaux 42*d6050574SRomain Jobredeaux The `env` object requires the following attribute: 43*d6050574SRomain Jobredeaux * ctx: The test's ctx. 44*d6050574SRomain Jobredeaux 45*d6050574SRomain Jobredeaux The `env` object allows the following attributes to customize behavior: 46*d6050574SRomain Jobredeaux * fail: A callable that accepts a single string, which is the failure 47*d6050574SRomain Jobredeaux message. Its return value is ignored. This is called when an assertion 48*d6050574SRomain Jobredeaux fails. It's generally expected that it records a failure instead of 49*d6050574SRomain Jobredeaux immediately failing. 50*d6050574SRomain Jobredeaux * has_provider: (callable) it accepts two positional args, target and 51*d6050574SRomain Jobredeaux provider and returns [`bool`]. This is used to implement `Provider in 52*d6050574SRomain Jobredeaux target` operations. 53*d6050574SRomain Jobredeaux * get_provider: (callable) it accepts two positional args, target and 54*d6050574SRomain Jobredeaux provider and returns the provider value. This is used to implement 55*d6050574SRomain Jobredeaux `target[Provider]`. 56*d6050574SRomain Jobredeaux 57*d6050574SRomain Jobredeaux Args: 58*d6050574SRomain Jobredeaux env: unittest env struct or some approximation. 59*d6050574SRomain Jobredeaux exprs: ([`list`] of [`str`]) the expression strings of the call chain for 60*d6050574SRomain Jobredeaux the subject. 61*d6050574SRomain Jobredeaux details: ([`list`] of [`str`]) additional details to print on error. These 62*d6050574SRomain Jobredeaux are usually informative details of the objects under test. 63*d6050574SRomain Jobredeaux format_str_kwargs: optional dict of format() kwargs. These kwargs 64*d6050574SRomain Jobredeaux are propagated through `derive()` calls and used when 65*d6050574SRomain Jobredeaux `ExpectMeta.format_str()` is called. 66*d6050574SRomain Jobredeaux 67*d6050574SRomain Jobredeaux Returns: 68*d6050574SRomain Jobredeaux [`ExpectMeta`] object. 69*d6050574SRomain Jobredeaux """ 70*d6050574SRomain Jobredeaux if format_str_kwargs == None: 71*d6050574SRomain Jobredeaux format_str_kwargs = {} 72*d6050574SRomain Jobredeaux format_str_kwargs.setdefault("workspace", env.ctx.workspace_name) 73*d6050574SRomain Jobredeaux format_str_kwargs.setdefault("test_name", env.ctx.label.name) 74*d6050574SRomain Jobredeaux 75*d6050574SRomain Jobredeaux # buildifier: disable=uninitialized 76*d6050574SRomain Jobredeaux self = struct( 77*d6050574SRomain Jobredeaux ctx = env.ctx, 78*d6050574SRomain Jobredeaux env = env, 79*d6050574SRomain Jobredeaux add_failure = lambda *a, **k: _expect_meta_add_failure(self, *a, **k), 80*d6050574SRomain Jobredeaux current_expr = lambda *a, **k: _expect_meta_current_expr(self, *a, **k), 81*d6050574SRomain Jobredeaux derive = lambda *a, **k: _expect_meta_derive(self, *a, **k), 82*d6050574SRomain Jobredeaux format_str = lambda *a, **k: _expect_meta_format_str(self, *a, **k), 83*d6050574SRomain Jobredeaux get_provider = lambda *a, **k: _expect_meta_get_provider(self, *a, **k), 84*d6050574SRomain Jobredeaux has_provider = lambda *a, **k: _expect_meta_has_provider(self, *a, **k), 85*d6050574SRomain Jobredeaux _exprs = exprs, 86*d6050574SRomain Jobredeaux _details = details, 87*d6050574SRomain Jobredeaux _format_str_kwargs = format_str_kwargs, 88*d6050574SRomain Jobredeaux ) 89*d6050574SRomain Jobredeaux return self 90*d6050574SRomain Jobredeaux 91*d6050574SRomain Jobredeauxdef _expect_meta_derive(self, expr = None, details = None, format_str_kwargs = {}): 92*d6050574SRomain Jobredeaux """Create a derivation of the current meta object for a child-Subject. 93*d6050574SRomain Jobredeaux 94*d6050574SRomain Jobredeaux Method: ExpectMeta.derive 95*d6050574SRomain Jobredeaux 96*d6050574SRomain Jobredeaux When a Subject needs to create a child-Subject, it derives a new meta 97*d6050574SRomain Jobredeaux object to pass to the child. This separates the parent's state from 98*d6050574SRomain Jobredeaux the child's state and allows any failures generated by the child to 99*d6050574SRomain Jobredeaux include the context of the parent creator. 100*d6050574SRomain Jobredeaux 101*d6050574SRomain Jobredeaux Example usage: 102*d6050574SRomain Jobredeaux 103*d6050574SRomain Jobredeaux def _foo_subject_action_named(self, name): 104*d6050574SRomain Jobredeaux meta = self.meta.derive("action_named({})".format(name), 105*d6050574SRomain Jobredeaux "action: {}".format(...)) 106*d6050574SRomain Jobredeaux return ActionSubject(..., meta) 107*d6050574SRomain Jobredeaux def _foo_subject_name(self): 108*d6050574SRomain Jobredeaux # No extra detail to include) 109*d6050574SRomain Jobredeaux meta self.meta.derive("name()", None) 110*d6050574SRomain Jobredeaux 111*d6050574SRomain Jobredeaux 112*d6050574SRomain Jobredeaux Args: 113*d6050574SRomain Jobredeaux self: implicitly added. 114*d6050574SRomain Jobredeaux expr: ([`str`]) human-friendly description of the call chain expression. 115*d6050574SRomain Jobredeaux e.g., if `foo_subject.bar_named("baz")` returns a child-subject, 116*d6050574SRomain Jobredeaux then "bar_named("bar")" would be the expression. 117*d6050574SRomain Jobredeaux details: (optional [`list`] of [`str`]) human-friendly descriptions of additional 118*d6050574SRomain Jobredeaux detail to include in errors. This is usually additional information 119*d6050574SRomain Jobredeaux the child Subject wouldn't include itself. e.g. if 120*d6050574SRomain Jobredeaux `foo.first_action_argv().contains(1)`, returned a ListSubject, then 121*d6050574SRomain Jobredeaux including "first action: Action FooCompile" helps add context to the 122*d6050574SRomain Jobredeaux error message. If there is no additional detail to include, pass 123*d6050574SRomain Jobredeaux None. 124*d6050574SRomain Jobredeaux format_str_kwargs: ([`dict`] of format()-kwargs) additional kwargs to 125*d6050574SRomain Jobredeaux make available to [`format_str`] calls. 126*d6050574SRomain Jobredeaux 127*d6050574SRomain Jobredeaux Returns: 128*d6050574SRomain Jobredeaux [`ExpectMeta`] object. 129*d6050574SRomain Jobredeaux """ 130*d6050574SRomain Jobredeaux if not details: 131*d6050574SRomain Jobredeaux details = [] 132*d6050574SRomain Jobredeaux if expr: 133*d6050574SRomain Jobredeaux exprs = [expr] 134*d6050574SRomain Jobredeaux else: 135*d6050574SRomain Jobredeaux exprs = [] 136*d6050574SRomain Jobredeaux 137*d6050574SRomain Jobredeaux if format_str_kwargs: 138*d6050574SRomain Jobredeaux final_format_kwargs = {k: v for k, v in self._format_str_kwargs.items()} 139*d6050574SRomain Jobredeaux final_format_kwargs.update(format_str_kwargs) 140*d6050574SRomain Jobredeaux else: 141*d6050574SRomain Jobredeaux final_format_kwargs = self._format_str_kwargs 142*d6050574SRomain Jobredeaux 143*d6050574SRomain Jobredeaux return _expect_meta_new( 144*d6050574SRomain Jobredeaux env = self.env, 145*d6050574SRomain Jobredeaux exprs = self._exprs + exprs, 146*d6050574SRomain Jobredeaux details = self._details + details, 147*d6050574SRomain Jobredeaux format_str_kwargs = final_format_kwargs, 148*d6050574SRomain Jobredeaux ) 149*d6050574SRomain Jobredeaux 150*d6050574SRomain Jobredeauxdef _expect_meta_format_str(self, template): 151*d6050574SRomain Jobredeaux """Interpolate contextual keywords into a string. 152*d6050574SRomain Jobredeaux 153*d6050574SRomain Jobredeaux This uses the normal `format()` style (i.e. using `{}`). Keywords 154*d6050574SRomain Jobredeaux refer to parts of the call chain. 155*d6050574SRomain Jobredeaux 156*d6050574SRomain Jobredeaux The particular keywords supported depend on the call chain. The following 157*d6050574SRomain Jobredeaux are always present: 158*d6050574SRomain Jobredeaux {workspace}: The name of the workspace, e.g. "rules_proto". 159*d6050574SRomain Jobredeaux {test_name}: The base name of the current test. 160*d6050574SRomain Jobredeaux 161*d6050574SRomain Jobredeaux Args: 162*d6050574SRomain Jobredeaux self: implicitly added. 163*d6050574SRomain Jobredeaux template: ([`str`]) the format template string to use. 164*d6050574SRomain Jobredeaux 165*d6050574SRomain Jobredeaux Returns: 166*d6050574SRomain Jobredeaux [`str`]; the template with parameters replaced. 167*d6050574SRomain Jobredeaux """ 168*d6050574SRomain Jobredeaux return template.format(**self._format_str_kwargs) 169*d6050574SRomain Jobredeaux 170*d6050574SRomain Jobredeauxdef _expect_meta_get_provider(self, target, provider): 171*d6050574SRomain Jobredeaux """Get a provider from a target. 172*d6050574SRomain Jobredeaux 173*d6050574SRomain Jobredeaux This is equivalent to `target[provider]`; the extra level of indirection 174*d6050574SRomain Jobredeaux is to aid testing. 175*d6050574SRomain Jobredeaux 176*d6050574SRomain Jobredeaux Args: 177*d6050574SRomain Jobredeaux self: implicitly added. 178*d6050574SRomain Jobredeaux target: ([`Target`]) the target to get the provider from. 179*d6050574SRomain Jobredeaux provider: The provider type to get. 180*d6050574SRomain Jobredeaux 181*d6050574SRomain Jobredeaux Returns: 182*d6050574SRomain Jobredeaux The found provider, or fails if not present. 183*d6050574SRomain Jobredeaux """ 184*d6050574SRomain Jobredeaux if hasattr(self.env, "get_provider"): 185*d6050574SRomain Jobredeaux return self.env.get_provider(target, provider) 186*d6050574SRomain Jobredeaux else: 187*d6050574SRomain Jobredeaux return target[provider] 188*d6050574SRomain Jobredeaux 189*d6050574SRomain Jobredeauxdef _expect_meta_has_provider(self, target, provider): 190*d6050574SRomain Jobredeaux """Tells if a target has a provider. 191*d6050574SRomain Jobredeaux 192*d6050574SRomain Jobredeaux This is equivalent to `provider in target`; the extra level of indirection 193*d6050574SRomain Jobredeaux is to aid testing. 194*d6050574SRomain Jobredeaux 195*d6050574SRomain Jobredeaux Args: 196*d6050574SRomain Jobredeaux self: implicitly added. 197*d6050574SRomain Jobredeaux target: ([`Target`]) the target to check for the provider. 198*d6050574SRomain Jobredeaux provider: the provider type to check for. 199*d6050574SRomain Jobredeaux 200*d6050574SRomain Jobredeaux Returns: 201*d6050574SRomain Jobredeaux True if the target has the provider, False if not. 202*d6050574SRomain Jobredeaux """ 203*d6050574SRomain Jobredeaux if hasattr(self.env, "has_provider"): 204*d6050574SRomain Jobredeaux return self.env.has_provider(target, provider) 205*d6050574SRomain Jobredeaux else: 206*d6050574SRomain Jobredeaux return provider in target 207*d6050574SRomain Jobredeaux 208*d6050574SRomain Jobredeauxdef _expect_meta_add_failure(self, problem, actual): 209*d6050574SRomain Jobredeaux """Adds a failure with context. 210*d6050574SRomain Jobredeaux 211*d6050574SRomain Jobredeaux Method: ExpectMeta.add_failure 212*d6050574SRomain Jobredeaux 213*d6050574SRomain Jobredeaux Adds the given error message. Context from the subject and prior call chains 214*d6050574SRomain Jobredeaux is automatically added. 215*d6050574SRomain Jobredeaux 216*d6050574SRomain Jobredeaux Args: 217*d6050574SRomain Jobredeaux self: implicitly added. 218*d6050574SRomain Jobredeaux problem: ([`str`]) a string describing the expected value or problem 219*d6050574SRomain Jobredeaux detected, and the expected values that weren't satisfied. A colon 220*d6050574SRomain Jobredeaux should be used to separate the description from the values. 221*d6050574SRomain Jobredeaux The description should be brief and include the word "expected", 222*d6050574SRomain Jobredeaux e.g. "expected: foo", or "expected values missing: <list of missing>", 223*d6050574SRomain Jobredeaux the key point being the reader can easily take the values shown 224*d6050574SRomain Jobredeaux and look for it in the actual values displayed below it. 225*d6050574SRomain Jobredeaux actual: ([`str`]) a string describing the values observed. A colon should 226*d6050574SRomain Jobredeaux be used to separate the description from the observed values. 227*d6050574SRomain Jobredeaux The description should be brief and include the word "actual", e.g., 228*d6050574SRomain Jobredeaux "actual: bar". The values should include the actual, observed, 229*d6050574SRomain Jobredeaux values and pertinent information about them. 230*d6050574SRomain Jobredeaux """ 231*d6050574SRomain Jobredeaux details = "\n".join([ 232*d6050574SRomain Jobredeaux " {}".format(detail) 233*d6050574SRomain Jobredeaux for detail in self._details 234*d6050574SRomain Jobredeaux if detail 235*d6050574SRomain Jobredeaux ]) 236*d6050574SRomain Jobredeaux if details: 237*d6050574SRomain Jobredeaux details = "where... (most recent context last)\n" + details 238*d6050574SRomain Jobredeaux msg = """\ 239*d6050574SRomain Jobredeauxin test: {test} 240*d6050574SRomain Jobredeauxvalue of: {expr} 241*d6050574SRomain Jobredeaux{problem} 242*d6050574SRomain Jobredeaux{actual} 243*d6050574SRomain Jobredeaux{details} 244*d6050574SRomain Jobredeaux""".format( 245*d6050574SRomain Jobredeaux test = self.ctx.label, 246*d6050574SRomain Jobredeaux expr = _expect_meta_current_expr(self), 247*d6050574SRomain Jobredeaux problem = problem, 248*d6050574SRomain Jobredeaux actual = actual, 249*d6050574SRomain Jobredeaux details = details, 250*d6050574SRomain Jobredeaux ) 251*d6050574SRomain Jobredeaux _expect_meta_call_fail(self, msg) 252*d6050574SRomain Jobredeaux 253*d6050574SRomain Jobredeauxdef _expect_meta_current_expr(self): 254*d6050574SRomain Jobredeaux """Get a string representing the current expression. 255*d6050574SRomain Jobredeaux 256*d6050574SRomain Jobredeaux Args: 257*d6050574SRomain Jobredeaux self: implicitly added. 258*d6050574SRomain Jobredeaux 259*d6050574SRomain Jobredeaux Returns: 260*d6050574SRomain Jobredeaux [`str`] A string representing the current expression, e.g. 261*d6050574SRomain Jobredeaux "foo.bar(something).baz()" 262*d6050574SRomain Jobredeaux """ 263*d6050574SRomain Jobredeaux return ".".join(self._exprs) 264*d6050574SRomain Jobredeaux 265*d6050574SRomain Jobredeauxdef _expect_meta_call_fail(self, msg): 266*d6050574SRomain Jobredeaux """Adds a failure to the test run. 267*d6050574SRomain Jobredeaux 268*d6050574SRomain Jobredeaux Args: 269*d6050574SRomain Jobredeaux self: implicitly added. 270*d6050574SRomain Jobredeaux msg: ([`str`]) the failure message. 271*d6050574SRomain Jobredeaux """ 272*d6050574SRomain Jobredeaux fail_func = getattr(self.env, "fail", None) 273*d6050574SRomain Jobredeaux if fail_func != None: 274*d6050574SRomain Jobredeaux fail_func(msg) 275*d6050574SRomain Jobredeaux else: 276*d6050574SRomain Jobredeaux # Add a leading newline because unittest prepends the repr() of the 277*d6050574SRomain Jobredeaux # function under test, which is often long and uninformative, making 278*d6050574SRomain Jobredeaux # the first line of our message hard to see. 279*d6050574SRomain Jobredeaux ut_asserts.true(self.env, False, "\n" + msg) 280*d6050574SRomain Jobredeaux 281*d6050574SRomain Jobredeaux# We use this name so it shows up nice in docs. 282*d6050574SRomain Jobredeaux# buildifier: disable=name-conventions 283*d6050574SRomain JobredeauxExpectMeta = struct( 284*d6050574SRomain Jobredeaux new = _expect_meta_new, 285*d6050574SRomain Jobredeaux derive = _expect_meta_derive, 286*d6050574SRomain Jobredeaux format_str = _expect_meta_format_str, 287*d6050574SRomain Jobredeaux get_provider = _expect_meta_get_provider, 288*d6050574SRomain Jobredeaux has_provider = _expect_meta_has_provider, 289*d6050574SRomain Jobredeaux add_failure = _expect_meta_add_failure, 290*d6050574SRomain Jobredeaux call_fail = _expect_meta_call_fail, 291*d6050574SRomain Jobredeaux) 292