diff --git a/diff/tests/BUILD.bazel b/diff/tests/BUILD.bazel index 7521b6d..e47f9ac 100644 --- a/diff/tests/BUILD.bazel +++ b/diff/tests/BUILD.bazel @@ -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") diff --git a/e2e/diffutils_from_source/BUILD.bazel b/e2e/diffutils_from_source/BUILD.bazel index 8411c42..f3eff47 100644 --- a/e2e/diffutils_from_source/BUILD.bazel +++ b/e2e/diffutils_from_source/BUILD.bazel @@ -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") diff --git a/e2e/smoke/BUILD.bazel b/e2e/smoke/BUILD.bazel index 12438f3..fe84ca4 100644 --- a/e2e/smoke/BUILD.bazel +++ b/e2e/smoke/BUILD.bazel @@ -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") diff --git a/e2e/validate_override/BUILD.bazel b/e2e/validate_override/BUILD.bazel index b8f7059..0312397 100644 --- a/e2e/validate_override/BUILD.bazel +++ b/e2e/validate_override/BUILD.bazel @@ -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") diff --git a/tools/test/BUILD.bazel b/tools/test/BUILD.bazel new file mode 100644 index 0000000..b57ec53 --- /dev/null +++ b/tools/test/BUILD.bazel @@ -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", +]) diff --git a/tools/test/build_test.bzl b/tools/test/build_test.bzl new file mode 100644 index 0000000..4985630 --- /dev/null +++ b/tools/test/build_test.bzl @@ -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 + ) diff --git a/tools/test/build_test.sh b/tools/test/build_test.sh new file mode 100755 index 0000000..016848a --- /dev/null +++ b/tools/test/build_test.sh @@ -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" diff --git a/tools/test/emit-test-xml.sh b/tools/test/emit-test-xml.sh new file mode 100755 index 0000000..9b73b01 --- /dev/null +++ b/tools/test/emit-test-xml.sh @@ -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 ']]>]]' 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|]]>|]]>]]|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="" +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}" < + + + ${error_msg} + +Generated test.log (if the file is not UTF-8, then this may be unreadable): + + + + +EOF +exit "$FAILED" diff --git a/tools/test/run_with_xml.sh b/tools/test/run_with_xml.sh new file mode 100755 index 0000000..8de4e60 --- /dev/null +++ b/tools/test/run_with_xml.sh @@ -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"