aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/rules/android/AndroidInstrumentationTest.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/rules/android/AndroidInstrumentationTest.java')
-rw-r--r--src/main/java/com/google/devtools/build/lib/rules/android/AndroidInstrumentationTest.java334
1 files changed, 334 insertions, 0 deletions
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);
+ }
+}