/*
* Copyright 2014-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.cxx;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.ExternalTestRunnerRule;
import com.facebook.buck.rules.ExternalTestRunnerTestSpec;
import com.facebook.buck.rules.ForwardingBuildTargetSourcePath;
import com.facebook.buck.rules.HasRuntimeDeps;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.SourcePathRuleFinder;
import com.facebook.buck.rules.Tool;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.test.TestResultSummary;
import com.facebook.buck.test.TestRunningOptions;
import com.facebook.buck.test.result.type.ResultType;
import com.facebook.buck.util.ChunkAccumulator;
import com.facebook.buck.util.XmlDomParser;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
@SuppressWarnings("PMD.TestClassWithoutTestCases")
public class CxxGtestTest extends CxxTest implements HasRuntimeDeps, ExternalTestRunnerRule {
private static final Pattern START = Pattern.compile("^\\[\\s*RUN\\s*\\] (.*)$");
private static final Pattern END = Pattern.compile("^\\[\\s*(FAILED|OK)\\s*\\] .*");
private static final String NOTRUN = "notrun";
private final SourcePathRuleFinder ruleFinder;
private final BuildRule binary;
private final long maxTestOutputSize;
public CxxGtestTest(
BuildRuleParams params,
SourcePathRuleFinder ruleFinder,
BuildRule binary,
Tool executable,
ImmutableMap<String, String> env,
Supplier<ImmutableList<String>> args,
ImmutableSortedSet<? extends SourcePath> resources,
ImmutableSet<SourcePath> additionalCoverageTargets,
Supplier<ImmutableSortedSet<BuildRule>> additionalDeps,
ImmutableSet<String> labels,
ImmutableSet<String> contacts,
boolean runTestSeparately,
Optional<Long> testRuleTimeoutMs,
long maxTestOutputSize) {
super(
params,
executable,
env,
args,
resources,
additionalCoverageTargets,
additionalDeps,
labels,
contacts,
runTestSeparately,
testRuleTimeoutMs);
this.ruleFinder = ruleFinder;
this.binary = binary;
this.maxTestOutputSize = maxTestOutputSize;
}
@Override
public SourcePath getSourcePathToOutput() {
return new ForwardingBuildTargetSourcePath(
getBuildTarget(), Preconditions.checkNotNull(binary.getSourcePathToOutput()));
}
@Override
protected ImmutableList<String> getShellCommand(SourcePathResolver pathResolver, Path output) {
return ImmutableList.<String>builder()
.addAll(getExecutableCommand().getCommandPrefix(pathResolver))
.add("--gtest_color=no")
.add("--gtest_output=xml:" + getProjectFilesystem().resolve(output).toString())
.build();
}
private TestResultSummary getProgramFailureSummary(String message, String output) {
return new TestResultSummary(
getBuildTarget().toString(), "main", ResultType.FAILURE, 0L, message, "", output, "");
}
@Override
protected ImmutableList<TestResultSummary> parseResults(Path exitCode, Path output, Path results)
throws IOException, SAXException {
// Try to parse the results file first, which should be written if the test suite exited
// normally (even in the event of a failing test). If this fails, just construct a test
// summary with the output we have.
Document doc;
try {
doc = XmlDomParser.parse(getProjectFilesystem().resolve(results));
} catch (SAXException e) {
return ImmutableList.of(
getProgramFailureSummary(
"test program aborted before finishing",
getProjectFilesystem().readFileIfItExists(output).orElse("")));
}
ImmutableList.Builder<TestResultSummary> summariesBuilder = ImmutableList.builder();
// It's possible the test output had invalid characters in it's output, so make sure to
// ignore these as we parse the output lines.
Optional<String> currentTest = Optional.empty();
Map<String, ChunkAccumulator> stdout = new HashMap<>();
CharsetDecoder decoder = Charsets.UTF_8.newDecoder();
decoder.onMalformedInput(CodingErrorAction.IGNORE);
try (InputStream input = getProjectFilesystem().newFileInputStream(output);
BufferedReader reader = new BufferedReader(new InputStreamReader(input, decoder))) {
String line;
while ((line = reader.readLine()) != null) {
Matcher matcher;
if ((matcher = START.matcher(line.trim())).matches()) {
String test = matcher.group(1);
currentTest = Optional.of(test);
stdout.put(test, new ChunkAccumulator(Charsets.UTF_8, maxTestOutputSize));
} else if (END.matcher(line.trim()).matches()) {
currentTest = Optional.empty();
} else if (currentTest.isPresent()) {
Preconditions.checkNotNull(stdout.get(currentTest.get())).append(line);
}
}
}
NodeList testcases = doc.getElementsByTagName("testcase");
for (int index = 0; index < testcases.getLength(); index++) {
Node testcase = testcases.item(index);
NamedNodeMap attributes = testcase.getAttributes();
String testCase = attributes.getNamedItem("classname").getNodeValue();
String testName = attributes.getNamedItem("name").getNodeValue();
String testFull = String.format("%s.%s", testCase, testName);
Double time = Double.parseDouble(attributes.getNamedItem("time").getNodeValue()) * 1000;
// Prepare the result message and type
ResultType type = ResultType.SUCCESS;
String message = "";
if (testcase.getChildNodes().getLength() > 0) {
Node failure = testcase.getChildNodes().item(1);
type = ResultType.FAILURE;
message = failure.getAttributes().getNamedItem("message").getNodeValue();
} else if (attributes.getNamedItem("status").getNodeValue().equals(NOTRUN)) {
type = ResultType.ASSUMPTION_VIOLATION;
message = "DISABLED";
}
// Prepare the tests stdout.
String testStdout = "";
if (stdout.containsKey(testFull)) {
testStdout = Joiner.on(System.lineSeparator()).join(stdout.get(testFull).getChunks());
}
summariesBuilder.add(
new TestResultSummary(
testCase, testName, type, time.longValue(), message, "", testStdout, ""));
}
return summariesBuilder.build();
}
// The C++ test rules just wrap a test binary produced by another rule, so make sure that's
// always available to run the test.
@Override
public Stream<BuildTarget> getRuntimeDeps() {
return Stream.concat(
super.getRuntimeDeps(),
getExecutableCommand().getDeps(ruleFinder).stream().map(BuildRule::getBuildTarget));
}
@Override
public ExternalTestRunnerTestSpec getExternalTestRunnerSpec(
ExecutionContext executionContext,
TestRunningOptions testRunningOptions,
SourcePathResolver pathResolver) {
return ExternalTestRunnerTestSpec.builder()
.setTarget(getBuildTarget())
.setType("gtest")
.addAllCommand(getExecutableCommand().getCommandPrefix(pathResolver))
.addAllCommand(getArgs().get())
.putAllEnv(getEnv(pathResolver))
.addAllLabels(getLabels())
.addAllContacts(getContacts())
.addAllAdditionalCoverageTargets(
pathResolver.getAllAbsolutePaths(getAdditionalCoverageTargets()))
.build();
}
}