Write a test script based on analysis tests' AnalysisTestResultInfo

The generated script will result in test pass/failure based on the info object returned by the implementation function.

Progress toward #6237

RELNOTES: None.
PiperOrigin-RevId: 218424616
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTargetBuilder.java b/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTargetBuilder.java
index 1679f60..222125c 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTargetBuilder.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RuleConfiguredTargetBuilder.java
@@ -30,6 +30,8 @@
 import com.google.devtools.build.lib.analysis.constraints.SupportedEnvironments;
 import com.google.devtools.build.lib.analysis.constraints.SupportedEnvironmentsProvider;
 import com.google.devtools.build.lib.analysis.constraints.SupportedEnvironmentsProvider.RemovedEnvironmentCulprit;
+import com.google.devtools.build.lib.analysis.test.AnalysisTestActionBuilder;
+import com.google.devtools.build.lib.analysis.test.AnalysisTestResultInfo;
 import com.google.devtools.build.lib.analysis.test.ExecutionInfo;
 import com.google.devtools.build.lib.analysis.test.InstrumentedFilesProvider;
 import com.google.devtools.build.lib.analysis.test.TestActionBuilder;
@@ -147,13 +149,24 @@
     }
 
     TransitiveInfoProviderMap providers = providersBuilder.build();
-    AnalysisEnvironment analysisEnvironment = ruleContext.getAnalysisEnvironment();
-    GeneratingActions generatingActions =
-        Actions.filterSharedActionsAndThrowActionConflict(
-            analysisEnvironment.getActionKeyContext(), analysisEnvironment.getRegisteredActions());
+
     if (ruleContext.getRule().isAnalysisTest()) {
-      Preconditions.checkState(generatingActions.getActions().isEmpty());
+      // If the target is an analysis test that returned AnalysisTestResultInfo, register a
+      // test pass/fail action on behalf of the target.
+      AnalysisTestResultInfo testResultInfo =
+          providers.get(AnalysisTestResultInfo.SKYLARK_CONSTRUCTOR);
+
+      if (testResultInfo == null) {
+        ruleContext.ruleError(
+            "rules with analysis_test=true must return an instance of AnalysisTestResultInfo");
+        return null;
+      }
+
+      AnalysisTestActionBuilder.writeAnalysisTestAction(ruleContext, testResultInfo);
     }
+    AnalysisEnvironment analysisEnvironment = ruleContext.getAnalysisEnvironment();
+    GeneratingActions generatingActions = Actions.filterSharedActionsAndThrowActionConflict(
+          analysisEnvironment.getActionKeyContext(), analysisEnvironment.getRegisteredActions());
     return new RuleConfiguredTarget(
         ruleContext,
         providers,
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java b/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
index 66d071e..d59175c 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
@@ -93,6 +93,7 @@
 import com.google.devtools.build.lib.syntax.Type;
 import com.google.devtools.build.lib.syntax.Type.LabelClass;
 import com.google.devtools.build.lib.util.FileTypeSet;
+import com.google.devtools.build.lib.util.OS;
 import com.google.devtools.build.lib.util.OrderedSetMultimap;
 import com.google.devtools.build.lib.util.StringUtil;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
@@ -549,7 +550,31 @@
    * which this target (which must be an OutputFile or a Rule) is associated.
    */
   public Artifact createOutputArtifact() {
-    return internalCreateOutputArtifact(getTarget(), OutputFile.Kind.FILE);
+    Target target = getTarget();
+    PathFragment rootRelativePath = getPackageDirectory()
+        .getRelative(PathFragment.create(target.getName()));
+
+    return internalCreateOutputArtifact(rootRelativePath, target, OutputFile.Kind.FILE);
+  }
+
+  /**
+   * Returns an artifact beneath the root of either the "bin" or "genfiles"
+   * tree, whose path is based on the name of this target and the current
+   * configuration, with a script suffix appropriate for the current host platform. ({@code .cmd}
+   * for Windows, otherwise {@code .sh}). The choice of which tree to use is based on the rule with
+   * which this target (which must be an OutputFile or a Rule) is associated.
+   */
+  public Artifact createOutputArtifactScript() {
+    Target target = getTarget();
+    // TODO(laszlocsomor): Use the execution platform, not the host platform.
+    boolean isExecutedOnWindows = OS.getCurrent() == OS.WINDOWS;
+
+    String fileExtension = isExecutedOnWindows ? ".cmd" : ".sh";
+
+    PathFragment rootRelativePath = getPackageDirectory()
+        .getRelative(PathFragment.create(target.getName() + fileExtension));
+
+    return internalCreateOutputArtifact(rootRelativePath, target, OutputFile.Kind.FILE);
   }
 
   /**
@@ -558,7 +583,9 @@
    * @see #createOutputArtifact()
    */
   public Artifact createOutputArtifact(OutputFile out) {
-    return internalCreateOutputArtifact(out, out.getKind());
+    PathFragment packageRelativePath = getPackageDirectory()
+        .getRelative(PathFragment.create(out.getName()));
+    return internalCreateOutputArtifact(packageRelativePath, out, out.getKind());
   }
 
   /**
@@ -567,19 +594,19 @@
    * {@link #createOutputArtifact(OutputFile)} can have a more specific
    * signature.
    */
-  private Artifact internalCreateOutputArtifact(Target target, OutputFile.Kind outputFileKind) {
+  private Artifact internalCreateOutputArtifact(PathFragment rootRelativePath,
+      Target target, OutputFile.Kind outputFileKind) {
     Preconditions.checkState(
         target.getLabel().getPackageIdentifier().equals(getLabel().getPackageIdentifier()),
         "Creating output artifact for target '%s' in different package than the rule '%s' "
             + "being analyzed", target.getLabel(), getLabel());
     ArtifactRoot root = getBinOrGenfilesDirectory();
-    PathFragment packageRelativePath = getPackageDirectory()
-        .getRelative(PathFragment.create(target.getName()));
+
     switch (outputFileKind) {
       case FILE:
-        return getDerivedArtifact(packageRelativePath, root);
+        return getDerivedArtifact(rootRelativePath, root);
       case FILESET:
-        return getAnalysisEnvironment().getFilesetArtifact(packageRelativePath, root);
+        return getAnalysisEnvironment().getFilesetArtifact(rootRelativePath, root);
       default:
         throw new IllegalStateException();
     }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java
index fa17854..c24d151 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java
@@ -285,6 +285,8 @@
               + "general use. It is subject to change at any time. It may be enabled by specifying "
               + "--experimental_analysis_testing_improvements");
     }
+    // analysis_test=true implies test=true.
+    test |= Boolean.TRUE.equals(analysisTest);
 
     RuleClassType type = test ? RuleClassType.TEST : RuleClassType.NORMAL;
     RuleClass parent =
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleConfiguredTargetUtil.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleConfiguredTargetUtil.java
index e110c46..954f655 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleConfiguredTargetUtil.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleConfiguredTargetUtil.java
@@ -13,6 +13,7 @@
 // limitations under the License.
 package com.google.devtools.build.lib.analysis.skylark;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -491,6 +492,16 @@
         }
     }
 
+    if (context.getRuleContext().getRule().isAnalysisTest()) {
+      // The Starlark Build API should already throw exception if the rule implementation attempts
+      // to register any actions. This is just a sanity check of this invariant.
+      Preconditions.checkState(
+          context.getRuleContext().getAnalysisEnvironment().getRegisteredActions().isEmpty(),
+          "%s", context.getRuleContext().getLabel());
+
+      executable = context.getRuleContext().createOutputArtifactScript();
+    }
+
     if (executable == null && context.isExecutable()) {
       if (context.isDefaultExecutableCreated()) {
         // This doesn't actually create a new Artifact just returns the one
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/AnalysisTestActionBuilder.java b/src/main/java/com/google/devtools/build/lib/analysis/test/AnalysisTestActionBuilder.java
new file mode 100644
index 0000000..18a9eb6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/test/AnalysisTestActionBuilder.java
@@ -0,0 +1,68 @@
+// 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.
+
+package com.google.devtools.build.lib.analysis.test;
+
+import com.google.common.base.Splitter;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
+import com.google.devtools.build.lib.util.OS;
+
+/**
+ * Helper for writing test actions for analysis test rules. Analysis test rules are
+ * restricted to disallow the rule implementation functions from registering actions themselves;
+ * such rules register test success/failure via {@link AnalysisTestResultInfo}. This helper
+ * registers the appropriate test script simulating success or failure of the test.
+ */
+public class AnalysisTestActionBuilder {
+
+  /**
+   * Register and return an action to write a test script to the default executable location
+   * reflecting the given info object.
+   */
+  public static FileWriteAction writeAnalysisTestAction(
+      RuleContext ruleContext,
+      AnalysisTestResultInfo infoObject) {
+    FileWriteAction action;
+    // TODO(laszlocsomor): Use the execution platform, not the host platform.
+    boolean isExecutedOnWindows = OS.getCurrent() == OS.WINDOWS;
+
+    if (isExecutedOnWindows) {
+      StringBuilder sb = new StringBuilder().append("@echo off\n");
+      for (String line : Splitter.on("\n").split(infoObject.getMessage())) {
+        sb.append("echo ").append(line).append("\n");
+      }
+      String content = sb
+          .append("exit /b ").append(infoObject.getSuccess() ? "0" : "1")
+          .toString();
+
+      action = FileWriteAction.create(ruleContext,
+          ruleContext.createOutputArtifactScript(), content, /* executable */ true);
+
+    } else {
+      String content =
+          "cat << EOF\n"
+              + infoObject.getMessage()
+              + "\n"
+              + "EOF\n"
+              + "exit "
+              + (infoObject.getSuccess() ? "0" : "1");
+      action = FileWriteAction.create(ruleContext,
+          ruleContext.createOutputArtifactScript(), content, /* executable */ true);
+    }
+
+    ruleContext.registerAction(action);
+    return action;
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java
index c1e7e22..c0374df 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java
@@ -2133,10 +2133,10 @@
         "def custom_rule_impl(ctx):",
         "  return []",
         "",
-        "custom_rule = rule(implementation = custom_rule_impl, analysis_test = True)");
+        "custom_test = rule(implementation = custom_rule_impl, analysis_test = True)");
 
     scratch.file(
-        "test/BUILD", "load('//test:extension.bzl', 'custom_rule')", "", "custom_rule(name = 'r')");
+        "test/BUILD", "load('//test:extension.bzl', 'custom_test')", "", "custom_test(name = 'r')");
 
     reporter.removeHandler(failFastHandler);
     getConfiguredTarget("//test:r");
@@ -2155,10 +2155,10 @@
         "  ctx.actions.write(output=out_file, content='hi')",
         "  return []",
         "",
-        "custom_rule = rule(implementation = custom_rule_impl, analysis_test = True)");
+        "custom_test = rule(implementation = custom_rule_impl, analysis_test = True)");
 
     scratch.file(
-        "test/BUILD", "load('//test:extension.bzl', 'custom_rule')", "", "custom_rule(name = 'r')");
+        "test/BUILD", "load('//test:extension.bzl', 'custom_test')", "", "custom_test(name = 'r')");
 
     reporter.removeHandler(failFastHandler);
     getConfiguredTarget("//test:r");
@@ -2175,10 +2175,10 @@
         "def custom_rule_impl(ctx):",
         "  return [AnalysisTestResultInfo(success = True, message = 'message contents')]",
         "",
-        "custom_rule = rule(implementation = custom_rule_impl, analysis_test = True)");
+        "custom_test = rule(implementation = custom_rule_impl, analysis_test = True)");
 
     scratch.file(
-        "test/BUILD", "load('//test:extension.bzl', 'custom_rule')", "", "custom_rule(name = 'r')");
+        "test/BUILD", "load('//test:extension.bzl', 'custom_test')", "", "custom_test(name = 'r')");
 
     ConfiguredTarget target = getConfiguredTarget("//test:r");
     AnalysisTestResultInfo info =
@@ -2203,7 +2203,8 @@
         "",
         "def outer_rule_impl(ctx):",
         "  return [MyInfo(strict_java_deps = ctx.fragments.java.strict_java_deps),",
-        "          MyDep(info = ctx.attr.dep[0][MyInfo])]",
+        "          MyDep(info = ctx.attr.dep[0][MyInfo]),",
+        "          AnalysisTestResultInfo(success = True, message = 'message contents')]",
         "def inner_rule_impl(ctx):",
         "  return [MyInfo(strict_java_deps = ctx.fragments.java.strict_java_deps)]",
         "",
@@ -2217,7 +2218,7 @@
         "",
         "inner_rule = rule(implementation = inner_rule_impl,",
         "                  fragments = ['java'])",
-        "outer_rule = rule(",
+        "outer_rule_test = rule(",
         "  implementation = outer_rule_impl,",
         "  fragments = ['java'],",
         "  analysis_test = True,",
@@ -2227,10 +2228,10 @@
 
     scratch.file(
         "test/BUILD",
-        "load('//test:extension.bzl', 'inner_rule', 'outer_rule')",
+        "load('//test:extension.bzl', 'inner_rule', 'outer_rule_test')",
         "",
         "inner_rule(name = 'inner')",
-        "outer_rule(name = 'r', dep = ':inner')");
+        "outer_rule_test(name = 'r', dep = ':inner')");
 
     SkylarkKey myInfoKey =
         new SkylarkKey(Label.parseAbsolute("//test:extension.bzl", ImmutableMap.of()), "MyInfo");
diff --git a/src/test/shell/integration/BUILD b/src/test/shell/integration/BUILD
index a651ca9..159416b 100644
--- a/src/test/shell/integration/BUILD
+++ b/src/test/shell/integration/BUILD
@@ -183,6 +183,16 @@
 )
 
 sh_test(
+    name = "analysis_test_test",
+    size = "medium",
+    srcs = ["analysis_test_test.sh"],
+    data = [
+        ":test-deps",
+        "@bazel_tools//tools/bash/runfiles",
+    ],
+)
+
+sh_test(
     name = "aquery_test",
     size = "large",
     srcs = ["aquery_test.sh"],
diff --git a/src/test/shell/integration/analysis_test_test.sh b/src/test/shell/integration/analysis_test_test.sh
new file mode 100755
index 0000000..21be66b
--- /dev/null
+++ b/src/test/shell/integration/analysis_test_test.sh
@@ -0,0 +1,118 @@
+#!/bin/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.
+#
+# Tests the examples provided in Bazel
+#
+
+# --- begin runfiles.bash initialization ---
+# Copy-pasted from Bazel's 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 ---
+
+source "$(rlocation "io_bazel/src/test/shell/integration_test_setup.sh")" \
+  || { echo "integration_test_setup.sh not found!" >&2; exit 1; }
+
+case "$(uname -s | tr [:upper:] [:lower:])" in
+msys*|mingw*|cygwin*)
+  declare -r is_windows=true
+  ;;
+*)
+  declare -r is_windows=false
+  ;;
+esac
+
+if "$is_windows"; then
+  export MSYS_NO_PATHCONV=1
+  export MSYS2_ARG_CONV_EXCL="*"
+fi
+
+function test_passing_test() {
+  mkdir -p package
+  cat > package/test.bzl <<EOF
+def _rule_test_impl(ctx):
+  return [AnalysisTestResultInfo(success = True,
+                                 message = 'A success message!')]
+
+my_rule_test = rule(
+  implementation = _rule_test_impl,
+  analysis_test = True,
+)
+EOF
+
+  cat > package/BUILD <<EOF
+load(":test.bzl", "my_rule_test")
+
+my_rule_test(name = "r")
+EOF
+
+  bazel test package:r --experimental_analysis_testing_improvements \
+      >& "$TEST_log" || fail "Unexpected failure"
+
+  expect_log "PASSED"
+
+  cat "${PRODUCT_NAME}-testlogs/package/r/test.log" > "$TEST_log"
+
+  expect_log "A success message!"
+}
+
+function test_failing_test() {
+  mkdir -p package
+  cat > package/test.bzl <<EOF
+def _rule_test_impl(ctx):
+  return [AnalysisTestResultInfo(success = False,
+                                 message = 'A failure message!')]
+
+my_rule_test = rule(
+  implementation = _rule_test_impl,
+  analysis_test = True,
+)
+EOF
+
+  cat > package/BUILD <<EOF
+load(":test.bzl", "my_rule_test")
+
+my_rule_test(name = "r")
+EOF
+
+  ! bazel test package:r --experimental_analysis_testing_improvements \
+      >& "$TEST_log" || fail "Unexpected success"
+
+  expect_log "FAILED"
+
+  cat "${PRODUCT_NAME}-testlogs/package/r/test.log" > "$TEST_log"
+
+  expect_log "A failure message!"
+}
+
+run_suite "analysis_test rule tests"