/*
* 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.android;
import static com.facebook.buck.rules.BuildableProperties.Kind.ANDROID;
import static com.facebook.buck.rules.BuildableProperties.Kind.LIBRARY;
import static com.facebook.buck.rules.BuildableProperties.Kind.TEST;
import com.facebook.buck.jvm.java.ForkMode;
import com.facebook.buck.jvm.java.JavaLibrary;
import com.facebook.buck.jvm.java.JavaOptions;
import com.facebook.buck.jvm.java.JavaTest;
import com.facebook.buck.jvm.java.TestType;
import com.facebook.buck.log.Logger;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.Either;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildableProperties;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.SourcePathRuleFinder;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.TargetDevice;
import com.facebook.buck.util.OptionalCompat;
import com.facebook.buck.util.Optionals;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@SuppressWarnings("PMD.TestClassWithoutTestCases")
public class RobolectricTest extends JavaTest {
private static final Logger LOG = Logger.get(RobolectricTest.class);
private static final BuildableProperties PROPERTIES =
new BuildableProperties(ANDROID, LIBRARY, TEST);
private final SourcePathRuleFinder ruleFinder;
private final Optional<DummyRDotJava> optionalDummyRDotJava;
private final Optional<SourcePath> robolectricManifest;
private final Optional<String> robolectricRuntimeDependency;
/**
* Used by robolectric test runner to get list of resource directories that can be used for tests.
*/
static final String LIST_OF_RESOURCE_DIRECTORIES_PROPERTY_NAME =
"buck.robolectric_res_directories";
static final String LIST_OF_ASSETS_DIRECTORIES_PROPERTY_NAME =
"buck.robolectric_assets_directories";
private static final String ROBOLECTRIC_MANIFEST = "buck.robolectric_manifest";
private static final String ROBOLECTRIC_DEPENDENCY_DIR = "robolectric.dependency.dir";
private final Function<DummyRDotJava, ImmutableSet<BuildRule>> resourceRulesFunction =
input -> {
ImmutableSet.Builder<BuildRule> resourceDeps = ImmutableSet.builder();
for (HasAndroidResourceDeps hasAndroidResourceDeps : input.getAndroidResourceDeps()) {
SourcePath resSourcePath = hasAndroidResourceDeps.getRes();
if (resSourcePath == null) {
continue;
}
Optionals.addIfPresent(getRuleFinder().getRule(resSourcePath), resourceDeps);
}
return resourceDeps.build();
};
private final Function<DummyRDotJava, ImmutableSet<BuildRule>> assetsRulesFunction =
input -> {
ImmutableSet.Builder<BuildRule> assetsDeps = ImmutableSet.builder();
for (HasAndroidResourceDeps hasAndroidResourceDeps : input.getAndroidResourceDeps()) {
SourcePath assetsSourcePath = hasAndroidResourceDeps.getAssets();
if (assetsSourcePath == null) {
continue;
}
Optionals.addIfPresent(getRuleFinder().getRule(assetsSourcePath), assetsDeps);
}
return assetsDeps.build();
};
protected RobolectricTest(
BuildRuleParams buildRuleParams,
SourcePathRuleFinder ruleFinder,
JavaLibrary compiledTestsLibrary,
Set<String> labels,
Set<String> contacts,
TestType testType,
JavaOptions javaOptions,
List<String> vmArgs,
Map<String, String> nativeLibsEnvironment,
Optional<DummyRDotJava> optionalDummyRDotJava,
Optional<Long> testRuleTimeoutMs,
Optional<Long> testCaseTimeoutMs,
ImmutableMap<String, String> env,
boolean runTestSeparately,
ForkMode forkMode,
Optional<Level> stdOutLogLevel,
Optional<Level> stdErrLogLevel,
Optional<String> robolectricRuntimeDependency,
Optional<SourcePath> robolectricManifest) {
super(
buildRuleParams,
new SourcePathResolver(ruleFinder),
compiledTestsLibrary,
optionalDummyRDotJava
.map(
r ->
ImmutableSet.<Either<SourcePath, Path>>of(
Either.ofLeft(r.getSourcePathToOutput())))
.orElse(ImmutableSet.of()),
labels,
contacts,
testType,
javaOptions.getJavaRuntimeLauncher(),
vmArgs,
nativeLibsEnvironment,
testRuleTimeoutMs,
testCaseTimeoutMs,
env,
runTestSeparately,
forkMode,
stdOutLogLevel,
stdErrLogLevel);
this.ruleFinder = ruleFinder;
this.optionalDummyRDotJava = optionalDummyRDotJava;
this.robolectricRuntimeDependency = robolectricRuntimeDependency;
this.robolectricManifest = robolectricManifest;
}
@Override
public BuildableProperties getProperties() {
return PROPERTIES;
}
@Override
protected ImmutableSet<Path> getBootClasspathEntries(ExecutionContext context) {
return ImmutableSet.copyOf(context.getAndroidPlatformTarget().getBootclasspathEntries());
}
@Override
protected void onAmendVmArgs(
ImmutableList.Builder<String> vmArgsBuilder,
SourcePathResolver pathResolver,
Optional<TargetDevice> targetDevice) {
super.onAmendVmArgs(vmArgsBuilder, pathResolver, targetDevice);
Preconditions.checkState(
optionalDummyRDotJava.isPresent(), "DummyRDotJava must have been created!");
vmArgsBuilder.add(
getRobolectricResourceDirectories(
pathResolver, optionalDummyRDotJava.get().getAndroidResourceDeps()));
vmArgsBuilder.add(
getRobolectricAssetsDirectories(
pathResolver, optionalDummyRDotJava.get().getAndroidResourceDeps()));
// Force robolectric to only use local dependency resolution.
vmArgsBuilder.add("-Drobolectric.offline=true");
robolectricManifest.ifPresent(
s ->
vmArgsBuilder.add(
String.format("-D%s=%s", ROBOLECTRIC_MANIFEST, pathResolver.getAbsolutePath(s))));
robolectricRuntimeDependency.ifPresent(
s -> vmArgsBuilder.add(String.format("-D%s=%s", ROBOLECTRIC_DEPENDENCY_DIR, s)));
}
@VisibleForTesting
String getRobolectricResourceDirectories(
SourcePathResolver pathResolver, List<HasAndroidResourceDeps> resourceDeps) {
String resourceDirectories =
getDirs(resourceDeps.stream().map(HasAndroidResourceDeps::getRes), pathResolver);
return String.format(
"-D%s=%s", LIST_OF_RESOURCE_DIRECTORIES_PROPERTY_NAME, resourceDirectories);
}
@VisibleForTesting
String getRobolectricAssetsDirectories(
SourcePathResolver pathResolver, List<HasAndroidResourceDeps> resourceDeps) {
String assetsDirectories =
getDirs(resourceDeps.stream().map(HasAndroidResourceDeps::getAssets), pathResolver);
return String.format("-D%s=%s", LIST_OF_ASSETS_DIRECTORIES_PROPERTY_NAME, assetsDirectories);
}
private String getDirs(Stream<SourcePath> sourcePathStream, SourcePathResolver pathResolver) {
return sourcePathStream
.filter(Objects::nonNull)
.map(pathResolver::getRelativePath)
.filter(
input -> {
try {
if (!getProjectFilesystem().isDirectory(input)) {
throw new RuntimeException(
String.format(
"Path %s is needed to run robolectric test %s, but was not found.",
input, getBuildTarget()));
}
return !getProjectFilesystem().getDirectoryContents(input).isEmpty();
} catch (IOException e) {
LOG.warn(e, "Error filtering path for Robolectric res/assets.");
return true;
}
})
.map(Object::toString)
.collect(Collectors.joining(File.pathSeparator));
}
@Override
public Stream<BuildTarget> getRuntimeDeps() {
return Stream.concat(
// Inherit any runtime deps from `JavaTest`.
super.getRuntimeDeps(),
Stream.of(
// On top of the runtime dependencies of a normal {@link JavaTest}, we need to make the
// {@link DummyRDotJava} and any of its resource deps is available locally (if it exists)
// to run this test.
OptionalCompat.asSet(optionalDummyRDotJava).stream(),
optionalDummyRDotJava.map(resourceRulesFunction).orElse(ImmutableSet.of()).stream(),
optionalDummyRDotJava.map(assetsRulesFunction).orElse(ImmutableSet.of()).stream(),
// It's possible that the user added some tool as a dependency, so make sure we
// promote this rules first-order deps to runtime deps, so that these potential
// tools are available when this test runs.
getBuildDeps().stream())
.reduce(Stream.empty(), Stream::concat)
.map(BuildRule::getBuildTarget));
}
public SourcePathRuleFinder getRuleFinder() {
return ruleFinder;
}
}