Skip to content
Closed
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion diff/tests/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
load("@bazel_lib//lib:copy_to_directory.bzl", "copy_to_directory")
load("@bazel_skylib//lib:partial.bzl", "partial")
load("@bazel_skylib//rules:build_test.bzl", "build_test")
load("//tools/test:build_test.bzl", "build_test")
load("@bazel_skylib//rules:write_file.bzl", "write_file")
load("//diff:defs.bzl", "cmp", "diff", "diff3", "sdiff")

Expand Down
2 changes: 1 addition & 1 deletion e2e/diffutils_from_source/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Add a basic smoke-test target below.
"""

load("@bazel_skylib//lib:partial.bzl", "partial")
load("@bazel_skylib//rules:build_test.bzl", "build_test")
load("@diff.bzl//tools/test:build_test.bzl", "build_test")
load("@bazel_skylib//rules:write_file.bzl", "write_file")
load("@diff.bzl", "diff")

Expand Down
2 changes: 1 addition & 1 deletion e2e/smoke/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Add a basic smoke-test target below.
"""

load("@bazel_skylib//lib:partial.bzl", "partial")
load("@bazel_skylib//rules:build_test.bzl", "build_test")
load("@diff.bzl//tools/test:build_test.bzl", "build_test")
load("@bazel_skylib//rules:write_file.bzl", "write_file")
load("@diff.bzl", "diff")

Expand Down
2 changes: 1 addition & 1 deletion e2e/validate_override/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
load("@bazel_skylib//lib:partial.bzl", "partial")
load("@bazel_skylib//rules:build_test.bzl", "build_test")
load("@diff.bzl//tools/test:build_test.bzl", "build_test")
load("@bazel_skylib//rules:write_file.bzl", "write_file")
load("@diff.bzl", "cmp", "diff")

Expand Down
8 changes: 8 additions & 0 deletions tools/test/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package(default_visibility = ["//visibility:public"])

exports_files([
"build_test.bzl",
"build_test.sh",
"emit-test-xml.sh",
"run_with_xml.sh",
])
145 changes: 145 additions & 0 deletions tools/test/build_test.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""A test verifying other targets build as part of a `bazel test`.

Like @bazel_skylib//rules:build_test, but the test runner writes JUnit XML to
$XML_OUTPUT_FILE so tests work under Bazel 9 (bazelbuild/bazel#28111).
"""

load("@bazel_skylib//lib:new_sets.bzl", "sets")

def _build_test_impl(ctx):
is_windows = ctx.target_platform_has_constraint(ctx.attr._windows_constraint[platform_common.ConstraintValueInfo])
if is_windows:
fail("build_test with XML output is not supported on Windows")

emit_xml_link = ctx.actions.declare_file(ctx.label.name + ".emit-test-xml.sh")
ctx.actions.symlink(
output = emit_xml_link,
target_file = ctx.file._emit_test_xml,
is_executable = True,
)

executable = ctx.actions.declare_file(ctx.label.name + ".sh")
ctx.actions.write(
output = executable,
is_executable = True,
content = """#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
LOG="${{TEST_TMPDIR:-${{TMPDIR:-/tmp}}}}/test.log"
touch "$LOG"

START=$SECONDS
EXIT_CODE=0
DURATION=$((SECONDS - START))

if [[ -n "${{XML_OUTPUT_FILE:-}}" ]]; then
"${{SCRIPT_DIR}}/{emit_xml_name}" "$LOG" "$XML_OUTPUT_FILE" "$DURATION" "$EXIT_CODE"
fi

exit "$EXIT_CODE"
""".format(emit_xml_name = emit_xml_link.basename),
)

return [DefaultInfo(
executable = executable,
runfiles = ctx.runfiles(
files = ctx.files.data + [emit_xml_link],
),
)]

_build_test = rule(
implementation = _build_test_impl,
attrs = {
"data": attr.label_list(allow_files = True),
"_emit_test_xml": attr.label(
default = "//tools/test:emit-test-xml.sh",
allow_single_file = True,
),
"_windows_constraint": attr.label(default = "@platforms//os:windows"),
},
test = True,
)

_GENRULE_ATTRS = [
"compatible_with",
"exec_compatible_with",
"restricted_to",
"tags",
"target_compatible_with",
]

def build_test(name, targets, **kwargs):
"""Test rule checking that other targets build.

This works not by an instance of this test failing, but instead by
the targets it depends on failing to build, and hence failing
the attempt to run this test.

Typical usage:

```
load("@diff.bzl//tools/test:build_test.bzl", "build_test")
build_test(
name = "my_build_test",
targets = [
"//some/package:rule",
],
)
```

Args:
name: The name of the test rule.
targets: A list of targets to ensure build.
**kwargs: The [common attributes for tests](https://bazel.build/reference/be/common-definitions#common-attributes-tests).
"""
if len(targets) == 0:
fail("targets must be non-empty", "targets")
if kwargs.get("data", None):
fail("data is not supported on a build_test()", "data")

# Remove any duplicate test targets.
targets = sets.to_list(sets.make(targets))

# Use a genrule to ensure the targets are built (works because it forces
# the outputs of the other rules on as data for the genrule)

# Split into batches to hopefully avoid things becoming so large they are
# too much for a remote execution set up.
batch_size = max(1, len(targets) // 100)

# Pull a few args over from the test to the genrule.
genrule_args = {k: kwargs.get(k) for k in _GENRULE_ATTRS if k in kwargs}

# Only the test target should be used to determine whether or not the deps
# are built. Tagging the genrule targets as manual accomplishes this by
# preventing them from being picked up by recursive build patterns (`//...`).
genrule_tags = genrule_args.pop("tags", [])
if "manual" not in genrule_tags:
genrule_tags = genrule_tags + ["manual"]

# Pass an output from the genrules as data to a shell test to bundle
# it all up in a test.
test_data = []

for idx, batch in enumerate([targets[i:i + batch_size] for i in range(0, len(targets), batch_size)]):
full_name = "{name}_{idx}__deps".format(name = name, idx = idx)
test_data.append(full_name)
native.genrule(
name = full_name,
srcs = batch,
outs = [full_name + ".out"],
testonly = 1,
visibility = ["//visibility:private"],
cmd = "touch $@",
cmd_bat = "type nul > $@",
tags = genrule_tags,
**genrule_args
)

_build_test(
name = name,
data = test_data,
size = kwargs.pop("size", "small"), # Default to small for test size
**kwargs
)
20 changes: 20 additions & 0 deletions tools/test/build_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash

# Test entrypoint for build_test. Bazel builds data dependencies before this
# script runs; on success we emit JUnit XML to $XML_OUTPUT_FILE.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG="${TEST_TMPDIR:-${TMPDIR:-/tmp}}/test.log"
touch "$LOG"

START=$SECONDS
EXIT_CODE=0
DURATION=$((SECONDS - START))

if [[ -n "${XML_OUTPUT_FILE:-}" ]]; then
"${SCRIPT_DIR}/emit-test-xml.sh" "$LOG" "$XML_OUTPUT_FILE" "$DURATION" "$EXIT_CODE"
fi

exit "$EXIT_CODE"
132 changes: 132 additions & 0 deletions tools/test/emit-test-xml.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/env bash

# Copyright 2018 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.
#
# Adapted from tools/test/generate-xml.sh in github.com/bazelbuild/bazel.

set -euo pipefail

TEST_LOG="$1"
XML_OUTPUT_FILE="$2"
DURATION_IN_SECONDS="$3"
EXIT_CODE="$4"

function encode_stream {
# Replace invalid XML characters and invalid sequence in CDATA
# We do this in four steps:
#
# 1. Add a single whitespace character to the end of every line
#
# 2. Replace every sequence of legal characters followed by an illegal
# character *or* followed by a legal character at the end of the line with
# the same sequence of legal characters followed by a question mark
# character (replacing the illegal or last character). Since this will
# always replace the last character in a line with a question mark, we
# make sure to append a whitespace in step #1.
#
# A character is legal if it is a valid UTF-8 character that is allowed in
# an XML file (this excludes a few control codes, but otherwise allows
# most UTF-8 characters).
#
# We can't use sed in UTF-8 mode, because it would fail on the first
# illegal character. Instead, we have to match legal characters by their
# 8-bit binary sequences, and also switch sed to an 8-bit mode.
#
# The legal UTF codepoint ranges are 9,a,d,20-d7ff,e000-fffd,10000-10ffff,
# which results in the following 8-bit binary UTF-8 matchers:
# [\x9\xa\xd\x20-\x7f] <--- (9,A,D,20-7F)
# [\xc0-\xdf][\x80-\xbf] <--- (0080-07FF)
# [\xe0-\xec][\x80-\xbf][\x80-\xbf] <--- (0800-CFFF)
# [\xed][\x80-\x9f][\x80-\xbf] <--- (D000-D7FF)
# [\xee][\x80-\xbf][\x80-\xbf] <--- (E000-EFFF)
# [\xef][\x80-\xbe][\x80-\xbf] <--- (F000-FFEF)
# [\xef][\xbf][\x80-\xbd] <--- (FFF0-FFFD)
# [\xf0-\xf7][\x80-\xbf][\x80-\xbf][\x80-\xbf] <--- (010000-10FFFF)
#
# We omit \xa and \xd below since sed already splits the input into lines.
#
# 3. Remove the last character in the line, which we expect to be a
# question mark (that was originally added as a whitespace in step #1).
#
# 4. Replace the string ']]>' with ']]>]]<![CDATA[>' to prevent escaping the
# surrounding CDATA block.
#
# Sed supports the necessary operations as of version 4.4, but not in all
# earlier versions. Specifically, we have found that sed 4.1.5 is not 8-bit
# safe even when set to an 8-bit locale.
#
# OSX sed does not support escape sequences (\xhh), use echo as workaround.
#
# Alternatives considered:
# Perl - We originally used Perl, but wanted to avoid the dependency.
# Recent versions of Perl now error on invalid utf-8 characters.
# tr - tr only replaces single-byte sequences, so cannot handle utf-8.
LC_ALL=C sed -E \
-e 's/.*/& /g' \
-e 's/(('\
"$(echo -e '[\x9\x20-\x7f]')|"\
"$(echo -e '[\xc0-\xdf][\x80-\xbf]')|"\
"$(echo -e '[\xe0-\xec][\x80-\xbf][\x80-\xbf]')|"\
"$(echo -e '[\xed][\x80-\x9f][\x80-\xbf]')|"\
"$(echo -e '[\xee-\xef][\x80-\xbf][\x80-\xbf]')|"\
"$(echo -e '[\xf0][\x80-\x8f][\x80-\xbf][\x80-\xbf]')"\
')*)./\1?/g' \
-e 's/(.*)\?/\1/g' \
-e 's|]]>|]]>]]<![CDATA[>|g'
}

function encode_as_xml {
if [ -f "$1" ]; then
cat "$1" | encode_stream
fi
}

# For testing, we allow calling this script with "-", in which case we only
# perform the encoding step. We intentionally ignore the rest of the parameters.
if [ "$TEST_LOG" == "-" ]; then
encode_stream
exit 0
fi

test_name="${TEST_BINARY#./}"
test_name="${test_name#../}"
errors=0
error_msg=""
if (( $EXIT_CODE != 0 )); then
errors=1
error_msg="<error message=\"exited with error code $EXIT_CODE\"></error>"
fi

# Ensure that test shards have unique names in the xml output.
if [[ -n "${TEST_TOTAL_SHARDS+x}" ]] && ((TEST_TOTAL_SHARDS != 0)); then
((shard_num=TEST_SHARD_INDEX+1))
test_name="${test_name}"_shard_"${shard_num}"/"${TEST_TOTAL_SHARDS}"
fi

FAILED=0
ENCODED_LOG="$(encode_as_xml "${TEST_LOG}")" || FAILED=$?
cat >"${XML_OUTPUT_FILE}" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="${test_name}" tests="1" failures="0" errors="${errors}">
<testcase name="${test_name}" status="run" duration="${DURATION_IN_SECONDS}" time="${DURATION_IN_SECONDS}">${error_msg}</testcase>
<system-out>
Generated test.log (if the file is not UTF-8, then this may be unreadable):
<![CDATA[${ENCODED_LOG}]]>
</system-out>
</testsuite>
</testsuites>
EOF
exit "$FAILED"
22 changes: 22 additions & 0 deletions tools/test/run_with_xml.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash

# Runs a command, captures stdout/stderr to a log file, and emits JUnit XML to
# $XML_OUTPUT_FILE before exiting with the command's exit code.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG="${TEST_TMPDIR:-${TMPDIR:-/tmp}}/test.log"

START=$SECONDS
set +e
"$@" >"$LOG" 2>&1
EXIT_CODE=$?
set -e
DURATION=$((SECONDS - START))

if [[ -n "${XML_OUTPUT_FILE:-}" ]]; then
"${SCRIPT_DIR}/emit-test-xml.sh" "$LOG" "$XML_OUTPUT_FILE" "$DURATION" "$EXIT_CODE"
fi

exit "$EXIT_CODE"
Loading