blob: 3ab288f66efbccc96971de7586cd1bdb76b7fd87 [file] [log] [blame]
"""Utilities for testing bazel."""
#
# Copyright 2015 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("@rules_shell//shell:sh_test.bzl", "sh_test")
load(":test_rules_private.bzl", "BASH_RUNFILES_DEP", "INIT_BASH_RUNFILES")
_SH_STUB = "\n".join(["#!/bin/bash"] + INIT_BASH_RUNFILES + [
"function add_ws_name() {",
' [[ "$1" =~ external/* ]] && echo "${1#external/}" || echo "$TEST_WORKSPACE/$1"',
"}",
"",
])
def _bash_rlocation(f):
return '"$(rlocation "$(add_ws_name "%s")")"' % f.short_path
def _make_sh_test(name, **kwargs):
sh_test(
name = name,
srcs = [name + "_impl"],
data = [name + "_impl"],
deps = [BASH_RUNFILES_DEP],
**kwargs
)
_TEST_ATTRS = {
"args": None,
"size": None,
"timeout": None,
"flaky": None,
"local": None,
"shard_count": None,
}
def _helper_rule_attrs(test_attrs, own_attrs):
r = {}
r.update({k: v for k, v in test_attrs.items() if k not in _TEST_ATTRS})
r.update(own_attrs)
r.update(
dict(
testonly = 1,
visibility = ["//visibility:private"],
),
)
return r
### First, trivial tests that either always pass, always fail,
### or sometimes pass depending on a trivial computation.
def success_target(ctx, msg, exe = None):
"""Return a success for an analysis test.
The test rule must have an executable output.
Args:
ctx: the Bazel rule context
msg: an informative message to display
exe: the output artifact (must have been created with
ctx.actions.declare_file or declared in ctx.output), or None meaning
ctx.outputs.executable
Returns:
DefaultInfo that can be added to a sh_test's srcs AND data. The test will
always pass.
"""
exe = exe or ctx.outputs.executable
ctx.actions.write(
output = exe,
content = "#!/bin/bash\ncat <<'__eof__'\n" + msg + "\n__eof__\necho",
is_executable = True,
)
return [DefaultInfo(files = depset([exe]))]
def _successful_test_impl(ctx):
return success_target(ctx, ctx.attr.msg, exe = ctx.outputs.out)
_successful_rule = rule(
attrs = {
"msg": attr.string(mandatory = True),
"out": attr.output(),
},
implementation = _successful_test_impl,
)
def successful_test(name, msg, **kwargs):
_successful_rule(
**_helper_rule_attrs(
kwargs,
dict(
name = name + "_impl",
msg = msg,
out = name + "_impl.sh",
),
)
)
_make_sh_test(name, **kwargs)
def failure_target(ctx, msg, exe = None):
"""Return a failure for an analysis test.
Args:
ctx: the Bazel rule context
msg: an informative message to display
exe: the output artifact (must have been created with
ctx.actions.declare_file or declared in ctx.output), or None meaning
ctx.outputs.executable
Returns:
DefaultInfo that can be added to a sh_test's srcs AND data. The test will
always fail.
"""
### fail(msg) ### <--- This would fail at analysis time.
exe = exe or ctx.outputs.executable
ctx.actions.write(
output = exe,
content = "#!/bin/bash\ncat >&2 <<'__eof__'\n" + msg + "\n__eof__\nexit 1",
is_executable = True,
)
return [DefaultInfo(files = depset([exe]))]
def _failed_test_impl(ctx):
return failure_target(ctx, ctx.attr.msg, exe = ctx.outputs.out)
_failed_rule = rule(
attrs = {
"msg": attr.string(mandatory = True),
"out": attr.output(),
},
implementation = _failed_test_impl,
)
def failed_test(name, msg, **kwargs):
_failed_rule(
**_helper_rule_attrs(
kwargs,
dict(
name = name + "_impl",
msg = msg,
out = name + "_impl.sh",
),
)
)
_make_sh_test(name, **kwargs)
### Second, general purpose utilities
def assert_(condition, string = "assertion failed", *args):
"""Trivial assertion mechanism.
Args:
condition: a generalized boolean expected to be true
string: a format string for the error message should the assertion fail
*args: format arguments for the error message should the assertion fail
Returns:
None.
Raises:
an error if the condition isn't true.
"""
if not condition:
fail(string % args)
def strip_prefix(prefix, string):
assert_(
string.startswith(prefix),
"%s does not start with %s",
string,
prefix,
)
return string[len(prefix):len(string)]
def expectation_description(expect = None, expect_failure = None):
"""Turn expectation of result or error into a string."""
if expect_failure:
return "failure " + str(expect_failure)
else:
return "result " + repr(expect)
def check_results(result, failure, expect, expect_failure):
"""See if actual computation results match expectations.
Args:
result: the result returned by the test if it ran to completion
failure: the failure message caught while testing, if any
expect: the expected result for a successful test, if no failure expected
expect_failure: the expected failure message for the test, if any
Returns:
a pair (tuple) of a boolean (true if success) and a message (string).
"""
wanted = expectation_description(expect, expect_failure)
found = expectation_description(result, failure)
if wanted == found:
return (True, "successfully computed " + wanted)
else:
return (False, "expect " + wanted + " but found " + found)
def load_results(
name,
result = None,
failure = None,
expect = None,
expect_failure = None):
"""issue load-time results of a test.
Args:
name: the name of the Bazel rule at load time.
result: the result returned by the test if it ran to completion
failure: the failure message caught while testing, if any
expect: the expected result for a successful test, if no failure expected
expect_failure: the expected failure message for the test, if any
Returns:
None, after issuing a rule that will succeed at execution time if
expectations were met.
"""
(is_success, msg) = check_results(result, failure, expect, expect_failure)
this_test = successful_test if is_success else failed_test
return this_test(name = name, msg = msg)
def analysis_results(
ctx,
result = None,
failure = None,
expect = None,
expect_failure = None):
"""issue analysis-time results of a test.
Args:
ctx: the Bazel rule context
result: the result returned by the test if it ran to completion
failure: the failure message caught while testing, if any
expect: the expected result for a successful test, if no failure expected
expect_failure: the expected failure message for the test, if any
Returns:
DefaultInfo that can be added to a sh_test's srcs AND data. The test will
always succeed at execution time if expectation were met,
or fail at execution time if they didn't.
"""
(is_success, msg) = check_results(result, failure, expect, expect_failure)
this_test = success_target if is_success else failure_target
return this_test(ctx, msg)
### Simple tests
def _rule_test_rule_impl(ctx):
"""check that a rule generates the desired outputs and providers."""
rule_ = ctx.attr.rule
rule_name = str(rule_.label)
exe = ctx.outputs.out
if ctx.attr.generates:
# Generate the proper prefix to remove from generated files.
prefix_parts = []
if rule_.label.workspace_root:
# Create a prefix that is correctly relative to the output of this rule.
prefix_parts = ["..", strip_prefix("external/", rule_.label.workspace_root)]
if rule_.label.package:
prefix_parts.append(rule_.label.package)
prefix = "/".join(prefix_parts)
if prefix:
# If the prefix isn't empty, it needs a trailing slash.
prefix = prefix + "/"
# TODO(bazel-team): Use set() instead of sorted() once
# set comparison is implemented.
# TODO(bazel-team): Use a better way to determine if two paths refer to
# the same file.
generates = sorted(ctx.attr.generates)
generated = sorted([
strip_prefix(prefix, f.short_path)
for f in rule_.files.to_list()
])
if generates != generated:
fail("rule %s generates %s not %s" %
(rule_name, repr(generated), repr(generates)))
provides = ctx.attr.provides
if provides:
files = []
commands = []
for k in provides.keys():
if hasattr(rule_, k):
v = repr(getattr(rule_, k))
else:
fail(("rule %s doesn't provide attribute %s. " +
"Its list of attributes is: %s") %
(rule_name, k, dir(rule_)))
file_ = ctx.actions.declare_file(exe.basename + "." + k)
files += [file_]
regexp = provides[k]
commands += [
"file_=%s" % _bash_rlocation(file_),
"if ! grep %s \"$file_\" ; then echo 'bad %s:' ; cat \"$file_\" ; echo ; exit 1 ; fi" %
(repr(regexp), k),
]
ctx.actions.write(output = file_, content = v)
script = _SH_STUB + "\n".join(commands)
ctx.actions.write(output = exe, content = script, is_executable = True)
return [DefaultInfo(files = depset([exe]), runfiles = ctx.runfiles([exe] + files))]
else:
return success_target(ctx, "success", exe = exe)
_rule_test_rule = rule(
attrs = {
"rule": attr.label(mandatory = True),
"generates": attr.string_list(),
"provides": attr.string_dict(),
"out": attr.output(),
},
implementation = _rule_test_rule_impl,
)
def rule_test(name, rule, generates = None, provides = None, **kwargs):
_rule_test_rule(
**_helper_rule_attrs(
kwargs,
dict(
name = name + "_impl",
rule = rule,
generates = generates,
provides = provides,
out = name + ".sh",
),
)
)
_make_sh_test(name, **kwargs)
def _file_test_rule_impl(ctx):
"""check that a file has a given content."""
exe = ctx.outputs.out
file_ = ctx.file.file
content = ctx.attr.content
regexp = ctx.attr.regexp
matches = ctx.attr.matches
if bool(content) == bool(regexp):
fail("Must specify one and only one of content or regexp")
if content and matches != -1:
fail("matches only makes sense with regexp")
if content:
dat = ctx.actions.declare_file(exe.basename + ".dat")
ctx.actions.write(
output = dat,
content = content,
)
script = "diff -u %s %s" % (_bash_rlocation(dat), _bash_rlocation(file_))
ctx.actions.write(
output = exe,
content = _SH_STUB + script,
is_executable = True,
)
return [DefaultInfo(files = depset([exe]), runfiles = ctx.runfiles([exe, dat, file_]))]
if matches != -1:
script = "[ %s == $(grep -c %s %s) ]" % (
matches,
repr(regexp),
_bash_rlocation(file_),
)
else:
script = "grep %s %s" % (repr(regexp), _bash_rlocation(file_))
ctx.actions.write(
output = exe,
content = _SH_STUB + script,
is_executable = True,
)
return [DefaultInfo(files = depset([exe]), runfiles = ctx.runfiles([exe, file_]))]
_file_test_rule = rule(
attrs = {
"file": attr.label(
mandatory = True,
allow_single_file = True,
),
"content": attr.string(default = ""),
"regexp": attr.string(default = ""),
"matches": attr.int(default = -1),
"out": attr.output(),
},
implementation = _file_test_rule_impl,
)
def file_test(name, file, content = None, regexp = None, matches = None, **kwargs):
_file_test_rule(
**_helper_rule_attrs(
kwargs,
dict(
name = name + "_impl",
file = file,
content = content or "",
regexp = regexp or "",
matches = matches if (matches != None) else -1,
out = name + "_impl.sh",
),
)
)
_make_sh_test(name, **kwargs)