Enable unittest.suite to accept partial calls of test rules (#276)

* Enable unittest.suite to accept partial calls of rules

This permits using `unittest.suite` with test rules that have nondefault
attributes, while retaining compatibility with current usage.

For instance, this permits setting a `timeout` on each test in a
`unittest.suite`.  Previously, all tests in a `unittest.suite` would
have the default timeout, with no good way to alter this.  This
made it hard to eliminate all the warnings produced from using the
`--test_verbose_timeout_warnings` bazel option.

While timeouts were the motivation, the solution here is not specific
to timeouts. It will permit arbitrary additional arguments to the test
rules in a `unittest.suite`.

Fixes #98

* Respond to review feedback.

* Document a breaking change in bazel that this code needs to be aware of.
This commit is contained in:
David Sanderson 2020-11-12 21:04:39 -05:00 committed by GitHub
parent 182046f090
commit ed7f03cde6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 72 additions and 6 deletions

View File

@ -71,6 +71,7 @@ bzl_library(
srcs = ["unittest.bzl"], srcs = ["unittest.bzl"],
deps = [ deps = [
":new_sets", ":new_sets",
":partial",
":sets", ":sets",
":types", ":types",
], ],

View File

@ -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. 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): def _call(partial, *args, **kwargs):
"""Calls a partial created using `make`. """Calls a partial created using `make`.
@ -124,7 +129,29 @@ def _make(func, *args, **kwargs):
""" """
return struct(function = func, args = args, kwargs = 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( partial = struct(
make = _make, make = _make,
call = _call, call = _call,
is_instance = _is_instance,
) )

View File

@ -20,6 +20,7 @@ assertions used to within tests.
""" """
load(":new_sets.bzl", new_sets = "sets") load(":new_sets.bzl", new_sets = "sets")
load(":partial.bzl", "partial")
load(":types.bzl", "types") load(":types.bzl", "types")
# The following function should only be called from WORKSPACE files and workspace macros. # The following function should only be called from WORKSPACE files and workspace macros.
@ -232,10 +233,10 @@ def _suite(name, *test_rules):
writing a macro in your `.bzl` file to instantiate all targets, and calling 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. 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 -- You can use this function to create the targets and wrap them in a single
i.e., if your unit tests do not test rules -- you can use this function to test_suite target. If a test rule requires no arguments, you can simply list
create the targets and wrap them in a single test_suite target. In your it as an argument. If you wish to supply attributes explicitly, you can do so
`.bzl` file, write: using `partial.make()`. For instance, in your `.bzl` file, you could write:
``` ```
def your_test_suite(): def your_test_suite():
@ -243,7 +244,7 @@ def _suite(name, *test_rules):
"your_test_suite", "your_test_suite",
your_test, your_test,
your_other_test, your_other_test,
yet_another_test, partial.make(yet_another_test, timeout = "short"),
) )
``` ```
@ -269,6 +270,9 @@ def _suite(name, *test_rules):
test_names = [] test_names = []
for index, test_rule in enumerate(test_rules): for index, test_rule in enumerate(test_rules):
test_name = "%s_test_%d" % (name, index) test_name = "%s_test_%d" % (name, index)
if partial.is_instance(test_rule):
partial.call(test_rule, name = test_name)
else:
test_rule(name = test_name) test_rule(name = test_name)
test_names.append(test_name) test_names.append(test_name)

View File

@ -76,9 +76,27 @@ def _make_call_test(ctx):
make_call_test = unittest.make(_make_call_test) make_call_test = unittest.make(_make_call_test)
def _is_instance_test(ctx):
"""Unit test for partial.is_instance."""
env = unittest.begin(ctx)
# We happen to use make_call_test here, but it could be any valid test rule.
asserts.true(env, partial.is_instance(partial.make(make_call_test)))
asserts.true(env, partial.is_instance(partial.make(make_call_test, timeout = "short")))
asserts.true(env, partial.is_instance(partial.make(make_call_test, timeout = "short", tags = ["foo"])))
asserts.false(env, partial.is_instance(None))
asserts.false(env, partial.is_instance({}))
asserts.false(env, partial.is_instance(struct(foo = 1)))
asserts.false(env, partial.is_instance(struct(function = "not really function")))
return unittest.end(env)
is_instance_test = unittest.make(_is_instance_test)
def partial_test_suite(): def partial_test_suite():
"""Creates the test targets and test suite for partial.bzl tests.""" """Creates the test targets and test suite for partial.bzl tests."""
unittest.suite( unittest.suite(
"partial_tests", "partial_tests",
make_call_test, make_call_test,
is_instance_test,
) )

View File

@ -73,6 +73,7 @@ exports_files(["*.bzl"])
EOF EOF
ln -sf "$(rlocation bazel_skylib/lib/dicts.bzl)" lib/dicts.bzl ln -sf "$(rlocation bazel_skylib/lib/dicts.bzl)" lib/dicts.bzl
ln -sf "$(rlocation bazel_skylib/lib/new_sets.bzl)" lib/new_sets.bzl ln -sf "$(rlocation bazel_skylib/lib/new_sets.bzl)" lib/new_sets.bzl
ln -sf "$(rlocation bazel_skylib/lib/partial.bzl)" lib/partial.bzl
ln -sf "$(rlocation bazel_skylib/lib/sets.bzl)" lib/sets.bzl ln -sf "$(rlocation bazel_skylib/lib/sets.bzl)" lib/sets.bzl
ln -sf "$(rlocation bazel_skylib/lib/types.bzl)" lib/types.bzl ln -sf "$(rlocation bazel_skylib/lib/types.bzl)" lib/types.bzl
ln -sf "$(rlocation bazel_skylib/lib/unittest.bzl)" lib/unittest.bzl ln -sf "$(rlocation bazel_skylib/lib/unittest.bzl)" lib/unittest.bzl

View File

@ -14,6 +14,7 @@
"""Unit tests for unittest.bzl.""" """Unit tests for unittest.bzl."""
load("//lib:partial.bzl", "partial")
load("//lib:unittest.bzl", "analysistest", "asserts", "unittest") load("//lib:unittest.bzl", "analysistest", "asserts", "unittest")
################################### ###################################
@ -42,6 +43,19 @@ def _basic_passing_test(ctx):
basic_passing_test = unittest.make(_basic_passing_test) basic_passing_test = unittest.make(_basic_passing_test)
#################################################
####### basic_passing_short_timeout_test ########
#################################################
def _basic_passing_short_timeout_test(ctx):
"""Unit tests for a basic library verification test."""
env = unittest.begin(ctx)
asserts.equals(env, ctx.attr.timeout, "short")
return unittest.end(env)
basic_passing_short_timeout_test = unittest.make(_basic_passing_short_timeout_test)
################################### ###################################
####### change_setting_test ####### ####### change_setting_test #######
################################### ###################################
@ -240,6 +254,7 @@ def unittest_passing_tests_suite():
unittest.suite( unittest.suite(
"unittest_tests", "unittest_tests",
basic_passing_test, basic_passing_test,
partial.make(basic_passing_short_timeout_test, timeout = "short"),
) )
change_setting_test( change_setting_test(