Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docgen/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ sphinx_stardocs(
"//lib/private:run_environment_info_subject_bzl",
"//lib/private:runfiles_subject_bzl",
"//lib/private:str_subject_bzl",
"//lib/private:struct_subject_bzl",
"//lib/private:target_subject_bzl",
],
tags = ["docs"],
Expand Down
2 changes: 2 additions & 0 deletions docs/crossrefs.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
[`Ordered`]: /api/ordered
[`RunfilesSubject`]: /api/runfiles_subject
[`str`]: https://bazel.build/rules/lib/string
[`struct`]: https://bazel.build/rules/lib/builtins/struct
[`StrSubject`]: /api/str_subject
[`StructSubject`]: /api/struct_subject
[`Target`]: https://bazel.build/rules/lib/Target
[`TargetSubject`]: /api/target_subject
[target-name]: https://bazel.build/concepts/labels#target-names
Expand Down
1 change: 1 addition & 0 deletions lib/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ bzl_library(
"//lib/private:int_subject_bzl",
"//lib/private:label_subject_bzl",
"//lib/private:matching_bzl",
"//lib/private:struct_subject_bzl",
],
)

Expand Down
6 changes: 6 additions & 0 deletions lib/private/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ bzl_library(
],
)

bzl_library(
name = "struct_subject_bzl",
srcs = ["struct_subject.bzl"],
)

bzl_library(
name = "target_subject_bzl",
srcs = ["target_subject.bzl"],
Expand Down Expand Up @@ -247,6 +252,7 @@ bzl_library(
":file_subject_bzl",
":int_subject_bzl",
":str_subject_bzl",
":struct_subject_bzl",
":target_subject_bzl",
],
)
Expand Down
17 changes: 17 additions & 0 deletions lib/private/expect.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ load(":expect_meta.bzl", "ExpectMeta")
load(":file_subject.bzl", "FileSubject")
load(":int_subject.bzl", "IntSubject")
load(":str_subject.bzl", "StrSubject")
load(":struct_subject.bzl", "StructSubject")
load(":target_subject.bzl", "TargetSubject")

def _expect_new_from_env(env):
Expand Down Expand Up @@ -78,6 +79,7 @@ def _expect_new(env, meta):
that_file = lambda *a, **k: _expect_that_file(self, *a, **k),
that_int = lambda *a, **k: _expect_that_int(self, *a, **k),
that_str = lambda *a, **k: _expect_that_str(self, *a, **k),
that_struct = lambda *a, **k: _expect_that_struct(self, *a, **k),
that_target = lambda *a, **k: _expect_that_target(self, *a, **k),
where = lambda *a, **k: _expect_where(self, *a, **k),
# keep sorted end
Expand Down Expand Up @@ -207,6 +209,18 @@ def _expect_that_str(self, value):
"""
return StrSubject.new(value, self.meta.derive("string"))

def _expect_that_struct(self, value):
"""Creates a subject for asserting a `struct`.

Args:
self: implicitly added.
value: ([`struct`]) the value to check against.

Returns:
[`StructSubject`] object.
"""
return StructSubject.new(value, self.meta.derive("string"))

def _expect_that_target(self, target):
"""Creates a subject for asserting a `Target`.

Expand Down Expand Up @@ -257,6 +271,7 @@ def _expect_where(self, **details):
# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
Expect = struct(
# keep sorted start
new_from_env = _expect_new_from_env,
new = _expect_new,
that_action = _expect_that_action,
Expand All @@ -267,6 +282,8 @@ Expect = struct(
that_file = _expect_that_file,
that_int = _expect_that_int,
that_str = _expect_that_str,
that_struct = _expect_that_struct,
that_target = _expect_that_target,
where = _expect_where,
# keep sorted end
)
108 changes: 108 additions & 0 deletions lib/private/struct_subject.bzl
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().quals("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
)
16 changes: 16 additions & 0 deletions lib/private/truth_common.bzl
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# 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.

"""Common code used by truth."""

load("@bazel_skylib//lib:types.bzl", "types")
Expand All @@ -16,6 +30,8 @@ def _informative_str(value):
value_str = str(value)
if not value_str:
return "<empty string ∅>"
elif "\n" in value_str:
return '"""{}""" <sans triple-quotes; note newlines and whitespace>'.format(value_str)
elif value_str != value_str.strip():
return '"{}" <sans quotes; note whitespace within>'.format(value_str)
else:
Expand Down
2 changes: 2 additions & 0 deletions lib/truth.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ load("//lib/private:runfiles_subject.bzl", "RunfilesSubject")
load("//lib/private:str_subject.bzl", "StrSubject")
load("//lib/private:target_subject.bzl", "TargetSubject")
load("//lib/private:matching.bzl", _matching = "matching")
load("//lib/private:struct_subject.bzl", "StructSubject")

# Rather than load many symbols, just load this symbol, and then all the
# asserts will be available.
Expand All @@ -75,6 +76,7 @@ subjects = struct(
label = LabelSubject.new,
runfiles = RunfilesSubject.new,
str = StrSubject.new,
struct = StructSubject.new,
target = TargetSubject.new,
# keep sorted end
)
3 changes: 3 additions & 0 deletions tests/struct_subject/BUILD.bazel
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")
53 changes: 53 additions & 0 deletions tests/struct_subject/struct_subject_tests.bzl
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)
Loading