aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java
diff options
context:
space:
mode:
authorGravatar ajmichael <ajmichael@google.com>2017-06-29 01:29:19 +0200
committerGravatar Marcel Hlopko <hlopko@google.com>2017-06-29 09:33:56 +0200
commit8355237f65842d9df45b51cdfaebda9bb44c3a79 (patch)
tree79d43aa21145a3184ebba3aa4d08d925dfca2d0a /src/main/java
parentc1e0d7b9f6bfa8df950980f9370c638443d361e1 (diff)
Introduce new android_instrumentation_test rule.
This rule works with the android_instrumentation, android_device_script_fixture and android_host_service_fixture rules to support testing Android applications. None of these rules are installed yet, because the forthcoming Android test runner is not yet open sourced. https://github.com/bazelbuild/bazel/issues/903. RELNOTES: None PiperOrigin-RevId: 160465920
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/com/google/devtools/build/lib/BUILD2
-rw-r--r--src/main/java/com/google/devtools/build/lib/rules/android/AndroidDeviceScriptFixture.java8
-rw-r--r--src/main/java/com/google/devtools/build/lib/rules/android/AndroidHostServiceFixture.java1
-rw-r--r--src/main/java/com/google/devtools/build/lib/rules/android/AndroidHostServiceFixtureInfoProvider.java15
-rw-r--r--src/main/java/com/google/devtools/build/lib/rules/android/AndroidInstrumentation.java14
-rw-r--r--src/main/java/com/google/devtools/build/lib/rules/android/AndroidInstrumentationTest.java334
-rw-r--r--src/main/java/com/google/devtools/build/lib/rules/android/AndroidInstrumentationTestRule.java83
-rw-r--r--src/main/java/com/google/devtools/build/lib/rules/android/android_instrumentation_test_template.txt88
-rw-r--r--src/main/java/com/google/devtools/build/lib/rules/android/test_suite_property_name.txt1
9 files changed, 538 insertions, 8 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD
index 9be7d831d6..3091e7f3bb 100644
--- a/src/main/java/com/google/devtools/build/lib/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/BUILD
@@ -1058,7 +1058,9 @@ java_library(
),
resources = [
"rules/android/android_device_stub_template.txt",
+ "rules/android/android_instrumentation_test_template.txt",
"rules/android/databinding_annotation_template.txt",
+ "rules/android/test_suite_property_name.txt",
],
deps = [
":build-base",
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidDeviceScriptFixture.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidDeviceScriptFixture.java
index 648b282831..a422e62280 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidDeviceScriptFixture.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidDeviceScriptFixture.java
@@ -20,6 +20,7 @@ import com.google.devtools.build.lib.analysis.FileProvider;
import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
import com.google.devtools.build.lib.analysis.RunfilesProvider;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
@@ -36,7 +37,12 @@ public class AndroidDeviceScriptFixture implements RuleConfiguredTargetFactory {
Artifact fixtureScript = getFixtureScript(ruleContext);
return new RuleConfiguredTargetBuilder(ruleContext)
.setFilesToBuild(NestedSetBuilder.<Artifact>stableOrder().add(fixtureScript).build())
- .addProvider(RunfilesProvider.class, RunfilesProvider.EMPTY)
+ .addProvider(
+ RunfilesProvider.class,
+ RunfilesProvider.simple(
+ new Runfiles.Builder(ruleContext.getWorkspaceName())
+ .addArtifact(fixtureScript)
+ .build()))
.addNativeDeclaredProvider(
new AndroidDeviceScriptFixtureInfoProvider(
fixtureScript,
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidHostServiceFixture.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidHostServiceFixture.java
index 2ae4c81e29..a5f8935852 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidHostServiceFixture.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidHostServiceFixture.java
@@ -51,6 +51,7 @@ public class AndroidHostServiceFixture implements RuleConfiguredTargetFactory {
.addProvider(RunfilesProvider.class, RunfilesProvider.simple(runfiles))
.addNativeDeclaredProvider(
new AndroidHostServiceFixtureInfoProvider(
+ executable.getExecutable(),
ruleContext.getTokenizedStringListAttr("service_names"),
AndroidCommon.getSupportApks(ruleContext),
ruleContext.attributes().get("provides_test_args", Type.BOOLEAN),
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidHostServiceFixtureInfoProvider.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidHostServiceFixtureInfoProvider.java
index 715bfbc81b..3acd378695 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidHostServiceFixtureInfoProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidHostServiceFixtureInfoProvider.java
@@ -34,21 +34,28 @@ public class AndroidHostServiceFixtureInfoProvider extends SkylarkClassObject
static final NativeClassObjectConstructor ANDROID_HOST_SERVICE_FIXTURE_INFO =
new NativeClassObjectConstructor(SKYLARK_NAME) {};
+ private final Artifact executable;
private final ImmutableList<String> serviceNames;
private final NestedSet<Artifact> supportApks;
private final boolean providesTestArgs;
- private final boolean isDaemon;
+ private final boolean daemon;
AndroidHostServiceFixtureInfoProvider(
+ Artifact executable,
ImmutableList<String> serviceNames,
NestedSet<Artifact> supportApks,
boolean providesTestArgs,
boolean isDaemon) {
super(ANDROID_HOST_SERVICE_FIXTURE_INFO, ImmutableMap.<String, Object>of());
+ this.executable = executable;
this.serviceNames = serviceNames;
this.supportApks = supportApks;
this.providesTestArgs = providesTestArgs;
- this.isDaemon = isDaemon;
+ this.daemon = isDaemon;
+ }
+
+ public Artifact getExecutable() {
+ return executable;
}
public ImmutableList<String> getServiceNames() {
@@ -63,7 +70,7 @@ public class AndroidHostServiceFixtureInfoProvider extends SkylarkClassObject
return providesTestArgs;
}
- public boolean getIsDaemon() {
- return isDaemon;
+ public boolean getDaemon() {
+ return daemon;
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidInstrumentation.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidInstrumentation.java
index 75587bc9b6..d281b10134 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidInstrumentation.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidInstrumentation.java
@@ -20,9 +20,11 @@ import com.google.devtools.build.lib.analysis.FileProvider;
import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
import com.google.devtools.build.lib.analysis.RunfilesProvider;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.actions.SymlinkAction;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.packages.ImplicitOutputsFunction;
import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SafeImplicitOutputsFunction;
@@ -47,12 +49,18 @@ public class AndroidInstrumentation implements RuleConfiguredTargetFactory {
Artifact targetApk = getTargetApk(ruleContext);
Artifact instrumentationApk = createInstrumentationApk(ruleContext);
+ NestedSet<Artifact> filesToBuild =
+ NestedSetBuilder.<Artifact>stableOrder().add(targetApk).add(instrumentationApk).build();
RuleConfiguredTargetBuilder ruleBuilder = new RuleConfiguredTargetBuilder(ruleContext);
return ruleBuilder
- .setFilesToBuild(
- NestedSetBuilder.<Artifact>stableOrder().add(targetApk).add(instrumentationApk).build())
- .addProvider(RunfilesProvider.class, RunfilesProvider.EMPTY)
+ .setFilesToBuild(filesToBuild)
+ .addProvider(
+ RunfilesProvider.class,
+ RunfilesProvider.simple(
+ new Runfiles.Builder(ruleContext.getWorkspaceName())
+ .addTransitiveArtifacts(filesToBuild)
+ .build()))
.addNativeDeclaredProvider(
new AndroidInstrumentationInfoProvider(targetApk, instrumentationApk))
.build();
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidInstrumentationTest.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidInstrumentationTest.java
new file mode 100644
index 0000000000..0e5c7170a7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidInstrumentationTest.java
@@ -0,0 +1,334 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// 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.google.devtools.build.lib.rules.android;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.Runfiles;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.RunfilesSupport;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction;
+import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction.Substitution;
+import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction.Template;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.rules.test.ExecutionInfoProvider;
+import com.google.devtools.build.lib.syntax.Type;
+import com.google.devtools.build.lib.util.ResourceFileLoader;
+import java.io.IOException;
+import java.util.stream.StreamSupport;
+import javax.annotation.Nullable;
+
+/** An implementation of the {@code android_instrumentation} rule. */
+public class AndroidInstrumentationTest implements RuleConfiguredTargetFactory {
+
+ private static final Template ANDROID_INSTRUMENTATION_TEST_STUB_SCRIPT =
+ Template.forResource(
+ AndroidInstrumentationTest.class, "android_instrumentation_test_template.txt");
+ private static final String TEST_SUITE_PROPERTY_NAME_FILE = "test_suite_property_name.txt";
+
+ @Override
+ public ConfiguredTarget create(RuleContext ruleContext)
+ throws InterruptedException, RuleErrorException {
+ // The wrapper script that invokes the test entry point.
+ Artifact testExecutable = createTestExecutable(ruleContext);
+
+ ImmutableList<TransitiveInfoCollection> runfilesDeps =
+ ImmutableList.<TransitiveInfoCollection>builder()
+ .addAll(ruleContext.getPrerequisites("instrumentations", Mode.TARGET))
+ .addAll(ruleContext.getPrerequisites("fixtures", Mode.TARGET))
+ .add(ruleContext.getPrerequisite("target_device", Mode.HOST))
+ .add(ruleContext.getPrerequisite("$test_entry_point", Mode.HOST))
+ .build();
+
+ Runfiles runfiles =
+ new Runfiles.Builder(ruleContext.getWorkspaceName())
+ .addArtifact(testExecutable)
+ .addTargets(runfilesDeps, RunfilesProvider.DEFAULT_RUNFILES)
+ .addTransitiveArtifacts(AndroidCommon.getSupportApks(ruleContext))
+ .addTransitiveArtifacts(getAdb(ruleContext).getFilesToRun())
+ .addArtifacts(getDataDeps(ruleContext))
+ .build();
+
+ return new RuleConfiguredTargetBuilder(ruleContext)
+ .setFilesToBuild(NestedSetBuilder.<Artifact>stableOrder().add(testExecutable).build())
+ .addProvider(RunfilesProvider.class, RunfilesProvider.simple(runfiles))
+ .setRunfilesSupport(
+ RunfilesSupport.withExecutable(ruleContext, runfiles, testExecutable), testExecutable)
+ .addNativeDeclaredProvider(getExecutionInfoProvider(ruleContext))
+ .build();
+ }
+
+ /** Registers a {@link TemplateExpansionAction} to write the test executable. */
+ private Artifact createTestExecutable(RuleContext ruleContext) throws RuleErrorException {
+ Artifact testExecutable = ruleContext.createOutputArtifact();
+ ruleContext.registerAction(
+ new TemplateExpansionAction(
+ ruleContext.getActionOwner(),
+ testExecutable,
+ ANDROID_INSTRUMENTATION_TEST_STUB_SCRIPT,
+ getTemplateSubstitutions(ruleContext),
+ /* makeExecutable = */ true));
+ return testExecutable;
+ }
+
+ /**
+ * This method defines all substitutions need to fill in {@link
+ * #ANDROID_INSTRUMENTATION_TEST_STUB_SCRIPT}.
+ */
+ private ImmutableList<Substitution> getTemplateSubstitutions(RuleContext ruleContext)
+ throws RuleErrorException {
+ return ImmutableList.<Substitution>builder()
+ .add(Substitution.of("%workspace%", ruleContext.getWorkspaceName()))
+ .add(Substitution.of("%test_label%", ruleContext.getLabel().getCanonicalForm()))
+ .add(executableSubstitution("%adb%", getAdb(ruleContext)))
+ .add(executableSubstitution("%device_script%", getTargetDevice(ruleContext)))
+ .add(executableSubstitution("%test_entry_point%", getTestEntryPoint(ruleContext)))
+ .add(artifactListSubstitution("%target_apks%", getTargetApks(ruleContext)))
+ .add(
+ artifactListSubstitution("%instrumentation_apks%", getInstrumentationApks(ruleContext)))
+ .add(artifactListSubstitution("%support_apks%", getAllSupportApks(ruleContext)))
+ .add(Substitution.ofSpaceSeparatedMap("%test_args%", getTestArgs(ruleContext)))
+ .add(Substitution.ofSpaceSeparatedMap("%fixture_args%", getFixtureArgs(ruleContext)))
+ .add(Substitution.ofSpaceSeparatedMap("%log_levels%", getLogLevels(ruleContext)))
+ .add(deviceScriptFixturesSubstitution(ruleContext))
+ .addAll(hostServiceFixturesSubstitutions(ruleContext))
+ .add(artifactListSubstitution("%data_deps%", getDataDeps(ruleContext)))
+ .add(Substitution.of("%device_broker_type%", getDeviceBrokerType(ruleContext)))
+ .add(Substitution.of("%test_suite_property_name%", getTestSuitePropertyName(ruleContext)))
+ .build();
+ }
+
+ /**
+ * An ad-hoc substitution to put the information from the {@code android_device_script_fixture}s
+ * into the bash stub script.
+ *
+ * <p>TODO(ajmichael): Determine an actual protocol to pass this information to the test suite.
+ */
+ private static Substitution deviceScriptFixturesSubstitution(RuleContext ruleContext) {
+ ImmutableList.Builder<String> builder = ImmutableList.builder();
+ for (AndroidDeviceScriptFixtureInfoProvider deviceScriptFixture :
+ getDeviceScriptFixtures(ruleContext)) {
+ builder.add(
+ String.format(
+ "[%s]=%b,%b",
+ deviceScriptFixture.getFixtureScript().getRunfilesPathString(),
+ deviceScriptFixture.getDaemon(),
+ deviceScriptFixture.getStrictExit()));
+ }
+ return Substitution.ofSpaceSeparatedList("%device_script_fixtures%", builder.build());
+ }
+
+ /**
+ * An ad-hoc substitution to put the information from the {@code android_host_service_fixture}s
+ * into the bash stub script.
+ *
+ * <p>TODO(ajmichael): Determine an actual protocol to pass this information to the test suite.
+ */
+ private static ImmutableList<Substitution> hostServiceFixturesSubstitutions(
+ RuleContext ruleContext) {
+ AndroidHostServiceFixtureInfoProvider hostServiceFixture = getHostServiceFixture(ruleContext);
+ return ImmutableList.of(
+ Substitution.of(
+ "%host_service_fixture%",
+ hostServiceFixture != null
+ ? hostServiceFixture.getExecutable().getRunfilesPathString()
+ : ""),
+ Substitution.of(
+ "%host_service_fixture_services%",
+ hostServiceFixture != null
+ ? Joiner.on(",").join(hostServiceFixture.getServiceNames())
+ : ""));
+ }
+
+ private static Substitution executableSubstitution(
+ String key, FilesToRunProvider filesToRunProvider) {
+ return Substitution.of(key, filesToRunProvider.getExecutable().getRunfilesPathString());
+ }
+
+ private static Substitution artifactListSubstitution(String key, Iterable<Artifact> artifacts) {
+ return Substitution.ofSpaceSeparatedList(
+ key,
+ StreamSupport.stream(artifacts.spliterator(), false)
+ .map(Artifact::getRunfilesPathString)
+ .collect(ImmutableList.toImmutableList()));
+ }
+
+ /**
+ * The target APKs from each {@code android_instrumentation} in the {@code instrumentations}
+ * attribute.
+ */
+ private static Iterable<Artifact> getTargetApks(RuleContext ruleContext) {
+ return Iterables.transform(
+ ruleContext.getPrerequisites(
+ "instrumentations",
+ Mode.TARGET,
+ AndroidInstrumentationInfoProvider.ANDROID_INSTRUMENTATION_INFO.getKey(),
+ AndroidInstrumentationInfoProvider.class),
+ AndroidInstrumentationInfoProvider::getTargetApk);
+ }
+
+ /**
+ * The instrumentation APKs from each {@code android_instrumentation} in the {@code
+ * instrumentations} attribute.
+ */
+ private static Iterable<Artifact> getInstrumentationApks(RuleContext ruleContext) {
+ return Iterables.transform(
+ ruleContext.getPrerequisites(
+ "instrumentations",
+ Mode.TARGET,
+ AndroidInstrumentationInfoProvider.ANDROID_INSTRUMENTATION_INFO.getKey(),
+ AndroidInstrumentationInfoProvider.class),
+ AndroidInstrumentationInfoProvider::getInstrumentationApk);
+ }
+
+ /** The support APKs from the {@code support_apks} and {@code fixtures} attributes. */
+ private static NestedSet<Artifact> getAllSupportApks(RuleContext ruleContext) {
+ NestedSetBuilder<Artifact> allSupportApks =
+ NestedSetBuilder.<Artifact>stableOrder()
+ .addTransitive(AndroidCommon.getSupportApks(ruleContext));
+ for (AndroidDeviceScriptFixtureInfoProvider fixture :
+ ruleContext.getPrerequisites(
+ "fixtures", Mode.TARGET, AndroidDeviceScriptFixtureInfoProvider.class)) {
+ allSupportApks.addTransitive(fixture.getSupportApks());
+ }
+ for (AndroidHostServiceFixtureInfoProvider fixture :
+ ruleContext.getPrerequisites(
+ "fixtures",
+ Mode.TARGET,
+ AndroidInstrumentationInfoProvider.ANDROID_INSTRUMENTATION_INFO.getKey(),
+ AndroidHostServiceFixtureInfoProvider.class)) {
+ allSupportApks.addTransitive(fixture.getSupportApks());
+ }
+ return allSupportApks.build();
+ }
+
+ /** The deploy jar that interacts with the device. */
+ private static FilesToRunProvider getTestEntryPoint(RuleContext ruleContext) {
+ return ruleContext.getExecutablePrerequisite("$test_entry_point", Mode.HOST);
+ }
+
+ /** The {@code android_device} script to launch an emulator for the test. */
+ private static FilesToRunProvider getTargetDevice(RuleContext ruleContext) {
+ return ruleContext.getExecutablePrerequisite("target_device", Mode.HOST);
+ }
+
+ /** ADB binary from the Android SDK. */
+ private static FilesToRunProvider getAdb(RuleContext ruleContext) {
+ return AndroidSdkProvider.fromRuleContext(ruleContext).getAdb();
+ }
+
+ /** Map of {@code test_args} for the test runner to make available to test test code. */
+ private static ImmutableMap<String, String> getTestArgs(RuleContext ruleContext) {
+ return ImmutableMap.copyOf(ruleContext.attributes().get("test_args", Type.STRING_DICT));
+ }
+
+ /** Map of {@code fixture_args} for the test runner to pass to the {@code fixtures}. */
+ private static ImmutableMap<String, String> getFixtureArgs(RuleContext ruleContext) {
+ return ImmutableMap.copyOf(ruleContext.attributes().get("fixture_args", Type.STRING_DICT));
+ }
+
+ /** Map of {@code log_levels} to enable before the test run. */
+ private static ImmutableMap<String, String> getLogLevels(RuleContext ruleContext) {
+ return ImmutableMap.copyOf(ruleContext.attributes().get("log_levels", Type.STRING_DICT));
+ }
+
+ private static ImmutableList<Artifact> getDataDeps(RuleContext ruleContext) {
+ return ruleContext.getPrerequisiteArtifacts("data", Mode.DATA).list();
+ }
+
+ /**
+ * Checks for a {@code android_host_service_fixture} in the {@code fixtures} attribute. Returns
+ * null if there is none, a {@link AndroidHostServiceFixtureInfoProvider} if there is one or
+ * throws an error if there is more than one.
+ */
+ @Nullable
+ private static AndroidHostServiceFixtureInfoProvider getHostServiceFixture(
+ RuleContext ruleContext) {
+ ImmutableList<AndroidHostServiceFixtureInfoProvider> hostServiceFixtures =
+ ImmutableList.copyOf(
+ ruleContext.getPrerequisites(
+ "fixtures",
+ Mode.TARGET,
+ AndroidHostServiceFixtureInfoProvider.ANDROID_HOST_SERVICE_FIXTURE_INFO.getKey(),
+ AndroidHostServiceFixtureInfoProvider.class));
+ if (hostServiceFixtures.size() > 1) {
+ ruleContext.ruleError(
+ "android_instrumentation_test accepts at most one android_host_service_fixture");
+ }
+ return Iterables.getFirst(hostServiceFixtures, null);
+ }
+
+ private static Iterable<AndroidDeviceScriptFixtureInfoProvider> getDeviceScriptFixtures(
+ RuleContext ruleContext) {
+ return ruleContext.getPrerequisites(
+ "fixtures",
+ Mode.TARGET,
+ AndroidDeviceScriptFixtureInfoProvider.ANDROID_DEVICE_SCRIPT_FIXTURE_INFO.getKey(),
+ AndroidDeviceScriptFixtureInfoProvider.class);
+ }
+
+ private static String getDeviceBrokerType(RuleContext ruleContext) {
+ return ruleContext
+ .getPrerequisite("target_device", Mode.HOST, DeviceBrokerTypeProvider.class)
+ .getDeviceBrokerType();
+ }
+
+ /**
+ * Returns the name of the test suite property that the test runner uses to determine which test
+ * suite to run.
+ *
+ * <p>This is stored in a separate resource file to facilitate different runners for internal and
+ * external Bazel.
+ */
+ private static String getTestSuitePropertyName(RuleContext ruleContext)
+ throws RuleErrorException {
+ try {
+ return ResourceFileLoader.loadResource(
+ AndroidInstrumentationTest.class, TEST_SUITE_PROPERTY_NAME_FILE);
+ } catch (IOException e) {
+ ruleContext.throwWithRuleError("Cannot load test suite property name: " + e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * Propagates the {@link ExecutionInfoProvider} from the {@code android_device} rule in the {@code
+ * target_device} attribute.
+ *
+ * <p>This allows the dependent {@code android_device} rule to specify some requirements on the
+ * machine that the {@code android_instrumentation_test} runs on.
+ */
+ private static ExecutionInfoProvider getExecutionInfoProvider(RuleContext ruleContext) {
+ ExecutionInfoProvider executionInfoProvider =
+ (ExecutionInfoProvider)
+ ruleContext.getPrerequisite(
+ "target_device", Mode.HOST, ExecutionInfoProvider.SKYLARK_CONSTRUCTOR.getKey());
+ ImmutableMap<String, String> executionRequirements =
+ (executionInfoProvider != null)
+ ? executionInfoProvider.getExecutionInfo()
+ : ImmutableMap.of();
+ return new ExecutionInfoProvider(executionRequirements);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidInstrumentationTestRule.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidInstrumentationTestRule.java
new file mode 100644
index 0000000000..bb65e2e797
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidInstrumentationTestRule.java
@@ -0,0 +1,83 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// 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.google.devtools.build.lib.rules.android;
+
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.BuildType.LABEL;
+import static com.google.devtools.build.lib.packages.BuildType.LABEL_LIST;
+import static com.google.devtools.build.lib.syntax.Type.STRING_DICT;
+
+import com.google.devtools.build.lib.analysis.BaseRuleClasses;
+import com.google.devtools.build.lib.analysis.RuleDefinition;
+import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleClass.Builder;
+import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
+import com.google.devtools.build.lib.util.FileTypeSet;
+
+/** Rule definition for the {@code android_instrumentation_test} rule. */
+public class AndroidInstrumentationTestRule implements RuleDefinition {
+
+ @Override
+ public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+ return builder
+ .setUndocumented()
+ .add(
+ attr("instrumentations", LABEL_LIST)
+ .mandatory()
+ .allowedFileTypes(FileTypeSet.NO_FILE)
+ .allowedRuleClasses("android_instrumentation"))
+ .add(
+ attr("target_device", LABEL)
+ .mandatory()
+ .exec()
+ .cfg(HOST)
+ .allowedFileTypes(FileTypeSet.NO_FILE)
+ .allowedRuleClasses("android_device"))
+ .add(
+ attr("support_apks", LABEL_LIST)
+ .allowedFileTypes(AndroidRuleClasses.APK)
+ .allowedRuleClasses("android_binary"))
+ .add(attr("test_args", STRING_DICT))
+ .add(
+ attr("fixtures", LABEL_LIST)
+ .allowedFileTypes(FileTypeSet.NO_FILE)
+ .allowedRuleClasses(
+ "android_device_script_fixture", "android_host_service_fixture"))
+ .add(attr("fixture_args", STRING_DICT))
+ .add(attr("log_levels", STRING_DICT))
+ .add(
+ attr("$test_entry_point", LABEL)
+ .exec()
+ .cfg(HOST)
+ .value(
+ environment.getToolsLabel("//tools/android:instrumentation_test_entry_point")))
+ .removeAttribute("deps")
+ .removeAttribute("javacopts")
+ .removeAttribute("plugins")
+ .removeAttribute(":java_plugins")
+ .build();
+ }
+
+ @Override
+ public Metadata getMetadata() {
+ return RuleDefinition.Metadata.builder()
+ .name("android_instrumentation_test")
+ .type(RuleClassType.TEST)
+ .ancestors(AndroidRuleClasses.AndroidBaseRule.class, BaseRuleClasses.TestBaseRule.class)
+ .factoryClass(AndroidInstrumentationTest.class)
+ .build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/android_instrumentation_test_template.txt b/src/main/java/com/google/devtools/build/lib/rules/android/android_instrumentation_test_template.txt
new file mode 100644
index 0000000000..c4af683ddb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/android_instrumentation_test_template.txt
@@ -0,0 +1,88 @@
+#!/bin/bash --posix
+# Copyright 2017 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+set -eux
+
+# Unset TESTBRIDGE_TEST_ONLY environment variable set by Bazel's --test_filter
+# flag so that JUnit3 doesn't filter out the Android test suite class. Instead,
+# forward this variable as a Java flag with the same name.
+if [ -z "${TESTBRIDGE_TEST_ONLY+1}" ]; then
+ ANDROID_TESTBRIDGE_TEST_ONLY=""
+else
+ ANDROID_TESTBRIDGE_TEST_ONLY=${TESTBRIDGE_TEST_ONLY}
+ unset TESTBRIDGE_TEST_ONLY
+fi
+
+function join_paths() {
+ local base_dir=$1
+ local sep=$2
+ shift 2
+ local paths=$@
+
+ local result=""
+ for path in $paths
+ do
+ result=${base_dir}/${path}${sep}${result}
+ done
+ echo ${result}
+}
+
+test_label="%test_label%"
+test_entry_point="%test_entry_point%"
+log_levels="%log_levels%"
+WORKSPACE_DIR="${TEST_SRCDIR}/%workspace%"
+adb="${WORKSPACE_DIR}/%adb%"
+device_script="${WORKSPACE_DIR}/%device_script%"
+
+data_deps="%data_deps%"
+data_deps=$(join_paths ${WORKSPACE_DIR} "," ${data_deps})
+
+device_broker_type="%device_broker_type%"
+test_label="%test_label%"
+
+target_apks="%target_apks%"
+target_apks=$(join_paths ${WORKSPACE_DIR} "," ${target_apks})
+
+instrumentation_apks="%instrumentation_apks%"
+instrumentation_apks=$(join_paths ${WORKSPACE_DIR} "," ${instrumentation_apks})
+
+support_apks="%support_apks%"
+support_apks=$(join_paths ${WORKSPACE_DIR} "," ${support_apks})
+
+apks_to_install="${support_apks}${target_apks}${instrumentation_apks}"
+
+declare -A device_script_fixtures=( %device_script_fixtures% )
+
+host_service_fixture="%host_service_fixture%"
+host_service_fixture_services="%host_service_fixture_services%"
+
+fixture_args="%fixture_args%"
+
+test_suite_property_name='%test_suite_property_name%'
+
+$test_entry_point \
+ --wrapper_script_flag=--jvm_flag=-D$test_suite_property_name=com.google.android.apps.common.testing.suite.AndroidDeviceTestSuite \
+ --adb="${adb}" \
+ --device_broker_type="${device_broker_type}" \
+ --device_script="${device_script}" \
+ --data_deps="${data_deps}" \
+ --test_label="${test_label}" \
+ --apks_to_install="${apks_to_install}" \
+ --fixture_scripts="$(printf "%s," "${!device_script_fixtures[@]}")" \
+ --hermetic_server_script="${host_service_fixture}" \
+ --hermetic_servers="${host_service_fixture_services}" \
+ --data_deps="$(printf "%s," "${!device_script_fixtures[@]}")" \
+ --test_filter="${ANDROID_TESTBRIDGE_TEST_ONLY}" \
+ "$@"
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/test_suite_property_name.txt b/src/main/java/com/google/devtools/build/lib/rules/android/test_suite_property_name.txt
new file mode 100644
index 0000000000..650406adfe
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/test_suite_property_name.txt
@@ -0,0 +1 @@
+bazel.test_suite