/*
* 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.python;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.model.Pair;
import com.facebook.buck.rules.AbstractBuildRule;
import com.facebook.buck.rules.AddToRuleKey;
import com.facebook.buck.rules.BinaryBuildRule;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildableContext;
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.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.test.TestCaseSummary;
import com.facebook.buck.test.TestResultSummary;
import com.facebook.buck.test.TestResults;
import com.facebook.buck.test.TestRunningOptions;
import com.facebook.buck.util.MoreCollectors;
import com.facebook.buck.util.ObjectMappers;
import com.facebook.buck.util.RichStream;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
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.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.stream.Stream;
@SuppressWarnings("PMD.TestClassWithoutTestCases")
public class PythonTest extends AbstractBuildRule
implements TestRule, HasRuntimeDeps, ExternalTestRunnerRule, BinaryBuildRule {
private final SourcePathRuleFinder ruleFinder;
private final Supplier<ImmutableSortedSet<BuildRule>> originalDeclaredDeps;
@AddToRuleKey private final Supplier<ImmutableMap<String, String>> env;
@AddToRuleKey private final PythonBinary binary;
private final ImmutableSet<String> labels;
private final Optional<Long> testRuleTimeoutMs;
private final ImmutableSet<String> contacts;
private final ImmutableList<Pair<Float, ImmutableSet<Path>>> neededCoverage;
private PythonTest(
BuildRuleParams params,
SourcePathRuleFinder ruleFinder,
Supplier<ImmutableSortedSet<BuildRule>> originalDeclaredDeps,
Supplier<ImmutableMap<String, String>> env,
PythonBinary binary,
ImmutableSet<String> labels,
ImmutableList<Pair<Float, ImmutableSet<Path>>> neededCoverage,
Optional<Long> testRuleTimeoutMs,
ImmutableSet<String> contacts) {
super(params);
this.ruleFinder = ruleFinder;
this.originalDeclaredDeps = originalDeclaredDeps;
this.env = env;
this.binary = binary;
this.labels = labels;
this.neededCoverage = neededCoverage;
this.testRuleTimeoutMs = testRuleTimeoutMs;
this.contacts = contacts;
}
public static PythonTest from(
BuildRuleParams params,
SourcePathRuleFinder ruleFinder,
Supplier<ImmutableMap<String, String>> env,
PythonBinary binary,
ImmutableSet<String> labels,
ImmutableList<Pair<Float, ImmutableSet<Path>>> neededCoverage,
Optional<Long> testRuleTimeoutMs,
ImmutableSet<String> contacts) {
return new PythonTest(
params.copyReplacingDeclaredAndExtraDeps(
Suppliers.ofInstance(ImmutableSortedSet.of(binary)),
Suppliers.ofInstance(ImmutableSortedSet.of())),
ruleFinder,
params.getDeclaredDeps(),
env,
binary,
labels,
neededCoverage,
testRuleTimeoutMs,
contacts);
}
@Override
public ImmutableList<Step> getBuildSteps(
BuildContext context, BuildableContext buildableContext) {
return ImmutableList.of();
}
@Override
public SourcePath getSourcePathToOutput() {
return new ForwardingBuildTargetSourcePath(getBuildTarget(), binary.getSourcePathToOutput());
}
@Override
public ImmutableList<Step> runTests(
ExecutionContext executionContext,
TestRunningOptions options,
SourcePathResolver pathResolver,
TestReportingCallback testReportingCallback) {
return new ImmutableList.Builder<Step>()
.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), getPathToTestOutputDirectory()))
.add(
new PythonRunTestsStep(
getProjectFilesystem().getRootPath(),
getBuildTarget().getFullyQualifiedName(),
binary.getExecutableCommand().getCommandPrefix(pathResolver),
getMergedEnv(pathResolver),
options.getTestSelectorList(),
testRuleTimeoutMs,
getProjectFilesystem().resolve(getPathToTestOutputResult())))
.build();
}
private ImmutableMap<String, String> getMergedEnv(SourcePathResolver pathResolver) {
return new ImmutableMap.Builder<String, String>()
.putAll(binary.getExecutableCommand().getEnvironment(pathResolver))
.putAll(env.get())
.build();
}
@Override
public ImmutableSet<String> getContacts() {
return contacts;
}
@Override
public Path getPathToTestOutputDirectory() {
return BuildTargets.getGenPath(getProjectFilesystem(), getBuildTarget(), "__test_%s_output__");
}
private Path getPathToTestOutputResult() {
return getPathToTestOutputDirectory().resolve("results.json");
}
@Override
public ImmutableSet<String> getLabels() {
return labels;
}
@Override
public Callable<TestResults> interpretTestResults(
final ExecutionContext executionContext, boolean isUsingTestSelectors) {
return () -> {
Optional<String> resultsFileContents =
getProjectFilesystem().readFileIfItExists(getPathToTestOutputResult());
TestResultSummary[] testResultSummaries =
ObjectMappers.readValue(resultsFileContents.get(), TestResultSummary[].class);
return TestResults.of(
getBuildTarget(),
ImmutableList.of(
new TestCaseSummary(
getBuildTarget().getFullyQualifiedName(),
ImmutableList.copyOf(testResultSummaries))),
contacts,
labels.stream().map(Object::toString).collect(MoreCollectors.toImmutableSet()));
};
}
@Override
public boolean runTestSeparately() {
return false;
}
// A python test rule is actually just a {@link NoopBuildRule} which contains a references to
// a {@link PythonBinary} rule, which is the actual test binary. Therefore, we *need* this
// rule around to run this test, so model this via the {@link HasRuntimeDeps} interface.
@Override
public Stream<BuildTarget> getRuntimeDeps() {
return RichStream.<BuildTarget>empty()
.concat(originalDeclaredDeps.get().stream().map(BuildRule::getBuildTarget))
.concat(binary.getRuntimeDeps())
.concat(
binary
.getExecutableCommand()
.getDeps(ruleFinder)
.stream()
.map(BuildRule::getBuildTarget));
}
@Override
public boolean supportsStreamingTests() {
return false;
}
@VisibleForTesting
protected PythonBinary getBinary() {
return binary;
}
@Override
public Tool getExecutableCommand() {
return binary.getExecutableCommand();
}
@Override
public ExternalTestRunnerTestSpec getExternalTestRunnerSpec(
ExecutionContext executionContext,
TestRunningOptions testRunningOptions,
SourcePathResolver pathResolver) {
return ExternalTestRunnerTestSpec.builder()
.setTarget(getBuildTarget())
.setType("pyunit")
.setNeededCoverage(neededCoverage)
.addAllCommand(binary.getExecutableCommand().getCommandPrefix(pathResolver))
.putAllEnv(getMergedEnv(pathResolver))
.addAllLabels(getLabels())
.addAllContacts(getContacts())
.build();
}
}