file_test, rule_test: now as sh_test rules

All test rules in
@bazel_tools//tools/build_rule:test_rules.bzl are
now macros around sh_test.

This allows running them on Windows with the
Windows-native test wrapper.

Fixes https://github.com/bazelbuild/bazel/issues/8203
Unblocks https://github.com/bazelbuild/bazel/issues/6622

Closes #8352.

PiperOrigin-RevId: 248680587
diff --git a/tools/build_rules/BUILD b/tools/build_rules/BUILD
index 0b3f17a..7765a78 100644
--- a/tools/build_rules/BUILD
+++ b/tools/build_rules/BUILD
@@ -11,6 +11,7 @@
     srcs = [
         "BUILD.tools",
         "test_rules.bzl",
+        "test_rules_private.bzl",
     ],
     visibility = ["//visibility:public"],
 )
@@ -18,6 +19,9 @@
 py_test(
     name = "test_rules_test",
     srcs = ["test_rules_test.py"],
-    data = ["test_rules.bzl"],
+    data = [
+        "test_rules.bzl",
+        "test_rules_private.bzl",
+    ],
     deps = ["//src/test/py/bazel:test_base"],
 )
diff --git a/tools/build_rules/test_rules.bzl b/tools/build_rules/test_rules.bzl
index 57526f9..cb23294 100644
--- a/tools/build_rules/test_rules.bzl
+++ b/tools/build_rules/test_rules.bzl
@@ -14,10 +14,31 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+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):
+    native.sh_test(
+        name = name,
+        srcs = [name + "_impl"],
+        data = [name + "_impl"],
+        deps = [BASH_RUNFILES_DEP],
+        **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.
@@ -25,72 +46,100 @@
     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 = _SH_STUB + 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 = _SH_STUB + 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):
@@ -185,8 +234,8 @@
       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)
@@ -195,11 +244,11 @@
 
 ### 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 = []
@@ -244,30 +293,42 @@
             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 = _SH_STUB + "\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
@@ -282,28 +343,29 @@
             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 = _SH_STUB + 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 = _SH_STUB + 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,
@@ -312,8 +374,20 @@
         "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)
diff --git a/tools/build_rules/test_rules_private.bzl b/tools/build_rules/test_rules_private.bzl
new file mode 100644
index 0000000..30b638d
--- /dev/null
+++ b/tools/build_rules/test_rules_private.bzl
@@ -0,0 +1,46 @@
+# Copyright 2019 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.
+
+"""Bash runfiles library init code for test_rules.bzl."""
+
+# Init code to load the runfiles.bash file.
+# 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.
+INIT_BASH_RUNFILES = [
+    "# --- begin runfiles.bash initialization ---",
+    "# Copy-pasted from Bazel Bash runfiles library (tools/bash/runfiles/runfiles.bash).",
+    "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 ---",
+]
+
+# Label of the runfiles library.
+BASH_RUNFILES_DEP = "@bazel_tools//tools/bash/runfiles"
diff --git a/tools/build_rules/test_rules_test.py b/tools/build_rules/test_rules_test.py
index 5299c57..eb3881b 100644
--- a/tools/build_rules/test_rules_test.py
+++ b/tools/build_rules/test_rules_test.py
@@ -38,6 +38,9 @@
     self.CopyFile(
         self.Rlocation('io_bazel/tools/build_rules/test_rules.bzl'),
         'foo/test_rules.bzl')
+    self.CopyFile(
+        self.Rlocation('io_bazel/tools/build_rules/test_rules_private.bzl'),
+        'foo/test_rules_private.bzl')
     self.ScratchFile('foo/tested_file.txt',
                      ['The quick brown', 'fox jumps over', 'the lazy dog.'])
     self.ScratchFile('foo/BUILD', [