Gotcha, I see. Unless I missed something, I think you'd need to write an intermediate rule that just calls the function, and write an analysis test around that. Something like this:
BUILD:load(":myhelper_tests.bzl", "myhelper_tests")
myhelper_tests()
WORKSPACE:load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "bazel_skylib",
urls = [
"https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
"https://github.com/bazelbuild/bazel-skylib/releases/download/1.1.1/bazel-skylib-1.1.1.tar.gz",
],
sha256 = "c6966ec828da198c5d9adbaa94c05e3a1c7f21bd012a0b29ba8ddbccb2c93b0d",
)
load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace")
bazel_skylib_workspace()
myhelper.bzl:def myhelper(value):
if value == 1:
fail("1 is not allowed")
if value == 2:
fail("2 is not allowed")
return "abc"
myhelper_tests.bzl:load("@bazel_skylib//lib:unittest.bzl", "asserts", "analysistest")
load(":myhelper.bzl", "myhelper")
def _myhelper_failure_test_impl(ctx):
env = analysistest.begin(ctx)
asserts.expect_failure(env, ctx.attr.expected_failure)
return analysistest.end(env)
myhelper_failure_test = analysistest.make(
_myhelper_failure_test_impl,
expect_failure = True,
attrs = {
"expected_failure": attr.string(mandatory = True),
},
)
def _myhelper_caller_impl(ctx):
myhelper(ctx.attr.myhelper_param)
return []
_myhelper_caller = rule(
implementation = _myhelper_caller_impl,
attrs = {
"myhelper_param": attr.int(mandatory = True),
}
)
def _make_myhelper_test(name, param, expected_failure):
caller_target = "_%s_myhelper_caller" % name
_myhelper_caller(
name = caller_target,
myhelper_param = param,
tags = ["manual"],
)
myhelper_failure_test(
name = "_%s_myhelper_failure_test" % name,
target_under_test = caller_target,
expected_failure = expected_failure,
)
def myhelper_tests():
_make_myhelper_test("1", 1, "1 is not allowed")
_make_myhelper_test("2", 2, "2 is not allowed")
That's a lot of boilerplate. This helps reduce it a bit:
failure_unittest.bzl:load("@bazel_skylib//lib:unittest.bzl", "asserts", "analysistest")
def _failure_test_impl(ctx):
env = analysistest.begin(ctx)
asserts.expect_failure(env, ctx.attr.expected_failure)
return analysistest.end(env)
_failure_test = analysistest.make(
_failure_test_impl,
expect_failure = True,
attrs = {
"expected_failure": attr.string(mandatory = True),
},
)
def make_caller_rule(func, *args, **kwargs):
def _caller_impl(ctx):
func(*args, **kwargs)
return []
return rule(
implementation = _caller_impl,
)
def make_failure_unittest(name, caller_rule, expected_failure):
caller_target = "_%s_caller" % name
caller_rule(
name = caller_target,
tags = ["manual"],
)
_failure_test(
name = "_%s_failure_test" % name,
target_under_test = caller_target,
expected_failure = expected_failure,
)
myhelper_tests.bzl:load("failure_unittest.bzl", "make_failure_unittest", "make_caller_rule")
load(":myhelper.bzl", "myhelper")
myhelper_caller_1 = make_caller_rule(myhelper, 1)
myhelper_caller_2 = make_caller_rule(myhelper, 2)
def myhelper_tests():
make_failure_unittest("1", myhelper_caller_1, "1 is not allowed")
make_failure_unittest("2", myhelper_caller_2, "2 is not allowed")
Having to instantiate the rule classes for each case ("myhelper_caller_1 = make_caller_rule(myhelper, 1)") separately from creating the test targets ("make_failure_unittest(...)") is annoying, but it seems to be a limitation of how rule classes work. I tried this:
def _make_caller_rule(func, *args, **kwargs):
def _caller_impl(ctx):
func(*args, **kwargs)
return []
return rule(
implementation = _caller_impl,
)
def make_failure_unittest(name, expected_failure, func, *args, **kwargs):
caller_rule = _make_caller_rule(func, *args, **kwargs)
caller_target = "_%s_caller" % name
caller_rule(
name = caller_target,
tags = ["manual"],
)
_failure_test(
name = "_%s_failure_test" % name,
target_under_test = caller_target,
expected_failure = expected_failure,
)
BUILD:
def myhelper_tests():
make_failure_unittest("1", "1 is not allowed", myhelper, 1)
make_failure_unittest("2", "2 is not allowed", myhelper, 2)
But that failed with:
"Error in unexported rule: Invalid rule class hasn't been exported by a bzl file"
I think there's some magic that happens underneath with "name = rule(...)", where "name" becomes part of that rule.
Feel free to file a feature request to make this more convenient.