blob: ff1b1e5757fe3cf8047ad314f414546997bc07f1 [file] [log] [blame]
/*
* Copyright 2012-present Facebook, Inc.
*
* 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.facebook.buck.cli;
import com.facebook.buck.command.Build;
import com.facebook.buck.graph.AbstractBottomUpTraversal;
import com.facebook.buck.io.MorePaths;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.java.DefaultJavaPackageFinder;
import com.facebook.buck.java.GenerateCodeCoverageReportStep;
import com.facebook.buck.java.JUnitStep;
import com.facebook.buck.java.JavaLibrary;
import com.facebook.buck.java.JavaTest;
import com.facebook.buck.json.BuildFileParseException;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargetException;
import com.facebook.buck.parser.ParserConfig;
import com.facebook.buck.parser.TargetNodePredicateSpec;
import com.facebook.buck.rules.ActionGraph;
import com.facebook.buck.rules.ArtifactCache;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.BuildEngine;
import com.facebook.buck.rules.BuildEvent;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleSuccess;
import com.facebook.buck.rules.IndividualTestEvent;
import com.facebook.buck.rules.TargetGraph;
import com.facebook.buck.rules.TargetGraphToActionGraph;
import com.facebook.buck.rules.TargetGraphTransformer;
import com.facebook.buck.rules.TargetNode;
import com.facebook.buck.rules.TestRule;
import com.facebook.buck.rules.TestRunEvent;
import com.facebook.buck.step.DefaultStepRunner;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.StepFailedException;
import com.facebook.buck.step.StepRunner;
import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
import com.facebook.buck.test.CoverageReportFormat;
import com.facebook.buck.test.TestCaseSummary;
import com.facebook.buck.test.TestResultSummary;
import com.facebook.buck.test.TestResults;
import com.facebook.buck.test.result.groups.TestResultsGrouper;
import com.facebook.buck.test.result.type.ResultType;
import com.facebook.buck.util.BuckConstant;
import com.facebook.buck.util.Console;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.Verbosity;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.SettableFuture;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nullable;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
public class TestCommand extends AbstractCommandRunner<TestCommandOptions> {
public static final int TEST_FAILURES_EXIT_CODE = 42;
private final TargetGraphTransformer<ActionGraph> targetGraphTransformer;
public TestCommand(CommandRunnerParams params) {
super(params);
this.targetGraphTransformer = new TargetGraphToActionGraph(
params.getBuckEventBus(),
new BuildTargetNodeToBuildRuleTransformer());
}
@Override
TestCommandOptions createOptions(BuckConfig buckConfig) {
return new TestCommandOptions(buckConfig);
}
@Override
int runCommandWithOptionsInternal(final TestCommandOptions options)
throws IOException, InterruptedException {
// In the event the user specifies explicit targets, parse these out and post them for the
// build. However, if "--all" was specified, we won't have a list of targets until the build
// is already started, so BuildEvents will get an empty list.
ImmutableSet<BuildTarget> explicitBuildTargets =
options.isRunAllTests() ?
ImmutableSet.<BuildTarget>of() :
getBuildTargets(options.getArgumentsFormattedAsBuildTargets());
// Post the build started event, setting it to the Parser recorded start time if appropriate.
if (getParser().getParseStartTime().isPresent()) {
getBuckEventBus().post(
BuildEvent.started(explicitBuildTargets),
getParser().getParseStartTime().get());
} else {
getBuckEventBus().post(BuildEvent.started(explicitBuildTargets));
}
// The first step is to parse all of the build files. This will populate the parser and find all
// of the test rules.
ParserConfig parserConfig = new ParserConfig(options.getBuckConfig());
TargetGraph targetGraph;
try {
// If the user asked to run all of the tests, parse all of the build files looking for any
// test rules.
if (options.isRunAllTests()) {
targetGraph = getParser().buildTargetGraphForTargetNodeSpecs(
ImmutableList.of(
new TargetNodePredicateSpec(
new Predicate<TargetNode<?>>() {
@Override
public boolean apply(TargetNode<?> input) {
return input.getType().isTestRule();
}
},
getProjectFilesystem().getIgnorePaths())),
parserConfig,
getBuckEventBus(),
console,
environment,
options.getEnableProfiling());
// Otherwise, the user specified specific test targets to build and run, so build a graph
// around these.
} else {
targetGraph = getParser().buildTargetGraphForBuildTargets(
explicitBuildTargets,
parserConfig,
getBuckEventBus(),
console,
environment,
options.getEnableProfiling());
}
} catch (BuildTargetException | BuildFileParseException e) {
console.printBuildFailureWithoutStacktrace(e);
return 1;
}
ActionGraph graph = targetGraphTransformer.apply(targetGraph);
// Look up all of the test rules in the action graph.
Iterable<TestRule> testRules = Iterables.filter(graph.getNodes(), TestRule.class);
// Unless the user requests that we build filtered tests, filter them out here, before
// the build.
if (!options.isBuildFiltered()) {
testRules = filterTestRules(options, testRules);
}
if (options.isDryRun()) {
printMatchingTestRules(console, testRules);
}
// Create artifact cache to initialize Cassandra connection, if appropriate.
ArtifactCache artifactCache = getArtifactCache();
try (CommandThreadManager pool = new CommandThreadManager("Test", options.getNumThreads());
Build build = options.createBuild(
options.getBuckConfig(),
graph,
getProjectFilesystem(),
getAndroidPlatformTargetSupplier(),
getBuildEngine(),
artifactCache,
console,
getBuckEventBus(),
options.getTargetDeviceOptional(),
getCommandRunnerParams().getPlatform(),
getCommandRunnerParams().getEnvironment(),
getCommandRunnerParams().getObjectMapper(),
getCommandRunnerParams().getClock(),
pool.getExecutor())) {
// Build all of the test rules.
int exitCode = build.executeAndPrintFailuresToConsole(
testRules,
options.isKeepGoing(),
console,
options.getPathToBuildReport());
getBuckEventBus().post(BuildEvent.finished(explicitBuildTargets, exitCode));
if (exitCode != 0) {
return exitCode;
}
// If the user requests that we build tests that we filter out, then we perform
// the filtering here, after we've done the build but before we run the tests.
if (options.isBuildFiltered()) {
testRules = filterTestRules(options, testRules);
}
// Once all of the rules are built, then run the tests.
try {
return runTestsAndShutdownExecutor(
testRules,
Preconditions.checkNotNull(build.getBuildContext()),
build.getExecutionContext(),
options,
pool.getExecutor());
} catch (ExecutionException e) {
console.printBuildFailureWithoutStacktrace(e);
return 1;
}
}
}
/**
* Returns the ShellCommand object that is supposed to generate a code coverage report from data
* obtained during the test run. This method will also generate a set of source paths to the class
* files tested during the test run.
*/
private Step getReportCommand(
ImmutableSet<JavaLibrary> rulesUnderTest,
Optional<DefaultJavaPackageFinder> defaultJavaPackageFinderOptional,
ProjectFilesystem filesystem,
Path outputDirectory,
CoverageReportFormat format) {
ImmutableSet.Builder<String> srcDirectories = ImmutableSet.builder();
ImmutableSet.Builder<Path> pathsToClasses = ImmutableSet.builder();
// Add all source directories of java libraries that we are testing to -sourcepath.
for (JavaLibrary rule : rulesUnderTest) {
ImmutableSet<String> sourceFolderPath =
getPathToSourceFolders(rule, defaultJavaPackageFinderOptional, filesystem);
if (!sourceFolderPath.isEmpty()) {
srcDirectories.addAll(sourceFolderPath);
}
Path pathToOutput = rule.getPathToOutputFile();
if (pathToOutput == null) {
continue;
}
pathsToClasses.add(pathToOutput);
}
return new GenerateCodeCoverageReportStep(
srcDirectories.build(),
pathsToClasses.build(),
outputDirectory,
format);
}
/**
* Returns a set of source folders of the java files of a library.
*/
@VisibleForTesting
static ImmutableSet<String> getPathToSourceFolders(
JavaLibrary rule,
Optional<DefaultJavaPackageFinder> defaultJavaPackageFinderOptional,
ProjectFilesystem filesystem) {
ImmutableSet<Path> javaSrcs = rule.getJavaSrcs();
// A Java library rule with just resource files has an empty javaSrcs.
if (javaSrcs.isEmpty()) {
return ImmutableSet.of();
}
// If defaultJavaPackageFinderOptional is not present, then it could mean that there was an
// error reading from the buck configuration file.
if (!defaultJavaPackageFinderOptional.isPresent()) {
throw new HumanReadableException(
"Please include a [java] section with src_root property in the .buckconfig file.");
}
DefaultJavaPackageFinder defaultJavaPackageFinder = defaultJavaPackageFinderOptional.get();
// Iterate through all source paths to make sure we are generating a complete set of source
// folders for the source paths.
Set<String> srcFolders = Sets.newHashSet();
loopThroughSourcePath:
for (Path javaSrcPath : javaSrcs) {
if (!MorePaths.isGeneratedFile(javaSrcPath)) {
// If the source path is already under a known source folder, then we can skip this
// source path.
for (String srcFolder : srcFolders) {
if (javaSrcPath.startsWith(srcFolder)) {
continue loopThroughSourcePath;
}
}
// If the source path is under one of the source roots, then we can just add the source
// root.
ImmutableSortedSet<String> pathsFromRoot = defaultJavaPackageFinder.getPathsFromRoot();
for (String root : pathsFromRoot) {
if (javaSrcPath.startsWith(root)) {
srcFolders.add(root);
continue loopThroughSourcePath;
}
}
// Traverse the file system from the parent directory of the java file until we hit the
// parent of the src root directory.
ImmutableSet<String> pathElements = defaultJavaPackageFinder.getPathElements();
File directory = filesystem.getFileForRelativePath(javaSrcPath.getParent());
while (directory != null && !pathElements.contains(directory.getName())) {
directory = directory.getParentFile();
}
if (directory != null) {
String directoryPath = directory.getPath();
if (!directoryPath.endsWith("/")) {
directoryPath += "/";
}
srcFolders.add(directoryPath);
}
}
}
return ImmutableSet.copyOf(srcFolders);
}
private void printMatchingTestRules(Console console, Iterable<TestRule> testRules) {
PrintStream out = console.getStdOut();
ImmutableList<TestRule> list = ImmutableList.copyOf(testRules);
out.println(String.format("MATCHING TEST RULES (%d):", list.size()));
out.println("");
if (list.isEmpty()) {
out.println(" (none)");
} else {
for (TestRule testRule : testRules) {
out.println(" " + testRule.getBuildTarget());
}
}
out.println("");
}
@VisibleForTesting
static Iterable<TestRule> getCandidateRules(
ActionGraph graph) {
AbstractBottomUpTraversal<BuildRule, List<TestRule>> traversal =
new AbstractBottomUpTraversal<BuildRule, List<TestRule>>(graph) {
private final List<TestRule> results = Lists.newArrayList();
@Override
public void visit(BuildRule buildRule) {
TestRule testRule = null;
if (buildRule instanceof TestRule) {
testRule = (TestRule) buildRule;
}
if (testRule != null) {
results.add(testRule);
}
}
@Override
public List<TestRule> getResult() {
return results;
}
};
traversal.traverse();
return traversal.getResult();
}
@VisibleForTesting
static Iterable<TestRule> filterTestRules(final TestCommandOptions options,
Iterable<TestRule> testRules) {
ImmutableSortedSet.Builder<TestRule> builder =
ImmutableSortedSet.orderedBy(
new Comparator<TestRule>() {
@Override
public int compare(TestRule o1, TestRule o2) {
return o1.getBuildTarget().getFullyQualifiedName().compareTo(
o2.getBuildTarget().getFullyQualifiedName());
}
});
// We always want to run the rules that are given on the command line. Always. Unless we don't
// want to.
if (!options.shouldExcludeWin()) {
ImmutableSet<String> allTargets = options.getArgumentsFormattedAsBuildTargets();
for (TestRule rule : testRules) {
if (allTargets.contains(rule.getBuildTarget().getFullyQualifiedName())) {
builder.add(rule);
}
}
}
// Filter out all test rules that contain labels we've excluded.
builder.addAll(Iterables.filter(testRules, new Predicate<TestRule>() {
@Override
public boolean apply(TestRule rule) {
return options.isMatchedByLabelOptions(rule.getLabels());
}
}));
return builder.build();
}
private int runTestsAndShutdownExecutor(
Iterable<TestRule> tests,
BuildContext buildContext,
ExecutionContext executionContext,
TestCommandOptions options,
ListeningExecutorService service)
throws IOException, ExecutionException, InterruptedException {
DefaultStepRunner stepRunner = new DefaultStepRunner(executionContext, service);
return runTests(tests, buildContext, executionContext, stepRunner, options);
}
@SuppressWarnings("PMD.EmptyCatchBlock")
private int runTests(
Iterable<TestRule> tests,
BuildContext buildContext,
ExecutionContext executionContext,
StepRunner stepRunner,
final TestCommandOptions options)
throws IOException, ExecutionException, InterruptedException {
if (options.isUsingOneTimeOutputDirectories()) {
BuckConstant.setOneTimeTestSubdirectory(UUID.randomUUID().toString());
}
ImmutableSet<JavaLibrary> rulesUnderTest;
// If needed, we first run instrumentation on the class files.
if (options.isCodeCoverageEnabled()) {
rulesUnderTest = getRulesUnderTest(tests);
if (!rulesUnderTest.isEmpty()) {
try {
stepRunner.runStep(
new MakeCleanDirectoryStep(JUnitStep.JACOCO_OUTPUT_DIR));
} catch (StepFailedException e) {
console.printBuildFailureWithoutStacktrace(e);
return 1;
}
}
} else {
rulesUnderTest = ImmutableSet.of();
}
getBuckEventBus().post(TestRunEvent.started(
options.isRunAllTests(),
options.getTestSelectorList(),
options.shouldExplainTestSelectorList(),
options.getArgumentsFormattedAsBuildTargets()));
// Start running all of the tests. The result of each java_test() rule is represented as a
// ListenableFuture.
List<ListenableFuture<TestResults>> results = Lists.newArrayList();
// Unless `--verbose 0` is specified, print out test results as they become available.
// Failures with the ListenableFuture should always be printed, as they indicate an error with
// Buck, not the test being run.
Verbosity verbosity = console.getVerbosity();
final boolean printTestResults = (verbosity != Verbosity.SILENT);
// For grouping results!
TestResultsGrouper grouper = null;
if (options.isIgnoreFailingDependencies()) {
grouper = new TestResultsGrouper(tests);
}
TestRuleKeyFileHelper testRuleKeyFileHelper = new TestRuleKeyFileHelper(
executionContext.getProjectFilesystem(),
getBuildEngine());
for (TestRule test : tests) {
// Determine whether the test needs to be executed.
boolean isTestRunRequired;
isTestRunRequired = isTestRunRequiredForTest(
test,
getBuildEngine(),
executionContext,
testRuleKeyFileHelper,
options.isResultsCacheEnabled(),
!options.getTestSelectorList().isEmpty());
List<Step> steps;
if (isTestRunRequired) {
getBuckEventBus().post(IndividualTestEvent.started(
options.getArgumentsFormattedAsBuildTargets()));
ImmutableList.Builder<Step> stepsBuilder = ImmutableList.builder();
BuildEngine cachingBuildEngine = getBuildEngine();
Preconditions.checkState(cachingBuildEngine.isRuleBuilt(test.getBuildTarget()));
List<Step> testSteps = test.runTests(
buildContext,
executionContext,
options.isDryRun(),
options.isShufflingTests(),
options.getTestSelectorList());
if (!testSteps.isEmpty()) {
stepsBuilder.addAll(testSteps);
stepsBuilder.add(testRuleKeyFileHelper.createRuleKeyInDirStep(test));
}
steps = stepsBuilder.build();
} else {
steps = ImmutableList.of();
}
// Always run the commands, even if the list of commands as empty. There may be zero commands
// because the rule is cached, but its results must still be processed.
ListenableFuture<TestResults> testResults =
stepRunner.runStepsAndYieldResult(steps,
getCachingStatusTransformingCallable(
isTestRunRequired,
test.interpretTestResults(executionContext,
/*isUsingTestSelectors*/ !options.getTestSelectorList().isEmpty(),
/*isDryRun*/ options.isDryRun())),
test.getBuildTarget());
results.add(
transformTestResults(testResults, grouper, test, options, printTestResults));
}
// Block until all the tests have finished running.
ListenableFuture<List<TestResults>> uberFuture = Futures.allAsList(results);
List<TestResults> completedResults;
try {
completedResults = uberFuture.get();
} catch (ExecutionException e) {
e.printStackTrace(getStdErr());
return 1;
} catch (InterruptedException e) {
try {
uberFuture.cancel(true);
} catch (CancellationException ignored) {
// Rethrow original InterruptedException instead.
}
Thread.currentThread().interrupt();
throw e;
}
getBuckEventBus().post(TestRunEvent.finished(
options.getArgumentsFormattedAsBuildTargets(), completedResults));
// Write out the results as XML, if requested.
String path = options.getPathToXmlTestOutput();
if (path != null) {
try (Writer writer = Files.newWriter(
new File(path),
Charsets.UTF_8)) {
writeXmlOutput(completedResults, writer);
}
}
// Generate the code coverage report.
if (options.isCodeCoverageEnabled() && !rulesUnderTest.isEmpty()) {
try {
Optional<DefaultJavaPackageFinder> defaultJavaPackageFinderOptional =
options.getJavaPackageFinder();
stepRunner.runStep(
getReportCommand(rulesUnderTest,
defaultJavaPackageFinderOptional,
getProjectFilesystem(),
JUnitStep.JACOCO_OUTPUT_DIR,
options.getCoverageReportFormat()));
} catch (StepFailedException e) {
console.printBuildFailureWithoutStacktrace(e);
return 1;
}
}
boolean failures = Iterables.any(completedResults, new Predicate<TestResults>() {
@Override
public boolean apply(TestResults results) {
return !results.isSuccess();
}
});
boolean significantAssumptionViolations = false;
if (options.getBuckConfig().isTreatingAssumptionsAsErrors()) {
// Assumption-violations are only significant if we are treating them as errors.
significantAssumptionViolations =
Iterables.any(completedResults, new Predicate<TestResults>() {
@Override
public boolean apply(TestResults results) {
return results.hasAssumptionViolations();
}
});
}
return (failures || significantAssumptionViolations) ? TEST_FAILURES_EXIT_CODE : 0;
}
private ListenableFuture<TestResults> transformTestResults(
ListenableFuture<TestResults> originalTestResults,
@Nullable final TestResultsGrouper grouper,
final TestRule testRule,
final TestCommandOptions options,
final boolean printTestResults) {
final SettableFuture<TestResults> transformedTestResults = SettableFuture.create();
FutureCallback<TestResults> callback = new FutureCallback<TestResults>() {
private void postTestResults(TestResults testResults) {
getBuckEventBus().post(
IndividualTestEvent.finished(
options.getArgumentsFormattedAsBuildTargets(),
testResults));
}
private String getStackTrace(Throwable throwable) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
throwable.printStackTrace(pw);
return sw.toString();
}
@Override
public void onSuccess(TestResults testResults) {
if (printTestResults) {
if (grouper == null) {
postTestResults(testResults);
} else {
Map<TestRule, TestResults> postableTestResultsMap = grouper.post(testRule, testResults);
for (TestResults rr : postableTestResultsMap.values()) {
postTestResults(rr);
}
}
}
transformedTestResults.set(testResults);
}
@Override
public void onFailure(Throwable throwable) {
// If the test command steps themselves fail, report this as special test result.
TestResults testResults =
new TestResults(
ImmutableList.of(
new TestCaseSummary(
testRule.getBuildTarget().toString(),
ImmutableList.of(
new TestResultSummary(
testRule.getBuildTarget().toString(),
"main",
ResultType.FAILURE,
0L,
throwable.getMessage(),
getStackTrace(throwable),
"",
"")))));
if (printTestResults) {
postTestResults(testResults);
}
transformedTestResults.set(testResults);
}
};
Futures.addCallback(originalTestResults, callback);
return transformedTestResults;
}
private Callable<TestResults> getCachingStatusTransformingCallable(
boolean isTestRunRequired,
final Callable<TestResults> originalCallable) {
if (isTestRunRequired) {
return originalCallable;
}
return new Callable<TestResults>() {
@Override
public TestResults call() throws Exception {
TestResults originalTestResults = originalCallable.call();
ImmutableList<TestCaseSummary> cachedTestResults = FluentIterable
.from(originalTestResults.getTestCases())
.transform(TestCaseSummary.TO_CACHED_TRANSFORMATION)
.toList();
return new TestResults(
originalTestResults.getBuildTarget(),
cachedTestResults,
originalTestResults.getContacts());
}
};
}
@VisibleForTesting
static boolean isTestRunRequiredForTest(
TestRule test,
BuildEngine cachingBuildEngine,
ExecutionContext executionContext,
TestRuleKeyFileHelper testRuleKeyFileHelper,
boolean isResultsCacheEnabled,
boolean isRunningWithTestSelectors)
throws IOException, ExecutionException, InterruptedException {
boolean isTestRunRequired;
BuildRuleSuccess success;
if (executionContext.isDebugEnabled()) {
// If debug is enabled, then we should always run the tests as the user is expecting to
// hook up a debugger.
isTestRunRequired = true;
} else if (isRunningWithTestSelectors) {
// As a feature to aid developers, we'll assume that when we are using test selectors,
// we should always run each test (and never look at the cache.)
// TODO(user) When #3090004 and #3436849 are closed we can respect the cache again.
isTestRunRequired = true;
} else if (((success = cachingBuildEngine.getBuildRuleResult(
test.getBuildTarget())) != null) &&
success.getType() == BuildRuleSuccess.Type.MATCHING_RULE_KEY &&
isResultsCacheEnabled &&
test.hasTestResultFiles(executionContext) &&
testRuleKeyFileHelper.isRuleKeyInDir(test)) {
// If this build rule's artifacts (which includes the rule's output and its test result
// files) are up to date, then no commands are necessary to run the tests. The test result
// files will be read from the XML files in interpretTestResults().
isTestRunRequired = false;
} else {
isTestRunRequired = true;
}
return isTestRunRequired;
}
/**
* Generates the set of Java library rules under test.
*/
private ImmutableSet<JavaLibrary> getRulesUnderTest(Iterable<TestRule> tests) {
ImmutableSet.Builder<JavaLibrary> rulesUnderTest = ImmutableSet.builder();
// Gathering all rules whose source will be under test.
for (TestRule test : tests) {
if (test instanceof JavaTest) {
JavaTest javaTest = (JavaTest) test;
ImmutableSet<BuildRule> sourceUnderTest = javaTest.getSourceUnderTest();
for (BuildRule buildRule : sourceUnderTest) {
if (buildRule instanceof JavaLibrary) {
JavaLibrary javaLibrary = (JavaLibrary) buildRule;
rulesUnderTest.add(javaLibrary);
} else {
throw new HumanReadableException(
"Test '%s' is a java_test() " +
"but it is testing module '%s' " +
"which is not a java_library()!",
test.getBuildTarget(),
buildRule.getBuildTarget());
}
}
}
}
return rulesUnderTest.build();
}
/**
* Writes the test results in XML format to the supplied writer.
*
* This method does NOT close the writer object.
* @param allResults The test results.
* @param writer The writer in which the XML data will be written to.
*/
public static void writeXmlOutput(List<TestResults> allResults, Writer writer)
throws IOException {
try {
// Build the XML output.
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = dbf.newDocumentBuilder();
Document doc = docBuilder.newDocument();
// Create the <tests> tag. All test data will be within this tag.
Element testsEl = doc.createElement("tests");
doc.appendChild(testsEl);
for (TestResults results : allResults) {
for (TestCaseSummary testCase : results.getTestCases()) {
// Create the <test name="..." status="..." time="..."> tag.
// This records a single test case result in the test suite.
Element testEl = doc.createElement("test");
testEl.setAttribute("name", testCase.getTestCaseName());
testEl.setAttribute("status", testCase.isSuccess() ? "PASS" : "FAIL");
testEl.setAttribute("time", Long.toString(testCase.getTotalTime()));
testsEl.appendChild(testEl);
// Loop through the test case and add XML data (name, message, and
// stacktrace) for each individual test, if present.
addExtraXmlInfo(testCase, testEl);
}
}
// Write XML to the writer.
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.transform(new DOMSource(doc), new StreamResult(writer));
} catch (TransformerException | ParserConfigurationException ex) {
throw new IOException("Unable to build the XML document!");
}
}
/**
* A helper method that adds extra XML.
*
* This includes a test name, time (in ms), message, and stack trace, when
* present.
* Example:
*
* <pre>
* &lt;testresult name="failed_test" time="200">
* &lt;message>Reason for test failure&lt;/message>
* &lt;stacktrace>Stacktrace here&lt;/stacktrace>
* &lt;/testresult>
* </pre>
*
* @param testCase The test case summary containing one or more tests.
* @param testEl The XML element object for the <test> tag, in which extra
* information tags will be added.
*/
@VisibleForTesting
static void addExtraXmlInfo(TestCaseSummary testCase, Element testEl) {
Document doc = testEl.getOwnerDocument();
// Loop through the test case and extract test data.
for (TestResultSummary testResult : testCase.getTestResults()) {
// Extract the test name and time.
String name = Strings.nullToEmpty(testResult.getTestName());
String time = Long.toString(testResult.getTime());
// Create the tag: <testresult name="..." time="...">
Element testResultEl = doc.createElement("testresult");
testResultEl.setAttribute("name", name);
testResultEl.setAttribute("time", time);
testEl.appendChild(testResultEl);
// Create the tag: <message>(Error message here)</message>
Element messageEl = doc.createElement("message");
String message = Strings.nullToEmpty(testResult.getMessage());
messageEl.appendChild(doc.createTextNode(message));
testResultEl.appendChild(messageEl);
// Create the tag: <stacktrace>(Stacktrace here)</stacktrace>
Element stacktraceEl = doc.createElement("stacktrace");
String stacktrace = Strings.nullToEmpty(testResult.getStacktrace());
stacktraceEl.appendChild(doc.createTextNode(stacktrace));
testResultEl.appendChild(stacktraceEl);
}
}
@Override
String getUsageIntro() {
return "Specify build rules to test.";
}
}