blob: c80276e9b2a169ecd5451174a397c2b93f586e35 [file] [log] [blame]
// 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.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.devtools.build.lib.actions.util.ActionsTestUtil.getFirstArtifactEndingWith;
import static org.junit.Assert.fail;
import com.google.common.base.Ascii;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.MoreCollectors;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.CommandLineExpansionException;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.FileProvider;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
import com.google.devtools.build.lib.analysis.configuredtargets.OutputFileConfiguredTarget;
import com.google.devtools.build.lib.analysis.util.AnalysisTestUtil;
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.rules.android.deployinfo.AndroidDeployInfoOuterClass.AndroidDeployInfo;
import com.google.devtools.build.lib.rules.java.JavaCompileAction;
import com.google.devtools.build.lib.rules.java.JavaCompileActionTestHelper;
import com.google.devtools.build.lib.rules.java.JavaInfo;
import com.google.devtools.build.lib.rules.java.JavaRuleOutputJarsProvider;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
import com.google.devtools.build.lib.testutil.TestConstants;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import org.junit.Before;
/** Common methods shared between Android related {@link BuildViewTestCase}s. */
public abstract class AndroidBuildViewTestCase extends BuildViewTestCase {
@Before
public void setupStarlarkJavaLibrary() throws Exception {
setBuildLanguageOptions("--experimental_google_legacy_api");
}
/** Override this to trigger platform-based Android toolchain resolution. */
protected boolean platformBasedToolchains() {
return false;
}
protected String defaultPlatformFlag() {
return "";
}
@Override
protected void useConfiguration(ImmutableMap<String, Object> starlarkOptions, String... args)
throws Exception {
if (!platformBasedToolchains()) {
super.useConfiguration(
starlarkOptions,
ImmutableList.builder()
.add((Object[]) args)
.add("--noincompatible_enable_cc_toolchain_resolution")
.build()
.toArray(new String[0]));
return;
}
// Platform-based toolchain resolution:
ImmutableList.Builder<String> fullArgs = ImmutableList.builder();
fullArgs.add("--incompatible_enable_android_toolchain_resolution");
// Uncomment the below to get more info when tests fail because of toolchain resolution.
// fullArgs.add("--toolchain_resolution_debug=tools/android:.*toolchain_type");
boolean hasPlatform = false;
for (String arg : args) {
if (arg.startsWith("--android_sdk=")) {
// --android_sdk is a legacy toolchain resolution flag. Remap it to the platform-equivalent:
// wrap a toolchain definition around the SDK with no constraint requirements and register
// it with --extra_toolchains. --extra_toolchains guarantees this SDK will be chosen before
// anything registered in the WORKSPACE.
String sdkLabel = arg.substring("--android_sdk=".length());
scratch.file(
"legacy_to_platform_sdk/BUILD",
"toolchain(",
" name = 'custom_sdk_toolchain',",
String.format(" toolchain_type = '%s',", TestConstants.ANDROID_TOOLCHAIN_TYPE_LABEL),
String.format(" toolchain = '%s',", sdkLabel),
")");
fullArgs.add("--extra_toolchains=//legacy_to_platform_sdk:custom_sdk_toolchain");
} else {
fullArgs.add(arg);
}
if (arg.startsWith("--platforms=") || arg.startsWith("--android_platforms=")) {
hasPlatform = true;
}
}
if (!hasPlatform) {
fullArgs.add(
String.format(
"--platforms=%sandroid:armeabi-v7a", TestConstants.CONSTRAINTS_PACKAGE_ROOT));
fullArgs.add(defaultPlatformFlag());
}
fullArgs.add("--incompatible_enable_cc_toolchain_resolution");
super.useConfiguration(starlarkOptions, fullArgs.build().toArray(new String[0]));
}
protected Iterable<Artifact> getNativeLibrariesInApk(ConfiguredTarget target) {
return Iterables.filter(
getGeneratingAction(getCompressedUnsignedApk(target)).getInputs().toList(),
a -> a.getFilename().endsWith(".so"));
}
protected Label getGeneratingLabelForArtifact(Artifact artifact) {
Action generatingAction = getGeneratingAction(artifact);
return generatingAction != null ? getGeneratingAction(artifact).getOwner().getLabel() : null;
}
protected void assertNativeLibrariesCopiedNotLinked(
ConfiguredTarget target,
BuildConfigurationValue targetConfiguration,
String... expectedLibNames) {
Iterable<Artifact> copiedLibs = getNativeLibrariesInApk(target);
for (Artifact copiedLib : copiedLibs) {
assertWithMessage("Native libraries were linked to produce " + copiedLib)
.that(getGeneratingLabelForArtifact(copiedLib))
.isNotEqualTo(target.getLabel());
}
assertThat(
AnalysisTestUtil.artifactsToStrings(
targetConfiguration, getHostConfiguration(), copiedLibs))
.containsAtLeastElementsIn(ImmutableSet.copyOf(Arrays.asList(expectedLibNames)));
}
protected String flagValue(String flag, List<String> args) {
assertThat(args).contains(flag);
return args.get(args.indexOf(flag) + 1);
}
protected Set<String> flagValues(String flag, List<String> args) {
Set<String> result = new HashSet<>();
for (int i = 0; i < args.size(); i++) {
if (args.get(i).equals(flag)) {
result.add(args.get(++i));
}
}
return result;
}
/**
* The unsigned APK is created in two actions. The first action adds everything that needs to be
* unconditionally compressed in the APK. The second action adds everything else, preserving their
* compression.
*/
protected Artifact getCompressedUnsignedApk(ConfiguredTarget target) {
return artifactByPath(
actionsTestUtil().artifactClosureOf(getFinalUnsignedApk(target)),
"_unsigned.apk",
"_unsigned.apk");
}
protected Artifact getFinalUnsignedApk(ConfiguredTarget target) {
return getFirstArtifactEndingWith(
target.getProvider(FileProvider.class).getFilesToBuild(), "_unsigned.apk");
}
protected Artifact getResourceApk(ConfiguredTarget target) {
Artifact resourceApk =
getFirstArtifactEndingWith(
getGeneratingAction(getFinalUnsignedApk(target)).getInputs(), ".ap_");
assertThat(resourceApk).isNotNull();
return resourceApk;
}
protected void assertProguardUsed(ConfiguredTarget binary) {
assertWithMessage("proguard.jar is not in the rule output")
.that(
actionsTestUtil()
.getActionForArtifactEndingWith(getFilesToBuild(binary), "_proguard.jar"))
.isNotNull();
}
protected List<String> resourceArguments(ValidatedAndroidResources resource)
throws CommandLineExpansionException, InterruptedException {
return getGeneratingSpawnActionArgs(resource.getApk());
}
protected SpawnAction resourceGeneratingAction(ValidatedAndroidResources resource) {
return getGeneratingSpawnAction(resource.getApk());
}
protected static ValidatedAndroidResources getValidatedResources(ConfiguredTarget target) {
return getValidatedResources(target, /* transitive= */ false);
}
protected static ValidatedAndroidResources getValidatedResources(
ConfiguredTarget target, boolean transitive) {
Preconditions.checkNotNull(target);
final AndroidResourcesInfo info = target.get(AndroidResourcesInfo.PROVIDER);
assertWithMessage("No android resources exported from the target.").that(info).isNotNull();
return transitive
? info.getTransitiveAndroidResources().getSingleton()
: info.getDirectAndroidResources().getSingleton();
}
protected Artifact getResourceClassJar(final ConfiguredTargetAndData target) {
JavaRuleOutputJarsProvider jarProvider =
JavaInfo.getProvider(JavaRuleOutputJarsProvider.class, target.getConfiguredTarget());
assertThat(jarProvider).isNotNull();
return Iterables.find(
jarProvider.getJavaOutputs(),
javaOutput -> {
assertThat(javaOutput).isNotNull();
assertThat(javaOutput.getClassJar()).isNotNull();
return javaOutput
.getClassJar()
.getFilename()
.equals(target.getTarget().getName() + "_resources.jar");
})
.getClassJar();
}
// android resources related tests
protected void assertPrimaryResourceDirs(List<String> expectedPaths, List<String> actualArgs) {
assertThat(actualArgs).contains("--primaryData");
String actualFlagValue = actualArgs.get(actualArgs.indexOf("--primaryData") + 1);
List<String> actualPaths = null;
if (actualFlagValue.matches("[^;]*;[^;]*;.*")) {
actualPaths =
Arrays.asList(Iterables.get(Splitter.on(';').split(actualFlagValue), 0).split("#"));
} else if (actualFlagValue.matches("[^:]*:[^:]*:.*")) {
actualPaths =
Arrays.asList(Iterables.get(Splitter.on(':').split(actualFlagValue), 0).split("#"));
} else {
fail(String.format("Failed to parse --primaryData: %s", actualFlagValue));
}
assertThat(actualPaths).containsAtLeastElementsIn(expectedPaths);
}
protected List<String> getDirectDependentResourceDirs(List<String> actualArgs) {
assertThat(actualArgs).contains("--directData");
String actualFlagValue = actualArgs.get(actualArgs.indexOf("--directData") + 1);
return getDependentResourceDirs(actualFlagValue);
}
protected List<String> getTransitiveDependentResourceDirs(List<String> actualArgs) {
assertThat(actualArgs).contains("--data");
String actualFlagValue = actualArgs.get(actualArgs.indexOf("--data") + 1);
return getDependentResourceDirs(actualFlagValue);
}
private static List<String> getDependentResourceDirs(String actualFlagValue) {
String separator = null;
if (actualFlagValue.matches("[^;]*;[^;]*;[^;]*;.*")) {
separator = ";";
} else if (actualFlagValue.matches("[^:]*:[^:]*:[^:]*:.*")) {
separator = ":";
} else {
fail(String.format("Failed to parse flag: %s", actualFlagValue));
}
ImmutableList.Builder<String> actualPaths = ImmutableList.builder();
for (String resourceDependency : Splitter.on(',').split(actualFlagValue)) {
actualPaths.add(
Iterables.get(Splitter.on(separator).split(resourceDependency), 0).split("#"));
}
return actualPaths.build();
}
protected String execPathEndingWith(NestedSet<Artifact> inputs, String suffix) {
return getFirstArtifactEndingWith(inputs, suffix).getExecPathString();
}
protected String execPathEndingWith(Iterable<Artifact> inputs, String suffix) {
return getFirstArtifactEndingWith(inputs, suffix).getExecPathString();
}
@Nullable
protected AndroidDeployInfo getAndroidDeployInfo(Artifact artifact) throws IOException {
Action generatingAction = getGeneratingAction(artifact);
if (generatingAction instanceof AndroidDeployInfoAction) {
AndroidDeployInfoAction writeAction = (AndroidDeployInfoAction) generatingAction;
return writeAction.getDeployInfo();
}
return null;
}
protected List<String> getProcessorNames(JavaCompileAction compileAction) throws Exception {
return JavaCompileActionTestHelper.getProcessorNames(compileAction);
}
protected List<String> getProcessorNames(String outputTarget) throws Exception {
OutputFileConfiguredTarget out =
(OutputFileConfiguredTarget) getFileConfiguredTarget(outputTarget);
JavaCompileAction compileAction = (JavaCompileAction) getGeneratingAction(out.getArtifact());
return getProcessorNames(compileAction);
}
// Returns an artifact that will be generated when a rule has resources.
protected static Artifact getResourceArtifact(ConfiguredTarget target) {
// the last provider is the provider from the target.
return Iterables.getLast(
target.get(AndroidResourcesInfo.PROVIDER).getDirectAndroidResources().toList())
.getJavaClassJar();
}
protected Map<String, String> getBinaryMergeeManifests(ConfiguredTarget target) throws Exception {
return getMergeeManifests(target.get(ApkInfo.PROVIDER).getMergedManifest());
}
protected Map<String, String> getLocalTestMergeeManifests(ConfiguredTarget target)
throws Exception {
return getMergeeManifests(
collectRunfiles(target).toList().stream()
.filter(
(artifact) ->
artifact.getFilename().equals("AndroidManifest.xml")
&& artifact.getOwnerLabel().equals(target.getLabel()))
.collect(MoreCollectors.onlyElement()));
}
/** Gets the map of mergee manifests in the order specified on the command line. */
protected Map<String, String> getMergeeManifests(Artifact processedManifest) throws Exception {
List<String> processingActionArgs = getGeneratingSpawnActionArgs(processedManifest);
assertThat(processingActionArgs).contains("--primaryData");
String primaryData =
processingActionArgs.get(processingActionArgs.indexOf("--primaryData") + 1);
String mergedManifestExecPathString = Splitter.on(":").splitToList(primaryData).get(2);
SpawnAction processingAction = getGeneratingSpawnAction(processedManifest);
Artifact mergedManifest =
Iterables.find(
processingAction.getInputs().toList(),
(artifact) -> artifact.getExecPath().toString().equals(mergedManifestExecPathString));
List<String> mergeArgs = getGeneratingSpawnActionArgs(mergedManifest);
if (!mergeArgs.contains("--mergeeManifests")) {
return ImmutableMap.of();
}
Map<String, String> splitData =
Splitter.on(",")
.withKeyValueSeparator(Splitter.onPattern("(?<!\\\\):"))
.split(mergeArgs.get(mergeArgs.indexOf("--mergeeManifests") + 1));
ImmutableMap.Builder<String, String> results = new ImmutableMap.Builder<>();
for (Map.Entry<String, String> manifestAndLabel : splitData.entrySet()) {
results.put(manifestAndLabel.getKey(), manifestAndLabel.getValue().replace("\\:", ":"));
}
return results.build();
}
/** Gets the processed manifest exported by the given library. */
protected Artifact getLibraryManifest(ConfiguredTarget target) throws Exception {
if (target.get(AndroidManifestInfo.PROVIDER) != null) {
return target.get(AndroidManifestInfo.PROVIDER).getManifest();
}
return null;
}
// Returns an artifact that will be generated when a rule has assets that are processed seperately
static Artifact getDecoupledAssetArtifact(ConfiguredTarget target) {
return target.get(AndroidAssetsInfo.PROVIDER).getValidationResult();
}
protected static Set<Artifact> getNonToolInputs(Action action) {
return Sets.difference(action.getInputs().toSet(), action.getTools().toSet());
}
protected String getAndroidJarPath() throws Exception {
return getAndroidSdk().getAndroidJar().getExecPathString();
}
protected String getAndroidJarFilename() throws Exception {
return getAndroidSdk().getAndroidJar().getFilename();
}
protected Artifact getProguardBinary() throws Exception {
return getAndroidSdk().getProguard().getExecutable();
}
protected String getMainDexClassesPath() throws Exception {
return getAndroidSdk().getMainDexClasses().getExecPathString();
}
protected String getMainDexClassesFilename() throws Exception {
return getAndroidSdk().getMainDexClasses().getFilename();
}
private AndroidSdkProvider getAndroidSdk() throws Exception {
Label sdk = targetConfig.getFragment(AndroidConfiguration.class).getSdk();
return getConfiguredTarget(sdk, targetConfig).get(AndroidSdkProvider.PROVIDER);
}
/**
* Asserts that the expected jar is produced with the expected runtype. Returns the previous
* action behind lastStageAction.
*/
private SpawnAction assertJarAndRunType(String runType, SpawnAction lastStageAction, int pass)
throws Exception {
String jarMnemonic = Ascii.toLowerCase(runType);
Artifact lastStageOutput =
ActionsTestUtil.getFirstArtifactEndingWith(
lastStageAction.getInputs(), "_" + jarMnemonic + "_" + pass + ".jar");
assertWithMessage(jarMnemonic + "_" + pass + ".jar is not in rule output")
.that(lastStageOutput)
.isNotNull();
SpawnAction previousAction = getGeneratingSpawnAction(lastStageOutput);
assertThat(previousAction.getArguments()).contains("-runtype " + runType);
return previousAction;
}
protected void checkProguardUse(
ConfiguredTarget binary,
String artifact,
boolean expectMapping,
@Nullable Integer passes,
int bytecodeOptimizationPassActions,
String... expectedlibraryJars)
throws Exception {
assertProguardUsed(binary);
assertProguardGenerated(binary);
Action dexAction =
actionsTestUtil()
.getActionForArtifactEndingWith(
actionsTestUtil().artifactClosureOf(getFilesToBuild(binary)), "classes.dex.zip");
dexAction =
actionsTestUtil().getActionForArtifactEndingWith(dexAction.getInputs(), "classes.dex.zip");
Artifact trimmedJar = getFirstArtifactEndingWith(dexAction.getInputs(), artifact);
assertWithMessage("Dex should be built from jar trimmed with Proguard.")
.that(trimmedJar)
.isNotNull();
SpawnAction proguardAction = getGeneratingSpawnAction(trimmedJar);
if (passes == null) {
// Verify proguard as a single action.
Action proguardMap =
actionsTestUtil()
.getActionForArtifactEndingWith(getFilesToBuild(binary), "_proguard.map");
if (expectMapping) {
assertWithMessage("proguard.map is not in the rule output").that(proguardMap).isNotNull();
} else {
assertWithMessage("proguard.map is in the rule output").that(proguardMap).isNull();
}
checkProguardLibJars(proguardAction, expectedlibraryJars);
} else {
// Verify the multi-stage system generated the correct number of stages.
Artifact proguardMap =
ActionsTestUtil.getFirstArtifactEndingWith(proguardAction.getOutputs(), "_proguard.map");
if (expectMapping) {
assertWithMessage("proguard.map is not in the rule output").that(proguardMap).isNotNull();
} else {
assertWithMessage("proguard.map is in the rule output").that(proguardMap).isNull();
}
assertThat(proguardAction.getArguments()).contains("-runtype FINAL");
checkProguardLibJars(proguardAction, expectedlibraryJars);
SpawnAction lastStageAction = proguardAction;
// Verify Obfuscation/Optimization config.
for (int pass = passes; pass > 0; pass--) {
if (bytecodeOptimizationPassActions == 2) {
// This is used to provide coverage of the OPTIMIZATION_FINAL/OPTIMIZATION_INITIAL legacy
// behavior.
lastStageAction = assertJarAndRunType("OPTIMIZATION_FINAL", lastStageAction, pass);
lastStageAction = assertJarAndRunType("OPTIMIZATION_INITIAL", lastStageAction, pass);
} else {
for (int action = bytecodeOptimizationPassActions; action > 0; action--) {
lastStageAction =
assertJarAndRunType(
"OPTIMIZATION_ACTION_" + action + "_OF_" + bytecodeOptimizationPassActions,
lastStageAction,
pass);
}
}
checkProguardLibJars(lastStageAction, expectedlibraryJars);
}
Artifact preoptimizationOutput =
ActionsTestUtil.getFirstArtifactEndingWith(
lastStageAction.getInputs(), "proguard_preoptimization.jar");
assertWithMessage("proguard_preoptimization.jar is not in rule output")
.that(preoptimizationOutput)
.isNotNull();
SpawnAction proOptimization = getGeneratingSpawnAction(preoptimizationOutput);
// Verify intitial step.
assertThat(proOptimization.getArguments()).contains("-runtype INITIAL");
checkProguardLibJars(proOptimization, expectedlibraryJars);
}
}
void checkProguardLibJars(SpawnAction proguardAction, String... expectedlibraryJars)
throws Exception {
Collection<String> libraryJars = new ArrayList<>();
Iterator<String> argsIterator = proguardAction.getArguments().iterator();
for (String argument = argsIterator.next();
argsIterator.hasNext();
argument = argsIterator.next()) {
if (argument.equals("-libraryjars")) {
libraryJars.add(argsIterator.next());
}
}
assertThat(libraryJars).containsExactly((Object[]) expectedlibraryJars);
}
protected void assertProguardGenerated(ConfiguredTarget binary) {
Action generateProguardAction =
actionsTestUtil()
.getActionForArtifactEndingWith(
actionsTestUtil().artifactClosureOf(getFilesToBuild(binary)), "_proguard.cfg");
assertWithMessage("proguard generating action not spawned")
.that(generateProguardAction)
.isNotNull();
Action proguardAction =
actionsTestUtil().getActionForArtifactEndingWith(getFilesToBuild(binary), "_proguard.jar");
actionsTestUtil();
assertWithMessage("Generated config not in inputs to proguard action")
.that(proguardAction.getInputs().toList())
.contains(
ActionsTestUtil.getFirstArtifactEndingWith(
generateProguardAction.getOutputs(), "_proguard.cfg"));
}
protected void assertProguardNotUsed(ConfiguredTarget binary) {
assertWithMessage("proguard.jar is in the rule output")
.that(
actionsTestUtil()
.getActionForArtifactEndingWith(getFilesToBuild(binary), "_proguard.jar"))
.isNull();
}
}