aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/runtime/TestSummaryPrinter.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/runtime/TestSummaryPrinter.java')
-rw-r--r--src/main/java/com/google/devtools/build/lib/runtime/TestSummaryPrinter.java255
1 files changed, 255 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/TestSummaryPrinter.java b/src/main/java/com/google/devtools/build/lib/runtime/TestSummaryPrinter.java
new file mode 100644
index 0000000000..91c1488054
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/TestSummaryPrinter.java
@@ -0,0 +1,255 @@
+// Copyright 2014 Google Inc. 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.runtime;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.devtools.build.lib.rules.test.TestLogHelper;
+import com.google.devtools.build.lib.rules.test.TestStrategy.TestOutputFormat;
+import com.google.devtools.build.lib.util.LoggingUtil;
+import com.google.devtools.build.lib.util.io.AnsiTerminalPrinter;
+import com.google.devtools.build.lib.util.io.AnsiTerminalPrinter.Mode;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus;
+import com.google.devtools.build.lib.view.test.TestStatus.FailedTestCasesStatus;
+import com.google.devtools.build.lib.view.test.TestStatus.TestCase;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+
+/**
+ * Print test statistics in human readable form.
+ */
+public class TestSummaryPrinter {
+
+ /**
+ * Print the cached test log to the given printer.
+ */
+ public static void printCachedOutput(TestSummary summary,
+ TestOutputFormat testOutput,
+ AnsiTerminalPrinter printer) {
+
+ String testName = summary.getTarget().getLabel().toString();
+ List<String> allLogs = new ArrayList<>();
+ for (Path path : summary.getFailedLogs()) {
+ allLogs.add(path.getPathString());
+ }
+ for (Path path : summary.getPassedLogs()) {
+ allLogs.add(path.getPathString());
+ }
+ printer.printLn("" + TestSummary.getStatusMode(summary.getStatus()) + summary.getStatus() + ": "
+ + Mode.DEFAULT + testName + " (see " + Joiner.on(' ').join(allLogs) + ")");
+ printer.printLn(Mode.INFO + "INFO: " + Mode.DEFAULT + "From Testing " + testName);
+
+ // Whether to output the target at all was checked by the caller.
+ // Now check whether to output failing shards.
+ if (TestLogHelper.shouldOutputTestLog(testOutput, false)) {
+ for (Path path : summary.getFailedLogs()) {
+ try {
+ TestLogHelper.writeTestLog(path, testName, printer.getOutputStream());
+ } catch (IOException e) {
+ printer.printLn("==================== Could not read test output for " + testName);
+ LoggingUtil.logToRemote(Level.WARNING, "Error while reading test log", e);
+ }
+ }
+ }
+
+ // And passing shards, independently.
+ if (TestLogHelper.shouldOutputTestLog(testOutput, true)) {
+ for (Path path : summary.getPassedLogs()) {
+ try {
+ TestLogHelper.writeTestLog(path, testName, printer.getOutputStream());
+ } catch (Exception e) {
+ printer.printLn("==================== Could not read test output for " + testName);
+ LoggingUtil.logToRemote(Level.WARNING, "Error while reading test log", e);
+ }
+ }
+ }
+ }
+
+ private static String statusString(BlazeTestStatus status) {
+ return status.toString().replace('_', ' ');
+ }
+
+ /**
+ * Prints summary status for a single test.
+ * @param terminalPrinter The printer to print to
+ */
+ public static void print(
+ TestSummary summary,
+ AnsiTerminalPrinter terminalPrinter,
+ boolean verboseSummary, boolean printFailedTestCases) {
+ // Skip output for tests that failed to build.
+ if (summary.getStatus() == BlazeTestStatus.FAILED_TO_BUILD) {
+ return;
+ }
+ String message = getCacheMessage(summary) + statusString(summary.getStatus());
+ terminalPrinter.print(
+ Strings.padEnd(summary.getTarget().getLabel().toString(), 78 - message.length(), ' ')
+ + " " + TestSummary.getStatusMode(summary.getStatus()) + message + Mode.DEFAULT
+ + (verboseSummary ? getAttemptSummary(summary) + getTimeSummary(summary) : "") + "\n");
+
+ if (printFailedTestCases && summary.getStatus() == BlazeTestStatus.FAILED) {
+ if (summary.getFailedTestCasesStatus() == FailedTestCasesStatus.NOT_AVAILABLE) {
+ terminalPrinter.print(
+ Mode.WARNING + " (individual test case information not available) "
+ + Mode.DEFAULT + "\n");
+ } else {
+ for (TestCase testCase : summary.getFailedTestCases()) {
+ if (testCase.getStatus() != TestCase.Status.PASSED) {
+ TestSummaryPrinter.printTestCase(terminalPrinter, testCase);
+ }
+ }
+
+ if (summary.getFailedTestCasesStatus() != FailedTestCasesStatus.FULL) {
+ terminalPrinter.print(
+ Mode.WARNING
+ + " (some shards did not report details, list of failed test"
+ + " cases incomplete)\n"
+ + Mode.DEFAULT);
+ }
+ }
+ }
+
+ if (printFailedTestCases) {
+ // In this mode, test output and coverage files would just clutter up
+ // the output.
+ return;
+ }
+
+ for (String warning : summary.getWarnings()) {
+ terminalPrinter.print(" " + AnsiTerminalPrinter.Mode.WARNING + "WARNING: "
+ + AnsiTerminalPrinter.Mode.DEFAULT + warning + "\n");
+ }
+
+ for (Path path : summary.getFailedLogs()) {
+ if (path.exists()) {
+ // Don't use getPrettyPath() here - we want to print the absolute path,
+ // so that it cut and paste into a different terminal, and we don't
+ // want to use the blaze-bin etc. symbolic links because they could be changed
+ // by a subsequent build with different options.
+ terminalPrinter.print(" " + path.getPathString() + "\n");
+ }
+ }
+ for (Path path : summary.getCoverageFiles()) {
+ // Print only non-trivial coverage files.
+ try {
+ if (path.exists() && path.getFileSize() > 0) {
+ terminalPrinter.print(" " + path.getPathString() + "\n");
+ }
+ } catch (IOException e) {
+ LoggingUtil.logToRemote(Level.WARNING, "Error while reading coverage data file size",
+ e);
+ }
+ }
+ }
+
+ /**
+ * Prints the result of an individual test case. It is assumed not to have
+ * passed, since passed test cases are not reported.
+ */
+ static void printTestCase(
+ AnsiTerminalPrinter terminalPrinter, TestCase testCase) {
+ String timeSummary;
+ if (testCase.hasRunDurationMillis()) {
+ timeSummary = " ("
+ + timeInSec(testCase.getRunDurationMillis(), TimeUnit.MILLISECONDS)
+ + ")";
+ } else {
+ timeSummary = "";
+ }
+
+ terminalPrinter.print(
+ " "
+ + Mode.ERROR
+ + Strings.padEnd(testCase.getStatus().toString(), 8, ' ')
+ + Mode.DEFAULT
+ + testCase.getClassName()
+ + "."
+ + testCase.getName()
+ + timeSummary
+ + "\n");
+ }
+
+ /**
+ * Return the given time in seconds, to 1 decimal place,
+ * i.e. "32.1s".
+ */
+ static String timeInSec(long time, TimeUnit unit) {
+ double ms = TimeUnit.MILLISECONDS.convert(time, unit);
+ return String.format("%.1fs", ms / 1000.0);
+ }
+
+ static String getAttemptSummary(TestSummary summary) {
+ int attempts = summary.getPassedLogs().size() + summary.getFailedLogs().size();
+ if (attempts > 1) {
+ // Print number of failed runs for failed tests if testing was completed.
+ if (summary.getStatus() == BlazeTestStatus.FLAKY) {
+ return ", failed in " + summary.getFailedLogs().size() + " out of " + attempts;
+ }
+ if (summary.getStatus() == BlazeTestStatus.TIMEOUT
+ || summary.getStatus() == BlazeTestStatus.FAILED) {
+ return " in " + summary.getFailedLogs().size() + " out of " + attempts;
+ }
+ }
+ return "";
+ }
+
+ static String getCacheMessage(TestSummary summary) {
+ if (summary.getNumCached() == 0 || summary.getStatus() == BlazeTestStatus.INCOMPLETE) {
+ return "";
+ } else if (summary.getNumCached() == summary.totalRuns()) {
+ return "(cached) ";
+ } else {
+ return String.format("(%d/%d cached) ", summary.getNumCached(), summary.totalRuns());
+ }
+ }
+
+ static String getTimeSummary(TestSummary summary) {
+ if (summary.getTestTimes().isEmpty()) {
+ return "";
+ } else if (summary.getTestTimes().size() == 1) {
+ return " in " + timeInSec(summary.getTestTimes().get(0), TimeUnit.MILLISECONDS);
+ } else {
+ // We previously used com.google.math for this, which added about 1 MB of deps to the total
+ // size. If we re-introduce a dependency on that package, we could revert this change.
+ long min = summary.getTestTimes().get(0).longValue(), max = min, sum = 0;
+ double sumOfSquares = 0.0;
+ for (Long l : summary.getTestTimes()) {
+ long value = l.longValue();
+ min = value < min ? value : min;
+ max = value > max ? value : max;
+ sum += value;
+ sumOfSquares += ((double) value) * (double) value;
+ }
+ double mean = ((double) sum) / summary.getTestTimes().size();
+ double stddev = Math.sqrt((sumOfSquares - sum * mean) / summary.getTestTimes().size());
+ // For sharded tests, we print the max time on the same line as
+ // the test, and then print more detailed info about the
+ // distribution of times on the next line.
+ String maxTime = timeInSec(max, TimeUnit.MILLISECONDS);
+ return String.format(
+ " in %s\n Stats over %d runs: max = %s, min = %s, avg = %s, dev = %s",
+ maxTime,
+ summary.getTestTimes().size(),
+ maxTime,
+ timeInSec(min, TimeUnit.MILLISECONDS),
+ timeInSec((long) mean, TimeUnit.MILLISECONDS),
+ timeInSec((long) stddev, TimeUnit.MILLISECONDS));
+ }
+ }
+}