aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-09-29 01:06:04 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-09-29 01:06:04 +0000
commit09b3a0d2bb1fd662c14a1e7d8e2b6db9c0481fa8 (patch)
tree329839a67b2b6d2a26d17780d4002e4eee0808fd
parent74bdad7c2e3a8531da040325cd8102a6ec73d9cf (diff)
parentd605057434dcabba796c020773aab68d9790ff9f (diff)
downloadbazelbuild-rules_testing-android14-qpr2-s1-release.tar.gz
Change-Id: I5432c013e363b410651db122ce34f909f023d8bf
-rw-r--r--.bazelci/presubmit.yml12
-rw-r--r--CHANGELOG.md72
-rw-r--r--MODULE.bazel33
-rw-r--r--README.md6
-rw-r--r--RELEASING.md2
-rw-r--r--dev_extension.bzl2
-rw-r--r--docgen/BUILD3
-rw-r--r--docgen/docgen.bzl5
-rw-r--r--docs/crossrefs.md2
-rw-r--r--docs/source/best_practices.md21
-rw-r--r--docs/source/test_suite.md78
-rw-r--r--docs/source/unit_tests.md73
-rw-r--r--lib/BUILD34
-rw-r--r--lib/analysis_test.bzl236
-rw-r--r--lib/private/BUILD27
-rw-r--r--lib/private/analysis_test.bzl193
-rw-r--r--lib/private/collection_subject.bzl124
-rw-r--r--lib/private/default_info_subject.bzl127
-rw-r--r--lib/private/dict_subject.bzl22
-rw-r--r--lib/private/expect.bzl22
-rw-r--r--lib/private/expect_meta.bzl21
-rw-r--r--lib/private/matching.bzl37
-rw-r--r--lib/private/ordered.bzl2
-rw-r--r--lib/private/struct_subject.bzl108
-rw-r--r--lib/private/truth_common.bzl18
-rw-r--r--lib/private/util.bzl35
-rw-r--r--lib/test_suite.bzl64
-rw-r--r--lib/truth.bzl14
-rw-r--r--lib/unit_test.bzl46
-rw-r--r--lib/utils.bzl37
-rw-r--r--tests/BUILD7
-rw-r--r--tests/analysis_test_tests.bzl46
-rw-r--r--tests/default_info_subject/BUILD.bazel17
-rw-r--r--tests/default_info_subject/default_info_subject_tests.bzl126
-rw-r--r--tests/matching/BUILD.bazel3
-rw-r--r--tests/matching/matching_tests.bzl98
-rw-r--r--tests/struct_subject/BUILD.bazel3
-rw-r--r--tests/struct_subject/struct_subject_tests.bzl53
-rw-r--r--tests/test_util.bzl96
-rw-r--r--tests/truth_tests.bzl151
-rw-r--r--tests/unit_test_tests.bzl28
41 files changed, 1763 insertions, 341 deletions
diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index 2ea8c13..8bf06bb 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -16,10 +16,20 @@ tasks:
bazel: latest
test_flags:
- "--enable_bzlmod"
- - "--test_tag_filters=-skip-bzlmod"
+ - "--test_tag_filters=-skip-bzlmod,-docs"
test_targets:
- "..."
+ docs:
+ name: Docs generation
+ platform: ubuntu2004
+ bazel: latest
+ test_flags:
+ - "--enable_bzlmod"
+ test_targets:
+ - "//docgen/..."
+ - "//docs/..."
+
e2e_bzlmod:
platform: ${{platform}}
working_directory: e2e/bzlmod
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..270f10b
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,72 @@
+# rules_testing Changelog
+
+## Unreleased
+
+### Added
+ * Common attributes, such as `tags` and `target_compatible_with` can now
+ be set on tests themselves. This allows skipping tests based on platform
+ or filtering out tests using `--test_tag_filters`
+ ([#43](https://github.com/bazelbuild/rules_testing/issues/43))
+ * DefaultInfoSubject for asserting the builtin DefaultInfo provider
+ ([#52](https://github.com/bazelbuild/rules_testing/issues/52))
+ * StructSubject for asserting arbitrary structs.
+ ([#53](https://github.com/bazelbuild/rules_testing/issues/53))
+ * (docs) Created human-friendly changelog
+
+## [0.3.0] - 2023-07-06
+
+### Added
+ * Publically exposed subjects in `truth.bzl#subjects`. This allows
+ direct creation of subjects without having to go through the
+ `expect.that_*` functions. This makes it easier to implement
+ custom subjects. ([#54](https://github.com/bazelbuild/rules_testing/issues/54))
+ * `matching.file_basename_equals` for matching a File basename.
+ ([#44](https://github.com/bazelbuild/rules_testing/issues/44))
+ * `matching.file_extension_in` for matching a File extension.
+ ([#44](https://github.com/bazelbuild/rules_testing/issues/44))
+ * `DictSubject.get` for fetching sub-values within a dict as subjects.
+ ([#51](https://github.com/bazelbuild/rules_testing/issues/51))
+ * `CollectionSubject.transform` for arbitrary transforming and filtering
+ of a collection.
+ ([#45](https://github.com/bazelbuild/rules_testing/issues/45))
+
+[0.3.0]: https://github.com/bazelbuild/rules_testing/releases/tag/v0.3.0
+
+## [0.2.0] - 2023-06-14
+
+### Added
+ * Unit-test style tests. These are tests that don't require a "setup"
+ phase like analysis tests do, so all you need to write is the
+ implementation function that does asserts.
+ ([#37](https://github.com/bazelbuild/rules_testing/issues/37))
+ * (docs) Document some best practices for test naming and structure.
+
+### Deprecated
+ * `//lib:analysis_test.bzl#test_suite`: use `//lib:test_suite.bzl#test_suite`
+ instead. The name in `analysis_test.bzl` will be removed in a future
+ release.
+
+[0.2.0]: https://github.com/bazelbuild/rules_testing/releases/tag/v0.2.0
+
+## [0.1.0] - 2023-05-02
+
+### Fixed
+ * Don't require downstream user to register Python toolchains.
+ ([#33](https://github.com/bazelbuild/rules_testing/issues/33))
+
+[0.1.0]: https://github.com/bazelbuild/rules_testing/releases/tag/v0.1.0
+
+## [0.0.5] - 2023-04-25
+
+**NOTE: This version is broken with bzlmod**
+
+## Fixed
+ * Fix crash when equal collections with differing orders have
+ `in_order()` checked.
+ ([#29](https://github.com/bazelbuild/rules_testing/issues/29))
+
+## Added
+ * Generated docs with API reference at https://rules-testing.readthedocs.io
+ ([#28](https://github.com/bazelbuild/rules_testing/issues/28))
+
+[0.0.5]: https://github.com/bazelbuild/rules_testing/releases/tag/v0.0.5
diff --git a/MODULE.bazel b/MODULE.bazel
index 515a444..9b60855 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -14,30 +14,30 @@ bazel_dep(name = "rules_license", version = "0.0.4")
# work with bzlmod enabled. This defines the repo so load() works.
bazel_dep(
name = "stardoc",
- version = "0.5.3",
+ version = "0.5.6",
dev_dependency = True,
repo_name = "io_bazel_stardoc",
)
-bazel_dep(name = "rules_python", version = "0.20.0", dev_dependency = True)
+bazel_dep(name = "rules_python", version = "0.22.0", dev_dependency = True)
python = use_extension(
- "@rules_python//python:extensions.bzl",
+ "@rules_python//python/extensions:python.bzl",
"python",
dev_dependency = True,
)
python.toolchain(
- name = "python3_11",
+ name = "python_3_11",
python_version = "3.11",
)
# NOTE: use_repo() must be called for each platform that runs the docgen tools
use_repo(
python,
- "python3_11_toolchains",
- "python3_11_x86_64-unknown-linux-gnu",
+ "python_3_11_toolchains",
+ "python_3_11_x86_64-unknown-linux-gnu",
)
-# NOTE: This is actualy a dev dependency, but due to
+# NOTE: This is actually a dev dependency, but due to
# https://github.com/bazelbuild/bazel/issues/18248 it has to be non-dev to
# generate the repo name used in the subsequent register_toolchains() call.
# Once 6.2 is the minimum supported version, the register_toolchains
@@ -50,15 +50,30 @@ use_repo(dev, "rules_testing_dev_toolchains")
# NOTE: This call will be run by downstream users, so the
# repos it mentions must exist.
-register_toolchains("@rules_testing_dev_toolchains//:all")
+register_toolchains(
+ "@rules_testing_dev_toolchains//:all",
+ dev_dependency = True,
+)
+
+interpreter = use_extension(
+ "@rules_python//python/extensions:interpreter.bzl",
+ "interpreter",
+ dev_dependency = True,
+)
+interpreter.install(
+ name = "python_3_11_interpreter",
+ python_name = "python_3_11",
+)
+use_repo(interpreter, "python_3_11_interpreter")
pip = use_extension(
- "@rules_python//python:extensions.bzl",
+ "@rules_python//python/extensions:pip.bzl",
"pip",
dev_dependency = True,
)
pip.parse(
name = "docs-pypi",
+ python_interpreter_target = "@python_3_11_interpreter//:python",
requirements_lock = "//docs:requirements.txt",
)
use_repo(pip, "docs-pypi")
diff --git a/README.md b/README.md
index 9582b1f..6721801 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
[![Build
status](https://badge.buildkite.com/a82ebafd30ad56e0596dcd3a3a19f36985d064f7f7fb89e21e.svg?branch=master)](https://buildkite.com/bazel/rules-testing)
-# Framworks and utilities for testing Bazel Starlark rules
+# Frameworks and utilities for testing Bazel Starlark
-`rules_testing` provides frameworks and utilities to make testing Starlark rules
+`rules_testing` provides frameworks and utilities to make testing Starlark code
easier and convenient.
-For detailed docs, see the [docs directory](docs/index.md).
+For detailed docs, see the [docs website](https://rules-testing.readthedocs.io)
diff --git a/RELEASING.md b/RELEASING.md
index e2f3df0..72059f2 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -7,5 +7,5 @@ Assuming you have a remote named `upstream` pointing to the repo:
* `git tag v<VERSION> upstream/master && git push upstream --tags`
After pushing, the release action will trigger. It will package it up, create a
-relase on the GitHub release page, and trigger an update to the Bazel Central
+release on the GitHub release page, and trigger an update to the Bazel Central
Registry (BCR).
diff --git a/dev_extension.bzl b/dev_extension.bzl
index 8be534e..8925d26 100644
--- a/dev_extension.bzl
+++ b/dev_extension.bzl
@@ -29,7 +29,7 @@ def _dev_toolchains_repo_impl(rctx):
# If its the root module, then we're in rules_testing and
# it's a dev dependency situation.
if rctx.attr.is_root:
- toolchain_build = Label("@python3_11_toolchains//:BUILD.bazel")
+ toolchain_build = Label("@python_3_11_toolchains//:BUILD.bazel")
# NOTE: This is brittle. It only works because, luckily,
# rules_python's toolchain BUILD file is essentially self-contained.
diff --git a/docgen/BUILD b/docgen/BUILD
index 3acaa53..dbb8391 100644
--- a/docgen/BUILD
+++ b/docgen/BUILD
@@ -43,6 +43,9 @@ 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",
+ "//lib/private:default_info_subject_bzl",
],
+ tags = ["docs"],
)
diff --git a/docgen/docgen.bzl b/docgen/docgen.bzl
index f89328a..1aa2a0f 100644
--- a/docgen/docgen.bzl
+++ b/docgen/docgen.bzl
@@ -29,11 +29,6 @@ def sphinx_stardocs(name, bzl_libraries, **kwargs):
tags)
"""
- # Stardoc doesn't yet work with bzlmod; we can detect this by
- # looking for "@@" vs "@" in labels.
- if "@@" in str(Label("//:X")):
- kwargs["target_compatible_with"] = ["@platforms//:incompatible"]
-
docs = []
for label in bzl_libraries:
lib_name = Label(label).name.replace("_bzl", "")
diff --git a/docs/crossrefs.md b/docs/crossrefs.md
index 59d6be1..8c2106f 100644
--- a/docs/crossrefs.md
+++ b/docs/crossrefs.md
@@ -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
diff --git a/docs/source/best_practices.md b/docs/source/best_practices.md
new file mode 100644
index 0000000..ced3de5
--- /dev/null
+++ b/docs/source/best_practices.md
@@ -0,0 +1,21 @@
+# Best Practices
+
+Here we collection tips and techniques for keeping your tests maintainable and
+avoiding common pitfalls.
+
+### Put each suite of tests in its own sub-package
+
+It's recommended to put a given suite of unit tests in their own sub-package
+(directory with a BUILD file). This is because the names of your test functions
+become target names in the BUILD file, which makes it easier to create name
+conflicts. By moving them into their own package, you don't have to worry about
+unit test function names in one `.bzl` file conflicting with names in another.
+
+### Give test functions private names
+
+It's recommended to give test functions private names, i.e. start with a leading
+underscore. This is because if you forget to add a test to the list of tests (an
+easy mistake to make in a file with many tests), the test won't run, and it'll
+appear as if everything is OK. By using a leading underscore, tools like
+buildifier can detect the unused private function and will warn you that it's
+unused, preventing you from accidentally forgetting it.
diff --git a/docs/source/test_suite.md b/docs/source/test_suite.md
new file mode 100644
index 0000000..2bd9d26
--- /dev/null
+++ b/docs/source/test_suite.md
@@ -0,0 +1,78 @@
+# Test suites
+
+The `test_suite` macro is a front-end for easily instantiating groups of
+Starlark tests. It can handle both analysis tests and unit tests. Under the
+hood, each test is its own target with an aggregating `native.test_suite`
+for the group of tests.
+
+## Basic tests
+
+Basic tests are tests that don't require any custom setup or attributes. This is
+the common case for tests of utility code that doesn't interact with objects
+only available to rules (e.g. Targets). These tests are created using
+`unit_test`.
+
+To write such a test, simply write a `unit_test` compatible function (one that
+accepts `env`) and pass it to `test_suite.basic_tests`.
+
+```starlark
+# BUILD
+
+load(":my_tests.bzl", "my_test_suite")
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+
+my_test_suite(name = "my_tests")
+
+# my_tests.bzl
+
+def _foo_test(env):
+ env.expect.that_str(...).equals(...)
+
+def my_test_suite(name):
+ test_suite(
+ name = name,
+ basic_tests = [
+ _foo_test,
+ ]
+ )
+```
+
+Note that it isn't _required_ to write a custom test suite function, but doing
+so is preferred because it's uncommon for BUILD files to pass around function
+objects, and tools won't be confused by it.
+
+## Regular tests
+
+A regular test is a macro that acts as a setup function and is expected to
+create a target of the given name (which is added to the underlying test suite).
+
+The setup function can perform arbitrary logic, but in the end, it's expected to
+call `unit_test` or `analysis_test` to create a target with the provided name.
+
+If you're writing an `analysis_test`, then you're writing a regular test.
+
+```starlark
+# my_tests.bzl
+def _foo_test(name):
+ analysis_test(
+ name = name,
+ impl = _foo_test_impl,
+ attrs = {"myattr": attr.string(default="default")}
+ )
+
+def _foo_test_impl(env):
+ env.expect.that_str(...).equals(...)
+
+def my_test_suite(name):
+ test_suite(
+ name = name,
+ tests = [
+ _foo_test,
+ ]
+ )
+```
+
+Note that a using a setup function with `unit_test` test is not required to
+define custom attributes; the above is just an example. If you want to define
+custom attributes for every test in a suite, the `test_kwargs` argument of
+`test_suite` can be used to pass additional arguments to all tests in the suite.
diff --git a/docs/source/unit_tests.md b/docs/source/unit_tests.md
new file mode 100644
index 0000000..22ffab9
--- /dev/null
+++ b/docs/source/unit_tests.md
@@ -0,0 +1,73 @@
+# Unit tests
+
+Unit tests are for Starlark code that isn't specific to analysis-phase or
+loading phase cases; usually utility code of some sort. Such tests typically
+don't require a rule `ctx` or instantiating other targets to verify the code
+under test.
+
+To write such a test, simply write a function accepting `env` and pass it to
+`test_suite`. The test suite will pass your verification function to
+`unit_test()` for you.
+
+```starlark
+# BUILD
+
+load(":my_tests.bzl", "my_test_suite")
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+
+my_test_suite(name = "my_tests")
+
+# my_tests.bzl
+
+def _foo_test(env):
+ env.expect.that_str(...).equals(...)
+
+def my_test_suite(name):
+ test_suite(
+ name = name,
+ basic_tests = [
+ _foo_test,
+ ]
+ )
+```
+
+Note that it isn't _required_ to write a custom test suite function, but doing
+so is preferred because it's uncommon for BUILD files to pass around function
+objects, and tools won't be confused by it.
+
+## Customizing setup
+
+If you want to customize the setup (loading phase) of a unit test, e.g. to add
+custom attributes, then you need to write in the same style as an analysis test:
+one function is a verification function, and another function performs setup and
+calls `unit_test()`, passing in the verification function.
+
+Custom tests are like basic tests, except you can hook into the loading phase
+before the actual unit test is defined. Because you control the invocation of
+`unit_test`, you can e.g. define custom attributes specific to the test.
+
+```starlark
+# my_tests.bzl
+def _foo_test(name):
+ unit_test(
+ name = name,
+ impl = _foo_test_impl,
+ attrs = {"myattr": attr.string(default="default")}
+ )
+
+def _foo_test_impl(env):
+ env.expect.that_str(...).equals(...)
+
+def my_test_suite(name):
+ test_suite(
+ name = name,
+ custom_tests = [
+ _foo_test,
+ ]
+ )
+```
+
+Note that a custom test is not required to define custom attributes; the above
+is just an example. If you want to define custom attributes for every test in a
+suite, the `test_kwargs` argument of `test_suite` can be used to pass additional
+arguments to all tests in the suite.
diff --git a/lib/BUILD b/lib/BUILD
index 8d612b3..e2c3af7 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -13,10 +13,12 @@
# limitations under the License.
load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//lib/private:util.bzl", "do_nothing")
licenses(["notice"])
package(
+ default_applicable_licenses = ["//:package_license"],
default_visibility = ["//visibility:private"],
)
@@ -25,7 +27,10 @@ bzl_library(
srcs = ["analysis_test.bzl"],
visibility = ["//visibility:public"],
deps = [
- "//lib:truth_bzl",
+ ":test_suite_bzl",
+ ":truth_bzl",
+ "//lib/private:analysis_test_bzl",
+ "//lib/private:util_bzl",
],
)
@@ -36,11 +41,13 @@ bzl_library(
deps = [
"//lib/private:bool_subject_bzl",
"//lib/private:collection_subject_bzl",
+ "//lib/private:default_info_subject_bzl",
"//lib/private:depset_file_subject_bzl",
"//lib/private:expect_bzl",
"//lib/private:int_subject_bzl",
"//lib/private:label_subject_bzl",
"//lib/private:matching_bzl",
+ "//lib/private:struct_subject_bzl",
],
)
@@ -56,6 +63,25 @@ bzl_library(
],
)
+bzl_library(
+ name = "unit_test_bzl",
+ srcs = ["unit_test.bzl"],
+ visibility = ["//visibility:public"],
+ deps = [
+ "//lib/private:analysis_test_bzl",
+ ],
+)
+
+bzl_library(
+ name = "test_suite_bzl",
+ srcs = ["test_suite.bzl"],
+ visibility = ["//visibility:public"],
+ deps = [
+ ":unit_test_bzl",
+ "//lib/private:util_bzl",
+ ],
+)
+
filegroup(
name = "test_deps",
testonly = True,
@@ -80,3 +106,9 @@ exports_files(
"//docgen:__pkg__",
],
)
+
+# Unit tests need some target because they're based upon analysis tests.
+do_nothing(
+ name = "_stub_target_for_unit_tests",
+ visibility = ["//visibility:public"],
+)
diff --git a/lib/analysis_test.bzl b/lib/analysis_test.bzl
index 02164a4..d8ad2b1 100644
--- a/lib/analysis_test.bzl
+++ b/lib/analysis_test.bzl
@@ -17,237 +17,15 @@
Support for testing analysis phase logic, such as rules.
"""
-load("//lib:truth.bzl", "truth")
-load("//lib:util.bzl", "recursive_testing_aspect", "testing_aspect")
+load("//lib:test_suite.bzl", _test_suite = "test_suite")
+load("//lib/private:analysis_test.bzl", _analysis_test = "analysis_test")
-def _impl_function_name(impl):
- """Derives the name of the given rule implementation function.
+analysis_test = _analysis_test
- This can be used for better test feedback.
+def test_suite(**kwargs):
+ """This is an alias to lib/test_suite.bzl#test_suite.
Args:
- impl: the rule implementation function
-
- Returns:
- The name of the given function
- """
-
- # Starlark currently stringifies a function as "<function NAME>", so we use
- # that knowledge to parse the "NAME" portion out. If this behavior ever
- # changes, we'll need to update this.
- # TODO(bazel-team): Expose a ._name field on functions to avoid this.
- impl_name = str(impl)
- impl_name = impl_name.partition("<function ")[-1]
- impl_name = impl_name.rpartition(">")[0]
- impl_name = impl_name.partition(" ")[0]
-
- # Strip leading/trailing underscores so that test functions can
- # have private names. This better allows unused tests to be flagged by
- # buildifier (indicating a bug or code to delete)
- return impl_name.strip("_")
-
-def _fail(env, msg):
- """Unconditionally causes the current test to fail.
-
- Args:
- env: The test environment returned by `unittest.begin`.
- msg: The message to log describing the failure.
- """
- full_msg = "In test %s: %s" % (env.ctx.attr._impl_name, msg)
-
- # There isn't a better way to output the message in Starlark, so use print.
- # buildifier: disable=print
- print(full_msg)
- env.failures.append(full_msg)
-
-def _begin_analysis_test(ctx):
- """Begins a unit test.
-
- This should be the first function called in a unit test implementation
- function. It initializes a "test environment" that is used to collect
- assertion failures so that they can be reported and logged at the end of the
- test.
-
- Args:
- ctx: The Starlark context. Pass the implementation function's `ctx` argument
- in verbatim.
-
- Returns:
- An analysis_test "environment" struct. The following fields are public:
- * ctx: the underlying rule ctx
- * expect: a truth Expect object (see truth.bzl).
- * fail: A function to register failures for later reporting.
-
- Other attributes are private, internal details and may change at any time. Do not rely
- on internal details.
- """
- target = getattr(ctx.attr, "target")
- target = target[0] if type(target) == type([]) else target
- failures = []
- failures_env = struct(
- ctx = ctx,
- failures = failures,
- )
- truth_env = struct(
- ctx = ctx,
- fail = lambda msg: _fail(failures_env, msg),
- )
- analysis_test_env = struct(
- ctx = ctx,
- # Visibility: package; only exposed so that our own tests can verify
- # failure behavior.
- _failures = failures,
- fail = truth_env.fail,
- expect = truth.expect(truth_env),
- )
- return analysis_test_env, target
-
-def _end_analysis_test(env):
- """Ends an analysis test and logs the results.
-
- This must be called and returned at the end of an analysis test implementation function so
- that the results are reported.
-
- Args:
- env: The test environment returned by `analysistest.begin`.
-
- Returns:
- A list of providers needed to automatically register the analysis test result.
+ **kwargs: Args passed through to test_suite
"""
- return [AnalysisTestResultInfo(
- success = (len(env._failures) == 0),
- message = "\n".join(env._failures),
- )]
-
-def analysis_test(
- name,
- target,
- impl,
- expect_failure = False,
- attrs = {},
- fragments = [],
- config_settings = {},
- extra_target_under_test_aspects = [],
- collect_actions_recursively = False):
- """Creates an analysis test from its implementation function.
-
- An analysis test verifies the behavior of a "real" rule target by examining
- and asserting on the providers given by the real target.
-
- Each analysis test is defined in an implementation function. This function handles
- the boilerplate to create and return a test target and captures the
- implementation function's name so that it can be printed in test feedback.
-
- An example of an analysis test:
-
- ```
- def basic_test(name):
- my_rule(name = name + "_subject", ...)
-
- analysistest(name = name, target = name + "_subject", impl = _your_test)
-
- def _your_test(env, target, actions):
- env.assert_that(target).runfiles().contains_at_least("foo.txt")
- env.assert_that(find_action(actions, generating="foo.txt")).argv().contains("--a")
- ```
-
- Args:
- name: Name of the target. It should be a Starlark identifier, matching pattern
- '[A-Za-z_][A-Za-z0-9_]*'.
- target: The target to test.
- impl: The implementation function of the unit test.
- expect_failure: If true, the analysis test will expect the target
- to fail. Assertions can be made on the underlying failure using truth.expect_failure
- attrs: An optional dictionary to supplement the attrs passed to the
- unit test's `rule()` constructor.
- fragments: An optional list of fragment names that can be used to give rules access to
- language-specific parts of configuration.
- config_settings: A dictionary of configuration settings to change for the target under
- test and its dependencies. This may be used to essentially change 'build flags' for
- the target under test, and may thus be utilized to test multiple targets with different
- flags in a single build. NOTE: When values that are labels (e.g. for the
- --platforms flag), it's suggested to always explicitly call `Label()`
- on the value before passing it in. This ensures the label is resolved
- in your repository's context, not rule_testing's.
- extra_target_under_test_aspects: An optional list of aspects to apply to the target_under_test
- in addition to those set up by default for the test harness itself.
- collect_actions_recursively: If true, runs testing_aspect over all attributes, otherwise
- it is only applied to the target under test.
-
- Returns:
- (None)
- """
-
- attrs = dict(attrs)
- attrs["_impl_name"] = attr.string(default = _impl_function_name(impl))
-
- changed_settings = dict(config_settings)
- if expect_failure:
- changed_settings["//command_line_option:allow_analysis_failures"] = "True"
-
- target_attr_kwargs = {}
- if changed_settings:
- test_transition = analysis_test_transition(
- settings = changed_settings,
- )
- target_attr_kwargs["cfg"] = test_transition
-
- attrs["target"] = attr.label(
- aspects = [recursive_testing_aspect if collect_actions_recursively else testing_aspect] + extra_target_under_test_aspects,
- mandatory = True,
- **target_attr_kwargs
- )
-
- def wrapped_impl(ctx):
- env, target = _begin_analysis_test(ctx)
- impl(env, target)
- return _end_analysis_test(env)
-
- return testing.analysis_test(
- name,
- wrapped_impl,
- attrs = attrs,
- fragments = fragments,
- attr_values = {"target": target},
- )
-
-def test_suite(name, tests, test_kwargs = {}):
- """Instantiates given test macros and gathers their main targets into a `test_suite`.
-
- Use this function to wrap all tests into a single target.
-
- ```
- def simple_test_suite(name):
- test_suite(
- name = name,
- tests = [
- your_test,
- your_other_test,
- ]
- )
- ```
-
- Then, in your `BUILD` file, simply load the macro and invoke it to have all
- of the targets created:
-
- ```
- load("//path/to/your/package:tests.bzl", "simple_test_suite")
- simple_test_suite(name = "simple_test_suite")
- ```
-
- Args:
- name: The name of the `test_suite` target.
- tests: A list of test macros, each taking `name` as a parameter, which
- will be passed the computed name of the test.
- test_kwargs: Additional kwargs to pass onto each test function call.
- """
- test_targets = []
- for call in tests:
- test_name = _impl_function_name(call)
- call(name = test_name, **test_kwargs)
- test_targets.append(test_name)
-
- native.test_suite(
- name = name,
- tests = test_targets,
- )
+ _test_suite(**kwargs)
diff --git a/lib/private/BUILD b/lib/private/BUILD
index 6372128..07f9b99 100644
--- a/lib/private/BUILD
+++ b/lib/private/BUILD
@@ -27,6 +27,11 @@ exports_files(
)
bzl_library(
+ name = "analysis_test_bzl",
+ srcs = ["analysis_test.bzl"],
+)
+
+bzl_library(
name = "matching_bzl",
srcs = ["matching.bzl"],
)
@@ -58,6 +63,7 @@ bzl_library(
":int_subject_bzl",
":matching_bzl",
":truth_common_bzl",
+ ":util_bzl",
],
)
@@ -120,6 +126,16 @@ bzl_library(
)
bzl_library(
+ name = "default_info_subject_bzl",
+ srcs = ["default_info_subject.bzl"],
+ deps = [
+ ":depset_file_subject_bzl",
+ ":file_subject_bzl",
+ ":runfiles_subject_bzl",
+ ],
+)
+
+bzl_library(
name = "depset_file_subject_bzl",
srcs = ["depset_file_subject.bzl"],
deps = [
@@ -210,6 +226,11 @@ bzl_library(
)
bzl_library(
+ name = "struct_subject_bzl",
+ srcs = ["struct_subject.bzl"],
+)
+
+bzl_library(
name = "target_subject_bzl",
srcs = ["target_subject.bzl"],
deps = [
@@ -241,6 +262,12 @@ bzl_library(
":file_subject_bzl",
":int_subject_bzl",
":str_subject_bzl",
+ ":struct_subject_bzl",
":target_subject_bzl",
],
)
+
+bzl_library(
+ name = "util_bzl",
+ srcs = ["util.bzl"],
+)
diff --git a/lib/private/analysis_test.bzl b/lib/private/analysis_test.bzl
new file mode 100644
index 0000000..c4c95ac
--- /dev/null
+++ b/lib/private/analysis_test.bzl
@@ -0,0 +1,193 @@
+# Copyright 2022 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.
+
+"""# Analysis test
+
+Support for testing analysis phase logic, such as rules.
+"""
+
+load("@bazel_skylib//lib:dicts.bzl", "dicts")
+load("//lib:truth.bzl", "truth")
+load("//lib:util.bzl", "recursive_testing_aspect", "testing_aspect")
+load("//lib/private:util.bzl", "get_test_name_from_function")
+
+def _fail(env, msg):
+ """Unconditionally causes the current test to fail.
+
+ Args:
+ env: The test environment returned by `unittest.begin`.
+ msg: The message to log describing the failure.
+ """
+ full_msg = "In test %s: %s" % (env.ctx.attr._impl_name, msg)
+
+ # There isn't a better way to output the message in Starlark, so use print.
+ # buildifier: disable=print
+ print(full_msg)
+ env.failures.append(full_msg)
+
+def _begin_analysis_test(ctx):
+ """Begins a unit test.
+
+ This should be the first function called in a unit test implementation
+ function. It initializes a "test environment" that is used to collect
+ assertion failures so that they can be reported and logged at the end of the
+ test.
+
+ Args:
+ ctx: The Starlark context. Pass the implementation function's `ctx` argument
+ in verbatim.
+
+ Returns:
+ An analysis_test "environment" struct. The following fields are public:
+ * ctx: the underlying rule ctx
+ * expect: a truth Expect object (see truth.bzl).
+ * fail: A function to register failures for later reporting.
+
+ Other attributes are private, internal details and may change at any time. Do not rely
+ on internal details.
+ """
+ target = getattr(ctx.attr, "target")
+ target = target[0] if type(target) == type([]) else target
+ failures = []
+ failures_env = struct(
+ ctx = ctx,
+ failures = failures,
+ )
+ truth_env = struct(
+ ctx = ctx,
+ fail = lambda msg: _fail(failures_env, msg),
+ )
+ analysis_test_env = struct(
+ ctx = ctx,
+ # Visibility: package; only exposed so that our own tests can verify
+ # failure behavior.
+ _failures = failures,
+ fail = truth_env.fail,
+ expect = truth.expect(truth_env),
+ )
+ return analysis_test_env, target
+
+def _end_analysis_test(env):
+ """Ends an analysis test and logs the results.
+
+ This must be called and returned at the end of an analysis test implementation function so
+ that the results are reported.
+
+ Args:
+ env: The test environment returned by `analysistest.begin`.
+
+ Returns:
+ A list of providers needed to automatically register the analysis test result.
+ """
+ return [AnalysisTestResultInfo(
+ success = (len(env._failures) == 0),
+ message = "\n".join(env._failures),
+ )]
+
+def analysis_test(
+ name,
+ target,
+ impl,
+ expect_failure = False,
+ attrs = {},
+ attr_values = {},
+ fragments = [],
+ config_settings = {},
+ extra_target_under_test_aspects = [],
+ collect_actions_recursively = False):
+ """Creates an analysis test from its implementation function.
+
+ An analysis test verifies the behavior of a "real" rule target by examining
+ and asserting on the providers given by the real target.
+
+ Each analysis test is defined in an implementation function. This function handles
+ the boilerplate to create and return a test target and captures the
+ implementation function's name so that it can be printed in test feedback.
+
+ An example of an analysis test:
+
+ ```
+ def basic_test(name):
+ my_rule(name = name + "_subject", ...)
+
+ analysistest(name = name, target = name + "_subject", impl = _your_test)
+
+ def _your_test(env, target, actions):
+ env.assert_that(target).runfiles().contains_at_least("foo.txt")
+ env.assert_that(find_action(actions, generating="foo.txt")).argv().contains("--a")
+ ```
+
+ Args:
+ name: Name of the target. It should be a Starlark identifier, matching pattern
+ '[A-Za-z_][A-Za-z0-9_]*'.
+ target: The target to test.
+ impl: The implementation function of the analysis test.
+ expect_failure: If true, the analysis test will expect the target
+ to fail. Assertions can be made on the underlying failure using truth.expect_failure
+ attrs: An optional dictionary to supplement the attrs passed to the
+ unit test's `rule()` constructor.
+ attr_values: An optional dictionary of kwargs to pass onto the
+ analysis test target itself (e.g. common attributes like `tags`,
+ `target_compatible_with`, or attributes from `attrs`). Note that these
+ are for the analysis test target itself, not the target under test.
+ fragments: An optional list of fragment names that can be used to give rules access to
+ language-specific parts of configuration.
+ config_settings: A dictionary of configuration settings to change for the target under
+ test and its dependencies. This may be used to essentially change 'build flags' for
+ the target under test, and may thus be utilized to test multiple targets with different
+ flags in a single build. NOTE: When values that are labels (e.g. for the
+ --platforms flag), it's suggested to always explicitly call `Label()`
+ on the value before passing it in. This ensures the label is resolved
+ in your repository's context, not rule_testing's.
+ extra_target_under_test_aspects: An optional list of aspects to apply to the target_under_test
+ in addition to those set up by default for the test harness itself.
+ collect_actions_recursively: If true, runs testing_aspect over all attributes, otherwise
+ it is only applied to the target under test.
+
+ Returns:
+ (None)
+ """
+
+ attrs = dict(attrs)
+ attrs["_impl_name"] = attr.string(default = get_test_name_from_function(impl))
+
+ changed_settings = dict(config_settings)
+ if expect_failure:
+ changed_settings["//command_line_option:allow_analysis_failures"] = "True"
+
+ target_attr_kwargs = {}
+ if changed_settings:
+ test_transition = analysis_test_transition(
+ settings = changed_settings,
+ )
+ target_attr_kwargs["cfg"] = test_transition
+
+ attrs["target"] = attr.label(
+ aspects = [recursive_testing_aspect if collect_actions_recursively else testing_aspect] + extra_target_under_test_aspects,
+ mandatory = True,
+ **target_attr_kwargs
+ )
+
+ def wrapped_impl(ctx):
+ env, target = _begin_analysis_test(ctx)
+ impl(env, target)
+ return _end_analysis_test(env)
+
+ return testing.analysis_test(
+ name,
+ wrapped_impl,
+ attrs = attrs,
+ fragments = fragments,
+ attr_values = dicts.add(attr_values, {"target": target}),
+ )
diff --git a/lib/private/collection_subject.bzl b/lib/private/collection_subject.bzl
index 8b093eb..6d72efe 100644
--- a/lib/private/collection_subject.bzl
+++ b/lib/private/collection_subject.bzl
@@ -35,6 +35,14 @@ load(
load(":int_subject.bzl", "IntSubject")
load(":matching.bzl", "matching")
load(":truth_common.bzl", "to_list")
+load(":util.bzl", "get_function_name")
+
+def _identity(v):
+ return v
+
+def _always_true(v):
+ _ = v # @unused
+ return True
def _collection_subject_new(
values,
@@ -64,7 +72,6 @@ def _collection_subject_new(
public = struct(
# keep sorted start
actual = values,
- has_size = lambda *a, **k: _collection_subject_has_size(self, *a, **k),
contains = lambda *a, **k: _collection_subject_contains(self, *a, **k),
contains_at_least = lambda *a, **k: _collection_subject_contains_at_least(self, *a, **k),
contains_at_least_predicates = lambda *a, **k: _collection_subject_contains_at_least_predicates(self, *a, **k),
@@ -72,8 +79,11 @@ def _collection_subject_new(
contains_exactly_predicates = lambda *a, **k: _collection_subject_contains_exactly_predicates(self, *a, **k),
contains_none_of = lambda *a, **k: _collection_subject_contains_none_of(self, *a, **k),
contains_predicate = lambda *a, **k: _collection_subject_contains_predicate(self, *a, **k),
+ has_size = lambda *a, **k: _collection_subject_has_size(self, *a, **k),
not_contains = lambda *a, **k: _collection_subject_not_contains(self, *a, **k),
not_contains_predicate = lambda *a, **k: _collection_subject_not_contains_predicate(self, *a, **k),
+ offset = lambda *a, **k: _collection_subject_offset(self, *a, **k),
+ transform = lambda *a, **k: _collection_subject_transform(self, *a, **k),
# keep sorted end
)
self = struct(
@@ -334,17 +344,121 @@ def _collection_subject_not_contains_predicate(self, matcher):
sort = self.sortable,
)
+def _collection_subject_offset(self, offset, factory):
+ """Fetches an element from the collection as a subject.
+
+ Args:
+ self: implicitly added.
+ offset: ([`int`]) the offset to fetch
+ factory: ([`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)`.
+
+ Returns:
+ Object created by `factory`.
+ """
+ value = self.actual[offset]
+ return factory(
+ value,
+ meta = self.meta.derive("offset({})".format(offset)),
+ )
+
+def _collection_subject_transform(
+ self,
+ desc = None,
+ *,
+ map_each = None,
+ loop = None,
+ filter = None):
+ """Transforms a collections's value and returns another CollectionSubject.
+
+ This is equivalent to applying a list comprehension over the collection values,
+ but takes care of propagating context information and wrapping the value
+ in a `CollectionSubject`.
+
+ `transform(map_each=M, loop=L, filter=F)` is equivalent to
+ `[M(v) for v in L(collection) if F(v)]`.
+
+ Args:
+ self: implicitly added.
+ desc: (optional [`str`]) 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
+ 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
+ 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
+ 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.
+ """
+ if not desc:
+ if map_each or loop:
+ fail("description required when map_each or loop used")
+
+ if matching.is_matcher(filter):
+ desc = "filter=" + filter.desc
+ else:
+ func_name = get_function_name(filter)
+ if func_name == "lambda":
+ fail("description required: description cannot be " +
+ "inferred from lambdas. Explicitly specify the " +
+ "description, use a named function for the filter, " +
+ "or use a Matcher for the filter.")
+ else:
+ desc = "filter={}(...)".format(func_name)
+
+ map_each = map_each or _identity
+ loop = loop or _identity
+
+ if filter:
+ if matching.is_matcher(filter):
+ filter_func = filter.match
+ else:
+ filter_func = filter
+ else:
+ filter_func = _always_true
+
+ new_values = [map_each(v) for v in loop(self.actual) if filter_func(v)]
+
+ return _collection_subject_new(
+ new_values,
+ meta = self.meta.derive(
+ "transform()",
+ details = ["transform: {}".format(desc)],
+ ),
+ container_name = self.container_name,
+ sortable = self.sortable,
+ element_plural_name = self.element_plural_name,
+ )
+
# We use this name so it shows up nice in docs.
# buildifier: disable=name-conventions
CollectionSubject = struct(
- new = _collection_subject_new,
- has_size = _collection_subject_has_size,
+ # keep sorted start
contains = _collection_subject_contains,
+ contains_at_least = _collection_subject_contains_at_least,
+ contains_at_least_predicates = _collection_subject_contains_at_least_predicates,
contains_exactly = _collection_subject_contains_exactly,
contains_exactly_predicates = _collection_subject_contains_exactly_predicates,
contains_none_of = _collection_subject_contains_none_of,
contains_predicate = _collection_subject_contains_predicate,
- contains_at_least = _collection_subject_contains_at_least,
- contains_at_least_predicates = _collection_subject_contains_at_least_predicates,
+ has_size = _collection_subject_has_size,
+ new = _collection_subject_new,
not_contains_predicate = _collection_subject_not_contains_predicate,
+ offset = _collection_subject_offset,
+ transform = _collection_subject_transform,
+ # keep sorted end
)
diff --git a/lib/private/default_info_subject.bzl b/lib/private/default_info_subject.bzl
new file mode 100644
index 0000000..3a66a48
--- /dev/null
+++ b/lib/private/default_info_subject.bzl
@@ -0,0 +1,127 @@
+# 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.
+
+"""# DefaultInfoSubject"""
+
+load(":runfiles_subject.bzl", "RunfilesSubject")
+load(":depset_file_subject.bzl", "DepsetFileSubject")
+load(":file_subject.bzl", "FileSubject")
+
+def _default_info_subject_new(info, *, meta):
+ """Creates a `DefaultInfoSubject`
+
+ Args:
+ info: ([`DefaultInfo`]) the DefaultInfo object to wrap.
+ meta: ([`ExpectMeta`]) call chain information.
+
+ Returns:
+ [`DefaultInfoSubject`] object.
+ """
+ self = struct(actual = info, meta = meta)
+ public = struct(
+ # keep sorted start
+ actual = info,
+ runfiles = lambda *a, **k: _default_info_subject_runfiles(self, *a, **k),
+ data_runfiles = lambda *a, **k: _default_info_subject_data_runfiles(self, *a, **k),
+ default_outputs = lambda *a, **k: _default_info_subject_default_outputs(self, *a, **k),
+ executable = lambda *a, **k: _default_info_subject_executable(self, *a, **k),
+ runfiles_manifest = lambda *a, **k: _default_info_subject_runfiles_manifest(self, *a, **k),
+ # keep sorted end
+ )
+ return public
+
+def _default_info_subject_runfiles(self):
+ """Creates a subject for the default runfiles.
+
+ Args:
+ self: implicitly added.
+
+ Returns:
+ [`RunfilesSubject`] object
+ """
+ return RunfilesSubject.new(
+ self.actual.default_runfiles,
+ meta = self.meta.derive("runfiles()"),
+ kind = "default",
+ )
+
+def _default_info_subject_data_runfiles(self):
+ """Creates a subject for the data runfiles.
+
+ Args:
+ self: implicitly added.
+
+ Returns:
+ [`RunfilesSubject`] object
+ """
+ return RunfilesSubject.new(
+ self.actual.data_runfiles,
+ meta = self.meta.derive("data_runfiles()"),
+ kind = "data",
+ )
+
+def _default_info_subject_default_outputs(self):
+ """Creates a subject for the default outputs.
+
+ Args:
+ self: implicitly added.
+
+ Returns:
+ [`DepsetFileSubject`] object.
+ """
+ return DepsetFileSubject.new(
+ self.actual.files,
+ meta = self.meta.derive("default_outputs()"),
+ )
+
+def _default_info_subject_executable(self):
+ """Creates a subject for the executable file.
+
+ Args:
+ self: implicitly added.
+
+ Returns:
+ [`FileSubject`] object.
+ """
+ return FileSubject.new(
+ self.actual.files_to_run.executable,
+ meta = self.meta.derive("executable()"),
+ )
+
+def _default_info_subject_runfiles_manifest(self):
+ """Creates a subject for the runfiles manifest.
+
+ Args:
+ self: implicitly added.
+
+ Returns:
+ [`FileSubject`] object.
+ """
+ return FileSubject.new(
+ self.actual.files_to_run.runfiles_manifest,
+ meta = self.meta.derive("runfiles_manifest()"),
+ )
+
+# We use this name so it shows up nice in docs.
+# buildifier: disable=name-conventions
+DefaultInfoSubject = struct(
+ # keep sorted start
+ new = _default_info_subject_new,
+ runfiles = _default_info_subject_runfiles,
+ data_runfiles = _default_info_subject_data_runfiles,
+ default_outputs = _default_info_subject_default_outputs,
+ executable = _default_info_subject_executable,
+ runfiles_manifest = _default_info_subject_runfiles_manifest,
+ # keep sorted end
+)
diff --git a/lib/private/dict_subject.bzl b/lib/private/dict_subject.bzl
index 48d9463..f155a17 100644
--- a/lib/private/dict_subject.bzl
+++ b/lib/private/dict_subject.bzl
@@ -39,10 +39,13 @@ def _dict_subject_new(actual, meta, container_name = "dict", key_plural_name = "
# buildifier: disable=uninitialized
public = struct(
+ # keep sorted start
contains_exactly = lambda *a, **k: _dict_subject_contains_exactly(self, *a, **k),
contains_at_least = lambda *a, **k: _dict_subject_contains_at_least(self, *a, **k),
contains_none_of = lambda *a, **k: _dict_subject_contains_none_of(self, *a, **k),
+ get = lambda *a, **k: _dict_subject_get(self, *a, **k),
keys = lambda *a, **k: _dict_subject_keys(self, *a, **k),
+ # keep sorted end
)
self = struct(
actual = actual,
@@ -152,6 +155,25 @@ def _dict_subject_contains_none_of(self, none_of):
actual = "actual: {{\n{}\n}}".format(format_dict_as_lines(self.actual)),
)
+def _dict_subject_get(self, key, *, factory):
+ """Gets `key` from the actual dict wrapped in a subject.
+
+ Args:
+ self: implicitly added.
+ key: ([`object`]) the key to fetch.
+ factory: ([`callable`]) subject factory function, with the signature
+ of `def factory(value, *, meta)`, and returns the wrapped value.
+
+ Returns:
+ The return value of the `factory` arg.
+ """
+ if key not in self.actual:
+ fail("KeyError: '{key}' not found in {expr}".format(
+ key = key,
+ expr = self.meta.current_expr(),
+ ))
+ return factory(self.actual[key], meta = self.meta.derive("get({})".format(key)))
+
def _dict_subject_keys(self):
"""Returns a `CollectionSubject` for the dict's keys.
diff --git a/lib/private/expect.bzl b/lib/private/expect.bzl
index e568a54..ab90fd9 100644
--- a/lib/private/expect.bzl
+++ b/lib/private/expect.bzl
@@ -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):
@@ -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
@@ -120,18 +122,19 @@ def _expect_that_bool(self, value, expr = "boolean"):
meta = self.meta.derive(expr = expr),
)
-def _expect_that_collection(self, collection, expr = "collection"):
+def _expect_that_collection(self, collection, expr = "collection", **kwargs):
"""Creates a subject for asserting collections.
Args:
self: implicitly added.
collection: The collection (list or depset) to assert.
expr: ([`str`]) the starting "value of" expression to report in errors.
+ **kwargs: Additional kwargs to pass onto CollectionSubject.new
Returns:
[`CollectionSubject`] object.
"""
- return CollectionSubject.new(collection, self.meta.derive(expr))
+ return CollectionSubject.new(collection, self.meta.derive(expr), **kwargs)
def _expect_that_depset_of_files(self, depset_files):
"""Creates a subject for asserting a depset of files.
@@ -206,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`.
@@ -256,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,
@@ -266,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
)
diff --git a/lib/private/expect_meta.bzl b/lib/private/expect_meta.bzl
index 8ce9f1e..efe59fc 100644
--- a/lib/private/expect_meta.bzl
+++ b/lib/private/expect_meta.bzl
@@ -36,7 +36,7 @@ def _expect_meta_new(env, exprs = [], details = [], format_str_kwargs = None):
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 usuable by by rules_testing's analysis_test and skylib's
+ makes it usable by by rules_testing's analysis_test and skylib's
analysistest)
The `env` object requires the following attribute:
@@ -51,7 +51,7 @@ def _expect_meta_new(env, exprs = [], details = [], format_str_kwargs = None):
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 provder value. This is used to implement
+ provider and returns the provider value. This is used to implement
`target[Provider]`.
Args:
@@ -77,6 +77,7 @@ def _expect_meta_new(env, exprs = [], details = [], format_str_kwargs = None):
ctx = env.ctx,
env = env,
add_failure = lambda *a, **k: _expect_meta_add_failure(self, *a, **k),
+ current_expr = lambda *a, **k: _expect_meta_current_expr(self, *a, **k),
derive = lambda *a, **k: _expect_meta_derive(self, *a, **k),
format_str = lambda *a, **k: _expect_meta_format_str(self, *a, **k),
get_provider = lambda *a, **k: _expect_meta_get_provider(self, *a, **k),
@@ -233,7 +234,7 @@ def _expect_meta_add_failure(self, problem, actual):
if detail
])
if details:
- details = "where...\n" + details
+ details = "where... (most recent context last)\n" + details
msg = """\
in test: {test}
value of: {expr}
@@ -242,13 +243,25 @@ value of: {expr}
{details}
""".format(
test = self.ctx.label,
- expr = ".".join(self._exprs),
+ expr = _expect_meta_current_expr(self),
problem = problem,
actual = actual,
details = details,
)
_expect_meta_call_fail(self, msg)
+def _expect_meta_current_expr(self):
+ """Get a string representing the current expression.
+
+ Args:
+ self: implicitly added.
+
+ Returns:
+ [`str`] A string representing the current expression, e.g.
+ "foo.bar(something).baz()"
+ """
+ return ".".join(self._exprs)
+
def _expect_meta_call_fail(self, msg):
"""Adds a failure to the test run.
diff --git a/lib/private/matching.bzl b/lib/private/matching.bzl
index 6093488..9bd6610 100644
--- a/lib/private/matching.bzl
+++ b/lib/private/matching.bzl
@@ -79,6 +79,37 @@ def _match_file_path_matches(pattern):
match = lambda f: _match_parts_in_order(f.path, parts),
)
+def _match_file_basename_equals(value):
+ """Match that a `File.basename` string equals `value`.
+
+ Args:
+ value: ([`str`]) the basename to match.
+
+ Returns:
+ [`Matcher`] instance
+ """
+ return struct(
+ desc = "<file basename equals '{}'>".format(value),
+ match = lambda f: f.basename == value,
+ )
+
+def _match_file_extension_in(values):
+ """Match that a `File.extension` string is any of `values`.
+
+ See also: `file_path_matches` for matching extensions that
+ have multiple parts, e.g. `*.tar.gz` or `*.so.*`.
+
+ Args:
+ values: ([`list`] of [`str`]) the extensions to match.
+
+ Returns:
+ [`Matcher`] instance
+ """
+ return struct(
+ desc = "<file extension is any of {}>".format(repr(values)),
+ match = lambda f: f.extension in values,
+ )
+
def _match_is_in(values):
"""Match that the to-be-matched value is in a collection of other values.
@@ -183,6 +214,9 @@ def _match_parts_in_order(string, parts):
return False
return True
+def _is_matcher(obj):
+ return hasattr(obj, "desc") and hasattr(obj, "match")
+
# For the definition of a `Matcher` object, see `_match_custom`.
matching = struct(
# keep sorted start
@@ -190,11 +224,14 @@ matching = struct(
custom = _match_custom,
equals_wrapper = _match_equals_wrapper,
file_basename_contains = _match_file_basename_contains,
+ file_basename_equals = _match_file_basename_equals,
file_path_matches = _match_file_path_matches,
+ file_extension_in = _match_file_extension_in,
is_in = _match_is_in,
never = _match_never,
str_endswith = _match_str_endswith,
str_matches = _match_str_matches,
str_startswith = _match_str_startswith,
+ is_matcher = _is_matcher,
# keep sorted end
)
diff --git a/lib/private/ordered.bzl b/lib/private/ordered.bzl
index c9a0ed9..dec2662 100644
--- a/lib/private/ordered.bzl
+++ b/lib/private/ordered.bzl
@@ -31,7 +31,7 @@ def _ordered_incorrectly_new(format_problem, format_actual, meta):
Args:
format_problem: (callable) accepts no args and returns string (the
reported problem description).
- format_actual: (callable) accepts not args and returns tring (the
+ format_actual: (callable) accepts not args and returns string (the
reported actual description).
meta: ([`ExpectMeta`]) used to report the failure.
diff --git a/lib/private/struct_subject.bzl b/lib/private/struct_subject.bzl
new file mode 100644
index 0000000..7822341
--- /dev/null
+++ b/lib/private/struct_subject.bzl
@@ -0,0 +1,108 @@
+# Copyright 2023 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""# StructSubject
+
+A subject for arbitrary structs. This is most useful when wrapping an ad-hoc
+struct (e.g. a struct specific to a particular function). Such ad-hoc structs
+are usually just plain data objects, so they don't need special functionality
+that writing a full custom subject allows. If a struct would benefit from
+custom accessors or asserts, write a custom subject instead.
+
+This subject is usually used as a helper to a more formally defined subject that
+knows the shape of the struct it needs to wrap. For example, a `FooInfoSubject`
+implementation might use it to handle `FooInfo.struct_with_a_couple_fields`.
+
+Note the resulting subject object is not a direct replacement for the struct
+being wrapped:
+ * Structs wrapped by this subject have the attributes exposed as functions,
+ not as plain attributes. This matches the other subject classes and defers
+ converting an attribute to a subject unless necessary.
+ * The attribute name `actual` is reserved.
+
+
+## Example usages
+
+To use it as part of a custom subject returning a sub-value, construct it using
+`subjects.struct()` like so:
+
+```starlark
+load("@rules_testing//lib:truth.bzl", "subjects")
+
+def _my_subject_foo(self):
+ return subjects.struct(
+ self.actual.foo,
+ meta = self.meta.derive("foo()"),
+ attrs = dict(a=subjects.int, b=subjects.str),
+ )
+```
+
+If you're checking a struct directly in a test, then you can use
+`Expect.that_struct`. You'll still have to pass the `attrs` arg so it knows how
+to map the attributes to the matching subject factories.
+
+```starlark
+def _foo_test(env):
+ actual = env.expect.that_struct(
+ struct(a=1, b="x"),
+ attrs = dict(a=subjects.int, b=subjects.str)
+ )
+ actual.a().equals(1)
+ actual.b().equals("x")
+```
+"""
+
+def _struct_subject_new(actual, *, meta, attrs):
+ """Creates a `StructSubject`, which is a thin wrapper around a [`struct`].
+
+ Args:
+ actual: ([`struct`]) the struct to wrap.
+ meta: ([`ExpectMeta`]) object of call context information.
+ attrs: ([`dict`] of [`str`] to [`callable`]) the functions to convert
+ attributes to subjects. The keys are attribute names that must
+ exist on `actual`. The values are functions with the signature
+ `def factory(value, *, meta)`, where `value` is the actual attribute
+ value of the struct, and `meta` is an [`ExpectMeta`] object.
+
+ Returns:
+ [`StructSubject`] object, which is a struct with the following shape:
+ * `actual` attribute, the underlying struct that was wrapped.
+ * A callable attribute for each `attrs` entry; it takes no args
+ and returns what the corresponding factory from `attrs` returns.
+ """
+ attr_accessors = {}
+ for name, factory in attrs.items():
+ if not hasattr(actual, name):
+ fail("Struct missing attribute: '{}' (from expression {})".format(
+ name,
+ meta.current_expr(),
+ ))
+ attr_accessors[name] = _make_attr_accessor(actual, name, factory, meta)
+
+ public = struct(actual = actual, **attr_accessors)
+ return public
+
+def _make_attr_accessor(actual, name, factory, meta):
+ # A named function is used instead of a lambda so stack traces are easier to
+ # grok.
+ def attr_accessor():
+ return factory(getattr(actual, name), meta = meta.derive(name + "()"))
+
+ return attr_accessor
+
+# buildifier: disable=name-conventions
+StructSubject = struct(
+ # keep sorted start
+ new = _struct_subject_new,
+ # keep sorted end
+)
diff --git a/lib/private/truth_common.bzl b/lib/private/truth_common.bzl
index c7e6b60..1916901 100644
--- a/lib/private/truth_common.bzl
+++ b/lib/private/truth_common.bzl
@@ -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")
@@ -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:
@@ -84,7 +100,7 @@ def maybe_sorted(container, allow_sorting = True):
Args:
container: ([`list`] | (or other object convertible to list))
allow_sorting: ([`bool`]) whether to sort even if it can be sorted. This
- is primarly so that callers can avoid boilerplate when they have
+ is primarily so that callers can avoid boilerplate when they have
a "should it be sorted" arg, but also always convert to a list.
Returns:
diff --git a/lib/private/util.bzl b/lib/private/util.bzl
new file mode 100644
index 0000000..fc003f9
--- /dev/null
+++ b/lib/private/util.bzl
@@ -0,0 +1,35 @@
+"""Shared private utilities."""
+
+def _do_nothing_impl(ctx):
+ _ = ctx # @unused
+ return []
+
+do_nothing = rule(implementation = _do_nothing_impl)
+
+def get_test_name_from_function(func):
+ """Derives a suitable test name from a function.
+
+ This can be used for better test feedback.
+
+ Args:
+ func: (callable) A test implementation or setup function.
+
+ Returns:
+ (str) The name of the given function, suitable as a test name.
+ """
+
+ # Starlark currently stringifies a function as "<function NAME>", so we use
+ # that knowledge to parse the "NAME" portion out. If this behavior ever
+ # changes, we'll need to update this.
+ # TODO(bazel-team): Expose a ._name field on functions to avoid this.
+ func_name = str(func)
+ func_name = func_name.partition("<function ")[-1]
+ func_name = func_name.rpartition(">")[0]
+ func_name = func_name.partition(" ")[0]
+
+ # Strip leading/trailing underscores so that test functions can
+ # have private names. This better allows unused tests to be flagged by
+ # buildifier (indicating a bug or code to delete)
+ return func_name.strip("_")
+
+get_function_name = get_test_name_from_function
diff --git a/lib/test_suite.bzl b/lib/test_suite.bzl
new file mode 100644
index 0000000..d26c02f
--- /dev/null
+++ b/lib/test_suite.bzl
@@ -0,0 +1,64 @@
+"""# Test suite
+
+Aggregates multiple Starlark tests in a single test_suite.
+"""
+
+load("//lib/private:util.bzl", "get_test_name_from_function")
+load("//lib:unit_test.bzl", "unit_test")
+
+def test_suite(name, *, tests = [], basic_tests = [], test_kwargs = {}):
+ """Instantiates given test macros/implementations and gathers their main targets into a `test_suite`.
+
+ Use this function to wrap all tests into a single target.
+
+ ```
+ def simple_test_suite(name):
+ test_suite(
+ name = name,
+ tests = [
+ your_test,
+ your_other_test,
+ ]
+ )
+ ```
+
+ Then, in your `BUILD` file, simply load the macro and invoke it to have all
+ of the targets created:
+
+ ```
+ load("//path/to/your/package:tests.bzl", "simple_test_suite")
+ simple_test_suite(name = "simple_test_suite")
+ ```
+
+ Args:
+ name: (str) The name of the suite
+ tests: (list of callables) Test macros functions that
+ define a test. The signature is `def setup(name, **test_kwargs)`,
+ where (positional) `name` is name of the test target that must be
+ created, and `**test_kwargs` are the additional arguments from the
+ test suite's `test_kwargs` arg. The name of the function will
+ become the name of the test.
+ basic_tests: (list of callables) Test implementation functions
+ (functions that implement a test's asserts). Each callable takes a
+ single positional arg, `env`, which is information about the test
+ environment (see analysis_test docs). The name of the function will
+ become the name of the test.
+ test_kwargs: (dict) Additional kwargs to pass onto each test (both
+ regular and basic test callables).
+ """
+ test_targets = []
+
+ for setup_func in tests:
+ test_name = get_test_name_from_function(setup_func)
+ setup_func(name = test_name, **test_kwargs)
+ test_targets.append(test_name)
+
+ for impl in basic_tests:
+ test_name = get_test_name_from_function(impl)
+ unit_test(name = test_name, impl = impl, **test_kwargs)
+ test_targets.append(test_name)
+
+ native.test_suite(
+ name = name,
+ tests = test_targets,
+ )
diff --git a/lib/truth.bzl b/lib/truth.bzl
index 95f1fdd..3072f65 100644
--- a/lib/truth.bzl
+++ b/lib/truth.bzl
@@ -44,11 +44,18 @@ def foo_test(env, target):
load("//lib/private:bool_subject.bzl", "BoolSubject")
load("//lib/private:collection_subject.bzl", "CollectionSubject")
+load("//lib/private:default_info_subject.bzl", "DefaultInfoSubject")
load("//lib/private:depset_file_subject.bzl", "DepsetFileSubject")
+load("//lib/private:dict_subject.bzl", "DictSubject")
load("//lib/private:expect.bzl", "Expect")
+load("//lib/private:file_subject.bzl", "FileSubject")
load("//lib/private:int_subject.bzl", "IntSubject")
load("//lib/private:label_subject.bzl", "LabelSubject")
+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.
@@ -63,8 +70,15 @@ subjects = struct(
# keep sorted start
bool = BoolSubject.new,
collection = CollectionSubject.new,
+ default_info = DefaultInfoSubject.new,
depset_file = DepsetFileSubject.new,
+ dict = DictSubject.new,
+ file = FileSubject.new,
int = IntSubject.new,
label = LabelSubject.new,
+ runfiles = RunfilesSubject.new,
+ str = StrSubject.new,
+ struct = StructSubject.new,
+ target = TargetSubject.new,
# keep sorted end
)
diff --git a/lib/unit_test.bzl b/lib/unit_test.bzl
new file mode 100644
index 0000000..ddbf4d7
--- /dev/null
+++ b/lib/unit_test.bzl
@@ -0,0 +1,46 @@
+# Copyright 2022 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.
+"""# Unit test
+
+Support for testing generic Starlark code, i.e. code that doesn't require
+the analysis phase or instantiate rules.
+"""
+
+# We have to load the private impl to avoid a circular dependency
+load("//lib/private:analysis_test.bzl", "analysis_test")
+
+_TARGET = Label("//lib:_stub_target_for_unit_tests")
+
+def unit_test(name, impl, attrs = {}):
+ """Creates a test for generic Starlark code (i.e. non-rule/macro specific).
+
+ Unless you need custom attributes passed to the test, you probably don't need
+ this and can, instead, pass your test function directly to `test_suite.tests`.
+
+ See also: analysis_test, for testing analysis time behavior, such as rules.
+
+ Args:
+ name: (str) the name of the test
+ impl: (callable) the function implementing the test's asserts. It takes
+ a single position arg, `env`, which is information about the
+ test environment (see analysis_test docs).
+ attrs: (dict of str to str) additional attributes to make available to
+ the test.
+ """
+ analysis_test(
+ name = name,
+ impl = lambda env, target: impl(env),
+ target = _TARGET,
+ attrs = attrs,
+ )
diff --git a/lib/utils.bzl b/lib/utils.bzl
deleted file mode 100644
index ee41485..0000000
--- a/lib/utils.bzl
+++ /dev/null
@@ -1,37 +0,0 @@
-# Copyright 2022 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.
-
-"""Utility functions to use in analysis tests."""
-
-def find_action(env, artifact):
- """Finds the action generating the artifact.
-
- Args:
- env: The testing environment
- artifact: a File or a string
- Returns:
- The action"""
-
- if type(artifact) == type(""):
- basename = env.target.label.package + "/" + artifact.format(
- name = env.target.label.name,
- )
- else:
- basename = artifact.short_path
-
- for action in env.actions:
- for file in action.actual.outputs.to_list():
- if file.short_path == basename:
- return action
- return None
diff --git a/tests/BUILD b/tests/BUILD
index 8049732..8341db6 100644
--- a/tests/BUILD
+++ b/tests/BUILD
@@ -16,6 +16,7 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
load("@bazel_skylib//rules:build_test.bzl", "build_test")
load(":analysis_test_tests.bzl", "analysis_test_test_suite")
load(":truth_tests.bzl", "truth_test_suite")
+load(":unit_test_tests.bzl", "unit_test_test_suite")
licenses(["notice"])
@@ -43,9 +44,15 @@ analysis_test_test_suite(name = "analysis_test_test_suite")
truth_test_suite(name = "truth_tests")
+unit_test_test_suite(name = "unit_test_test_suite")
+
build_test(
name = "build_tests",
targets = [
"//lib:util_bzl",
+ "//lib:unit_test_bzl",
+ "//lib:analysis_test_bzl",
+ "//lib:test_suite_bzl",
+ "//lib:truth_bzl",
],
)
diff --git a/tests/analysis_test_tests.bzl b/tests/analysis_test_tests.bzl
index 61350b0..2592a81 100644
--- a/tests/analysis_test_tests.bzl
+++ b/tests/analysis_test_tests.bzl
@@ -209,11 +209,57 @@ def _inspect_output_dirs_fake_rule(ctx):
inspect_output_dirs_fake_rule = rule(implementation = _inspect_output_dirs_fake_rule)
+########################################
+####### common_attributes_test #######
+########################################
+
+def _test_common_attributes(name):
+ native.filegroup(name = name + "_subject")
+ _toolchain_template_vars(name = name + "_toolchain_template_vars")
+ analysis_test(
+ name = name,
+ impl = _test_common_attributes_impl,
+ target = name + "_subject",
+ attr_values = dict(
+ features = ["some-feature"],
+ tags = ["taga", "tagb"],
+ visibility = ["//visibility:private"],
+ toolchains = [name + "_toolchain_template_vars"],
+ # An empty list means "compatible with everything"
+ target_compatible_with = [],
+ ),
+ )
+
+def _test_common_attributes_impl(env, target):
+ _ = target # @unused
+ ctx = env.ctx
+ expect = env.expect
+
+ expect.that_collection(ctx.attr.tags).contains_at_least(["taga", "tagb"])
+
+ expect.that_collection(ctx.attr.features).contains_exactly(["some-feature"])
+
+ expect.that_collection(ctx.attr.visibility).contains_exactly([
+ Label("//visibility:private"),
+ ])
+
+ expect.that_collection(ctx.attr.target_compatible_with).contains_exactly([])
+
+ expanded = ctx.expand_make_variables("cmd", "$(key)", {})
+ expect.that_str(expanded).equals("value")
+
+def _toolchain_template_vars_impl(ctx):
+ _ = ctx # @unused
+ return [platform_common.TemplateVariableInfo({"key": "value"})]
+
+_toolchain_template_vars = rule(implementation = _toolchain_template_vars_impl)
+
def analysis_test_test_suite(name):
test_suite(
name = name,
tests = [
test_change_setting,
+ _test_common_attributes,
test_failure_testing,
test_change_setting_with_failure,
test_inspect_actions,
diff --git a/tests/default_info_subject/BUILD.bazel b/tests/default_info_subject/BUILD.bazel
new file mode 100644
index 0000000..99a29af
--- /dev/null
+++ b/tests/default_info_subject/BUILD.bazel
@@ -0,0 +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.
+
+load(":default_info_subject_tests.bzl", "default_info_subject_test_suite")
+
+default_info_subject_test_suite(name = "default_info_subject_tests")
diff --git a/tests/default_info_subject/default_info_subject_tests.bzl b/tests/default_info_subject/default_info_subject_tests.bzl
new file mode 100644
index 0000000..e6cfc10
--- /dev/null
+++ b/tests/default_info_subject/default_info_subject_tests.bzl
@@ -0,0 +1,126 @@
+# 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 DefaultInfoSubject."""
+
+load("//lib:analysis_test.bzl", "analysis_test")
+load("//lib:test_suite.bzl", "test_suite")
+load("//lib:truth.bzl", "matching", "subjects")
+load("//lib:util.bzl", "util")
+load("//tests:test_util.bzl", "test_util")
+
+_tests = []
+
+def _default_info_subject_test(name):
+ util.helper_target(
+ _simple,
+ name = name + "_subject",
+ )
+ analysis_test(
+ name = name,
+ target = name + "_subject",
+ impl = _default_info_subject_test_impl,
+ )
+
+def _default_info_subject_test_impl(env, target):
+ fake_meta = test_util.fake_meta(env)
+ actual = subjects.default_info(
+ target[DefaultInfo],
+ meta = fake_meta,
+ )
+
+ actual.runfiles().contains_predicate(
+ matching.str_matches("default_runfile.txt"),
+ )
+ test_util.expect_no_failures(env, fake_meta, "check default runfiles success")
+
+ actual.runfiles().contains_predicate(
+ matching.str_matches("not-present.txt"),
+ )
+ test_util.expect_failures(
+ env,
+ fake_meta,
+ "check default runfiles failure",
+ "not-present.txt",
+ )
+
+ actual.data_runfiles().contains_predicate(
+ matching.str_matches("data_runfile.txt"),
+ )
+ test_util.expect_no_failures(env, fake_meta, "check data runfiles success")
+
+ actual.data_runfiles().contains_predicate(
+ matching.str_matches("not-present.txt"),
+ )
+ test_util.expect_failures(
+ env,
+ fake_meta,
+ "check data runfiles failure",
+ "not-present.txt",
+ )
+
+ actual.default_outputs().contains_predicate(
+ matching.file_path_matches("default_output.txt"),
+ )
+ test_util.expect_no_failures(env, fake_meta, "check executable success")
+
+ actual.default_outputs().contains_predicate(
+ matching.file_path_matches("not-present.txt"),
+ )
+ test_util.expect_failures(
+ env,
+ fake_meta,
+ "check executable failure",
+ "not-present.txt",
+ )
+
+ actual.executable().path().contains("subject")
+ test_util.expect_no_failures(env, fake_meta, "check executable success")
+
+ actual.executable().path().contains("not-present")
+ test_util.expect_failures(
+ env,
+ fake_meta,
+ "check executable failure",
+ "not-present",
+ )
+ actual.runfiles_manifest().path().contains("MANIFEST")
+ test_util.expect_no_failures(env, fake_meta, "check runfiles_manifest success")
+
+_tests.append(_default_info_subject_test)
+
+def default_info_subject_test_suite(name):
+ test_suite(
+ name = name,
+ tests = _tests,
+ )
+
+def _simple_impl(ctx):
+ executable = ctx.actions.declare_file(ctx.label.name)
+ ctx.actions.write(executable, "")
+ return [DefaultInfo(
+ files = depset([ctx.file.default_output]),
+ default_runfiles = ctx.runfiles([ctx.file.default_runfile, executable]),
+ data_runfiles = ctx.runfiles([ctx.file.data_runfile]),
+ executable = executable,
+ )]
+
+_simple = rule(
+ implementation = _simple_impl,
+ attrs = {
+ "default_output": attr.label(default = "default_output.txt", allow_single_file = True),
+ "default_runfile": attr.label(default = "default_runfile.txt", allow_single_file = True),
+ "data_runfile": attr.label(default = "data_runfile.txt", allow_single_file = True),
+ },
+)
diff --git a/tests/matching/BUILD.bazel b/tests/matching/BUILD.bazel
new file mode 100644
index 0000000..3464e38
--- /dev/null
+++ b/tests/matching/BUILD.bazel
@@ -0,0 +1,3 @@
+load(":matching_tests.bzl", "matching_test_suite")
+
+matching_test_suite(name = "matching_tests")
diff --git a/tests/matching/matching_tests.bzl b/tests/matching/matching_tests.bzl
new file mode 100644
index 0000000..6ef67e3
--- /dev/null
+++ b/tests/matching/matching_tests.bzl
@@ -0,0 +1,98 @@
+"""Tests for matchers."""
+
+load("//lib:test_suite.bzl", "test_suite")
+load("//lib:truth.bzl", "matching")
+
+_tests = []
+
+def _file(path):
+ _, _, basename = path.rpartition("/")
+ _, _, extension = basename.rpartition(".")
+ return struct(
+ path = path,
+ basename = basename,
+ extension = extension,
+ )
+
+def _verify_matcher(env, matcher, match_true, match_false):
+ # Test positive match
+ env.expect.where(matcher = matcher.desc, value = match_true).that_bool(
+ matcher.match(match_true),
+ expr = "matcher.match(value)",
+ ).equals(True)
+
+ # Test negative match
+ env.expect.where(matcher = matcher.desc, value = match_false).that_bool(
+ matcher.match(match_false),
+ expr = "matcher.match(value)",
+ ).equals(False)
+
+def _contains_test(env):
+ _verify_matcher(
+ env,
+ matching.contains("x"),
+ match_true = "YYYxZZZ",
+ match_false = "zzzzz",
+ )
+
+_tests.append(_contains_test)
+
+def _file_basename_equals_test(env):
+ _verify_matcher(
+ env,
+ matching.file_basename_equals("bar.txt"),
+ match_true = _file("foo/bar.txt"),
+ match_false = _file("foo/bar.md"),
+ )
+
+_tests.append(_file_basename_equals_test)
+
+def _file_extension_in_test(env):
+ _verify_matcher(
+ env,
+ matching.file_extension_in(["txt", "rst"]),
+ match_true = _file("foo.txt"),
+ match_false = _file("foo.py"),
+ )
+
+_tests.append(_file_extension_in_test)
+
+def _is_in_test(env):
+ _verify_matcher(
+ env,
+ matching.is_in(["a", "b"]),
+ match_true = "a",
+ match_false = "z",
+ )
+
+_tests.append(_is_in_test)
+
+def _str_matchers_test(env):
+ _verify_matcher(
+ env,
+ matching.str_matches("f*b"),
+ match_true = "foobar",
+ match_false = "nope",
+ )
+
+ _verify_matcher(
+ env,
+ matching.str_endswith("123"),
+ match_true = "abc123",
+ match_false = "123xxx",
+ )
+
+ _verify_matcher(
+ env,
+ matching.str_startswith("true"),
+ match_true = "truechew",
+ match_false = "notbuck",
+ )
+
+_tests.append(_str_matchers_test)
+
+def matching_test_suite(name):
+ test_suite(
+ name = name,
+ basic_tests = _tests,
+ )
diff --git a/tests/struct_subject/BUILD.bazel b/tests/struct_subject/BUILD.bazel
new file mode 100644
index 0000000..17c9864
--- /dev/null
+++ b/tests/struct_subject/BUILD.bazel
@@ -0,0 +1,3 @@
+load(":struct_subject_tests.bzl", "struct_subject_test_suite")
+
+struct_subject_test_suite(name = "struct_subject_tests")
diff --git a/tests/struct_subject/struct_subject_tests.bzl b/tests/struct_subject/struct_subject_tests.bzl
new file mode 100644
index 0000000..58d18ff
--- /dev/null
+++ b/tests/struct_subject/struct_subject_tests.bzl
@@ -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)
diff --git a/tests/test_util.bzl b/tests/test_util.bzl
new file mode 100644
index 0000000..837f23c
--- /dev/null
+++ b/tests/test_util.bzl
@@ -0,0 +1,96 @@
+# 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.
+
+"""Utilities for testing rules_testing code."""
+
+# buildifier: disable=bzl-visibility
+load("//lib/private:expect_meta.bzl", "ExpectMeta")
+load("//lib:truth.bzl", "matching")
+
+def _fake_meta(real_env):
+ """Create a fake ExpectMeta object for testing.
+
+ The fake ExpectMeta object copies a real ExpectMeta object, except:
+ * Failures are only recorded and don't cause a failure in `real_env`.
+ * `failures` attribute is added; this is a list of failures seen.
+ * `reset` attribute is added; this clears the failures list.
+
+ Args:
+ real_env: A real env object from the rules_testing framework.
+
+ Returns:
+ struct, a fake ExpectMeta object.
+ """
+ failures = []
+ fake_env = struct(
+ ctx = real_env.ctx,
+ fail = lambda msg: failures.append(msg),
+ failures = failures,
+ )
+ meta_impl = ExpectMeta.new(fake_env)
+ meta_impl_kwargs = {
+ attr: getattr(meta_impl, attr)
+ for attr in dir(meta_impl)
+ if attr not in ("to_json", "to_proto")
+ }
+ fake_meta = struct(
+ failures = failures,
+ reset = lambda: failures.clear(),
+ **meta_impl_kwargs
+ )
+ return fake_meta
+
+def _expect_no_failures(env, fake_meta, case):
+ """Check that a fake meta object had no failures.
+
+ NOTE: This clears the list of failures after checking. This is done
+ so that an earlier failure is only reported once.
+
+ Args:
+ env: Real `Expect` object to perform asserts.
+ fake_meta: A fake meta object that had failures recorded.
+ case: str, a description of the case that was tested.
+ """
+ env.expect.that_collection(
+ fake_meta.failures,
+ expr = case,
+ ).contains_exactly([])
+ fake_meta.reset()
+
+def _expect_failures(env, fake_meta, case, *errors):
+ """Check that a fake meta object has matching error strings.
+
+ NOTE: This clears the list of failures after checking. This is done
+ so that an earlier failure is only reported once.
+
+ Args:
+ env: Real `Expect` object to perform asserts.
+ fake_meta: A fake meta object that had failures recorded.
+ case: str, a description of the case that was tested.
+ *errors: list of strings. These are patterns to match, as supported
+ by `matching.str_matches` (e.g. `*`-style patterns)
+ """
+ env.expect.that_collection(
+ fake_meta.failures,
+ expr = case,
+ ).contains_at_least_predicates(
+ [matching.str_matches(e) for e in errors],
+ )
+ fake_meta.reset()
+
+test_util = struct(
+ fake_meta = _fake_meta,
+ expect_no_failures = _expect_no_failures,
+ expect_failures = _expect_failures,
+)
diff --git a/tests/truth_tests.bzl b/tests/truth_tests.bzl
index d5fce52..ee942f4 100644
--- a/tests/truth_tests.bzl
+++ b/tests/truth_tests.bzl
@@ -180,7 +180,7 @@ def _bool_subject_test(env, _target):
fake_env,
["expected any of:", "None", "39", "actual: True"],
env = env,
- msg = "check is_in mismatchd values",
+ msg = "check is_in mismatched values",
)
_end(env, fake_env)
@@ -806,6 +806,107 @@ def _collection_not_contains_predicate_test(env, _target):
_suite.append(collection_not_contains_predicate_test)
+def collection_offset_test(name):
+ analysis_test(name, impl = _collection_offset_test, target = "truth_tests_helper")
+
+def _collection_offset_test(env, _target):
+ fake_env = _fake_env(env)
+ subject = truth.expect(fake_env).that_collection(["a", "b", "c"])
+
+ offset_value = subject.offset(0, factory = lambda v, meta: v)
+ ut_asserts.true(env, offset_value == "a", "unexpected offset value at 0")
+
+ offset_value = subject.offset(-1, factory = lambda v, meta: v)
+ ut_asserts.true(env, offset_value == "c", "unexpected offset value at -1")
+
+ subject.offset(1, factory = subjects.str).equals("not-b")
+
+ _assert_failure(
+ fake_env,
+ [".offset(1)"],
+ env = env,
+ msg = "offset error message context not found",
+ )
+
+ _end(env, fake_env)
+
+_suite.append(collection_offset_test)
+
+def _collection_transform_test(name):
+ analysis_test(name, impl = _collection_transform_test_impl, target = "truth_tests_helper")
+
+def _collection_transform_test_impl(env, target):
+ _ = target # @unused
+ fake_env = _fake_env(env)
+ starter = truth.expect(fake_env).that_collection(["alan", "bert", "cari"])
+
+ actual = starter.transform(
+ "values that contain a",
+ filter = lambda v: "a" in v,
+ )
+ actual.contains("not-present")
+ _assert_failure(
+ fake_env,
+ [
+ "transform()",
+ "0: alan",
+ "1: cari",
+ "transform: values that contain a",
+ ],
+ env = env,
+ msg = "transform with lambda filter",
+ )
+
+ actual = starter.transform(filter = matching.contains("b"))
+ actual.contains("not-present")
+ _assert_failure(
+ fake_env,
+ [
+ "0: bert",
+ "transform: filter=<contains b>",
+ ],
+ env = env,
+ msg = "transform with matcher filter",
+ )
+
+ def contains_c(v):
+ return "c" in v
+
+ actual = starter.transform(filter = contains_c)
+ actual.contains("not-present")
+ _assert_failure(
+ fake_env,
+ [
+ "0: cari",
+ "transform: filter=contains_c(...)",
+ ],
+ env = env,
+ msg = "transform with named function filter",
+ )
+
+ actual = starter.transform(
+ "v.upper(); match even offsets",
+ map_each = lambda v: "{}-{}".format(v[0], v[1].upper()),
+ loop = enumerate,
+ )
+ actual.contains("not-present")
+ _assert_failure(
+ fake_env,
+ [
+ "transform()",
+ "0: 0-ALAN",
+ "1: 1-BERT",
+ "2: 2-CARI",
+ "transform: v.upper(); match even offsets",
+ ],
+ env = env,
+ msg = "transform with all args",
+ )
+
+ _end(env, fake_env)
+
+_suite.append(_collection_transform_test)
+
def execution_info_test(name):
analysis_test(name, impl = _execution_info_test, target = "truth_tests_helper")
@@ -894,6 +995,14 @@ def _dict_subject_test(env, _target):
fake_env = _fake_env(env)
subject = truth.expect(fake_env).that_dict({"a": 1, "b": 2, "c": 3})
+ def factory(value, *, meta):
+ return struct(value = value, meta = meta)
+
+ actual = subject.get("a", factory = factory)
+
+ truth.expect(env).that_int(actual.value).equals(1)
+ truth.expect(env).that_collection(actual.meta._exprs).contains("get(a)")
+
subject.contains_exactly({"a": 1, "b": 2, "c": 3})
_assert_no_failures(fake_env, env = env)
@@ -1067,46 +1176,6 @@ def _label_subject_test(env, target):
_suite.append(label_subject_test)
-def matchers_contains_test(name):
- analysis_test(name, impl = _matchers_contains_test, target = "truth_tests_helper")
-
-def _matchers_contains_test(env, _target):
- fake_env = _fake_env(env)
- ut_asserts.true(env, matching.contains("x").match("YYYxZZZ"))
- ut_asserts.false(env, matching.contains("x").match("zzzzz"))
- _end(env, fake_env)
-
-_suite.append(matchers_contains_test)
-
-def matchers_str_matchers_test(name):
- analysis_test(name, impl = _matchers_str_matchers_test, target = "truth_tests_helper")
-
-def _matchers_str_matchers_test(env, _target):
- fake_env = _fake_env(env)
-
- ut_asserts.true(env, matching.str_matches("f*b").match("foobar"))
- ut_asserts.false(env, matching.str_matches("f*b").match("nope"))
-
- ut_asserts.true(env, matching.str_endswith("123").match("abc123"))
- ut_asserts.false(env, matching.str_endswith("123").match("123xxx"))
-
- ut_asserts.true(env, matching.str_startswith("true").match("truechew"))
- ut_asserts.false(env, matching.str_startswith("buck").match("notbuck"))
- _end(env, fake_env)
-
-_suite.append(matchers_str_matchers_test)
-
-def matchers_is_in_test(name):
- analysis_test(name, impl = _matchers_is_in_test, target = "truth_tests_helper")
-
-def _matchers_is_in_test(env, _target):
- fake_env = _fake_env(env)
- ut_asserts.true(env, matching.is_in(["a", "b"]).match("a"))
- ut_asserts.false(env, matching.is_in(["x", "y"]).match("z"))
- _end(env, fake_env)
-
-_suite.append(matchers_is_in_test)
-
def runfiles_subject_test(name):
analysis_test(name, impl = _runfiles_subject_test, target = "truth_tests_helper")
diff --git a/tests/unit_test_tests.bzl b/tests/unit_test_tests.bzl
new file mode 100644
index 0000000..97ec99c
--- /dev/null
+++ b/tests/unit_test_tests.bzl
@@ -0,0 +1,28 @@
+"""Tests for unit_test."""
+
+load("//lib:unit_test.bzl", "unit_test")
+load("//lib:test_suite.bzl", "test_suite")
+
+def _test_basic(env):
+ _ = env # @unused
+
+def _test_with_setup(name):
+ unit_test(
+ name = name,
+ impl = _test_with_setup_impl,
+ attrs = {"custom_attr": attr.string(default = "default")},
+ )
+
+def _test_with_setup_impl(env):
+ env.expect.that_str(env.ctx.attr.custom_attr).equals("default")
+
+def unit_test_test_suite(name):
+ test_suite(
+ name = name,
+ tests = [
+ _test_with_setup,
+ ],
+ basic_tests = [
+ _test_basic,
+ ],
+ )