aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2022-06-24 19:27:53 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2022-06-24 19:27:53 +0000
commitf106017751732e4baaf3e71170ef4f2bb337848d (patch)
tree3c15c050ced2749753c4d6f1cf9d3a9b9bd94085 /lib
parent76be740a2ab63f21bb42ee3590792fd39edc6a59 (diff)
parentf1fb8167b4ed64feb494fd1ea6a8a619bbb549de (diff)
downloadbazel-skylib-android-gs-tangorpro-5.10-u-beta5.2.tar.gz
Change-Id: Ia0ce9f969778c682312310ecbc9ab7eaa4ada766
Diffstat (limited to 'lib')
-rw-r--r--lib/BUILD1
-rw-r--r--lib/partial.bzl48
-rw-r--r--lib/structs.bzl12
-rw-r--r--lib/unittest.bzl175
4 files changed, 216 insertions, 20 deletions
diff --git a/lib/BUILD b/lib/BUILD
index 26a6120..7e7a9a4 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -71,6 +71,7 @@ bzl_library(
srcs = ["unittest.bzl"],
deps = [
":new_sets",
+ ":partial",
":sets",
":types",
],
diff --git a/lib/partial.bzl b/lib/partial.bzl
index e2f24b7..a642d96 100644
--- a/lib/partial.bzl
+++ b/lib/partial.bzl
@@ -19,6 +19,11 @@ Partial function objects allow some parameters are bound before the call.
Similar to https://docs.python.org/3/library/functools.html#functools.partial.
"""
+# create instance singletons to avoid unnecessary allocations
+_a_dict_type = type({})
+_a_tuple_type = type(())
+_a_struct_type = type(struct())
+
def _call(partial, *args, **kwargs):
"""Calls a partial created using `make`.
@@ -46,16 +51,22 @@ def _make(func, *args, **kwargs):
A partial 'function' can be defined with positional args and kwargs:
# function with no args
+ ```
def function1():
...
+ ```
# function with 2 args
+ ```
def function2(arg1, arg2):
...
+ ```
# function with 2 args and keyword args
+ ```
def function3(arg1, arg2, x, y):
...
+ ```
The positional args passed to the function are the args passed into make
followed by any additional positional args given to call. The below example
@@ -63,24 +74,30 @@ def _make(func, *args, **kwargs):
make and the other by call:
# function demonstrating 1 arg at make site, and 1 arg at call site
+ ```
def _foo(make_arg1, func_arg1):
- print(make_arg1 + " " + func_arg1 + "!")
+ print(make_arg1 + " " + func_arg1 + "!")
+ ```
For example:
+ ```
hi_func = partial.make(_foo, "Hello")
bye_func = partial.make(_foo, "Goodbye")
partial.call(hi_func, "Jennifer")
partial.call(hi_func, "Dave")
partial.call(bye_func, "Jennifer")
partial.call(bye_func, "Dave")
+ ```
prints:
+ ```
"Hello, Jennifer!"
"Hello, Dave!"
"Goodbye, Jennifer!"
"Goodbye, Dave!"
+ ```
The keyword args given to the function are the kwargs passed into make
unioned with the keyword args given to call. In case of a conflict, the
@@ -90,16 +107,20 @@ def _make(func, *args, **kwargs):
Example with a make site arg, a call site arg, a make site kwarg and a
call site kwarg:
+ ```
def _foo(make_arg1, call_arg1, make_location, call_location):
print(make_arg1 + " is from " + make_location + " and " +
call_arg1 + " is from " + call_location + "!")
func = partial.make(_foo, "Ben", make_location="Hollywood")
partial.call(func, "Jennifer", call_location="Denver")
+ ```
Prints "Ben is from Hollywood and Jennifer is from Denver!".
+ ```
partial.call(func, "Jennifer", make_location="LA", call_location="Denver")
+ ```
Prints "Ben is from LA and Jennifer is from Denver!".
@@ -107,11 +128,13 @@ def _make(func, *args, **kwargs):
whether they are given during the make or call step. For instance, you can't
do:
+ ```
def foo(x):
pass
func = partial.make(foo, 1)
partial.call(func, x=2)
+ ```
Args:
func: The function to be called.
@@ -124,7 +147,30 @@ def _make(func, *args, **kwargs):
"""
return struct(function = func, args = args, kwargs = kwargs)
+def _is_instance(v):
+ """Returns True if v is a partial created using `make`.
+
+ Args:
+ v: The value to check.
+
+ Returns:
+ True if v was created by `make`, False otherwise.
+ """
+
+ # Note that in bazel 3.7.0 and earlier, type(v.function) is the same
+ # as the type of a function even if v.function is a rule. But we
+ # cannot rely on this in later bazels due to breaking change
+ # https://github.com/bazelbuild/bazel/commit/e379ece1908aafc852f9227175dd3283312b4b82
+ #
+ # Since this check is heuristic anyway, we simply check for the
+ # presence of a "function" attribute without checking its type.
+ return type(v) == _a_struct_type and \
+ hasattr(v, "function") and \
+ hasattr(v, "args") and type(v.args) == _a_tuple_type and \
+ hasattr(v, "kwargs") and type(v.kwargs) == _a_dict_type
+
partial = struct(
make = _make,
call = _call,
+ is_instance = _is_instance,
)
diff --git a/lib/structs.bzl b/lib/structs.bzl
index f291152..78066ad 100644
--- a/lib/structs.bzl
+++ b/lib/structs.bzl
@@ -25,10 +25,14 @@ def _to_dict(s):
transformation is only applied to the struct's fields and not to any
nested values.
"""
- attributes = dir(s)
- attributes.remove("to_json")
- attributes.remove("to_proto")
- return {key: getattr(s, key) for key in attributes}
+
+ # to_json()/to_proto() are disabled by --incompatible_struct_has_no_methods
+ # and will be removed entirely in a future Bazel release.
+ return {
+ key: getattr(s, key)
+ for key in dir(s)
+ if key != "to_json" and key != "to_proto"
+ }
structs = struct(
to_dict = _to_dict,
diff --git a/lib/unittest.bzl b/lib/unittest.bzl
index 0cdbd94..3757b08 100644
--- a/lib/unittest.bzl
+++ b/lib/unittest.bzl
@@ -20,6 +20,7 @@ assertions used to within tests.
"""
load(":new_sets.bzl", new_sets = "sets")
+load(":partial.bzl", "partial")
load(":types.bzl", "types")
# The following function should only be called from WORKSPACE files and workspace macros.
@@ -35,7 +36,14 @@ TOOLCHAIN_TYPE = "@bazel_skylib//toolchains/unittest:toolchain_type"
_UnittestToolchainInfo = provider(
doc = "Execution platform information for rules in the bazel_skylib repository.",
- fields = ["file_ext", "success_templ", "failure_templ", "join_on"],
+ fields = [
+ "file_ext",
+ "success_templ",
+ "failure_templ",
+ "join_on",
+ "escape_chars_with",
+ "escape_other_chars_with",
+ ],
)
def _unittest_toolchain_impl(ctx):
@@ -46,6 +54,8 @@ def _unittest_toolchain_impl(ctx):
success_templ = ctx.attr.success_templ,
failure_templ = ctx.attr.failure_templ,
join_on = ctx.attr.join_on,
+ escape_chars_with = ctx.attr.escape_chars_with,
+ escape_other_chars_with = ctx.attr.escape_other_chars_with,
),
),
]
@@ -53,10 +63,59 @@ def _unittest_toolchain_impl(ctx):
unittest_toolchain = rule(
implementation = _unittest_toolchain_impl,
attrs = {
- "failure_templ": attr.string(mandatory = True),
- "file_ext": attr.string(mandatory = True),
- "join_on": attr.string(mandatory = True),
- "success_templ": attr.string(mandatory = True),
+ "failure_templ": attr.string(
+ mandatory = True,
+ doc = (
+ "Test script template with a single `%s`. That " +
+ "placeholder is replaced with the lines in the " +
+ "failure message joined with the string " +
+ "specified in `join_with`. The resulting script " +
+ "should print the failure message and exit with " +
+ "non-zero status."
+ ),
+ ),
+ "file_ext": attr.string(
+ mandatory = True,
+ doc = (
+ "File extension for test script, including leading dot."
+ ),
+ ),
+ "join_on": attr.string(
+ mandatory = True,
+ doc = (
+ "String used to join the lines in the failure " +
+ "message before including the resulting string " +
+ "in the script specified in `failure_templ`."
+ ),
+ ),
+ "success_templ": attr.string(
+ mandatory = True,
+ doc = (
+ "Test script generated when the test passes. " +
+ "Should exit with status 0."
+ ),
+ ),
+ "escape_chars_with": attr.string_dict(
+ doc = (
+ "Dictionary of characters that need escaping in " +
+ "test failure message to prefix appended to escape " +
+ "those characters. For example, " +
+ '`{"%": "%", ">": "^"}` would replace `%` with ' +
+ "`%%` and `>` with `^>` in the failure message " +
+ "before that is included in `success_templ`."
+ ),
+ ),
+ "escape_other_chars_with": attr.string(
+ default = "",
+ doc = (
+ "String to prefix every character in test failure " +
+ "message which is not a key in `escape_chars_with` " +
+ "before including that in `success_templ`. For " +
+ 'example, `"\"` would prefix every character in ' +
+ "the failure message (except those in the keys of " +
+ "`escape_chars_with`) with `\\`."
+ ),
+ ),
},
)
@@ -126,7 +185,10 @@ def _make(impl, attrs = {}):
toolchains = [TOOLCHAIN_TYPE],
)
-_ActionInfo = provider(fields = ["actions", "bin_path"])
+_ActionInfo = provider(
+ doc = "Information relating to the target under test.",
+ fields = ["actions", "bin_path"],
+)
def _action_retrieving_aspect_impl(target, ctx):
return [
@@ -147,7 +209,9 @@ def _make_analysis_test(
expect_failure = False,
attrs = {},
fragments = [],
- config_settings = {}):
+ config_settings = {},
+ extra_target_under_test_aspects = [],
+ doc = ""):
"""Creates an analysis test rule from its implementation function.
An analysis test verifies the behavior of a "real" rule target by examining
@@ -185,6 +249,9 @@ def _make_analysis_test(
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
+ 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.
+ doc: A description of the rule that can be extracted by documentation generating tools.
Returns:
A rule definition that should be stored in a global whose name ends in
@@ -205,13 +272,14 @@ def _make_analysis_test(
target_attr_kwargs["cfg"] = test_transition
attrs["target_under_test"] = attr.label(
- aspects = [_action_retrieving_aspect],
+ aspects = [_action_retrieving_aspect] + extra_target_under_test_aspects,
mandatory = True,
**target_attr_kwargs
)
return rule(
impl,
+ doc = doc,
attrs = attrs,
fragments = fragments,
test = True,
@@ -229,10 +297,10 @@ def _suite(name, *test_rules):
writing a macro in your `.bzl` file to instantiate all targets, and calling
that macro from your BUILD file so you only have to load one symbol.
- For the case where your unit tests do not take any (non-default) attributes --
- i.e., if your unit tests do not test rules -- you can use this function to
- create the targets and wrap them in a single test_suite target. In your
- `.bzl` file, write:
+ You can use this function to create the targets and wrap them in a single
+ test_suite target. If a test rule requires no arguments, you can simply list
+ it as an argument. If you wish to supply attributes explicitly, you can do so
+ using `partial.make()`. For instance, in your `.bzl` file, you could write:
```
def your_test_suite():
@@ -240,7 +308,7 @@ def _suite(name, *test_rules):
"your_test_suite",
your_test,
your_other_test,
- yet_another_test,
+ partial.make(yet_another_test, timeout = "short"),
)
```
@@ -266,7 +334,10 @@ def _suite(name, *test_rules):
test_names = []
for index, test_rule in enumerate(test_rules):
test_name = "%s_test_%d" % (name, index)
- test_rule(name = test_name)
+ if partial.is_instance(test_rule):
+ partial.call(test_rule, name = test_name)
+ else:
+ test_rule(name = test_name)
test_names.append(test_name)
native.test_suite(
@@ -326,7 +397,15 @@ def _end(env):
tc = env.ctx.toolchains[TOOLCHAIN_TYPE].unittest_toolchain_info
testbin = env.ctx.actions.declare_file(env.ctx.label.name + tc.file_ext)
if env.failures:
- cmd = tc.failure_templ % tc.join_on.join(env.failures)
+ failure_message_lines = "\n".join(env.failures).split("\n")
+ escaped_failure_message_lines = [
+ "".join([
+ tc.escape_chars_with.get(c, tc.escape_other_chars_with) + c
+ for c in line.elems()
+ ])
+ for line in failure_message_lines
+ ]
+ cmd = tc.failure_templ % tc.join_on.join(escaped_failure_message_lines)
else:
cmd = tc.success_templ
@@ -488,6 +567,67 @@ def _target_under_test(env):
fail("test rule does not have a target_under_test")
return result
+def _loading_test_impl(ctx):
+ tc = ctx.toolchains[TOOLCHAIN_TYPE].unittest_toolchain_info
+ content = tc.success_templ
+ if ctx.attr.failure_message:
+ content = tc.failure_templ % ctx.attr.failure_message
+
+ testbin = ctx.actions.declare_file("loading_test_" + ctx.label.name + tc.file_ext)
+ ctx.actions.write(
+ output = testbin,
+ content = content,
+ is_executable = True,
+ )
+ return [DefaultInfo(executable = testbin)]
+
+_loading_test = rule(
+ implementation = _loading_test_impl,
+ attrs = {
+ "failure_message": attr.string(),
+ },
+ toolchains = [TOOLCHAIN_TYPE],
+ test = True,
+)
+
+def _loading_make(name):
+ """Creates a loading phase test environment and test_suite.
+
+ Args:
+ name: name of the suite of tests to create
+
+ Returns:
+ loading phase environment passed to other loadingtest functions
+ """
+ native.test_suite(
+ name = name + "_tests",
+ tags = [name + "_test_case"],
+ )
+ return struct(name = name)
+
+def _loading_assert_equals(env, test_case, expected, actual):
+ """Creates a test case for asserting state at LOADING phase.
+
+ Args:
+ env: Loading test env created from loadingtest.make
+ test_case: Name of the test case
+ expected: Expected value to test
+ actual: Actual value received.
+
+ Returns:
+ None, creates test case
+ """
+
+ msg = None
+ if expected != actual:
+ msg = 'Expected "%s", but got "%s"' % (expected, actual)
+
+ _loading_test(
+ name = "%s_%s" % (env.name, test_case),
+ failure_message = msg,
+ tags = [env.name + "_test_case"],
+ )
+
asserts = struct(
expect_failure = _expect_failure,
equals = _assert_equals,
@@ -514,3 +654,8 @@ analysistest = struct(
target_bin_dir_path = _target_bin_dir_path,
target_under_test = _target_under_test,
)
+
+loadingtest = struct(
+ make = _loading_make,
+ equals = _loading_assert_equals,
+)