-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #61 from rickeylev:struct.subject
PiperOrigin-RevId: 546947647
- Loading branch information
Showing
11 changed files
with
305 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
# Copyright 2023 The Bazel Authors. All rights reserved. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
"""# StructSubject | ||
A subject for arbitrary structs. This is most useful when wrapping an ad-hoc | ||
struct (e.g. a struct specific to a particular function). Such ad-hoc structs | ||
are usually just plain data objects, so they don't need special functionality | ||
that writing a full custom subject allows. If a struct would benefit from | ||
custom accessors or asserts, write a custom subject instead. | ||
This subject is usually used as a helper to a more formally defined subject that | ||
knows the shape of the struct it needs to wrap. For example, a `FooInfoSubject` | ||
implementation might use it to handle `FooInfo.struct_with_a_couple_fields`. | ||
Note the resulting subject object is not a direct replacement for the struct | ||
being wrapped: | ||
* Structs wrapped by this subject have the attributes exposed as functions, | ||
not as plain attributes. This matches the other subject classes and defers | ||
converting an attribute to a subject unless necessary. | ||
* The attribute name `actual` is reserved. | ||
## Example usages | ||
To use it as part of a custom subject returning a sub-value, construct it using | ||
`subjects.struct()` like so: | ||
```starlark | ||
load("@rules_testing//lib:truth.bzl", "subjects") | ||
def _my_subject_foo(self): | ||
return subjects.struct( | ||
self.actual.foo, | ||
meta = self.meta.derive("foo()"), | ||
attrs = dict(a=subjects.int, b=subjects.str), | ||
) | ||
``` | ||
If you're checking a struct directly in a test, then you can use | ||
`Expect.that_struct`. You'll still have to pass the `attrs` arg so it knows how | ||
to map the attributes to the matching subject factories. | ||
```starlark | ||
def _foo_test(env): | ||
actual = env.expect.that_struct( | ||
struct(a=1, b="x"), | ||
attrs = dict(a=subjects.int, b=subjects.str) | ||
) | ||
actual.a().equals(1) | ||
actual.b().equals("x") | ||
``` | ||
""" | ||
|
||
def _struct_subject_new(actual, *, meta, attrs): | ||
"""Creates a `StructSubject`, which is a thin wrapper around a [`struct`]. | ||
Args: | ||
actual: ([`struct`]) the struct to wrap. | ||
meta: ([`ExpectMeta`]) object of call context information. | ||
attrs: ([`dict`] of [`str`] to [`callable`]) the functions to convert | ||
attributes to subjects. The keys are attribute names that must | ||
exist on `actual`. The values are functions with the signature | ||
`def factory(value, *, meta)`, where `value` is the actual attribute | ||
value of the struct, and `meta` is an [`ExpectMeta`] object. | ||
Returns: | ||
[`StructSubject`] object, which is a struct with the following shape: | ||
* `actual` attribute, the underlying struct that was wrapped. | ||
* A callable attribute for each `attrs` entry; it takes no args | ||
and returns what the corresponding factory from `attrs` returns. | ||
""" | ||
attr_accessors = {} | ||
for name, factory in attrs.items(): | ||
if not hasattr(actual, name): | ||
fail("Struct missing attribute: '{}' (from expression {})".format( | ||
name, | ||
meta.current_expr(), | ||
)) | ||
attr_accessors[name] = _make_attr_accessor(actual, name, factory, meta) | ||
|
||
public = struct(actual = actual, **attr_accessors) | ||
return public | ||
|
||
def _make_attr_accessor(actual, name, factory, meta): | ||
# A named function is used instead of a lambda so stack traces are easier to | ||
# grok. | ||
def attr_accessor(): | ||
return factory(getattr(actual, name), meta = meta.derive(name + "()")) | ||
|
||
return attr_accessor | ||
|
||
# buildifier: disable=name-conventions | ||
StructSubject = struct( | ||
# keep sorted start | ||
new = _struct_subject_new, | ||
# keep sorted end | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
load(":struct_subject_tests.bzl", "struct_subject_test_suite") | ||
|
||
struct_subject_test_suite(name = "struct_subject_tests") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# Copyright 2023 The Bazel Authors. All rights reserved. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
"""Tests for StructSubject""" | ||
|
||
load("//lib:truth.bzl", "subjects") | ||
load("//lib:test_suite.bzl", "test_suite") | ||
load("//tests:test_util.bzl", "test_util") | ||
|
||
_tests = [] | ||
|
||
def _struct_subject_test(env): | ||
fake_meta = test_util.fake_meta(env) | ||
actual = subjects.struct( | ||
struct(n = 1, x = "foo"), | ||
meta = fake_meta, | ||
attrs = dict( | ||
n = subjects.int, | ||
x = subjects.str, | ||
), | ||
) | ||
actual.n().equals(1) | ||
test_util.expect_no_failures(env, fake_meta, "struct.n()") | ||
|
||
actual.n().equals(99) | ||
test_util.expect_failures( | ||
env, | ||
fake_meta, | ||
"struct.n() failure", | ||
"expected: 99", | ||
) | ||
|
||
actual.x().equals("foo") | ||
test_util.expect_no_failures(env, fake_meta, "struct.foo()") | ||
|
||
actual.x().equals("not-foo") | ||
test_util.expect_failures(env, fake_meta, "struct.foo() failure", "expected: not-foo") | ||
|
||
_tests.append(_struct_subject_test) | ||
|
||
def struct_subject_test_suite(name): | ||
test_suite(name = name, basic_tests = _tests) |
Oops, something went wrong.