blob: 64b6070d188ec458d13fb222c40e3acac5e6ca76 [file] [log] [blame]
/*
* 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.cxx.CxxPlatform;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.model.Flavor;
import com.facebook.buck.model.FlavorDomain;
import com.facebook.buck.model.FlavorDomainException;
import com.facebook.buck.model.HasSourceUnderTest;
import com.facebook.buck.model.ImmutableFlavor;
import com.facebook.buck.rules.AbstractBuildRule;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildRuleResolver;
import com.facebook.buck.rules.BuildRuleType;
import com.facebook.buck.rules.BuildTargetSourcePath;
import com.facebook.buck.rules.BuildableContext;
import com.facebook.buck.rules.Description;
import com.facebook.buck.rules.ImmutableBuildRuleType;
import com.facebook.buck.rules.Label;
import com.facebook.buck.rules.PathSourcePath;
import com.facebook.buck.rules.RuleKey;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.fs.MkdirStep;
import com.facebook.buck.step.fs.WriteFileStep;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.infer.annotation.SuppressFieldNotInitialized;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableCollection;
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.nio.file.Paths;
public class PythonTestDescription implements Description<PythonTestDescription.Arg> {
private static final BuildRuleType TYPE = ImmutableBuildRuleType.of("python_test");
private static final Flavor BINARY_FLAVOR = ImmutableFlavor.of("binary");
private final ProjectFilesystem projectFilesystem;
private final Path pathToPex;
private final Optional<Path> pathToPythonTestMain;
private final PythonEnvironment pythonEnvironment;
private final CxxPlatform defaultCxxPlatform;
private final FlavorDomain<CxxPlatform> cxxPlatforms;
public PythonTestDescription(
ProjectFilesystem projectFilesystem,
Path pathToPex,
Optional<Path> pathToPythonTestMain,
PythonEnvironment pythonEnvironment,
CxxPlatform defaultCxxPlatform,
FlavorDomain<CxxPlatform> cxxPlatforms) {
this.projectFilesystem = projectFilesystem;
this.pathToPex = pathToPex;
this.pathToPythonTestMain = pathToPythonTestMain;
this.pythonEnvironment = pythonEnvironment;
this.defaultCxxPlatform = defaultCxxPlatform;
this.cxxPlatforms = cxxPlatforms;
}
@Override
public BuildRuleType getBuildRuleType() {
return TYPE;
}
@Override
public Arg createUnpopulatedConstructorArg() {
return new Arg();
}
@VisibleForTesting
protected Path getTestMainName() {
return Paths.get("__test_main__.py");
}
@VisibleForTesting
protected Path getTestModulesListName() {
return Paths.get("__test_modules__.py");
}
@VisibleForTesting
protected Path getTestModulesListPath(BuildTarget buildTarget) {
return BuildTargets.getGenPath(buildTarget, "%s").resolve(getTestModulesListName());
}
@VisibleForTesting
protected BuildTarget getBinaryBuildTarget(BuildTarget target) {
return BuildTargets.createFlavoredBuildTarget(target.checkUnflavored(), BINARY_FLAVOR);
}
/**
* Create the contents of a python source file that just contains a list of
* the given test modules.
*/
private static String getTestModulesListContents(ImmutableSet<String> modules) {
String contents = "TEST_MODULES = [\n";
for (String module : modules) {
contents += String.format(" \"%s\",\n", module);
}
contents += "]";
return contents;
}
/**
* Return a {@link BuildRule} that constructs the source file which contains the list
* of test modules this python test rule will run. Setting up a separate build rule
* for this allows us to use the existing python binary rule without changes to account
* for the build-time creation of this file.
*/
private static BuildRule createTestModulesSourceBuildRule(
BuildRuleParams params,
BuildRuleResolver resolver,
final Path outputPath,
ImmutableSet<String> testModules) {
// Modify the build rule params to change the target, type, and remove all deps.
BuildRuleParams newParams = params.copyWithChanges(
ImmutableBuildRuleType.of("create_test_modules_list"),
BuildTargets.createFlavoredBuildTarget(
params.getBuildTarget().checkUnflavored(),
ImmutableFlavor.of("test_module")),
Suppliers.ofInstance(ImmutableSortedSet.<BuildRule>of()),
Suppliers.ofInstance(ImmutableSortedSet.<BuildRule>of()));
final String contents = getTestModulesListContents(testModules);
return new AbstractBuildRule(newParams, new SourcePathResolver(resolver)) {
@Override
protected ImmutableCollection<Path> getInputsToCompareToOutput() {
return ImmutableList.of();
}
@Override
protected RuleKey.Builder appendDetailsToRuleKey(RuleKey.Builder builder) {
return builder
.setReflectively("contents", contents)
.setReflectively("output", outputPath.toString());
}
@Override
public ImmutableList<Step> getBuildSteps(
BuildContext context, BuildableContext buildableContext) {
buildableContext.recordArtifact(outputPath);
return ImmutableList.of(
new MkdirStep(outputPath.getParent()),
new WriteFileStep(contents, outputPath));
}
@Override
public Path getPathToOutputFile() {
return outputPath;
}
};
}
@Override
public <A extends Arg> PythonTest createBuildRule(
BuildRuleParams params,
BuildRuleResolver resolver,
A args) {
if (!pathToPythonTestMain.isPresent()) {
throw new HumanReadableException(
"Please configure python -> path_to_python_test_main in your .buckconfig");
}
// Extract the platform from the flavor, falling back to the default platform if none are
// found.
CxxPlatform cxxPlatform;
try {
cxxPlatform = cxxPlatforms
.getValue(ImmutableSet.copyOf(params.getBuildTarget().getFlavors()))
.or(defaultCxxPlatform);
} catch (FlavorDomainException e) {
throw new HumanReadableException("%s: %s", params.getBuildTarget(), e.getMessage());
}
SourcePathResolver pathResolver = new SourcePathResolver(resolver);
Path baseModule = PythonUtil.getBasePath(params.getBuildTarget(), args.baseModule);
ImmutableMap<Path, SourcePath> srcs = PythonUtil.toModuleMap(
params.getBuildTarget(),
pathResolver,
"srcs",
baseModule, args.srcs);
ImmutableMap<Path, SourcePath> resources = PythonUtil.toModuleMap(
params.getBuildTarget(),
pathResolver,
"resources",
baseModule, args.resources);
// Convert the passed in module paths into test module names.
ImmutableSet.Builder<String> testModulesBuilder = ImmutableSet.builder();
for (Path name : srcs.keySet()) {
testModulesBuilder.add(
PythonUtil.toModuleName(params.getBuildTarget(), name.toString()));
}
ImmutableSet<String> testModules = testModulesBuilder.build();
// Construct a build rule to generate the test modules list source file and
// add it to the build.
BuildRule testModulesBuildRule = createTestModulesSourceBuildRule(
params,
resolver,
getTestModulesListPath(params.getBuildTarget()),
testModules);
resolver.addToIndex(testModulesBuildRule);
// Build up the list of everything going into the python test.
PythonPackageComponents testComponents = ImmutablePythonPackageComponents.of(
ImmutableMap
.<Path, SourcePath>builder()
.put(
getTestModulesListName(),
new BuildTargetSourcePath(
testModulesBuildRule.getProjectFilesystem(),
testModulesBuildRule.getBuildTarget()))
.put(
getTestMainName(),
new PathSourcePath(projectFilesystem, pathToPythonTestMain.get()))
.putAll(srcs)
.build(),
resources,
ImmutableMap.<Path, SourcePath>of());
PythonPackageComponents allComponents =
PythonUtil.getAllComponents(params, testComponents, cxxPlatform);
// Build the PEX using a python binary rule with the minimum dependencies.
BuildRuleParams binaryParams = params.copyWithChanges(
PythonBinaryDescription.TYPE,
getBinaryBuildTarget(params.getBuildTarget()),
Suppliers.ofInstance(PythonUtil.getDepsFromComponents(pathResolver, allComponents)),
Suppliers.ofInstance(ImmutableSortedSet.<BuildRule>of()));
PythonBinary binary = new PythonBinary(
binaryParams,
pathResolver,
pathToPex,
pythonEnvironment,
getTestMainName(),
allComponents);
resolver.addToIndex(binary);
// Generate and return the python test rule, which depends on the python binary rule above.
return new PythonTest(
params.copyWithDeps(
Suppliers.ofInstance(
ImmutableSortedSet.<BuildRule>naturalOrder()
.addAll(params.getDeclaredDeps())
.add(binary)
.build()),
Suppliers.ofInstance(params.getExtraDeps())),
pathResolver,
binary,
resolver.getAllRules(args.sourceUnderTest.or(ImmutableSortedSet.<BuildTarget>of())),
args.labels.or(ImmutableSet.<Label>of()),
args.contacts.or(ImmutableSet.<String>of()));
}
@SuppressFieldNotInitialized
public static class Arg extends PythonLibraryDescription.Arg implements HasSourceUnderTest {
public Optional<ImmutableSet<String>> contacts;
public Optional<ImmutableSet<Label>> labels;
public Optional<ImmutableSortedSet<BuildTarget>> sourceUnderTest;
@Override
public ImmutableSortedSet<BuildTarget> getSourceUnderTest() {
return sourceUnderTest.get();
}
}
}