Add aquery summary output formatter.

For executed actions we already have a summary in the BEP, but we didn't have
one for created actions.

PiperOrigin-RevId: 384648645
diff --git a/src/main/java/com/google/devtools/build/lib/query2/aquery/ActionGraphQueryEnvironment.java b/src/main/java/com/google/devtools/build/lib/query2/aquery/ActionGraphQueryEnvironment.java
index d43add6..9d95713 100644
--- a/src/main/java/com/google/devtools/build/lib/query2/aquery/ActionGraphQueryEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/query2/aquery/ActionGraphQueryEnvironment.java
@@ -175,6 +175,8 @@
             StreamedOutputHandler.OutputType.JSON,
             actionFilters),
         new ActionGraphTextOutputFormatterCallback(
+            eventHandler, aqueryOptions, out, skyframeExecutor, accessor, actionFilters),
+        new ActionGraphSummaryOutputFormatterCallback(
             eventHandler, aqueryOptions, out, skyframeExecutor, accessor, actionFilters));
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/query2/aquery/ActionGraphSummaryOutputFormatterCallback.java b/src/main/java/com/google/devtools/build/lib/query2/aquery/ActionGraphSummaryOutputFormatterCallback.java
new file mode 100644
index 0000000..e7b0031
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/query2/aquery/ActionGraphSummaryOutputFormatterCallback.java
@@ -0,0 +1,145 @@
+// 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.aquery;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
+import com.google.devtools.build.lib.actions.ActionOwner;
+import com.google.devtools.build.lib.analysis.AspectValue;
+import com.google.devtools.build.lib.analysis.ConfiguredTargetValue;
+import com.google.devtools.build.lib.buildeventstream.BuildEvent;
+import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos;
+import com.google.devtools.build.lib.events.ExtendedEventHandler;
+import com.google.devtools.build.lib.packages.AspectDescriptor;
+import com.google.devtools.build.lib.query2.engine.QueryEnvironment.TargetAccessor;
+import com.google.devtools.build.lib.skyframe.RuleConfiguredTargetValue;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/** Output callback for aquery, prints a human readable summary. */
+class ActionGraphSummaryOutputFormatterCallback extends AqueryThreadsafeCallback {
+
+  private final AqueryActionFilter actionFilters;
+  private final Map<String, Integer> mnemonicToCount = new HashMap<>();
+  private final Map<String, Integer> configurationToCount = new HashMap<>();
+  private final Map<String, Integer> execPlatformToCount = new HashMap<>();
+  private final Map<String, Integer> aspectToCount = new HashMap<>();
+
+  ActionGraphSummaryOutputFormatterCallback(
+      ExtendedEventHandler eventHandler,
+      AqueryOptions options,
+      OutputStream out,
+      SkyframeExecutor skyframeExecutor,
+      TargetAccessor<ConfiguredTargetValue> accessor,
+      AqueryActionFilter actionFilters) {
+    super(eventHandler, options, out, skyframeExecutor, accessor);
+    this.actionFilters = actionFilters;
+  }
+
+  @Override
+  public String getName() {
+    return "summary";
+  }
+
+  @Override
+  public void processOutput(Iterable<ConfiguredTargetValue> partialResult)
+      throws IOException, InterruptedException {
+    // Enabling includeParamFiles should enable includeCommandline by default.
+    options.includeCommandline |= options.includeParamFiles;
+
+    for (ConfiguredTargetValue configuredTargetValue : partialResult) {
+      if (!(configuredTargetValue instanceof RuleConfiguredTargetValue)) {
+        // We have to include non-rule values in the graph to visit their dependencies, but they
+        // don't have any actions to print out.
+        continue;
+      }
+      for (ActionAnalysisMetadata action :
+          ((RuleConfiguredTargetValue) configuredTargetValue).getActions()) {
+        processAction(action);
+      }
+      if (options.useAspects) {
+        for (AspectValue aspectValue : accessor.getAspectValues(configuredTargetValue)) {
+          for (ActionAnalysisMetadata action : aspectValue.getActions()) {
+            processAction(action);
+          }
+        }
+      }
+    }
+  }
+
+  private void processAction(ActionAnalysisMetadata action) throws InterruptedException {
+    if (!AqueryUtils.matchesAqueryFilters(action, actionFilters)) {
+      return;
+    }
+
+    mnemonicToCount.merge(action.getMnemonic(), 1, Integer::sum);
+    ActionOwner actionOwner = action.getOwner();
+    if (actionOwner != null) {
+      BuildEvent configuration = actionOwner.getConfiguration();
+      BuildEventStreamProtos.Configuration configProto =
+          configuration.asStreamProto(/*context=*/ null).getConfiguration();
+      configurationToCount.merge(configProto.getMnemonic(), 1, Integer::sum);
+
+      if (actionOwner.getExecutionPlatform() != null) {
+        execPlatformToCount.merge(
+            actionOwner.getExecutionPlatform().label().toString(), 1, Integer::sum);
+      }
+
+      // In the case of aspect-on-aspect, AspectDescriptors are listed in
+      // topological order of the dependency graph.
+      // e.g. [A -> B] would imply that aspect A is applied on top of aspect B.
+      ImmutableList<AspectDescriptor> aspectDescriptors =
+          actionOwner.getAspectDescriptors().reverse();
+      if (!aspectDescriptors.isEmpty()) {
+        aspectDescriptors.forEach(
+            aspectDescriptor ->
+                aspectToCount.merge(aspectDescriptor.getAspectClass().getName(), 1, Integer::sum));
+      }
+    }
+  }
+
+  @Override
+  public void close(boolean failFast) throws InterruptedException, IOException {
+    if (failFast) {
+      return;
+    }
+
+    int totalActions = mnemonicToCount.values().stream().mapToInt(v -> v).sum();
+    if (totalActions == 0) {
+      printStream.println("No actions matched.");
+    } else {
+      printStream.println(totalActions + " total action" + (totalActions == 1 ? "" : "s") + ".");
+    }
+
+    printSummary(mnemonicToCount, "Mnemonics:");
+    printSummary(configurationToCount, "Configurations:");
+    printSummary(execPlatformToCount, "Execution Platforms:");
+    printSummary(aspectToCount, "Aspects:");
+  }
+
+  private void printSummary(Map<String, Integer> actionsCount, String s) {
+    if (!actionsCount.isEmpty()) {
+      printStream.println();
+      printStream.println(s);
+      actionsCount.entrySet().stream()
+          .sorted(Comparator.comparingInt(Entry::getValue))
+          .forEach(entry -> printStream.println("  " + entry.getKey() + ": " + entry.getValue()));
+    }
+  }
+}
diff --git a/src/test/shell/integration/aquery_test.sh b/src/test/shell/integration/aquery_test.sh
index 59bf5d4..f51ee77 100755
--- a/src/test/shell/integration/aquery_test.sh
+++ b/src/test/shell/integration/aquery_test.sh
@@ -1381,10 +1381,31 @@
   bazel clean
 
   bazel build --experimental_aquery_dump_after_build_format=text --experimental_aquery_dump_after_build_output_file="$TEST_TMPDIR/foo.out" "//$pkg:foo" \
-    &> "$TEST_log" && fail "Expected success"
+    &> "$TEST_log" && fail "Expected failure"
   expect_log "--skyframe_state must be used with --output=proto\|textproto\|jsonproto. Invalid aquery output format: text"
 }
 
+function test_summary_output() {
+  local pkg="${FUNCNAME[0]}"
+  mkdir -p "$pkg" || fail "mkdir -p $pkg"
+  cat > "$pkg/BUILD" <<'EOF'
+genrule(
+    name = "foo",
+    srcs = ["in"],
+    outs = ["out"],
+    cmd = "touch $(OUTS)",
+)
+EOF
+  touch "$pkg/in"
+
+  bazel aquery --output=summary "//$pkg:all" \
+    &> "$TEST_log" || fail "Expected success"
+  expect_log "1 total action"
+  expect_log "Genrule: 1"
+  # Not matching the full string here since it's OS dependent.
+  expect_log "-fastbuild: 1"
+}
+
 # Usage: assert_matches expected_pattern actual
 function assert_matches() {
   [[ "$2" =~ $1 ]] || fail "Expected to match '$1', was: $2"