/*
* 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.android;
import com.android.ddmlib.IDevice;
import com.facebook.buck.jvm.java.JavaRuntimeLauncher;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.rules.AbstractBuildRule;
import com.facebook.buck.rules.AddToRuleKey;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildableContext;
import com.facebook.buck.rules.ExplicitBuildTargetSourcePath;
import com.facebook.buck.rules.ExternalTestRunnerRule;
import com.facebook.buck.rules.ExternalTestRunnerTestSpec;
import com.facebook.buck.rules.HasRuntimeDeps;
import com.facebook.buck.rules.PathSourcePath;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.TestRule;
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.test.XmlTestResultParser;
import com.facebook.buck.test.result.type.ResultType;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.MoreCollectors;
import com.facebook.buck.util.PackagedResource;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.stream.Stream;
@SuppressWarnings("PMD.TestClassWithoutTestCases")
public class AndroidInstrumentationTest extends AbstractBuildRule
implements ExternalTestRunnerRule, HasRuntimeDeps, TestRule {
private static final String TEST_RESULT_FILE = "test_result.xml";
// TODO(#9027062): Migrate this to a PackagedResource so we don't make assumptions
// about the ant build.
private static final Path TESTRUNNER_CLASSES =
Paths.get(
System.getProperty(
"buck.testrunner_classes", new File("build/testrunner/classes").getAbsolutePath()));
@AddToRuleKey private final JavaRuntimeLauncher javaRuntimeLauncher;
private final ImmutableSet<String> labels;
private final ImmutableSet<String> contacts;
private final HasInstallableApk apk;
private final Optional<Long> testRuleTimeoutMs;
protected AndroidInstrumentationTest(
BuildRuleParams params,
HasInstallableApk apk,
Set<String> labels,
Set<String> contacts,
JavaRuntimeLauncher javaRuntimeLauncher,
Optional<Long> testRuleTimeoutMs) {
super(params);
this.apk = apk;
this.javaRuntimeLauncher = javaRuntimeLauncher;
this.labels = ImmutableSet.copyOf(labels);
this.contacts = ImmutableSet.copyOf(contacts);
this.testRuleTimeoutMs = testRuleTimeoutMs;
}
@Override
public ImmutableSet<String> getLabels() {
return labels;
}
@Override
public ImmutableSet<String> getContacts() {
return contacts;
}
@Override
public boolean supportsStreamingTests() {
return false;
}
@Override
public ImmutableList<Step> runTests(
ExecutionContext executionContext,
TestRunningOptions options,
SourcePathResolver pathResolver,
TestReportingCallback testReportingCallback) {
Preconditions.checkArgument(executionContext.getAdbOptions().isPresent());
if (executionContext.getAdbOptions().get().isMultiInstallModeEnabled()) {
throw new HumanReadableException(
"Running android instrumentation tests with multiple devices is not supported.");
}
ImmutableList.Builder<Step> steps = ImmutableList.builder();
Path pathToTestOutput = getPathToTestOutputDirectory();
steps.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), pathToTestOutput));
steps.add(new ApkInstallStep(pathResolver, apk));
if (apk instanceof AndroidInstrumentationApk) {
steps.add(
new ApkInstallStep(pathResolver, ((AndroidInstrumentationApk) apk).getApkUnderTest()));
}
AdbHelper adb = AdbHelper.get(executionContext, true);
IDevice device;
try {
device = adb.getSingleDevice();
} catch (InterruptedException e) {
throw new HumanReadableException("Unable to get connected device.");
}
steps.add(
getInstrumentationStep(
pathResolver,
executionContext.getPathToAdbExecutable(),
Optional.of(getProjectFilesystem().resolve(pathToTestOutput)),
Optional.of(device.getSerialNumber()),
Optional.empty(),
getFilterString(options),
Optional.empty()));
return steps.build();
}
@VisibleForTesting
static Optional<String> getFilterString(TestRunningOptions options) {
List<String> rawSelectors = options.getTestSelectorList().getRawSelectors();
if (rawSelectors.size() == 1) { // multiple selectors not supported
return Optional.of(stripRegexs(rawSelectors.get(0)));
}
return Optional.empty();
}
/**
* Buck adds some regex support to TestSelectors. Instrumentation tests don't support that so
* let's strip that and make it a plan Class#method string filter.
*/
private static String stripRegexs(String selector) {
return selector.replaceAll("[$]", "").replaceAll("#$", "");
}
private InstrumentationStep getInstrumentationStep(
SourcePathResolver pathResolver,
String pathToAdbExecutable,
Optional<Path> directoryForTestResults,
Optional<String> deviceSerial,
Optional<Path> instrumentationApkPath,
Optional<String> classFilterArg,
Optional<Path> apkUnderTestPath) {
String packageName =
AdbHelper.tryToExtractPackageNameFromManifest(pathResolver, apk.getApkInfo());
String testRunner =
AdbHelper.tryToExtractInstrumentationTestRunnerFromManifest(pathResolver, apk.getApkInfo());
String ddmlib = getPathForResourceJar("ddmlib.jar");
String kxml2 = getPathForResourceJar("kxml2.jar");
String guava = getPathForResourceJar("guava.jar");
String toolsCommon = getPathForResourceJar("android-tools-common.jar");
AndroidInstrumentationTestJVMArgs jvmArgs =
AndroidInstrumentationTestJVMArgs.builder()
.setApkUnderTestPath(apkUnderTestPath)
.setPathToAdbExecutable(pathToAdbExecutable)
.setDeviceSerial(deviceSerial)
.setDirectoryForTestResults(directoryForTestResults)
.setInstrumentationApkPath(instrumentationApkPath)
.setTestPackage(packageName)
.setTestRunner(testRunner)
.setTestRunnerClasspath(TESTRUNNER_CLASSES)
.setDdmlibJarPath(ddmlib)
.setTestFilter(classFilterArg)
.setKxmlJarPath(kxml2)
.setGuavaJarPath(guava)
.setAndroidToolsCommonJarPath(toolsCommon)
.build();
return new InstrumentationStep(
getProjectFilesystem(), javaRuntimeLauncher, jvmArgs, testRuleTimeoutMs);
}
private String getPathForResourceJar(String jarName) {
return new PathSourcePath(
this.getProjectFilesystem(),
AndroidInstrumentationTest.class + "/" + jarName,
new PackagedResource(
this.getProjectFilesystem(), AndroidInstrumentationTest.class, jarName))
.getRelativePath()
.toString();
}
@Override
public Path getPathToTestOutputDirectory() {
return BuildTargets.getGenPath(
getProjectFilesystem(), getBuildTarget(), "__android_instrumentation_test_%s_output__");
}
private TestCaseSummary getTestClassAssumedSummary() {
return new TestCaseSummary(
getBuildTarget().getFullyQualifiedName(),
ImmutableList.of(
new TestResultSummary(
getBuildTarget().getFullyQualifiedName(),
"none",
ResultType.ASSUMPTION_VIOLATION,
0L,
"No tests run",
null,
null,
null)));
}
@Override
public Callable<TestResults> interpretTestResults(
final ExecutionContext context, final boolean isUsingTestSelectors) {
return () -> {
final ImmutableList.Builder<TestCaseSummary> summaries = ImmutableList.builder();
IDevice device;
AdbHelper adbHelper = AdbHelper.get(context, true);
try {
device = adbHelper.getSingleDevice();
} catch (InterruptedException e) {
device = null;
}
if (device == null) {
summaries.add(getTestClassAssumedSummary());
} else {
Path testResultPath =
getProjectFilesystem()
.resolve(getPathToTestOutputDirectory().resolve(TEST_RESULT_FILE));
summaries.addAll(
XmlTestResultParser.parseAndroid(testResultPath, device.getSerialNumber()));
}
return TestResults.of(
getBuildTarget(),
summaries.build(),
contacts,
labels.stream().map(Object::toString).collect(MoreCollectors.toImmutableSet()));
};
}
@Override
public SourcePath getSourcePathToOutput() {
return new ExplicitBuildTargetSourcePath(getBuildTarget(), getPathToTestOutputDirectory());
}
@Override
public final ImmutableList<Step> getBuildSteps(
BuildContext context, BuildableContext buildableContext) {
ImmutableList.Builder<Step> steps = ImmutableList.builder();
return steps.build();
}
@Override
public boolean runTestSeparately() {
return false;
}
@Override
public ExternalTestRunnerTestSpec getExternalTestRunnerSpec(
ExecutionContext executionContext,
TestRunningOptions testRunningOptions,
SourcePathResolver pathResolver) {
Optional<Path> apkUnderTestPath = Optional.empty();
if (apk instanceof AndroidInstrumentationApk) {
apkUnderTestPath =
Optional.of(
pathResolver.getAbsolutePath(
((AndroidInstrumentationApk) apk).getApkUnderTest().getApkInfo().getApkPath()));
}
InstrumentationStep step =
getInstrumentationStep(
pathResolver,
executionContext.getPathToAdbExecutable(),
Optional.empty(),
Optional.empty(),
Optional.of(pathResolver.getAbsolutePath(apk.getApkInfo().getApkPath())),
Optional.empty(),
apkUnderTestPath);
return ExternalTestRunnerTestSpec.builder()
.setTarget(getBuildTarget())
.setType("android_instrumentation")
.setCommand(step.getShellCommandInternal(executionContext))
.setLabels(getLabels())
.setContacts(getContacts())
.build();
}
@Override
public Stream<BuildTarget> getRuntimeDeps() {
Stream.Builder<BuildTarget> builder = Stream.builder();
builder.add(apk.getBuildTarget());
if (apk instanceof ApkGenrule) {
builder.add(((ApkGenrule) apk).getInstallableApk().getBuildTarget());
}
return builder.build();
}
}