Add text output to aquery.

RELNOTES[NEW]: The aquery command now supports --output=text.

PiperOrigin-RevId: 207096607
diff --git a/src/main/java/com/google/devtools/build/lib/query2/ActionGraphQueryEnvironment.java b/src/main/java/com/google/devtools/build/lib/query2/ActionGraphQueryEnvironment.java
index e3dd353..47509b5 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/ActionGraphQueryEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/ActionGraphQueryEnvironment.java
@@ -140,6 +140,8 @@
     OutputStream out = reporter.getOutErr().getOutputStream();
     return ImmutableList.of(
         new ActionGraphProtoOutputFormatterCallback(
+            reporter, aqueryOptions, out, skyframeExecutor, accessor),
+        new ActionGraphTextOutputFormatterCallback(
             reporter, aqueryOptions, out, skyframeExecutor, accessor));
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/query2/ActionGraphTextOutputFormatterCallback.java b/src/main/java/com/google/devtools/build/lib/query2/ActionGraphTextOutputFormatterCallback.java
new file mode 100644
index 0000000..23be8bd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/ActionGraphTextOutputFormatterCallback.java
@@ -0,0 +1,155 @@
+// 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.query2;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Streams;
+import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
+import com.google.devtools.build.lib.actions.ActionExecutionMetadata;
+import com.google.devtools.build.lib.actions.ActionKeyContext;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.actions.CommandLineExpansionException;
+import com.google.devtools.build.lib.analysis.actions.SpawnAction;
+import com.google.devtools.build.lib.buildeventstream.BuildEvent;
+import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.TargetAccessor;
+import com.google.devtools.build.lib.query2.output.AqueryOptions;
+import com.google.devtools.build.lib.skyframe.ConfiguredTargetValue;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
+import com.google.devtools.build.lib.util.CommandDescriptionForm;
+import com.google.devtools.build.lib.util.CommandFailureUtils;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** Output callback for aquery, prints human readable output. */
+public class ActionGraphTextOutputFormatterCallback extends AqueryThreadsafeCallback {
+
+  private final ActionKeyContext actionKeyContext = new ActionKeyContext();
+
+  ActionGraphTextOutputFormatterCallback(
+      Reporter reporter,
+      AqueryOptions options,
+      OutputStream out,
+      SkyframeExecutor skyframeExecutor,
+      TargetAccessor<ConfiguredTargetValue> accessor) {
+    super(reporter, options, out, skyframeExecutor, accessor);
+  }
+
+  @Override
+  public String getName() {
+    return "text";
+  }
+
+  @Override
+  public void processOutput(Iterable<ConfiguredTargetValue> partialResult)
+      throws IOException, InterruptedException {
+    try {
+      for (ConfiguredTargetValue configuredTargetValue : partialResult) {
+        List<ActionAnalysisMetadata> actions = configuredTargetValue.getActions();
+        for (ActionAnalysisMetadata action : actions) {
+          writeAction(action, printStream);
+        }
+      }
+    } catch (CommandLineExpansionException e) {
+      throw new IOException(e.getMessage());
+    }
+  }
+
+  private void writeAction(ActionAnalysisMetadata action, PrintStream printStream)
+      throws IOException, CommandLineExpansionException {
+    ActionOwner actionOwner = action.getOwner();
+    StringBuilder stringBuilder = new StringBuilder();
+    stringBuilder
+        .append(action.prettyPrint())
+        .append('\n')
+        .append("  Mnemonic: ")
+        .append(action.getMnemonic())
+        .append('\n');
+
+    if (actionOwner != null) {
+      BuildEvent configuration = actionOwner.getConfiguration();
+      BuildEventStreamProtos.Configuration configProto =
+          configuration.asStreamProto(/*context=*/ null).getConfiguration();
+      stringBuilder
+          .append("  Owner: ")
+          .append(actionOwner.getLabel().toString())
+          .append('\n')
+          .append("  Configuration: ")
+          .append(configProto.getMnemonic())
+          .append('\n');
+    }
+
+    if (action instanceof ActionExecutionMetadata) {
+      ActionExecutionMetadata actionExecutionMetadata = (ActionExecutionMetadata) action;
+      stringBuilder
+          .append("  ActionKey: ")
+          .append(actionExecutionMetadata.getKey(actionKeyContext))
+          .append('\n');
+    }
+
+    stringBuilder
+        .append("  Inputs: [")
+        .append(
+            Streams.stream(action.getInputs())
+                .map(input -> input.getExecPathString())
+                .sorted()
+                .collect(Collectors.joining(", ")))
+        .append("]\n")
+        .append("  Outputs: [")
+        .append(
+            Streams.stream(action.getOutputs())
+                .map(input -> input.getExecPathString())
+                .sorted()
+                .collect(Collectors.joining(", ")))
+        .append("]\n");
+
+    if (action instanceof SpawnAction) {
+      SpawnAction spawnAction = (SpawnAction) action;
+      // TODO(twerth): This handles the fixed environment. We probably want to output the inherited
+      // environment as well.
+      ImmutableMap<String, String> fixedEnvironment = spawnAction.getEnvironment().getFixedEnv();
+      stringBuilder
+          .append("  Environment: [")
+          .append(
+              Streams.stream(fixedEnvironment.entrySet())
+                  .map(
+                      environmentVariable ->
+                          environmentVariable.getKey() + "=" + environmentVariable.getValue())
+                  .sorted()
+                  .collect(Collectors.joining(", ")))
+          .append("]\n")
+
+          // TODO(twerth): Add option to only optionally include the command line.
+          .append("  Command Line: ")
+          .append(
+              CommandFailureUtils.describeCommand(
+                  CommandDescriptionForm.COMPLETE,
+                  /* prettyPrintArgs= */ true,
+                  spawnAction.getArguments(),
+                  /* environment= */ null,
+                  /* cwd= */ null))
+          .append("\n");
+    }
+
+    stringBuilder.append('\n');
+
+    printStream.write(stringBuilder.toString().getBytes(UTF_8));
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/query2/BUILD b/src/main/java/com/google/devtools/build/lib/query2/BUILD
index b3ded13..3ccf9fb 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/query2/BUILD
@@ -27,6 +27,8 @@
         "//src/main/java/com/google/devtools/build/lib:packages-internal",
         "//src/main/java/com/google/devtools/build/lib:util",
         "//src/main/java/com/google/devtools/build/lib/actions",
+        "//src/main/java/com/google/devtools/build/lib/buildeventstream",
+        "//src/main/java/com/google/devtools/build/lib/buildeventstream/proto:build_event_stream_java_proto",
         "//src/main/java/com/google/devtools/build/lib/causes",
         "//src/main/java/com/google/devtools/build/lib/collect/compacthashset",
         "//src/main/java/com/google/devtools/build/lib/collect/nestedset",
diff --git a/src/test/shell/integration/aquery_test.sh b/src/test/shell/integration/aquery_test.sh
index 735967a..e21f0c4 100755
--- a/src/test/shell/integration/aquery_test.sh
+++ b/src/test/shell/integration/aquery_test.sh
@@ -45,9 +45,39 @@
   assert_contains "//$pkg:foo" output
   assert_not_contains "//$pkg:bar" output
 
-  bazel aquery "deps(//$pkg:foo)" > output 2> "$TEST_log" || fail "Expected success"
+  bazel aquery "deps(//$pkg:foo)" > output 2> "$TEST_log" \
+    || fail "Expected success"
   assert_contains "//$pkg:foo" output
   assert_contains "//$pkg:bar" output
 }
 
+function test_aquery_text() {
+  local pkg="${FUNCNAME[0]}"
+  mkdir -p "$pkg" || fail "mkdir -p $pkg"
+  cat > "$pkg/BUILD" <<'EOF'
+genrule(
+    name = "bar",
+    srcs = ["dummy.txt"],
+    outs = ["bar_out.txt"],
+    cmd = "echo unused > $(OUTS)",
+)
+EOF
+  echo "hello aquery" > "$pkg/in.txt"
+
+  bazel aquery --output=text "//$pkg:bar" > output 2> "$TEST_log" \
+    || fail "Expected success"
+  cat output >> "$TEST_log"
+  assert_contains "action 'Executing genrule //$pkg:bar'" output
+  assert_contains "Mnemonic: Genrule" output
+  assert_contains "Owner: //$pkg:bar" output
+  assert_contains "Configuration: .*-fastbuild" output
+  # Only check that the inputs/outputs/command line/environment exist, but not
+  # their actual contents since that would be too much.
+  assert_contains "Inputs: \[" output
+  assert_contains "Outputs: \[" output
+  assert_contains "Command Line: (" output
+  assert_contains "Environment: \[" output
+
+}
+
 run_suite "${PRODUCT_NAME} action graph query tests"