/*
* Copyright 2015-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.go;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.rules.AddToRuleKey;
import com.facebook.buck.rules.BinaryBuildRule;
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.HasRuntimeDeps;
import com.facebook.buck.rules.NoopBuildRule;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.SourcePathRuleFinder;
import com.facebook.buck.rules.TestRule;
import com.facebook.buck.rules.Tool;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
import com.facebook.buck.step.fs.SymlinkTreeStep;
import com.facebook.buck.test.TestCaseSummary;
import com.facebook.buck.test.TestResultSummary;
import com.facebook.buck.test.TestResults;
import com.facebook.buck.test.TestRunningOptions;
import com.facebook.buck.test.result.type.ResultType;
import com.facebook.buck.util.MoreCollectors;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Throwables;
import com.google.common.collect.FluentIterable;
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 com.google.common.collect.Maps;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@SuppressWarnings("PMD.TestClassWithoutTestCases")
public class GoTest extends NoopBuildRule
implements TestRule, HasRuntimeDeps, ExternalTestRunnerRule, BinaryBuildRule {
private static final Pattern TEST_START_PATTERN = Pattern.compile("^=== RUN\\s+(?<name>.*)$");
private static final Pattern TEST_FINISHED_PATTERN =
Pattern.compile(
"^--- (?<status>PASS|FAIL|SKIP): (?<name>.+) \\((?<duration>\\d+\\.\\d+)(?: seconds|s)\\)$");
// Extra time to wait for the process to exit on top of the test timeout
private static final int PROCESS_TIMEOUT_EXTRA_MS = 5000;
private final SourcePathRuleFinder ruleFinder;
private final GoBinary testMain;
private final ImmutableSet<String> labels;
private final Optional<Long> testRuleTimeoutMs;
private final ImmutableSet<String> contacts;
@AddToRuleKey private final boolean runTestsSeparately;
@AddToRuleKey private final ImmutableSortedSet<SourcePath> resources;
public GoTest(
BuildRuleParams buildRuleParams,
SourcePathRuleFinder ruleFinder,
GoBinary testMain,
ImmutableSet<String> labels,
ImmutableSet<String> contacts,
Optional<Long> testRuleTimeoutMs,
boolean runTestsSeparately,
ImmutableSortedSet<SourcePath> resources) {
super(buildRuleParams);
this.ruleFinder = ruleFinder;
this.testMain = testMain;
this.labels = labels;
this.contacts = contacts;
this.testRuleTimeoutMs = testRuleTimeoutMs;
this.runTestsSeparately = runTestsSeparately;
this.resources = resources;
}
@Override
public ImmutableList<Step> runTests(
ExecutionContext executionContext,
TestRunningOptions options,
SourcePathResolver pathResolver,
TestReportingCallback testReportingCallback) {
Optional<Long> processTimeoutMs =
testRuleTimeoutMs.isPresent()
? Optional.of(testRuleTimeoutMs.get() + PROCESS_TIMEOUT_EXTRA_MS)
: Optional.empty();
ImmutableList.Builder<String> args = ImmutableList.builder();
args.addAll(testMain.getExecutableCommand().getCommandPrefix(pathResolver));
args.add("-test.v");
if (testRuleTimeoutMs.isPresent()) {
args.add("-test.timeout", testRuleTimeoutMs.get() + "ms");
}
return new ImmutableList.Builder<Step>()
.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), getPathToTestOutputDirectory()))
.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), getPathToTestWorkingDirectory()))
.add(
new SymlinkTreeStep(
getProjectFilesystem(),
getPathToTestWorkingDirectory(),
ImmutableMap.copyOf(
FluentIterable.from(resources)
.transform(
input ->
Maps.immutableEntry(
getProjectFilesystem()
.getPath(
pathResolver.getSourcePathName(
getBuildTarget(), input)),
pathResolver.getAbsolutePath(input))))))
.add(
new GoTestStep(
getProjectFilesystem(),
getPathToTestWorkingDirectory(),
args.build(),
testMain.getExecutableCommand().getEnvironment(pathResolver),
getPathToTestExitCode(),
processTimeoutMs,
getPathToTestResults()))
.build();
}
private ImmutableList<TestResultSummary> parseTestResults() throws IOException {
ImmutableList.Builder<TestResultSummary> summariesBuilder = ImmutableList.builder();
try (BufferedReader reader =
Files.newBufferedReader(
getProjectFilesystem().resolve(getPathToTestResults()), Charsets.UTF_8)) {
Optional<String> currentTest = Optional.empty();
List<String> stdout = new ArrayList<>();
String line;
while ((line = reader.readLine()) != null) {
Matcher matcher;
if ((matcher = TEST_START_PATTERN.matcher(line)).matches()) {
currentTest = Optional.of(matcher.group("name"));
} else if ((matcher = TEST_FINISHED_PATTERN.matcher(line)).matches()) {
if (!currentTest.orElse("").equals(matcher.group("name"))) {
throw new RuntimeException(
String.format(
"Error parsing test output: test case end '%s' does not match start '%s'",
matcher.group("name"), currentTest.orElse("")));
}
ResultType result = ResultType.FAILURE;
if ("PASS".equals(matcher.group("status"))) {
result = ResultType.SUCCESS;
} else if ("SKIP".equals(matcher.group("status"))) {
result = ResultType.ASSUMPTION_VIOLATION;
}
double timeTaken = 0.0;
try {
timeTaken = Float.parseFloat(matcher.group("duration"));
} catch (NumberFormatException ex) {
Throwables.throwIfUnchecked(ex);
}
summariesBuilder.add(
new TestResultSummary(
"go_test",
matcher.group("name"),
result,
(long) (timeTaken * 1000),
"",
"",
Joiner.on(System.lineSeparator()).join(stdout),
""));
currentTest = Optional.empty();
stdout.clear();
} else {
stdout.add(line);
}
}
if (currentTest.isPresent()) {
// This can happen in case of e.g. a panic.
summariesBuilder.add(
new TestResultSummary(
"go_test",
currentTest.get(),
ResultType.FAILURE,
0,
"incomplete",
"",
Joiner.on(System.lineSeparator()).join(stdout),
""));
}
}
return summariesBuilder.build();
}
@Override
public Callable<TestResults> interpretTestResults(
ExecutionContext executionContext, boolean isUsingTestSelectors) {
return () -> {
return TestResults.of(
getBuildTarget(),
ImmutableList.of(
new TestCaseSummary(getBuildTarget().getFullyQualifiedName(), parseTestResults())),
contacts,
labels.stream().map(Object::toString).collect(MoreCollectors.toImmutableSet()));
};
}
@Override
public ImmutableSet<String> getLabels() {
return labels;
}
@Override
public ImmutableSet<String> getContacts() {
return contacts;
}
@Override
public Path getPathToTestOutputDirectory() {
return BuildTargets.getGenPath(getProjectFilesystem(), getBuildTarget(), "__test_%s_output__");
}
protected Path getPathToTestResults() {
return getPathToTestOutputDirectory().resolve("results");
}
protected Path getPathToTestWorkingDirectory() {
return getPathToTestOutputDirectory().resolve("working_dir");
}
protected Path getPathToTestExitCode() {
return getPathToTestOutputDirectory().resolve("exitCode");
}
@Override
public boolean runTestSeparately() {
return runTestsSeparately;
}
@Override
public boolean supportsStreamingTests() {
return false;
}
@Override
public Stream<BuildTarget> getRuntimeDeps() {
return Stream.concat(
Stream.of((testMain.getBuildTarget())),
resources
.stream()
.map(ruleFinder::filterBuildRuleInputs)
.flatMap(ImmutableSet::stream)
.map(BuildRule::getBuildTarget));
}
@Override
public ExternalTestRunnerTestSpec getExternalTestRunnerSpec(
ExecutionContext executionContext,
TestRunningOptions testRunningOptions,
SourcePathResolver pathResolver) {
return ExternalTestRunnerTestSpec.builder()
.setTarget(getBuildTarget())
.setType("go")
.putAllEnv(testMain.getExecutableCommand().getEnvironment(pathResolver))
.addAllCommand(testMain.getExecutableCommand().getCommandPrefix(pathResolver))
.addAllLabels(getLabels())
.addAllContacts(getContacts())
.build();
}
@Override
public Tool getExecutableCommand() {
return testMain.getExecutableCommand();
}
}