Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

file_test, rule_test: now as sh_test rules #8352

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 144 additions & 49 deletions tools/build_rules/test_rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -14,83 +14,153 @@
# See the License for the specific language governing permissions and
# limitations under the License.

_INIT_BASH_RUNFILES = "\n".join([
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment? It's not obvious to see what it does.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should the comment say? The variable's name is descriptive and the first line of the code says what it does.

"#!/bin/bash",
"# --- begin runfiles.bash initialization ---",
"# Copy-pasted from Bazel Bash runfiles library (tools/bash/runfiles/runfiles.bash).",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why copy-pasted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# The runfiles library itself defines rlocation which you would need to look
# up the library's runtime location, thus we have a chicken-and-egg problem.
# Insert the following code snippet to the top of your main script:

"set -euo pipefail",
'if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then',
' if [[ -f "$0.runfiles_manifest" ]]; then',
' export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"',
' elif [[ -f "$0.runfiles/MANIFEST" ]]; then',
' export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"',
' elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then',
' export RUNFILES_DIR="$0.runfiles"',
" fi",
"fi",
'if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then',
' source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash"',
'elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then',
' source "$(grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \\',
' "$RUNFILES_MANIFEST_FILE" | cut -d " " -f 2-)"',
"else",
' echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash"',
" exit 1",
"fi",
"# --- end runfiles.bash initialization ---",
"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):
native.sh_test(
name = name,
srcs = [name + "_impl"],
data = [name + "_impl"],
deps = ["@bazel_tools//tools/bash/runfiles"],
**kwargs
)

### First, trivial tests that either always pass, always fail,
### or sometimes pass depending on a trivial computation.

def success_target(ctx, msg):
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:
a suitable rule implementation struct(),
with actions that always succeed at execution time.
DefaultInfo that can be added to a sh_test's srcs AND data. The test will
always pass.
"""
exe = ctx.outputs.executable
exe = exe or ctx.outputs.executable
dat = ctx.actions.declare_file(exe.basename + ".dat")
ctx.actions.write(
output = dat,
content = msg,
)
script = "cat " + _bash_rlocation(dat) + " ; echo"
ctx.actions.write(
output = exe,
content = "cat " + dat.path + " ; echo",
content = _INIT_BASH_RUNFILES + script,
is_executable = True,
)
return [DefaultInfo(runfiles = ctx.runfiles([exe, dat]))]
return [DefaultInfo(files = depset([exe]), runfiles = ctx.runfiles([exe, dat]))]

def _successful_test_impl(ctx):
return success_target(ctx, ctx.attr.msg)
return success_target(ctx, ctx.attr.msg, exe = ctx.outputs.out)

successful_test = rule(
attrs = {"msg": attr.string(mandatory = True)},
executable = True,
test = True,
_successful_rule = rule(
attrs = {
"msg": attr.string(mandatory = True),
"out": attr.output(),
},
implementation = _successful_test_impl,
)

def failure_target(ctx, msg):
"""Return a failure for an analysis test.
def successful_test(name, msg, **kwargs):
_successful_rule(
name = name + "_impl",
msg = msg,
out = name + "_impl.sh",
visibility = ["//visibility:private"],
)

The test rule must have an executable output.
_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:
a suitable rule implementation struct(),
with actions that always fail at execution time.
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 = ctx.outputs.executable
exe = exe or ctx.outputs.executable
dat = ctx.actions.declare_file(exe.basename + ".dat")
ctx.actions.write(
output = dat,
content = msg,
)
script = "(cat " + _bash_rlocation(dat) + " ; echo ) >&2 ; exit 1"
ctx.actions.write(
output = exe,
content = "(cat " + dat.short_path + " ; echo ) >&2 ; exit 1",
content = _INIT_BASH_RUNFILES + script,
is_executable = True,
)
return [DefaultInfo(runfiles = ctx.runfiles([exe, dat]))]
return [DefaultInfo(files = depset([exe]), runfiles = ctx.runfiles([exe, dat]))]

def _failed_test_impl(ctx):
return failure_target(ctx, ctx.attr.msg)
return failure_target(ctx, ctx.attr.msg, exe = ctx.outputs.out)

failed_test = rule(
attrs = {"msg": attr.string(mandatory = True)},
executable = True,
test = True,
_failed_rule = rule(
attrs = {
"msg": attr.string(mandatory = True),
"out": attr.output(),
},
implementation = _failed_test_impl,
)

def failed_test(name, msg, **kwargs):
_failed_rule(
name = name + "_impl",
msg = msg,
out = name + "_impl.sh",
visibility = ["//visibility:private"],
)

_make_sh_test(name, **kwargs)

### Second, general purpose utilities

def assert_(condition, string = "assertion failed", *args):
Expand Down Expand Up @@ -185,8 +255,8 @@ def analysis_results(
expect_failure: the expected failure message for the test, if any

Returns:
a suitable rule implementation struct(),
with actions that succeed at execution time if expectation were met,
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)
Expand All @@ -195,11 +265,11 @@ def analysis_results(

### Simple tests

def _rule_test_impl(ctx):
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.executable
exe = ctx.outputs.out
if ctx.attr.generates:
# Generate the proper prefix to remove from generated files.
prefix_parts = []
Expand Down Expand Up @@ -244,30 +314,42 @@ def _rule_test_impl(ctx):
files += [file_]
regexp = provides[k]
commands += [
"if ! grep %s %s ; then echo 'bad %s:' ; cat %s ; echo ; exit 1 ; fi" %
(repr(regexp), file_.short_path, k, file_.short_path),
"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 = "\n".join(commands + ["true"])
script = _INIT_BASH_RUNFILES + "\n".join(commands)
ctx.actions.write(output = exe, content = script, is_executable = True)
return [DefaultInfo(runfiles = ctx.runfiles([exe] + files))]
return [DefaultInfo(files = depset([exe]), runfiles = ctx.runfiles([exe] + files))]
else:
return success_target(ctx, "success")
return success_target(ctx, "success", exe = exe)

rule_test = rule(
_rule_test_rule = rule(
attrs = {
"rule": attr.label(mandatory = True),
"generates": attr.string_list(),
"provides": attr.string_dict(),
"out": attr.output(),
},
executable = True,
test = True,
implementation = _rule_test_impl,
implementation = _rule_test_rule_impl,
)

def _file_test_impl(ctx):
def rule_test(name, rule, generates = None, provides = None, **kwargs):
_rule_test_rule(
name = name + "_impl",
rule = rule,
generates = generates,
provides = provides,
out = name + ".sh",
visibility = ["//visibility:private"],
)

_make_sh_test(name, **kwargs)

def _file_test_rule_impl(ctx):
"""check that a file has a given content."""
exe = ctx.outputs.executable
exe = ctx.outputs.out
file_ = ctx.file.file
content = ctx.attr.content
regexp = ctx.attr.regexp
Expand All @@ -282,28 +364,29 @@ def _file_test_impl(ctx):
output = dat,
content = content,
)
script = "diff -u %s %s" % (_bash_rlocation(dat), _bash_rlocation(file_))
ctx.actions.write(
output = exe,
content = "diff -u %s %s" % (dat.short_path, file_.short_path),
content = _INIT_BASH_RUNFILES + script,
is_executable = True,
)
return [DefaultInfo(runfiles = ctx.runfiles([exe, dat, file_]))]
return [DefaultInfo(files = depset([exe]), runfiles = ctx.runfiles([exe, dat, file_]))]
if matches != -1:
script = "[ %s == $(grep -c %s %s) ]" % (
matches,
repr(regexp),
file_.short_path,
_bash_rlocation(file_),
)
else:
script = "grep %s %s" % (repr(regexp), file_.short_path)
script = "grep %s %s" % (repr(regexp), _bash_rlocation(file_))
ctx.actions.write(
output = exe,
content = script,
content = _INIT_BASH_RUNFILES + script,
is_executable = True,
)
return [DefaultInfo(runfiles = ctx.runfiles([exe, file_]))]
return [DefaultInfo(files = depset([exe]), runfiles = ctx.runfiles([exe, file_]))]

file_test = rule(
_file_test_rule = rule(
attrs = {
"file": attr.label(
mandatory = True,
Expand All @@ -312,8 +395,20 @@ file_test = rule(
"content": attr.string(default = ""),
"regexp": attr.string(default = ""),
"matches": attr.int(default = -1),
"out": attr.output(),
},
executable = True,
test = True,
implementation = _file_test_impl,
implementation = _file_test_rule_impl,
)

def file_test(name, file, content = None, regexp = None, matches = None, **kwargs):
_file_test_rule(
name = name + "_impl",
file = file,
content = content or "",
regexp = regexp or "",
matches = matches if (matches != None) else -1,
out = name + "_impl.sh",
visibility = ["//visibility:private"],
)

_make_sh_test(name, **kwargs)