blob: 33d0819e0c11f7d754502d89e71f0acbf5d0fa96 [file] [log] [blame]
// Copyright 2014 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.runtime;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.devtools.build.lib.exec.TestLogHelper;
import com.google.devtools.build.lib.exec.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.Locale;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
/**
* Print test statistics in human readable form.
*/
public class TestSummaryPrinter {
/**
* Interface for getting the {@link String} to display to the user for a {@link Path}
* corresponding to a test output (e.g. test log).
*/
public interface TestLogPathFormatter {
String getPathStringToPrint(Path path);
}
/**
* Print the cached test log to the given printer.
*/
public static void printCachedOutput(
TestSummary summary,
TestOutputFormat testOutput,
AnsiTerminalPrinter printer,
TestLogPathFormatter testLogPathFormatter) {
String testName = summary.getLabel().toString();
List<String> allLogs = new ArrayList<>();
for (Path path : summary.getFailedLogs()) {
allLogs.add(testLogPathFormatter.getPathStringToPrint(path));
}
for (Path path : summary.getPassedLogs()) {
allLogs.add(testLogPathFormatter.getPathStringToPrint(path));
}
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,
TestLogPathFormatter testLogPathFormatter,
boolean verboseSummary,
boolean printFailedTestCases) {
print(
summary,
terminalPrinter,
testLogPathFormatter,
verboseSummary,
printFailedTestCases,
false);
}
/**
* Prints summary status for a single test.
* @param terminalPrinter The printer to print to
*/
public static void print(
TestSummary summary,
AnsiTerminalPrinter terminalPrinter,
TestLogPathFormatter testLogPathFormatter,
boolean verboseSummary,
boolean printFailedTestCases,
boolean withConfigurationName) {
BlazeTestStatus status = summary.getStatus();
// Skip output for tests that failed to build.
if ((!verboseSummary && status == BlazeTestStatus.FAILED_TO_BUILD)
|| status == BlazeTestStatus.BLAZE_HALTED_BEFORE_TESTING) {
return;
}
String message = getCacheMessage(summary) + statusString(summary.getStatus());
String targetName = summary.getLabel().toString();
if (withConfigurationName) {
targetName += " (" + summary.getConfiguration().getMnemonic() + ")";
}
terminalPrinter.print(
Strings.padEnd(targetName, 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) {
for (String warning : summary.getWarnings()) {
terminalPrinter.print(" " + AnsiTerminalPrinter.Mode.WARNING + "WARNING: "
+ AnsiTerminalPrinter.Mode.DEFAULT + warning + "\n");
}
for (Path path : summary.getFailedLogs()) {
if (path.exists()) {
terminalPrinter.print(" " + testLogPathFormatter.getPathStringToPrint(path) + "\n");
}
}
}
for (Path path : summary.getCoverageFiles()) {
// Print only non-trivial coverage files.
try {
if (path.exists() && path.getFileSize() > 0) {
terminalPrinter.print(" " + testLogPathFormatter.getPathStringToPrint(path) + "\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(Locale.US, "%.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(
Locale.US, "(%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();
long max = min;
long sum = 0;
double sumOfSquares = 0.0;
for (Long l : summary.getTestTimes()) {
long value = l.longValue();
min = Math.min(value, min);
max = Math.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(
Locale.US,
" 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));
}
}
}