Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
wip: use sphinxdocs for docgen
Browse files Browse the repository at this point in the history
rickeylev committed Oct 26, 2024
1 parent 8337731 commit a4f3e53
Showing 53 changed files with 1,199 additions and 976 deletions.
10 changes: 5 additions & 5 deletions .bazelci/presubmit.yml
Original file line number Diff line number Diff line change
@@ -12,13 +12,14 @@ tasks:
- "--enable_workspace"
test_targets:
- "..."
all_tests_workspace_5.x:
name: Workspace (Bazel 5.x)
all_tests_workspace_minimum_bazel:
name: Workspace (Bazel 6.x)
platform: ${{platform}}
skip_in_bazel_downstream_pipeline: Already tested on latest
bazel: "5.x"
bazel: "6.x"
test_flags:
- "--noexperimental_enable_bzlmod"
- "--noenable_bzlmod"
- "--test_tag_filters=-skip-bzlmod,-docs"
test_targets:
- "..."
all_tests_bzlmod:
@@ -38,7 +39,6 @@ tasks:
test_flags:
- "--enable_bzlmod"
test_targets:
- "//docgen/..."
- "//docs/..."

e2e_bzlmod:
5 changes: 0 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +0,0 @@
# Generated sphinx docs
docs/_build/
# Generated API docs
/docs/source/api/
!/docs/source/api/index.md
23 changes: 7 additions & 16 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@

version: 2

formats:
- pdf
- htmlzip

sphinx:
configuration: docs/source/conf.py

build:
os: "ubuntu-22.04"
tools:
python: "3.11"
nodejs: "19"
jobs:
pre_build:
- npm install -g @bazel/bazelisk
- bazel run //docs:run_sphinx_build

python:
install:
- requirements: docs/requirements.txt
commands:
- env
- npm install -g @bazel/bazelisk
- bazel version
# Put the actual action behind a shell script because it's
# easier to modify than the yaml config.
- docs/readthedocs_build.sh
7 changes: 6 additions & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ module(
compatibility_level = 1,
)

bazel_dep(name = "rules_python", version = "0.37.0")
bazel_dep(name = "platforms", version = "0.0.6")
bazel_dep(name = "bazel_skylib", version = "1.3.0")
bazel_dep(name = "rules_license", version = "0.0.4")
@@ -18,7 +19,6 @@ bazel_dep(
dev_dependency = True,
repo_name = "io_bazel_stardoc",
)
bazel_dep(name = "rules_python", version = "0.27.0", dev_dependency = True)

python = use_extension(
"@rules_python//python/extensions:python.bzl",
@@ -40,3 +40,8 @@ pip.parse(
requirements_lock = "//docs:requirements.txt",
)
use_repo(pip, "docs-pypi")

##local_path_override(
## module_name = "rules_python",
## path = "../rules_python",
##)
161 changes: 161 additions & 0 deletions MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 11 additions & 9 deletions WORKSPACE.bazel
Original file line number Diff line number Diff line change
@@ -31,6 +31,17 @@ http_archive(
],
)

http_archive(
name = "rules_python",
sha256 = "0cc05ddb27614baecace068986931e2a6e9f69114e6115fc5dc58250faf56e0f",
strip_prefix = "rules_python-0.37.0",
url = "https://github.com/bazelbuild/rules_python/releases/download/0.37.0/rules_python-0.37.0.tar.gz",
)

load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains")

py_repositories()

load("@io_bazel_stardoc//:setup.bzl", "stardoc_repositories")

stardoc_repositories()
@@ -52,15 +63,6 @@ http_archive(
],
)

http_archive(
name = "rules_python",
sha256 = "a644da969b6824cc87f8fe7b18101a8a6c57da5db39caa6566ec6109f37d2141",
strip_prefix = "rules_python-0.20.0",
url = "https://github.com/bazelbuild/rules_python/releases/download/0.20.0/rules_python-0.20.0.tar.gz",
)

load("@rules_python//python:repositories.bzl", "python_register_toolchains")

python_register_toolchains(
name = "python3_11",
# Available versions are listed in @rules_python//python:versions.bzl.
51 changes: 0 additions & 51 deletions docgen/BUILD

This file was deleted.

65 changes: 0 additions & 65 deletions docgen/docgen.bzl

This file was deleted.

56 changes: 0 additions & 56 deletions docgen/func_template.vm

This file was deleted.

1 change: 0 additions & 1 deletion docgen/header_template.vm

This file was deleted.

29 changes: 0 additions & 29 deletions docgen/provider_template.vm

This file was deleted.

48 changes: 0 additions & 48 deletions docgen/rule_template.vm

This file was deleted.

103 changes: 66 additions & 37 deletions docs/BUILD
Original file line number Diff line number Diff line change
@@ -12,63 +12,92 @@
# See the License for the specific language governing permissions and
# limitations under the License.

load("@docs-pypi//:requirements.bzl", "requirement")
load("@rules_python//python:pip.bzl", "compile_pip_requirements")
load("@rules_python//python:py_binary.bzl", "py_binary")
load("@rules_python//sphinxdocs:readthedocs.bzl", "readthedocs_install")
load("@rules_python//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs")
load("@rules_python//sphinxdocs:sphinx_stardoc.bzl", "sphinx_stardocs")

package(
default_applicable_licenses = ["//:package_license"],
)

sh_binary(
name = "run_sphinx_build",
srcs = ["run_sphinx_build.sh"],
args = [
"$(rootpath :sphinx_build)",
"$(rootpath :crossrefs.md)",
"$(rootpaths //docgen:docs)",
sphinx_docs(
name = "docs",
srcs = glob(
include = [
"*.md",
"**/*.md",
],
exclude = ["README.md"],
),
config = "conf.py",
formats = ["html"],
renamed_srcs = {
"@rules_python//sphinxdocs/inventories:bazel_inventory": "bazel_inventory.inv",
},
sphinx = ":sphinx-build",
strip_prefix = package_name() + "/",
target_compatible_with = ["@platforms//os:linux"],
deps = [
":bzl_docs",
],
data = [
"crossrefs.md",
":sphinx_build",
":sphinx_sources",
"//docgen:docs",
)

sphinx_stardocs(
name = "bzl_docs",
srcs = [
"//lib:analysis_test_bzl",
"//lib:truth_bzl",
"//lib:util_bzl",
"//lib/private:action_subject_bzl",
"//lib/private:bool_subject_bzl",
"//lib/private:collection_subject_bzl",
"//lib/private:default_info_subject_bzl",
"//lib/private:depset_file_subject_bzl",
"//lib/private:dict_subject_bzl",
"//lib/private:execution_info_subject_bzl",
"//lib/private:expect_bzl",
"//lib/private:expect_meta_bzl",
"//lib/private:file_subject_bzl",
"//lib/private:instrumented_files_info_subject_bzl",
"//lib/private:int_subject_bzl",
"//lib/private:label_subject_bzl",
"//lib/private:ordered_bzl",
"//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",
],
prefix = "api/",
target_compatible_with = ["@platforms//os:linux"],
)

py_binary(
name = "sphinx_build",
srcs = ["sphinx_build.py"],
sphinx_build_binary(
name = "sphinx-build",
deps = [
requirement("sphinx"),
requirement("sphinx_rtd_theme"),
requirement("myst_parser"),
"@docs-pypi//myst_parser",
"@docs-pypi//readthedocs_sphinx_ext",
"@docs-pypi//sphinx",
"@docs-pypi//sphinx_rtd_theme",
"@docs-pypi//typing_extensions",
"@rules_python//sphinxdocs/src/sphinx_bzl",
],
)

readthedocs_install(
name = "readthedocs_install",
docs = [":docs"],
target_compatible_with = ["@platforms//os:linux"],
)

# Run bazel run //docs:requirements.update
compile_pip_requirements(
name = "requirements",
extra_args = ["--upgrade"],
requirements_in = "requirements.in",
requirements_txt = "requirements.txt",
# The requirements output differs on Windows, so just restrict it to Linux.
# The build process is only run on, and only works for, Linux anyways.
target_compatible_with = ["@platforms//os:linux"],
)

filegroup(
name = "sphinx_sources",
srcs = [
# This isn't generated like the other files under the api directory,
# but it can't go in the glob because the exclude param will ignore it.
"source/api/index.md",
] + glob(
[
"**",
],
exclude = [
"source/api/**", # These are all generated files
"_build/**",
],
),
)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 3 additions & 1 deletion docs/source/conf.py → docs/conf.py
Original file line number Diff line number Diff line change
@@ -25,16 +25,18 @@
'sphinx.ext.autosectionlabel',
'myst_parser',
'sphinx_rtd_theme', # Necessary to get jquery to make flyout work
'sphinx_bzl.bzl',
]

intersphinx_mapping = {
"bazel": ("https://bazel.build/", "bazel_inventory.inv"),
}

intersphinx_disabled_domains = ['std']

# Prevent local refs from inadvertently linking elsewhere, per
# https://docs.readthedocs.io/en/stable/guides/intersphinx.html#using-intersphinx
intersphinx_disabled_reftypes = ["*"]
##intersphinx_disabled_reftypes = ["*"]

templates_path = ['_templates']

28 changes: 0 additions & 28 deletions docs/crossrefs.md

This file was deleted.

File renamed without changes.
File renamed without changes.
20 changes: 20 additions & 0 deletions docs/readthedocs_build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash

set -eou pipefail

declare -a extra_env
while IFS='=' read -r -d '' name value; do
if [[ "$name" == READTHEDOCS* ]]; then
extra_env+=("--@rules_python//sphinxdocs:extra_env=$name=$value")
fi
done < <(env -0)

# In order to get the build number, we extract it from the host name
extra_env+=("--@rules_python//sphinxdocs:extra_env=HOSTNAME=$HOSTNAME")

set -x
bazel run \
--stamp \
"--@rules_python//sphinxdocs:extra_defines=version=$READTHEDOCS_VERSION" \
"${extra_env[@]}" \
//docs:readthedocs_install
2 changes: 2 additions & 0 deletions docs/requirements.in
Original file line number Diff line number Diff line change
@@ -3,3 +3,5 @@
sphinx
myst-parser
sphinx_rtd_theme
typing-extensions
readthedocs-sphinx-ext
527 changes: 297 additions & 230 deletions docs/requirements.txt

Large diffs are not rendered by default.

48 changes: 0 additions & 48 deletions docs/run_sphinx_build.sh

This file was deleted.

4 changes: 0 additions & 4 deletions docs/sphinx_build.py

This file was deleted.

File renamed without changes.
File renamed without changes.
File renamed without changes.
7 changes: 2 additions & 5 deletions lib/analysis_test.bzl
Original file line number Diff line number Diff line change
@@ -12,18 +12,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""# Analysis test
Support for testing analysis phase logic, such as rules.
"""
"""Support for testing analysis phase logic, such as rules."""

load("//lib:test_suite.bzl", _test_suite = "test_suite")
load("//lib/private:analysis_test.bzl", _analysis_test = "analysis_test")

analysis_test = _analysis_test

def test_suite(**kwargs):
"""This is an alias to lib/test_suite.bzl#test_suite.
"""This is an alias to {obj}`//lib:test_suite.bzl%test_suite`
Args:
**kwargs: Args passed through to test_suite
57 changes: 37 additions & 20 deletions lib/private/action_subject.bzl
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""# ActionSubject"""
""

load(":collection_subject.bzl", "CollectionSubject")
load(":depset_file_subject.bzl", "DepsetFileSubject")
@@ -36,11 +36,11 @@ def _action_subject_new(action, meta):
expect(env).that_action(action).not_contains_arg("foo")
Args:
action: ([`Action`]) value to check against.
meta: ([`ExpectMeta`]) of call chain information.
action: {type}`Action` value to check against.
meta: {type}`ExpectMeta` of call chain information.
Returns:
[`ActionSubject`] object.
{type}`ActionSubject` object.
"""

# buildifier: disable=uninitialized
@@ -98,7 +98,7 @@ def _action_subject_argv(self):
Method: ActionSubject.argv
Returns:
[`CollectionSubject`] object.
{type}`CollectionSubject` object.
"""
meta = self.meta.derive("argv()")
return CollectionSubject.new(
@@ -118,11 +118,11 @@ def _action_subject_contains_at_least_args(self, args):
Args:
self: implicitly added.
args: ([`list`] of [`str`]) all the args must be in the argv exactly
args: {type}`list[str]` all the args must be in the argv exactly
as provided. Multiplicity is respected.
Returns:
[`Ordered`] (see `_ordered_incorrectly_new`).
{type}`Ordered` (see `_ordered_incorrectly_new`).
"""
return CollectionSubject.new(
self.action.argv,
@@ -140,7 +140,7 @@ def _action_subject_not_contains_arg(self, arg):
Args:
self: implicitly added.
arg: ([`str`]) the arg that cannot be present in the argv.
arg: {type}`str` the arg that cannot be present in the argv.
"""
if arg in self.action.argv:
problem, actual = format_failure_unexpected_value(
@@ -160,7 +160,7 @@ def _action_subject_substitutions(self):
self: implicitly added
Returns:
`DictSubject` struct.
{type}`DictSubject` struct.
"""
return DictSubject.new(
actual = self.action.substitutions,
@@ -181,13 +181,13 @@ def _action_subject_has_flags_specified(self, flags):
Args:
self: implicitly added.
flags: ([`list`] of [`str`]) The flags to check for. Include the leading "--".
flags: {type}`list[str]` The flags to check for. Include the leading "--".
Multiplicity is respected. A flag is considered present if any of
these forms are detected: `--flag=value`, `--flag value`, or a lone
`--flag`.
Returns:
[`Ordered`] (see `_ordered_incorrectly_new`).
{type}`Ordered` (see `_ordered_incorrectly_new`).
"""
return CollectionSubject.new(
# Starlark dict keys maintain insertion order, so it's OK to
@@ -205,7 +205,7 @@ def _action_subject_mnemonic(self):
Method: ActionSubject.mnemonic
Returns:
[`StrSubject`] object.
{type}`StrSubject` object.
"""
return StrSubject.new(
self.action.mnemonic,
@@ -218,7 +218,7 @@ def _action_subject_inputs(self):
Method: ActionSubject.inputs
Returns:
`DepsetFileSubject` of the action's inputs.
{type}`DepsetFileSubject` of the action's inputs.
"""
meta = self.meta.derive("inputs()")
return DepsetFileSubject.new(self.action.inputs, meta)
@@ -247,9 +247,10 @@ def _action_subject_contains_flag_values(self, flag_values):
Args:
self: implicitly added.
flag_values: ([`list`] of ([`str`] name, [`str`]) tuples) Include the
leading "--" in the flag name. Order and duplicates aren't checked.
Flags without a value found use `None` as their value.
flag_values: {type}`list[tuple[str, str]]`, where the first tuple
element is the flag name, and the second is the flag value. Include
the leading "--" in the flag name. Order and duplicates aren't
checked. Flags without a value found use `None` as their value.
"""
missing = []
for flag, value in sorted(flag_values):
@@ -285,7 +286,8 @@ def _action_subject_contains_none_of_flag_values(self, flag_values):
Args:
self: implicitly added.
flag_values: ([`list`] of ([`str`] name, [`str`] value) tuples) Include
flag_values: {type}`list[tuple[str, str]]`, where the first tuple
element is the flag name, and the second is the flag value. Include
the leading "--" in the flag name. Order and duplicates aren't
checked.
"""
@@ -316,11 +318,11 @@ def _action_subject_contains_at_least_inputs(self, inputs):
Args:
self: implicitly added.
inputs: (collection of [`File`]) All must be present. Multiplicity
inputs: {type}`collection[File]` All must be present. Multiplicity
is respected.
Returns:
[`Ordered`] (see `_ordered_incorrectly_new`).
{type}`Ordered` (see `_ordered_incorrectly_new`).
"""
return DepsetFileSubject.new(
self.action.inputs,
@@ -335,7 +337,7 @@ def _action_subject_content(self):
Method: ActionSubject.content
Returns:
[`StrSubject`] object.
{type}`StrSubject` object.
"""
return StrSubject.new(
self.action.content,
@@ -357,9 +359,24 @@ def _action_subject_env(self):
key_plural_name = "envvars",
)

def _action_subject_typedef():
"""A wrapper around {obj}`Action` objects for testing.
These can be created using {obj}`subjects.action`, but more typically
are created through {obj}`Expect.that_action()` or
{obj}`TargetSubject.action_generating`.
:::{field} actual
:type: Action
The underlying action that is asserted against.
:::
"""

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
ActionSubject = struct(
TYPEDEF = _action_subject_typedef,
new = _action_subject_new,
parse_flags = _action_subject_parse_flags,
argv = _action_subject_argv,
2 changes: 1 addition & 1 deletion lib/private/analysis_test.bzl
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""# Analysis test
"""Analysis test
Support for testing analysis phase logic, such as rules.
"""
27 changes: 21 additions & 6 deletions lib/private/bool_subject.bzl
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""# BoolSubject"""
"""BoolSubject"""

load(":check_util.bzl", "check_not_equals", "common_subject_is_in")

@@ -22,15 +22,16 @@ def _bool_subject_new(value, meta):
Method: BoolSubject.new
Args:
value: ([`bool`]) the value to assert against.
meta: ([`ExpectMeta`]) the metadata about the call chain.
value: {type}`bool` the value to assert against.
meta: {type}`ExpectMeta` the metadata about the call chain.
Returns:
A [`BoolSubject`].
{type}`BoolSubject`
"""
self = struct(actual = value, meta = meta)
public = struct(
# keep sorted start
actual = value,
equals = lambda *a, **k: _bool_subject_equals(self, *a, **k),
is_in = lambda *a, **k: common_subject_is_in(self, *a, **k),
not_equals = lambda *a, **k: _bool_subject_not_equals(self, *a, **k),
@@ -45,7 +46,7 @@ def _bool_subject_equals(self, expected):
Args:
self: implicitly added.
expected: ([`bool`]) the expected value.
expected: {type}`bool` the expected value.
"""
if self.actual == expected:
return
@@ -61,17 +62,31 @@ def _bool_subject_not_equals(self, unexpected):
Args:
self: implicitly added.
unexpected: ([`bool`]) the value actual cannot equal.
unexpected: {type}`bool` the value actual cannot equal.
"""
return check_not_equals(
actual = self.actual,
unexpected = unexpected,
meta = self.meta,
)

def _bool_typedef():
"""A wrapper around {obj}`bool` objects for testing.
These can be created using {obj}`subjects.bool` or
{obj}`Expect.that_bool()`.
:::{field} actual
:type: bool | None
The underlying value that is asserted against.
:::
"""

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
BoolSubject = struct(
TYPEDEF = _bool_typedef,
new = _bool_subject_new,
equals = _bool_subject_equals,
not_equals = _bool_subject_not_equals,
71 changes: 41 additions & 30 deletions lib/private/collection_subject.bzl
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""# CollectionSubject"""
"""CollectionSubject"""

load(
":check_util.bzl",
@@ -54,18 +54,15 @@ def _collection_subject_new(
Method: CollectionSubject.new
Public Attributes:
* `actual`: The wrapped collection.
Args:
values: ([`collection`]) the values to assert against.
meta: ([`ExpectMeta`]) the metadata about the call chain.
container_name: ([`str`]) conceptual name of the container.
sortable: ([`bool`]) True if output should be sorted for display, False if not.
element_plural_name: ([`str`]) the plural word for the values in the container.
values: {type}`collection` the values to assert against.
meta: {type}`ExpectMeta` the metadata about the call chain.
container_name: {type}`str` conceptual name of the container.
sortable: {type}`bool` True if output should be sorted for display, False if not.
element_plural_name: {type}`str` the plural word for the values in the container.
Returns:
[`CollectionSubject`].
{type}`CollectionSubject`
"""

# buildifier: disable=uninitialized
@@ -104,7 +101,7 @@ def _collection_subject_has_size(self, expected):
Args:
self: implicitly added.
expected: ([`int`]) the expected size of the collection.
expected: {type}`int` the expected size of the collection.
"""
return IntSubject.new(
len(self.actual),
@@ -118,7 +115,7 @@ def _collection_subject_contains(self, expected):
Args:
self: implicitly added.
expected: ([`str`]) the value that must be present.
expected: {type}`str` the value that must be present.
"""
matcher = matching.equals_wrapper(expected)
return self.contains_predicate(matcher)
@@ -139,10 +136,10 @@ def _collection_subject_contains_exactly(self, expected):
Args:
self: implicitly added.
expected: ([`list`]) values that must exist.
expected: {type}`list` values that must exist.
Returns:
[`Ordered`] (see `_ordered_incorrectly_new`).
{type}`Ordered` (see `_ordered_incorrectly_new`).
"""
expected = to_list(expected)
return check_contains_exactly(
@@ -195,10 +192,10 @@ def _collection_subject_contains_exactly_predicates(self, expected):
Args:
self: implicitly added.
expected: ([`list`] of [`Matcher`]) that must match.
expected: {type}`list[Matcher]` that must match.
Returns:
[`Ordered`] (see `_ordered_incorrectly_new`).
{type}`Ordered` (see `_ordered_incorrectly_new`).
"""
expected = to_list(expected)
return check_contains_exactly_predicates(
@@ -232,7 +229,7 @@ def _collection_subject_contains_none_of(self, values):
Args:
self: implicitly added
values: ([`collection`]) values of which none of are allowed to exist.
values: {type}`collection` values of which none of are allowed to exist.
"""
check_contains_none_of(
collection = self.actual,
@@ -248,7 +245,7 @@ def _collection_subject_contains_predicate(self, matcher):
Args:
self: implicitly added.
matcher: ([`Matcher`]) (see `matchers` struct).
matcher: {type}`Matcher` (see `matchers` struct).
"""
check_contains_predicate(
self.actual,
@@ -274,10 +271,10 @@ def _collection_subject_contains_at_least(self, expect_contains):
Args:
self: implicitly added.
expect_contains: ([`list`]) values that must be in the collection.
expect_contains: {type}`list` values that must be in the collection.
Returns:
[`Ordered`] (see `_ordered_incorrectly_new`).
{type}`Ordered` (see `_ordered_incorrectly_new`).
"""
matchers = [
matching.equals_wrapper(expected)
@@ -297,10 +294,10 @@ def _collection_subject_contains_at_least_predicates(self, matchers):
Args:
self: implicitly added.
matchers: ([`list`] of [`Matcher`]) (see `matchers` struct).
matchers: {type}`list[Matcher]` (see `matchers` struct).
Returns:
[`Ordered`] (see `_ordered_incorrectly_new`).
{type}`Ordered` (see `_ordered_incorrectly_new`).
"""
ordered = check_contains_at_least_predicates(
self.actual,
@@ -335,7 +332,7 @@ def _collection_subject_not_contains_predicate(self, matcher):
Args:
self: implicitly added.
matcher: [`Matcher`] object (see `matchers` struct).
matcher: {type}`Matcher` object (see `matchers` struct).
"""
check_not_contains_predicate(
self.actual,
@@ -349,8 +346,8 @@ def _collection_subject_offset(self, offset, factory):
Args:
self: implicitly added.
offset: ([`int`]) the offset to fetch
factory: ([`callable`]). The factory function to use to create
offset: {type}`int` the offset to fetch
factory: {type}`callable`. The factory function to use to create
the subject for the offset's value. It must have the following
signature: `def factory(value, *, meta)`.
@@ -381,28 +378,28 @@ def _collection_subject_transform(
Args:
self: implicitly added.
desc: (optional [`str`]) a human-friendly description of the transform
desc: {type}`str|None` a human-friendly description of the transform
for use in error messages. Required when a description can't be
inferred from the other args. The description can be inferred if the
filter arg is a named function (non-lambda) or Matcher object.
map_each: (optional [`callable`]) function to transform an element in
map_each: {type}`callable|None` function to transform an element in
the collection. It takes one positional arg, the loop's
current iteration value, and its return value will be the element's
new value. If not specified, the values from the loop iteration are
returned unchanged.
loop: (optional [`callable`]) function to produce values from the
loop: {type}`callable | None` function to produce values from the
original collection and whose values are iterated over. It takes one
positional arg, which is the original collection. If not specified,
the original collection values are iterated over.
filter: (optional [`callable`]) function that decides what values are
filter: {type}`callable|None` function that decides what values are
passed onto `map_each` for inclusion in the final result. It takes
one positional arg, the value to match (which is the current
iteration value before `map_each` is applied), and returns a bool
(True if the value should be included in the result, False if it
should be skipped).
Returns:
[`CollectionSubject`] of the transformed values.
{type}`CollectionSubject` of the transformed values.
"""
if not desc:
if map_each or loop:
@@ -444,9 +441,23 @@ def _collection_subject_transform(
element_plural_name = self.element_plural_name,
)

def _collection_subject_typedef():
"""A wrapper around collection objects for testing.
These can be created using {obj}`subjects.collection` or
{obj}`Expect.that_collection()`.
:::{field} actual
:type: collection | None
The underlying value that is asserted against.
:::
"""

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
CollectionSubject = struct(
TYPEDEF = _collection_subject_typedef,
# keep sorted start
contains = _collection_subject_contains,
contains_at_least = _collection_subject_contains_at_least,
29 changes: 20 additions & 9 deletions lib/private/default_info_subject.bzl
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""# DefaultInfoSubject"""
"""DefaultInfoSubject"""

load(":depset_file_subject.bzl", "DepsetFileSubject")
load(":file_subject.bzl", "FileSubject")
@@ -22,11 +22,11 @@ def _default_info_subject_new(info, *, meta):
"""Creates a `DefaultInfoSubject`
Args:
info: ([`DefaultInfo`]) the DefaultInfo object to wrap.
meta: ([`ExpectMeta`]) call chain information.
info: {type}`DefaultInfo` the DefaultInfo object to wrap.
meta: {type}`ExpectMeta` call chain information.
Returns:
[`DefaultInfoSubject`] object.
{type}`DefaultInfoSubject` object.
"""
self = struct(actual = info, meta = meta)
public = struct(
@@ -48,7 +48,7 @@ def _default_info_subject_runfiles(self):
self: implicitly added.
Returns:
[`RunfilesSubject`] object
{type}`RunfilesSubject` object
"""
return RunfilesSubject.new(
self.actual.default_runfiles,
@@ -63,7 +63,7 @@ def _default_info_subject_data_runfiles(self):
self: implicitly added.
Returns:
[`RunfilesSubject`] object
{type}`RunfilesSubject` object
"""
return RunfilesSubject.new(
self.actual.data_runfiles,
@@ -78,7 +78,7 @@ def _default_info_subject_default_outputs(self):
self: implicitly added.
Returns:
[`DepsetFileSubject`] object.
{type}`DepsetFileSubject` object.
"""
return DepsetFileSubject.new(
self.actual.files,
@@ -92,7 +92,7 @@ def _default_info_subject_executable(self):
self: implicitly added.
Returns:
[`FileSubject`] object.
{type}`FileSubject` object.
"""
return FileSubject.new(
self.actual.files_to_run.executable,
@@ -106,16 +106,27 @@ def _default_info_subject_runfiles_manifest(self):
self: implicitly added.
Returns:
[`FileSubject`] object.
{type}`FileSubject` object.
"""
return FileSubject.new(
self.actual.files_to_run.runfiles_manifest,
meta = self.meta.derive("runfiles_manifest()"),
)

def _default_info_subject_typedef():
"""Subject for {obj}`DefaultInfo`
:::{field} actual
:type: DefaultInfo
The underlying object asserted against.
:::
"""

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
DefaultInfoSubject = struct(
TYPEDEF = _default_info_subject_typedef,
# keep sorted start
new = _default_info_subject_new,
runfiles = _default_info_subject_runfiles,
57 changes: 34 additions & 23 deletions lib/private/depset_file_subject.bzl
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""# DepsetFileSubject"""
"""DepsetFileSubject"""

load("//lib:util.bzl", "is_file")
load(
@@ -42,18 +42,19 @@ def _depset_file_subject_new(files, meta, container_name = "depset", element_plu
Method: DepsetFileSubject.new
Args:
files: ([`depset`] of [`File`]) the values to assert on.
meta: ([`ExpectMeta`]) of call chain information.
container_name: ([`str`]) conceptual name of the container.
element_plural_name: ([`str`]) the plural word for the values in the container.
files: {type}`depset[File]` the values to assert on.
meta: {type}`ExpectMeta` of call chain information.
container_name: {type}`str` conceptual name of the container.
element_plural_name: {type}`str` the plural word for the values in the container.
Returns:
[`DepsetFileSubject`] object.
{type}`DepsetFileSubject` object.
"""

# buildifier: disable=uninitialized
public = struct(
# keep sorted start
actual = files,
contains = lambda *a, **k: _depset_file_subject_contains(self, *a, **k),
contains_any_in = lambda *a, **k: _depset_file_subject_contains_any_in(self, *a, **k),
contains_at_least = lambda *a, **k: _depset_file_subject_contains_at_least(self, *a, **k),
@@ -81,9 +82,9 @@ def _depset_file_subject_contains(self, expected):
Args:
self: implicitly added
expected: ([`str`] | [`File`]) If a string path is provided, it is
expected: {type}`str | File` If a string path is provided, it is
compared to the short path of the files and are formatted using
[`ExpectMeta.format_str`] and its current contextual keywords. Note
{obj}`ExpectMeta.format_str()` and its current contextual keywords. Note
that, when using `File` objects, two files' configurations must be
the same for them to be considered equal.
"""
@@ -107,15 +108,15 @@ def _depset_file_subject_contains_at_least(self, expected):
Args:
self: implicitly added
expected: ([`collection`] of [`str`] | collection of [`File`]) multiplicity
expected: {type}`collection[str] | collection[File]` multiplicity
is respected. If string paths are provided, they are compared to the
short path of the files and are formatted using
`ExpectMeta.format_str` and its current contextual keywords. Note
{obj}`ExpectMeta.format_str()` and its current contextual keywords. Note
that, when using `File` objects, two files' configurations must be the
same for them to be considered equal.
Returns:
[`Ordered`] (see `_ordered_incorrectly_new`).
{type}`Ordered` (see `_ordered_incorrectly_new`).
"""
expected = to_list(expected)
if len(expected) < 1 or is_file(expected[0]):
@@ -138,11 +139,11 @@ def _depset_file_subject_contains_any_in(self, expected):
Args:
self: implicitly added.
expected: ([`collection`] of [`str`] paths | [`collection`] of [`File`])
at least one of the values must exist. Note that, when using `File`
objects, two files' configurations must be the same for them to be
considered equal. When string paths are provided, they are compared
to `File.short_path`.
expected: {type}`collection[str] | collection[File]` at least one of the
values must exist. Note that, when using `File` objects, two files'
configurations must be the same for them to be considered equal.
When string paths are provided, they are compared to
`File.short_path`.
"""
expected = to_list(expected)
if len(expected) < 1 or is_file(expected[0]):
@@ -175,11 +176,10 @@ def _depset_file_subject_contains_at_least_predicates(self, matchers):
Args:
self: implicitly added.
matchers: ([`list`] of [`Matcher`]) (see `matchers` struct) that
accept [`File`] objects.
matchers: {type}`list[Matcher]` that accept `File` objects.
Returns:
[`Ordered`] (see `_ordered_incorrectly_new`).
{type}`Ordered` (see `_ordered_incorrectly_new`).
"""
ordered = check_contains_at_least_predicates(
self.files,
@@ -205,7 +205,7 @@ def _depset_file_subject_contains_predicate(self, matcher):
Args:
self: implicitly added.
matcher: [`Matcher`] (see `matching` struct) that accepts `File` objects.
matcher: {type}`Matcher` (see `matching` struct) that accepts `File` objects.
"""
check_contains_predicate(
self.files,
@@ -225,7 +225,7 @@ def _depset_file_subject_contains_exactly(self, paths):
Args:
self: implicitly added.
paths: ([`collection`] of [`str`]) the paths that must exist. These are
paths: {type}`collection[str]` the paths that must exist. These are
compared to the `short_path` values of the files in the depset.
All the paths, and no more, must exist.
"""
@@ -260,7 +260,7 @@ def _depset_file_subject_not_contains(self, short_path):
Args:
self: implicitly added.
short_path: ([`str`]) the short path that should not be present.
short_path: {type}`str` the short path that should not be present.
"""
short_path = self.meta.format_str(short_path)
matcher = matching.custom(short_path, lambda f: f.short_path == short_path)
@@ -273,13 +273,24 @@ def _depset_file_subject_not_contains_predicate(self, matcher):
Args:
self: implicitly added.
matcher: ([`Matcher`]) that must match. It operates on [`File`] objects.
matcher: {type}`Matcher` that must match. It operates on {obj}`File` objects.
"""
check_not_contains_predicate(self.files, matcher, meta = self.meta)

def _depset_file_subject_typedef():
"""Subject for {obj}`depset` of {obj}`File`.
:::{field} actual
:type: depset[File]
The underlying object asserted against.
:::
"""

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
DepsetFileSubject = struct(
TYPEDEF = _depset_file_subject_typedef,
new = _depset_file_subject_new,
contains = _depset_file_subject_contains,
contains_at_least = _depset_file_subject_contains_at_least,
35 changes: 23 additions & 12 deletions lib/private/dict_subject.bzl
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""# DictSubject"""
"""DictSubject"""

load(":collection_subject.bzl", "CollectionSubject")
load(":compare_util.bzl", "compare_dicts")
@@ -28,13 +28,13 @@ def _dict_subject_new(actual, meta, container_name = "dict", key_plural_name = "
Method: DictSubject.new
Args:
actual: ([`dict`]) the dict to assert against.
meta: ([`ExpectMeta`]) of call chain information.
container_name: ([`str`]) conceptual name of the dict.
key_plural_name: ([`str`]) the plural word for the keys of the dict.
actual: {type}`dict` the dict to assert against.
meta: {type}`ExpectMeta` of call chain information.
container_name: {type}`str` conceptual name of the dict.
key_plural_name: {type}`str` the plural word for the keys of the dict.
Returns:
New `DictSubject` struct.
{type}`DictSubject` struct.
"""

# buildifier: disable=uninitialized
@@ -62,7 +62,7 @@ def _dict_subject_contains_at_least(self, at_least):
Args:
self: implicitly added.
at_least: ([`dict`]) the subset of keys/values that must exist. Extra
at_least: {type}`dict` the subset of keys/values that must exist. Extra
keys are allowed. Order is not checked.
"""
result = compare_dicts(
@@ -91,7 +91,7 @@ def _dict_subject_contains_exactly(self, expected):
Args:
self: implicitly added
expected: ([`dict`]) the values that must exist. Missing values or
expected: {type}`dict` the values that must exist. Missing values or
extra values are not allowed. Order is not checked.
"""
result = compare_dicts(
@@ -122,7 +122,7 @@ def _dict_subject_contains_none_of(self, none_of):
Args:
self: implicitly added
none_of: ([`dict`]) the keys/values that must not exist. Order is not
none_of: {type}`dict` the keys/values that must not exist. Order is not
checked.
"""
result = compare_dicts(
@@ -160,8 +160,8 @@ def _dict_subject_get(self, key, *, factory):
Args:
self: implicitly added.
key: ([`object`]) the key to fetch.
factory: ([`callable`]) subject factory function, with the signature
key: {type}`object` the key to fetch.
factory: {type}`callable` subject factory function, with the signature
of `def factory(value, *, meta)`, and returns the wrapped value.
Returns:
@@ -183,7 +183,7 @@ def _dict_subject_keys(self):
self: implicitly added
Returns:
[`CollectionSubject`] of the keys.
{type}`CollectionSubject` of the keys.
"""
return CollectionSubject.new(
self.actual.keys(),
@@ -192,9 +192,20 @@ def _dict_subject_keys(self):
element_plural_name = "keys",
)

def _dict_subject_typedef():
"""Subject for `dict` assertions.
:::{field} actual
:type: dict
Underlying object asserted against
:::
"""

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
DictSubject = struct(
TYPEDEF = _dict_subject_typedef,
new = _dict_subject_new,
contains_at_least = _dict_subject_contains_at_least,
contains_exactly = _dict_subject_contains_exactly,
24 changes: 18 additions & 6 deletions lib/private/execution_info_subject.bzl
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""# ExecutionInfoSubject"""
"""ExecutionInfoSubject"""

load(":dict_subject.bzl", "DictSubject")
load(":str_subject.bzl", "StrSubject")
@@ -23,16 +23,17 @@ def _execution_info_subject_new(info, *, meta):
Method: ExecutionInfoSubject.new
Args:
info: ([`testing.ExecutionInfo`]) provider instance.
meta: ([`ExpectMeta`]) of call chain information.
info: {type}`testing.ExecutionInfo` provider instance.
meta: {type}`ExpectMeta` of call chain information.
Returns:
`ExecutionInfoSubject` struct.
{type}`ExecutionInfoSubject` struct.
"""

# buildifier: disable=uninitialized
public = struct(
# keep sorted start
actual = info,
requirements = lambda *a, **k: _execution_info_subject_requirements(self, *a, **k),
exec_group = lambda *a, **k: _execution_info_subject_exec_group(self, *a, **k),
# keep sorted end
@@ -52,7 +53,7 @@ def _execution_info_subject_requirements(self):
self: implicitly added
Returns:
`DictSubject` of the requirements.
{type}`DictSubject` of the requirements.
"""
return DictSubject.new(
self.actual.requirements,
@@ -68,16 +69,27 @@ def _execution_info_subject_exec_group(self):
self: implicitly added
Returns:
A [`StrSubject`] for the exec group.
{type}`StrSubject` for the exec group.
"""
return StrSubject.new(
self.actual.exec_group,
meta = self.meta.derive("exec_group()"),
)

def _execution_info_subject_typedef():
"""Subject for {obj}`testing.ExecutionInfo`
:::{field} actual
:type: testing.ExecutionInfo
The underlying object asserted against.
:::
"""

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
ExecutionInfoSubject = struct(
TYPEDEF = _execution_info_subject_typedef,
new = _execution_info_subject_new,
requirements = _execution_info_subject_requirements,
exec_group = _execution_info_subject_exec_group,
90 changes: 50 additions & 40 deletions lib/private/expect.bzl
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""# Expect"""
"""Expect"""

load(":action_subject.bzl", "ActionSubject")
load(":bool_subject.bzl", "BoolSubject")
@@ -37,18 +37,19 @@ def _expect_new_from_env(env):
customize behavior. Usually this is helpful for testing. See `_fake_env()`
in truth_tests.bzl for examples.
* `fail`: callable that takes a failure message. If present, it
will be called instead of the regular `Expect.add_failure` logic.
will be called instead of the regular {obj}`Expect.add_failure()` logic.
* `get_provider`: callable that takes 2 positional args (target and
provider) and returns the found provider or fails.
* `has_provider`: callable that takes 2 positional args (a [`Target`] and
a [`provider`]) and returns [`bool`] (`True` if present, `False` otherwise) or fails.
* `has_provider`: callable that takes 2 positional args (a {obj}`Target` and
a {obj}`provider`) and returns {obj}`bool` (`True` if present, `False`
otherwise) or fails.
Args:
env: unittest env struct, or some approximation. There are several
env: {type}`Env` unittest env struct, or some approximation. There are several
attributes that override regular behavior; see above doc.
Returns:
[`Expect`] object
{type}`Expect` object
"""
return _expect_new(env, None)

@@ -58,11 +59,11 @@ def _expect_new(env, meta):
Internal; only other `Expect` methods should be calling this.
Args:
env: unittest env struct or some approximation.
meta: ([`ExpectMeta`]) metadata about call chain and state.
env: {type}`Env` unittest env struct or some approximation.
meta: {type}`ExpectMeta` metadata about call chain and state.
Returns:
[`Expect`] object
{type}`Expect` object
"""

meta = meta or ExpectMeta.new(env)
@@ -94,10 +95,10 @@ def _expect_that_action(self, action):
Args:
self: implicitly added.
action: ([`Action`]) the action to check.
action: {type}`Action` the action to check.
Returns:
[`ActionSubject`] object.
{type}`ActionSubject` object.
"""
return ActionSubject.new(
action,
@@ -112,11 +113,11 @@ def _expect_that_bool(self, value, expr = "boolean"):
Args:
self: implicitly added.
value: ([`bool`]) the bool to check.
expr: ([`str`]) the starting "value of" expression to report in errors.
value: {type}`bool` the bool to check.
expr: {type}`str` the starting "value of" expression to report in errors.
Returns:
[`BoolSubject`] object.
{type}`BoolSubject` object.
"""
return BoolSubject.new(
value,
@@ -129,11 +130,11 @@ def _expect_that_collection(self, collection, expr = "collection", **kwargs):
Args:
self: implicitly added.
collection: The collection (list or depset) to assert.
expr: ([`str`]) the starting "value of" expression to report in errors.
expr: {type}`str` the starting "value of" expression to report in errors.
**kwargs: Additional kwargs to pass onto CollectionSubject.new
Returns:
[`CollectionSubject`] object.
{type}`CollectionSubject` object.
"""
return CollectionSubject.new(collection, self.meta.derive(expr), **kwargs)

@@ -144,10 +145,10 @@ def _expect_that_depset_of_files(self, depset_files):
Args:
self: implicitly added.
depset_files: ([`depset`] of [`File`]) the values to assert on.
depset_files: {type}`depset[File]` the values to assert on.
Returns:
[`DepsetFileSubject`] object.
{type}`DepsetFileSubject` object.
"""
return DepsetFileSubject.new(depset_files, self.meta.derive("depset_files"))

@@ -158,11 +159,11 @@ def _expect_that_dict(self, mapping, meta = None):
Args:
self: implicitly added
mapping: ([`dict`]) the values to assert on
meta: ([`ExpectMeta`]) optional custom call chain information to use instead
mapping: {type}`dict` the values to assert on
meta: {type}`ExpectMeta` optional custom call chain information to use instead
Returns:
[`DictSubject`] object.
{type}`DictSubject` object.
"""
meta = meta or self.meta.derive("dict")
return DictSubject.new(mapping, meta = meta)
@@ -174,11 +175,11 @@ def _expect_that_file(self, file, meta = None):
Args:
self: implicitly added.
file: ([`File`]) the value to assert.
meta: ([`ExpectMeta`]) optional custom call chain information to use instead
file: {type}`File` the value to assert.
meta: {type}`ExpectMeta` optional custom call chain information to use instead
Returns:
[`FileSubject`] object.
{type}`FileSubject` object.
"""
meta = meta or self.meta.derive("file")
return FileSubject.new(file, meta = meta)
@@ -190,11 +191,11 @@ def _expect_that_int(self, value, expr = "integer"):
Args:
self: implicitly added.
value: ([`int`]) the value to check against.
expr: ([`str`]) the starting "value of" expression to report in errors.
value: {type}`int` the value to check against.
expr: {type}`str` the starting "value of" expression to report in errors.
Returns:
[`IntSubject`] object.
{type}`IntSubject` object.
"""
return IntSubject.new(value, self.meta.derive(expr))

@@ -203,10 +204,10 @@ def _expect_that_str(self, value):
Args:
self: implicitly added.
value: ([`str`]) the value to check against.
value: {type}`str` the value to check against.
Returns:
[`StrSubject`] object.
{type}`StrSubject` object.
"""
return StrSubject.new(value, self.meta.derive("string"))

@@ -215,17 +216,17 @@ def _expect_that_struct(self, value, *, attrs, expr = "struct"):
Args:
self: implicitly added.
value: ([`struct`]) the value to check against.
expr: ([`str`]) The starting "value of" expression to report in errors.
attrs: ([`dict`] of [`str`] to [`callable`]) the functions to convert
value: {type}`struct` the value to check against.
expr: {type}`str` The starting "value of" expression to report in errors.
attrs: {type}`dict[str, 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.
value of the struct, and `meta` is an {type}`ExpectMeta` object.
Returns:
[`StructSubject`] object.
{type}`StructSubject` object.
"""
return StructSubject.new(value, meta = self.meta.derive(expr), attrs = attrs)

@@ -238,10 +239,10 @@ def _expect_that_target(self, target):
Args:
self: implicitly added.
target: ([`Target`]) subject target to check against.
target: {obj}`Target` subject target to check against.
Returns:
[`TargetSubject`] object.
{type}`TargetSubject` object.
"""
return TargetSubject.new(target, self.meta.derive(
expr = "target({})".format(target.label),
@@ -257,10 +258,10 @@ def _expect_that_value(self, value, *, factory, expr = "value"):
Args:
self: implicitly added.
value: ([`struct`]) the value to check against.
value: {type}`struct` the value to check against.
factory: A subject factory (a function that takes value and meta).
Eg. subjects.collection
expr: ([`str`]) The starting "value of" expression to report in errors.
expr: {type}`str` The starting "value of" expression to report in errors.
Returns:
A subject corresponding to the type returned by the factory.
@@ -279,21 +280,30 @@ def _expect_where(self, **details):
Args:
self: implicitly added.
**details: ([`dict`] of [`str`] to value) Each named arg is added to
**details: {type}`dict[str, value]` Each named arg is added to
the metadata details with the provided string, which is printed as
part of displaying any failures.
Returns:
[`Expect`] object with separate metadata derived from the original self.
{type}`Expect` object with separate metadata derived from the original self.
"""
meta = self.meta.derive(
details = ["{}: {}".format(k, v) for k, v in details.items()],
)
return _expect_new(env = self.env, meta = meta)

def _expect_typedef():
"""Entry point for truth-style assertions with context.
This is typically created through `env.expect` as part of the
analysis test framework. It holds context related to the test
state for later use in asserts and error reporting.
"""

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
Expect = struct(
TYPEDEF = _expect_typedef,
# keep sorted start
new = _expect_new,
new_from_env = _expect_new_from_env,
141 changes: 87 additions & 54 deletions lib/private/expect_meta.bzl
Original file line number Diff line number Diff line change
@@ -11,61 +11,29 @@
# 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.
"""# ExpectMeta
ExpectMeta object implementation.
"""
"""ExpectMeta object implementation."""

load("//lib:unittest.bzl", ut_asserts = "asserts")

def _expect_meta_new(env, exprs = [], details = [], format_str_kwargs = None):
"""Creates a new "ExpectMeta" struct".
Method: ExpectMeta.new
ExpectMeta objects are internal helpers for the Expect object and Subject
objects. They are used for Subjects to store and communicate state through a
series of call chains and asserts.
This constructor should only be directly called by `Expect` objects. When a
parent Subject is creating a child-Subject, then [`derive()`] should be
used.
### Env objects
"""Creates a new `ExpectMeta` struct".
The `env` object basically provides a way to interact with things outside
of the truth assertions framework. This allows easier testing of the
framework itself and decouples it from a particular test framework (which
makes it usable by by rules_testing's analysis_test and skylib's
analysistest)
The `env` object requires the following attribute:
* ctx: The test's ctx.
The `env` object allows the following attributes to customize behavior:
* fail: A callable that accepts a single string, which is the failure
message. Its return value is ignored. This is called when an assertion
fails. It's generally expected that it records a failure instead of
immediately failing.
* has_provider: (callable) it accepts two positional args, target and
provider and returns [`bool`]. This is used to implement `Provider in
target` operations.
* get_provider: (callable) it accepts two positional args, target and
provider and returns the provider value. This is used to implement
`target[Provider]`.
This constructor should only be directly called by {obj}`Expect` objects.
When a parent Subject is creating a child-Subject, then {obj}`derive()`
should be used.
Args:
env: unittest env struct or some approximation.
exprs: ([`list`] of [`str`]) the expression strings of the call chain for
env: {type}`Env` unittest env struct or some approximation.
exprs: {type}`list[str]` the expression strings of the call chain for
the subject.
details: ([`list`] of [`str`]) additional details to print on error. These
details: {type}`list[str]` additional details to print on error. These
are usually informative details of the objects under test.
format_str_kwargs: optional dict of format() kwargs. These kwargs
are propagated through `derive()` calls and used when
`ExpectMeta.format_str()` is called.
Returns:
[`ExpectMeta`] object.
{type}`ExpectMeta` object.
"""
if format_str_kwargs == None:
format_str_kwargs = {}
@@ -111,21 +79,21 @@ def _expect_meta_derive(self, expr = None, details = None, format_str_kwargs = {
Args:
self: implicitly added.
expr: ([`str`]) human-friendly description of the call chain expression.
expr: {type}`str` human-friendly description of the call chain expression.
e.g., if `foo_subject.bar_named("baz")` returns a child-subject,
then "bar_named("bar")" would be the expression.
details: (optional [`list`] of [`str`]) human-friendly descriptions of additional
details: {type}`list[str] | None` human-friendly descriptions of additional
detail to include in errors. This is usually additional information
the child Subject wouldn't include itself. e.g. if
`foo.first_action_argv().contains(1)`, returned a ListSubject, then
including "first action: Action FooCompile" helps add context to the
error message. If there is no additional detail to include, pass
None.
format_str_kwargs: ([`dict`] of format()-kwargs) additional kwargs to
make available to [`format_str`] calls.
format_str_kwargs: {type}`dict[str, object]` additional kwargs to
make available to {obj}`format_str` calls.
Returns:
[`ExpectMeta`] object.
{type}`ExpectMeta` object.
"""
if not details:
details = []
@@ -160,10 +128,10 @@ def _expect_meta_format_str(self, template):
Args:
self: implicitly added.
template: ([`str`]) the format template string to use.
template: {type}`str` the format template string to use.
Returns:
[`str`]; the template with parameters replaced.
{type}`str` the template with parameters replaced.
"""
return template.format(**self._format_str_kwargs)

@@ -175,7 +143,7 @@ def _expect_meta_get_provider(self, target, provider):
Args:
self: implicitly added.
target: ([`Target`]) the target to get the provider from.
target: {type}`Target` the target to get the provider from.
provider: The provider type to get.
Returns:
@@ -194,7 +162,7 @@ def _expect_meta_has_provider(self, target, provider):
Args:
self: implicitly added.
target: ([`Target`]) the target to check for the provider.
target: {type}`Target` the target to check for the provider.
provider: the provider type to check for.
Returns:
@@ -215,14 +183,14 @@ def _expect_meta_add_failure(self, problem, actual):
Args:
self: implicitly added.
problem: ([`str`]) a string describing the expected value or problem
problem: {type}`str` a string describing the expected value or problem
detected, and the expected values that weren't satisfied. A colon
should be used to separate the description from the values.
The description should be brief and include the word "expected",
e.g. "expected: foo", or "expected values missing: <list of missing>",
the key point being the reader can easily take the values shown
and look for it in the actual values displayed below it.
actual: ([`str`]) a string describing the values observed. A colon should
actual: {type}`str` a string describing the values observed. A colon should
be used to separate the description from the observed values.
The description should be brief and include the word "actual", e.g.,
"actual: bar". The values should include the actual, observed,
@@ -257,7 +225,7 @@ def _expect_meta_current_expr(self):
self: implicitly added.
Returns:
[`str`] A string representing the current expression, e.g.
{type}`str` A string representing the current expression, e.g.
"foo.bar(something).baz()"
"""
return ".".join(self._exprs)
@@ -267,7 +235,7 @@ def _expect_meta_call_fail(self, msg):
Args:
self: implicitly added.
msg: ([`str`]) the failure message.
msg: {type}`str` the failure message.
"""
fail_func = getattr(self.env, "fail", None)
if fail_func != None:
@@ -278,9 +246,31 @@ def _expect_meta_call_fail(self, msg):
# the first line of our message hard to see.
ut_asserts.true(self.env, False, "\n" + msg)

def _expect_meta_typedef():
"""Internal helper for `Expect` objects.
ExpectMeta objects are internal helpers for the {obj}`Expect` object and
Subject objects. They are used for Subjects to store and communicate state
through a series of call chains and asserts.
:::{field} ctx
:type: ctx
The test's ctx
:::
:::{field} env
:type: Env
The underlying env
:::
"""

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
ExpectMeta = struct(
TYPEDEF = _expect_meta_typedef,
new = _expect_meta_new,
derive = _expect_meta_derive,
format_str = _expect_meta_format_str,
@@ -289,3 +279,46 @@ ExpectMeta = struct(
add_failure = _expect_meta_add_failure,
call_fail = _expect_meta_call_fail,
)

def _env_typedef():
"""Interface for interacting outside the truth framework.
The `env` object basically provides a way to interact with things outside
of the truth assertions framework. This allows easier testing of the
framework itself and decouples it from a particular test framework (which
makes it usable by by rules_testing's analysis_test and skylib's
analysistest)
:::{field} ctx
:type: ctx
The test's ctx
:::
:::{field} fail
:type: callable | unset
A optional callable that accepts a single string, which is the failure
message. Its return value is ignored. This is called when an assertion
fails. It's generally expected that it records a failure instead of
immediately failing.
:::
:::{field} has_provider
:type: callable | unset
A callable that accepts two positional args, target and provider, and returns
`bool`. This is used to implement `Provider in target` operations.
:::
:::{field} get_provider
:type: callable
A callable that accepts two positional args, target and provider, and
returns the provider value. This is used to implement `target[Provider]`.
"""

# buildifier: disable=name-conventions
Env = struct(
TYPEDEF = _env_typedef,
)
28 changes: 20 additions & 8 deletions lib/private/file_subject.bzl
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""# FileSubject"""
"""FileSubject"""

load(":str_subject.bzl", "StrSubject")

@@ -22,16 +22,17 @@ def _file_subject_new(file, meta):
Method: FileSubject.new
Args:
file: ([`File`]) the file to assert against.
meta: ([`ExpectMeta`])
file: {type}`File` the file to assert against.
meta: {type}`ExpectMeta`
Returns:
[`FileSubject`] object.
{type}`FileSubject` object.
"""

# buildifier: disable=uninitialized
public = struct(
# keep sorted start
actual = file,
equals = lambda *a, **k: _file_subject_equals(self, *a, **k),
path = lambda *a, **k: _file_subject_path(self, *a, **k),
short_path_equals = lambda *a, **k: _file_subject_short_path_equals(self, *a, **k),
@@ -43,7 +44,7 @@ def _file_subject_new(file, meta):
def _file_subject_equals(self, expected):
"""Asserts that `expected` references the same file as `self`.
This uses Bazel's notion of [`File`] equality, which usually includes
This uses Bazel's notion of {obj}`File` equality, which usually includes
the configuration, owning action, internal hash, etc of a `File`. The
particulars of comparison depend on the actual Java type implementing
the `File` object (some ignore owner, for example).
@@ -52,7 +53,7 @@ def _file_subject_equals(self, expected):
NOTE: Same files generated by different owners are likely considered
not equal to each other. The alternative for this is to assert the
`File.path` paths are equal using [`FileSubject.path()`]
`File.path` paths are equal using {obj}`FileSubject.path()`
Method: FileSubject.equals
"""
@@ -70,7 +71,7 @@ def _file_subject_path(self):
Method: FileSubject.path
Returns:
[`StrSubject`] object.
{type}`StrSubject` object.
"""
return StrSubject.new(
self.file.path,
@@ -84,7 +85,7 @@ def _file_subject_short_path_equals(self, path):
Args:
self: implicitly added.
path: ([`str`]) the value the file's `short_path` must be equal to.
path: {type}`str` the value the underlying {obj}`File.short_path` must be equal to.
"""
path = self.meta.format_str(path)
if path == self.file.short_path:
@@ -94,9 +95,20 @@ def _file_subject_short_path_equals(self, path):
"actual: {}".format(self.file.short_path),
)

def _file_subject_typedef():
"""Subject for asserting on {obj}`File` objects.
:::{field} actual
:type: File
The underlying file asserted against.
:::
"""

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
FileSubject = struct(
TYPEDEF = _file_subject_typedef,
new = _file_subject_new,
equals = _file_subject_equals,
path = _file_subject_path,
29 changes: 23 additions & 6 deletions lib/private/instrumented_files_info_subject.bzl
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@
# 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.
"""# InstrumentedFilesInfoSubject"""
"""InstrumentedFilesInfoSubject"""

load(":depset_file_subject.bzl", "DepsetFileSubject")

@@ -21,11 +21,11 @@ def _instrumented_files_info_subject_new(info, *, meta):
Method: InstrumentedFilesInfoSubject.new
Args:
info: ([`InstrumentedFilesInfo`]) provider instance.
meta: ([`ExpectMeta`]) the meta data about the call chain.
info: {type}`InstrumentedFilesInfo` provider instance.
meta: {type}`ExpectMeta` the meta data about the call chain.
Returns:
An `InstrumentedFilesInfoSubject` struct.
{type}`InstrumentedFilesInfoSubject` struct.
"""
self = struct(
actual = info,
@@ -39,34 +39,51 @@ def _instrumented_files_info_subject_new(info, *, meta):
return public

def _instrumented_files_info_subject_instrumented_files(self):
"""Returns a `DesetFileSubject` of the instrumented files.
"""Returns a `DepsetFileSubject` of the instrumented files.
Method: InstrumentedFilesInfoSubject.instrumented_files
Args:
self: implicitly added
Returns:
{type}`DepsetFileSubject`
"""
return DepsetFileSubject.new(
self.actual.instrumented_files,
meta = self.meta.derive("instrumented_files()"),
)

def _instrumented_files_info_subject_metadata_files(self):
"""Returns a `DesetFileSubject` of the metadata files.
"""Returns a `DepsetFileSubject` of the metadata files.
Method: InstrumentedFilesInfoSubject.metadata_files
Args:
self: implicitly added
Returns:
{type}`DepsetFileSubject`
"""
return DepsetFileSubject.new(
self.actual.metadata_files,
meta = self.meta.derive("metadata_files()"),
)

def _instrumented_files_info_subject_typedef():
"""Wrapper for asserting {type}`InstrumentedFilesInfo` objects.
:::{field} actual
:type: InstrumentedFilesInfo
The underlying object assert against
:::
"""

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
InstrumentedFilesInfoSubject = struct(
TYPEDEF = _instrumented_files_info_subject_typedef,
new = _instrumented_files_info_subject_new,
instrumented_files = _instrumented_files_info_subject_instrumented_files,
metadata_files = _instrumented_files_info_subject_metadata_files,
26 changes: 19 additions & 7 deletions lib/private/int_subject.bzl
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""# IntSubject"""
"""IntSubject"""

load("@bazel_skylib//lib:types.bzl", "types")
load(":check_util.bzl", "check_not_equals", "common_subject_is_in")
@@ -24,18 +24,19 @@ def _int_subject_new(value, meta):
Method: IntSubject.new
Args:
value: (optional [`int`]) the value to perform asserts against may be None.
meta: ([`ExpectMeta`]) the meta data about the call chain.
value: {type}`int | None` the value to perform asserts against may be None.
meta: {type}`ExpectMeta` the meta data about the call chain.
Returns:
[`IntSubject`].
{type}`IntSubject`
"""
if not types.is_int(value) and value != None:
fail("int required, got: {}".format(repr_with_type(value)))

# buildifier: disable=uninitialized
public = struct(
# keep sorted start
actual = value,
equals = lambda *a, **k: _int_subject_equals(self, *a, **k),
is_greater_than = lambda *a, **k: _int_subject_is_greater_than(self, *a, **k),
is_in = lambda *a, **k: common_subject_is_in(self, *a, **k),
@@ -52,7 +53,7 @@ def _int_subject_equals(self, other):
Args:
self: implicitly added.
other: ([`int`]) value the subject must be equal to.
other: {type}`int` value the subject must be equal to.
"""
if self.actual == other:
return
@@ -68,7 +69,7 @@ def _int_subject_is_greater_than(self, other):
Args:
self: implicitly added.
other: ([`int`]) value the subject must be greater than.
other: {type}`int` value the subject must be greater than.
"""
if self.actual != None and other != None and self.actual > other:
return
@@ -84,17 +85,28 @@ def _int_subject_not_equals(self, unexpected):
Args:
self: implicitly added
unexpected: ([`int`]) the value actual cannot equal.
unexpected: {type}`int` the value actual cannot equal.
"""
return check_not_equals(
actual = self.actual,
unexpected = unexpected,
meta = self.meta,
)

def _int_subject_typedef():
"""Wrapper for asserting int values.
:::{field} actual
:type: int | None
The underlying value to assert against.
:::
"""

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
IntSubject = struct(
TYPEDEF = _int_subject_typedef,
new = _int_subject_new,
equals = _int_subject_equals,
is_greater_than = _int_subject_is_greater_than,
24 changes: 18 additions & 6 deletions lib/private/label_subject.bzl
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""# LabelSubject"""
"""LabelSubject"""

load("@bazel_skylib//lib:types.bzl", "types")
load(":check_util.bzl", "common_subject_is_in")
@@ -24,16 +24,17 @@ def _label_subject_new(label, meta):
Method: LabelSubject.new
Args:
label: ([`Label`]) the label to check against.
meta: ([`ExpectMeta`]) the metadata about the call chain.
label: {type}`Label` the label to check against.
meta: {type}`ExpectMeta` the metadata about the call chain.
Returns:
[`LabelSubject`].
{type}`LabelSubject`.
"""

# buildifier: disable=uninitialized
public = struct(
# keep sorted start
actual = label,
equals = lambda *a, **k: _label_subject_equals(self, *a, **k),
is_in = lambda *a, **k: _label_subject_is_in(self, *a, **k),
# keep sorted end
@@ -48,7 +49,7 @@ def _label_subject_equals(self, other):
Args:
self: implicitly added.
other: ([`Label`] | [`str`]) the expected value. If a `str` is passed, it
other: {type}`Label` | str` the expected value. If a `str` is passed, it
will be converted to a `Label` using the `Label` function.
"""
if types.is_string(other):
@@ -65,7 +66,7 @@ def _label_subject_is_in(self, any_of):
Args:
self: implicitly added.
any_of: ([`collection`] of ([`Label`] | [`str`])) If strings are
any_of: {type}`collection[Label | str]` If strings are
provided, they must be parsable by `Label`.
"""
any_of = [
@@ -74,9 +75,20 @@ def _label_subject_is_in(self, any_of):
]
common_subject_is_in(self, any_of)

def _label_subject_typedef():
"""Wrapper for asserts on Label objects
:::{field} actual
:type: Label
The underlying value to assert against.
:::
"""

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
LabelSubject = struct(
TYPEDEF = _label_subject_typedef,
new = _label_subject_new,
equals = _label_subject_equals,
is_in = _label_subject_is_in,
84 changes: 49 additions & 35 deletions lib/private/matching.bzl
Original file line number Diff line number Diff line change
@@ -18,11 +18,11 @@ def _match_all(*matchers):
"""Match that all of multiple matchers match.
Args:
*matchers: `list` of [`Matcher`]. If all match, then it is
*matchers: {type}`list[Matcher]` If all match, then it is
considered a match.
Returns:
[`Matcher`] (see `_match_custom`)
{type}`Matcher` (see `_match_custom`)
"""
desc = " and ".join([str(m.desc) for m in matchers])

@@ -37,21 +37,13 @@ def _match_all(*matchers):
def _match_custom(desc, func):
"""Wrap an arbitrary function up as a Matcher.
Method: Matcher.new
`Matcher` struct attributes:
* `desc`: ([`str`]) a human-friendly description
* `match`: (callable) accepts 1 positional arg (the value to match) and
returns [`bool`] (`True` if it matched, `False` if not).
Args:
desc: ([`str`]) a human-friendly string describing what is matched.
func: (callable) accepts 1 positional arg (the value to match) and
returns [`bool`] (`True` if it matched, `False` if not).
desc: {type}`str` a human-friendly string describing what is matched.
func: {type}`callable` accepts 1 positional arg (the value to match) and
returns `bool` (`True` if it matched, `False` if not).
Returns:
[`Matcher`] (see above).
{type}`Matcher`
"""
return struct(desc = desc, match = func)

@@ -65,18 +57,18 @@ def _match_equals_wrapper(value):
value: object, the value that must be equal to.
Returns:
[`Matcher`] (see `_match_custom()`), whose description is `value`.
{type}`Matcher` (see `_match_custom()`), whose description is `value`.
"""
return _match_custom(value, lambda other: other == value)

def _match_file_basename_contains(substr):
"""Match that a a `File.basename` string contains a substring.
Args:
substr: ([`str`]) the substring to match.
substr: {type}`str` the substring to match.
Returns:
[`Matcher`] (see `_match_custom()`).
{type}`Matcher` (see `_match_custom()`).
"""
return struct(
desc = "<basename contains '{}'>".format(substr),
@@ -87,11 +79,11 @@ def _match_file_path_matches(pattern):
"""Match that a `File.path` string matches a glob-style pattern.
Args:
pattern: ([`str`]) the pattern to match. "*" can be used to denote
pattern: {type}`str` the pattern to match. "*" can be used to denote
"match anything".
Returns:
[`Matcher`] (see `_match_custom`).
{type}`Matcher` (see `_match_custom`).
"""
parts = pattern.split("*")
return struct(
@@ -103,10 +95,10 @@ def _match_file_basename_equals(value):
"""Match that a `File.basename` string equals `value`.
Args:
value: ([`str`]) the basename to match.
value: {type}`str` the basename to match.
Returns:
[`Matcher`] instance
{type}`Matcher` instance
"""
return struct(
desc = "<file basename equals '{}'>".format(value),
@@ -120,10 +112,10 @@ def _match_file_extension_in(values):
have multiple parts, e.g. `*.tar.gz` or `*.so.*`.
Args:
values: ([`list`] of [`str`]) the extensions to match.
values: {type}`list[str]` the extensions to match.
Returns:
[`Matcher`] instance
{type}`Matcher` instance
"""
return struct(
desc = "<file extension is any of {}>".format(repr(values)),
@@ -142,7 +134,7 @@ def _match_is_in(values):
values: The collection that the value must be within.
Returns:
[`Matcher`] (see `_match_custom()`).
{type}`Matcher` (see `_match_custom()`).
"""
return struct(
desc = "<is any of {}>".format(repr(values)),
@@ -156,10 +148,10 @@ def _match_never(desc):
while providing a custom description.
Args:
desc: ([`str`]) human-friendly string.
desc: {type}`str` human-friendly string.
Returns:
[`Matcher`] (see `_match_custom`).
{type}`Matcher` (see `_match_custom`).
"""
return struct(
desc = desc,
@@ -173,11 +165,11 @@ def _match_any(*matchers):
values.
Args:
*matchers: `list` of [`Matcher`]. If any match, then it is
*matchers: {type}`list[Matcher]` If any match, then it is
considered a match.
Returns:
[`Matcher`] (see `_match_custom`)
{type}`Matcher` (see `_match_custom`)
"""
desc = " or ".join([str(m.desc) for m in matchers])

@@ -199,7 +191,7 @@ def _match_contains(contained):
contained: the value that to-be-matched value must contain.
Returns:
[`Matcher`] (see `_match_custom`).
{type}`Matcher` (see `_match_custom`).
"""
return struct(
desc = "<contains {}>".format(contained),
@@ -210,10 +202,10 @@ def _match_str_endswith(suffix):
"""Match that a string contains another string.
Args:
suffix: ([`str`]) the suffix that must be present
suffix: {type}`str` the suffix that must be present
Returns:
[`Matcher`] (see `_match_custom`).
{type}`Matcher` (see `_match_custom`).
"""
return struct(
desc = "<endswith '{}'>".format(suffix),
@@ -224,12 +216,12 @@ def _match_str_matches(pattern):
"""Match that a string matches a glob-style pattern.
Args:
pattern: ([`str`]) the pattern to match. `*` can be used to denote
pattern: {type}`str` the pattern to match. `*` can be used to denote
"match anything". There is an implicit `*` at the start and
end of the pattern.
Returns:
[`Matcher`] object.
{type}`Matcher` object.
"""
parts = pattern.split("*")
return struct(
@@ -241,10 +233,10 @@ def _match_str_startswith(prefix):
"""Match that a string contains another string.
Args:
prefix: ([`str`]) the prefix that must be present
prefix: {type}`str` the prefix that must be present
Returns:
[`Matcher`] (see `_match_custom`).
{type}`Matcher` (see `_match_custom`).
"""
return struct(
desc = "<startswith '{}'>".format(prefix),
@@ -262,6 +254,28 @@ def _match_parts_in_order(string, parts):
def _is_matcher(obj):
return hasattr(obj, "desc") and hasattr(obj, "match")

def _matcher_typedef():
"""A struct to represent matching with information metadata.
To create a custom Matcher, use {obj}`matching.custom()`.
:::{field} desc
:type: str
A human friendly description of what matches.
:::
:::{field} match
:type: callable
callable that accepts 1 positional arg (the value to match) and
returns `bool` (`True` if it matched, `False` if not).
:::
"""

# buildifier: disable=name-conventions
Matcher = struct(
TYPEDEF = _matcher_typedef,
)

# For the definition of a `Matcher` object, see `_match_custom`.
matching = struct(
# keep sorted start
19 changes: 18 additions & 1 deletion lib/private/ordered.bzl
Original file line number Diff line number Diff line change
@@ -12,13 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""# Ordered"""
"""Ordered"""

# This is just a stub so doc generation is nicer.
def _ordered_in_order():
"""Checks that the values were in order."""

def _in_order_typedef():
"""Singleton Ordered implementation for when order was respected."""

IN_ORDER = struct(
TYPEDEF = _in_order_typedef,
in_order = _ordered_in_order,
)

@@ -56,9 +60,22 @@ def _ordered_incorrectly_in_order(self):
"""
self.meta.add_failure(self.format_problem(), self.format_actual())

def _ordered_incorrectly_typedef():
"""Ordered implementation for when order was not respected."""

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
OrderedIncorrectly = struct(
TYPEDEF = _ordered_incorrectly_typedef,
new = _ordered_incorrectly_new,
in_order = _ordered_incorrectly_in_order,
)

def _ordered_typedef():
"""Interface for asserting values were matched in order."""

# buildifier: disable=name-conventions
Ordered = struct(
TYPEDEF = _ordered_typedef,
in_order = _ordered_in_order,
)
23 changes: 17 additions & 6 deletions lib/private/run_environment_info_subject.bzl
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""# RunEnvironmentInfoSubject"""
"""RunEnvironmentInfoSubject"""

load(":collection_subject.bzl", "CollectionSubject")
load(":dict_subject.bzl", "DictSubject")
@@ -23,8 +23,8 @@ def _run_environment_info_subject_new(info, *, meta):
Method: RunEnvironmentInfoSubject.new
Args:
info: ([`RunEnvironmentInfo`]) provider instance.
meta: ([`ExpectMeta`]) of call chain information.
info: {type}`RunEnvironmentInfo` provider instance.
meta: {type}`ExpectMeta` of call chain information.
"""

# buildifier: disable=uninitialized
@@ -47,7 +47,7 @@ def _run_environment_info_subject_environment(self):
self: implicitly added
Returns:
[`DictSubject`] of the str->str environment map.
{type}`DictSubject[str, str]` of the str->str environment map.
"""
return DictSubject.new(
self.actual.environment,
@@ -63,17 +63,28 @@ def _run_environment_info_subject_inherited_environment(self):
self: implicitly added
Returns:
[`CollectionSubject`] of [`str`]; from the
[`RunEnvironmentInfo.inherited_environment`] list.
{type}`CollectionSubject[str]` from
{obj}`RunEnvironmentInfo.inherited_environment` list.
"""
return CollectionSubject.new(
self.actual.inherited_environment,
meta = self.meta.derive("inherited_environment()"),
)

def _run_environment_info_subject_typedef():
"""Subject for {obj}`RunEnvironmentInfo` object
:::{field} actual
:type: RunEnvironmentInfo
The underlying value to assert against.
:::
"""

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
RunEnvironmentInfoSubject = struct(
TYPEDEF = _run_environment_info_subject_typedef,
new = _run_environment_info_subject_new,
environment = _run_environment_info_subject_environment,
inherited_environment = _run_environment_info_subject_inherited_environment,
49 changes: 30 additions & 19 deletions lib/private/runfiles_subject.bzl
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""# RunfilesSubject"""
"""RunfilesSubject"""

load(
"//lib:util.bzl",
@@ -43,13 +43,13 @@ def _runfiles_subject_new(runfiles, meta, kind = None):
Method: RunfilesSubject.new
Args:
runfiles: ([`runfiles`]) the runfiles to check against.
meta: ([`ExpectMeta`]) the metadata about the call chain.
kind: (optional [`str`]) what type of runfiles they are, usually "data"
runfiles: {type}`runfiles` the runfiles to check against.
meta: {type}`ExpectMeta` the metadata about the call chain.
kind: {type}`str | None` what type of runfiles they are, usually "data"
or "default". If not known or not applicable, use None.
Returns:
[`RunfilesSubject`] object.
{type}`RunfilesSubject` object.
"""
self = struct(
runfiles = runfiles,
@@ -81,7 +81,7 @@ def _runfiles_subject_contains(self, expected):
Args:
self: implicitly added.
expected: ([`str`]) the path to check is present. This will be formatted
expected: {type}`str` the path to check is present. This will be formatted
using `ExpectMeta.format_str` and its current contextual
keywords. Note that paths are runfiles-root relative (i.e.
you likely need to include the workspace name.)
@@ -100,9 +100,9 @@ def _runfiles_subject_contains_at_least(self, paths):
Args:
self: implicitly added.
paths: ((collection of [`str`]) | [`runfiles`]) the paths that must
paths: {type}`collection[str] | runfiles` the paths that must
exist. If a collection of strings is provided, they will be
formatted using [`ExpectMeta.format_str`], so its template keywords
formatted using {type}`ExpectMeta.format_str`, so its template keywords
can be directly passed. If a `runfiles` object is passed, it is
converted to a set of path strings.
"""
@@ -147,8 +147,8 @@ def _runfiles_subject_contains_predicate(self, matcher):
Args:
self: implicitly added.
matcher: callable that takes 1 positional arg ([`str`] path) and returns
boolean.
matcher: {type}`callable` callable that takes 1 positional arg
({type}`str` path) and returns boolean.
"""
check_contains_predicate(
self.actual_paths,
@@ -168,10 +168,10 @@ def _runfiles_subject_contains_exactly(self, paths):
Args:
self: implicitly added.
paths: ([`collection`] of [`str`]) the paths to check. These will be
formatted using `meta.format_str`, so its template keywords can
be directly passed. All the paths must exist in the runfiles exactly
as provided, and no extra paths may exist.
paths: {type}`collection[str]` the paths to check. These will be
formatted using {obj}`ExpectMeta.format_str()`, so its template
keywords can be directly passed. All the paths must exist in the
runfiles exactly as provided, and no extra paths may exist.
"""
paths = [self.meta.format_str(p) for p in to_list(paths)]
runfiles_name = "{}runfiles".format(self.kind + " " if self.kind else "")
@@ -224,11 +224,11 @@ def _runfiles_subject_contains_none_of(self, paths, require_workspace_prefix = T
Args:
self: implicitly added.
paths: ([`collection`] of [`str`]) the paths that should not exist. They should
paths: {type}`collection[str]` the paths that should not exist. They should
be runfiles root-relative paths (not workspace relative). The value
is formatted using `ExpectMeta.format_str` and the current
contextual keywords.
require_workspace_prefix: ([`bool`]) True to check that the path includes the
require_workspace_prefix: {type}`bool` True to check that the path includes the
workspace prefix. This is to guard against accidentallly passing a
workspace relative path, which will (almost) never exist, and cause
the test to always pass. Specify False if the file being checked for
@@ -254,11 +254,11 @@ def _runfiles_subject_not_contains(self, path, require_workspace_prefix = True):
Args:
self: implicitly added.
path: ([`str`]) the path that should not exist. It should be a runfiles
path: {type}`str` the path that should not exist. It should be a runfiles
root-relative path (not workspace relative). The value is formatted
using `format_str`, so its template keywords can be directly
passed.
require_workspace_prefix: ([`bool`]) True to check that the path includes the
require_workspace_prefix: {type}`bool` True to check that the path includes the
workspace prefix. This is to guard against accidentallly passing a
workspace relative path, which will (almost) never exist, and cause
the test to always pass. Specify False if the file being checked for
@@ -284,7 +284,7 @@ def _runfiles_subject_not_contains_predicate(self, matcher):
Args:
self: implicitly added.
matcher: [`Matcher`] that accepts a string (runfiles root-relative path).
matcher: {type}`Matcher` that accepts a string (runfiles root-relative path).
"""
check_not_contains_predicate(self.actual_paths, matcher, meta = self.meta)

@@ -311,9 +311,20 @@ def _runfiles_subject_check_workspace_prefix(self, path):
"require_workspace_prefix=False if the path is truly " +
"runfiles-root relative, not workspace relative.\npath=" + path)

def _runfiles_subject_typedef():
"""Subject for asserting runfiles objects
:::{field} actual
:type: runfiles
Underlying object to assert against.
:::
"""

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
RunfilesSubject = struct(
TYPEDEF = _runfiles_subject_typedef,
new = _runfiles_subject_new,
contains = _runfiles_subject_contains,
contains_at_least = _runfiles_subject_contains_at_least,
33 changes: 26 additions & 7 deletions lib/private/str_subject.bzl
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""# StrSubject"""
"""StrSubject"""

load(
":check_util.bzl",
@@ -27,15 +27,16 @@ def _str_subject_new(actual, meta):
Method: StrSubject.new
Args:
actual: ([`str`]) the string to check against.
meta: ([`ExpectMeta`]) of call chain information.
actual: {type}`str` the string to check against.
meta: {type}`ExpectMeta` of call chain information.
Returns:
[`StrSubject`] object.
{type}`StrSubject` object.
"""
self = struct(actual = actual, meta = meta)
public = struct(
# keep sorted start
actual = actual,
contains = lambda *a, **k: _str_subject_contains(self, *a, **k),
equals = lambda *a, **k: _str_subject_equals(self, *a, **k),
is_in = lambda *a, **k: common_subject_is_in(self, *a, **k),
@@ -52,7 +53,7 @@ def _str_subject_contains(self, substr):
Args:
self: implicitly added.
substr: ([`str`]) the substring to check for.
substr: {type}`str` the substring to check for.
"""
if substr in self.actual:
return
@@ -68,7 +69,7 @@ def _str_subject_equals(self, other):
Args:
self: implicitly added.
other: ([`str`]) the expected value it should equal.
other: {type}`str` the expected value it should equal.
"""
if self.actual == other:
return
@@ -84,7 +85,7 @@ def _str_subject_not_equals(self, unexpected):
Args:
self: implicitly added.
unexpected: ([`str`]) the value actual cannot equal.
unexpected: {type}`str` the value actual cannot equal.
"""
return check_not_equals(
actual = self.actual,
@@ -96,6 +97,13 @@ def _str_subject_split(self, sep):
"""Return a `CollectionSubject` for the actual string split by `sep`.
Method: StrSubject.split
Args:
self: implicitly added.
sep: {type}`str` string to split by
Returns:
{type}`CollectionSubject[str]`
"""
return CollectionSubject.new(
self.actual.split(sep),
@@ -105,9 +113,20 @@ def _str_subject_split(self, sep):
element_plural_name = "parts",
)

def _str_subject_typedef():
"""Subject for asserting strings.
:::{field} actual
:type: str
Underlying object to assert against.
:::
"""

# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
StrSubject = struct(
TYPEDEF = _str_subject_typedef,
new = _str_subject_new,
contains = _str_subject_contains,
equals = _str_subject_equals,
36 changes: 26 additions & 10 deletions lib/private/struct_subject.bzl
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@
# 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
"""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
@@ -63,22 +63,19 @@ def _foo_test(env):
"""

def _struct_subject_new(actual, *, meta, attrs):
"""Creates a `StructSubject`, which is a thin wrapper around a [`struct`].
"""Creates a `StructSubject`, which is a thin wrapper around a {type}`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
actual: {type}`struct` the struct to wrap.
meta: {type}`ExpectMeta` object of call context information.
attrs: {type}`dict[str, 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.
value of the struct, and `meta` is an {type}`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.
{type}`StructSubject` object
"""
attr_accessors = {}
for name, factory in attrs.items():
@@ -100,9 +97,28 @@ def _make_attr_accessor(actual, name, factory, meta):

return attr_accessor

def _struct_subject_typedef():
"""Subject for wrapping arbitrary structs.
:::{field} actual
:type: struct
The underlying struct being asserted against.
:::
:::{field} <various>
:type: callable
A callable attribute exists for each attribute of `actual`. Each callable
takes no args and returns what the corresponding factory from `attrs`
returns.
:::
"""

# buildifier: disable=name-conventions
StructSubject = struct(
# keep sorted start
TYPEDEF = _struct_subject_typedef,
new = _struct_subject_new,
# keep sorted end
)
Loading

0 comments on commit a4f3e53

Please sign in to comment.