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