blob: a7bee2486c583083afb24055447bb65d400464cd [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 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.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException;
import com.google.devtools.build.lib.rules.android.AndroidConfiguration.AndroidAaptVersion;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests {@link AndroidResources} */
@RunWith(JUnit4.class)
public class AndroidResourcesTest extends ResourceTestBase {
private static final PathFragment DEFAULT_RESOURCE_ROOT = PathFragment.create(RESOURCE_ROOT);
private static final ImmutableList<PathFragment> RESOURCES_ROOTS =
ImmutableList.of(DEFAULT_RESOURCE_ROOT);
@Before
@Test
public void testGetResourceRootsNoResources() throws Exception {
assertThat(getResourceRoots()).isEmpty();
}
@Test
public void testGetResourceRootsInvalidResourceDirectory() throws Exception {
try {
getResourceRoots("is-this-drawable-or-values/foo.xml");
assertWithMessage("Expected exception not thrown!").fail();
} catch (RuleErrorException e) {
// expected
}
errorConsumer.assertAttributeError(
"resource_files", "is not in the expected resource directory structure");
}
@Test
public void testGetResourceRootsMultipleRoots() throws Exception {
try {
getResourceRoots("subdir/values/foo.xml", "otherdir/values/bar.xml");
assertWithMessage("Expected exception not thrown!").fail();
} catch (RuleErrorException e) {
// expected
}
errorConsumer.assertAttributeError(
"resource_files", "All resources must share a common directory");
}
@Test
public void testGetResourceRoots() throws Exception {
assertThat(getResourceRoots("values-hdpi/foo.xml", "values-mdpi/bar.xml"))
.isEqualTo(RESOURCES_ROOTS);
}
@Test
public void testGetResourceRootsCommonSubdirectory() throws Exception {
assertThat(getResourceRoots("subdir/values-hdpi/foo.xml", "subdir/values-mdpi/bar.xml"))
.containsExactly(DEFAULT_RESOURCE_ROOT.getRelative("subdir"));
}
private ImmutableList<PathFragment> getResourceRoots(String... pathResourceStrings)
throws Exception {
return getResourceRoots(getResources(pathResourceStrings));
}
private ImmutableList<PathFragment> getResourceRoots(ImmutableList<Artifact> artifacts)
throws Exception {
return AndroidResources.getResourceRoots(errorConsumer, artifacts, "resource_files");
}
@Test
public void testFilterEmpty() throws Exception {
assertFilter(ImmutableList.of(), ImmutableList.of());
}
@Test
public void testFilterNoop() throws Exception {
ImmutableList<Artifact> resources = getResources("values-en/foo.xml", "values-es/bar.xml");
assertFilter(resources, resources);
}
@Test
public void testFilterToEmpty() throws Exception {
assertFilter(getResources("values-en/foo.xml", "values-es/bar.xml"), ImmutableList.of());
}
@Test
public void testPartiallyFilter() throws Exception {
Artifact keptResource = getResource("values-en/foo.xml");
assertFilter(
ImmutableList.of(keptResource, getResource("values-es/bar.xml")),
ImmutableList.of(keptResource));
}
@Test
public void testFilterIsDependency() throws Exception {
Artifact keptResource = getResource("values-en/foo.xml");
assertFilter(
ImmutableList.of(keptResource, getResource("drawable/bar.png")),
ImmutableList.of(keptResource),
/* isDependency = */ true);
}
@Test
public void testFilterValidatedNoop() throws Exception {
ImmutableList<Artifact> resources = getResources("values-en/foo.xml", "values-es/bar.xml");
assertFilterValidated(resources, resources);
}
@Test
public void testFilterValidated() throws Exception {
Artifact keptResource = getResource("values-en/foo.xml");
assertFilterValidated(
ImmutableList.of(keptResource, getResource("drawable/bar.png")),
ImmutableList.of(keptResource));
}
private void assertFilterValidated(
ImmutableList<Artifact> unfilteredResources, ImmutableList<Artifact> filteredResources)
throws Exception {
RuleContext ruleContext = getRuleContext();
ValidatedAndroidResources unfiltered =
new AndroidResources(unfilteredResources, getResourceRoots(unfilteredResources))
.process(ruleContext, getManifest(), /* neverlink = */ false);
Optional<? extends AndroidResources> maybeFiltered =
assertFilter(unfiltered, filteredResources, /* isDependency = */ true);
if (maybeFiltered.isPresent()) {
AndroidResources filtered = maybeFiltered.get();
assertThat(filtered instanceof ValidatedAndroidResources).isTrue();
ValidatedAndroidResources validated = (ValidatedAndroidResources) filtered;
// Validate fields related to validation are unchanged
assertThat(validated.getRTxt()).isEqualTo(unfiltered.getRTxt());
assertThat(validated.getAapt2RTxt()).isEqualTo(unfiltered.getAapt2RTxt());
}
}
private void assertFilter(
ImmutableList<Artifact> unfilteredResources, ImmutableList<Artifact> filteredResources)
throws Exception {
assertFilter(unfilteredResources, filteredResources, /* isDependency = */ false);
}
private void assertFilter(
ImmutableList<Artifact> unfilteredResources,
ImmutableList<Artifact> filteredResources,
boolean isDependency)
throws Exception {
AndroidResources unfiltered =
new AndroidResources(unfilteredResources, getResourceRoots(unfilteredResources));
assertFilter(unfiltered, filteredResources, isDependency);
}
private Optional<? extends AndroidResources> assertFilter(
AndroidResources unfiltered, ImmutableList<Artifact> filteredResources, boolean isDependency)
throws Exception {
ImmutableList.Builder<Artifact> filteredDepsBuilder = ImmutableList.builder();
ResourceFilter fakeFilter =
ResourceFilter.of(ImmutableSet.copyOf(filteredResources), filteredDepsBuilder::add);
Optional<? extends AndroidResources> filtered =
unfiltered.maybeFilter(errorConsumer, fakeFilter, isDependency);
if (filteredResources.equals(unfiltered.getResources())) {
// We expect filtering to have been a no-op
assertThat(filtered.isPresent()).isFalse();
} else {
// The resources and their roots should be filtered
assertThat(filtered.get().getResources())
.containsExactlyElementsIn(filteredResources)
.inOrder();
assertThat(filtered.get().getResourceRoots())
.containsExactlyElementsIn(getResourceRoots(filteredResources))
.inOrder();
}
if (!isDependency) {
// The filter should not record any filtered dependencies
assertThat(filteredDepsBuilder.build()).isEmpty();
} else {
// The filtered dependencies should be exactly the list of filtered resources
assertThat(unfiltered.getResources())
.containsExactlyElementsIn(
Iterables.concat(filteredDepsBuilder.build(), filteredResources));
}
return filtered;
}
@Test
public void testParseNoCompile() throws Exception {
useConfiguration("--android_aapt=aapt");
RuleContext ruleContext = getRuleContext();
ParsedAndroidResources parsed = assertParse(ruleContext, /* enableDataBinding = */ true);
// Since we are not using aapt2, there should be no compiled symbols
assertThat(parsed.getCompiledSymbols()).isNull();
// The parse action should take resources in and output symbols
assertActionArtifacts(
ruleContext,
/* inputs = */ parsed.getResources(),
/* outputs = */ ImmutableList.of(parsed.getSymbols()));
}
@Test
public void testParseAndCompile() throws Exception {
mockAndroidSdkWithAapt2();
useConfiguration("--android_sdk=//sdk:sdk", "--android_aapt=aapt2");
RuleContext ruleContext = getRuleContext();
ParsedAndroidResources parsed = assertParse(ruleContext);
assertThat(parsed.getCompiledSymbols()).isNotNull();
// The parse action should take resources in and output symbols
assertActionArtifacts(
ruleContext,
/* inputs = */ parsed.getResources(),
/* outputs = */ ImmutableList.of(parsed.getSymbols()));
// Since there was no data binding, the compile action should just take in resources and output
// compiled symbols.
assertActionArtifacts(
ruleContext,
/* inputs = */ parsed.getResources(),
/* outputs = */ ImmutableList.of(parsed.getCompiledSymbols()));
}
@Test
public void testParseWithDataBinding() throws Exception {
mockAndroidSdkWithAapt2();
useConfiguration("--android_sdk=//sdk:sdk", "--android_aapt=aapt2");
RuleContext ruleContext = getRuleContext();
ParsedAndroidResources parsed = assertParse(ruleContext, /* enableDataBinding = */ true);
// The parse action should take resources and busybox artifacts in and output symbols
assertActionArtifacts(
ruleContext,
/* inputs = */ parsed.getResources(),
/* outputs = */ ImmutableList.of(parsed.getSymbols()));
// The compile action should take in resources and manifest in and output compiled symbols and
// an unused data binding zip.
assertActionArtifacts(
ruleContext,
/* inputs = */ ImmutableList.<Artifact>builder()
.addAll(parsed.getResources())
.add(parsed.getManifest())
.build(),
/* outputs = */ ImmutableList.of(
parsed.getCompiledSymbols(), DataBinding.getSuffixedInfoFile(ruleContext, "_unused")));
}
@Test
public void testMergeDataBinding() throws Exception {
useConfiguration("--android_aapt=aapt");
RuleContext ruleContext = getRuleContext();
ParsedAndroidResources parsed = assertParse(ruleContext);
MergedAndroidResources merged =
parsed.merge(
ruleContext,
ResourceDependencies.empty(),
/* enableDataBinding = */ true,
AndroidAaptVersion.chooseTargetAaptVersion(ruleContext));
// Besides processed manifest, inherited values should be equal
assertThat(parsed).isEqualTo(new ParsedAndroidResources(merged, parsed.getStampedManifest()));
// There should be a new processed manifest
assertThat(merged.getManifest()).isNotEqualTo(parsed.getManifest());
assertThat(merged.getDataBindingInfoZip()).isNotNull();
assertActionArtifacts(
ruleContext,
/* inputs = */ ImmutableList.<Artifact>builder()
.addAll(merged.getResources())
.add(merged.getSymbols())
.add(parsed.getManifest())
.build(),
/* outputs = */ ImmutableList.of(
merged.getMergedResources(),
merged.getClassJar(),
merged.getDataBindingInfoZip(),
merged.getManifest()));
}
@Test
public void testMergeCompiled() throws Exception {
mockAndroidSdkWithAapt2();
useConfiguration(
"--android_sdk=//sdk:sdk", "--android_aapt=aapt2", "--experimental_skip_parsing_action");
RuleContext ruleContext = getRuleContext();
ParsedAndroidResources parsed = assertParse(ruleContext);
MergedAndroidResources merged =
parsed.merge(
ruleContext,
ResourceDependencies.fromRuleDeps(ruleContext, /* neverlink = */ false),
DataBinding.isEnabled(ruleContext),
AndroidAaptVersion.chooseTargetAaptVersion(ruleContext));
// Besides processed manifest, inherited values should be equal
assertThat(parsed).isEqualTo(new ParsedAndroidResources(merged, parsed.getStampedManifest()));
// There should be a new processed manifest
assertThat(merged.getManifest()).isNotEqualTo(parsed.getManifest());
assertThat(merged.getDataBindingInfoZip()).isNull();
assertThat(merged.getCompiledSymbols()).isNotNull();
// We use the compiled symbols file to build the resource class jar
assertActionArtifacts(
ruleContext,
/* inputs = */ ImmutableList.<Artifact>builder()
.addAll(merged.getResources())
.add(merged.getCompiledSymbols())
.add(parsed.getManifest())
.build(),
/* outputs = */ ImmutableList.of(merged.getClassJar(), merged.getManifest()));
// The old symbols file is still needed to build the merged resources zip
assertActionArtifacts(
ruleContext,
/* inputs = */ ImmutableList.<Artifact>builder()
.addAll(merged.getResources())
.add(merged.getSymbols())
.add(parsed.getManifest())
.build(),
/* outputs = */ ImmutableList.of(merged.getMergedResources()));
}
@Test
public void testValidateAapt() throws Exception {
useConfiguration("--android_aapt=aapt");
RuleContext ruleContext = getRuleContext();
MergedAndroidResources merged = makeMergedResources(ruleContext);
ValidatedAndroidResources validated =
merged.validate(ruleContext, AndroidAaptVersion.chooseTargetAaptVersion(ruleContext));
// Inherited values should be equal
assertThat(merged).isEqualTo(new MergedAndroidResources(validated));
// aapt artifacts should be generated
assertActionArtifacts(
ruleContext,
/* inputs = */ ImmutableList.of(validated.getMergedResources(), validated.getManifest()),
/* outputs = */ ImmutableList.of(
validated.getRTxt(), validated.getJavaSourceJar(), validated.getApk()));
// aapt2 artifacts should not be generated
assertThat(validated.getCompiledSymbols()).isNull();
assertThat(validated.getAapt2RTxt()).isNull();
assertThat(validated.getAapt2SourceJar()).isNull();
assertThat(validated.getStaticLibrary()).isNull();
}
@Test
public void testValidateAapt2() throws Exception {
mockAndroidSdkWithAapt2();
useConfiguration("--android_sdk=//sdk:sdk", "--android_aapt=aapt2");
RuleContext ruleContext = getRuleContext();
MergedAndroidResources merged = makeMergedResources(ruleContext);
ValidatedAndroidResources validated =
merged.validate(ruleContext, AndroidAaptVersion.chooseTargetAaptVersion(ruleContext));
// Inherited values should be equal
assertThat(merged).isEqualTo(new MergedAndroidResources(validated));
// aapt artifacts should be generated
assertActionArtifacts(
ruleContext,
/* inputs = */ ImmutableList.of(validated.getMergedResources(), validated.getManifest()),
/* outputs = */ ImmutableList.of(
validated.getRTxt(), validated.getJavaSourceJar(), validated.getApk()));
// aapt2 artifacts should be recorded
assertThat(validated.getCompiledSymbols()).isNotNull();
assertThat(validated.getAapt2RTxt()).isNotNull();
assertThat(validated.getAapt2SourceJar()).isNotNull();
assertThat(validated.getStaticLibrary()).isNotNull();
// Compile the resources into compiled symbols files
assertActionArtifacts(
ruleContext,
/* inputs = */ validated.getResources(),
/* outputs = */ ImmutableList.of(validated.getCompiledSymbols()));
// Use the compiled symbols and manifest to build aapt2 packaging outputs
assertActionArtifacts(
ruleContext,
/* inputs = */ ImmutableList.of(validated.getCompiledSymbols(), validated.getManifest()),
/* outputs = */ ImmutableList.of(
validated.getAapt2RTxt(), validated.getAapt2SourceJar(), validated.getStaticLibrary()));
}
@Test
public void testGenerateRClass() throws Exception {
RuleContext ruleContext = getRuleContext();
Artifact rTxt = ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_R_TXT);
ProcessedAndroidManifest manifest = getManifest();
ProcessedAndroidData processedData =
ProcessedAndroidData.of(
makeParsedResources(ruleContext),
AndroidAssets.from(ruleContext).process(ruleContext, /* neverlink = */ false),
manifest,
rTxt,
ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_JAVA_SOURCE_JAR),
ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_RESOURCES_APK),
/* dataBindingInfoZip = */ null,
ResourceDependencies.fromRuleDeps(ruleContext, /* neverlink = */ false),
null,
null);
ValidatedAndroidData validated =
processedData
.generateRClass(ruleContext, AndroidAaptVersion.chooseTargetAaptVersion(ruleContext))
.getValidatedResources();
// An action to generate the R.class file should be registered.
assertActionArtifacts(
ruleContext,
/* inputs = */ ImmutableList.of(rTxt, manifest.getManifest()),
/* outputs = */ ImmutableList.of(validated.getJavaClassJar()));
}
@Test
public void testProcessBinaryDataGeneratesProguardOutput() throws Exception {
RuleContext ruleContext = getRuleContext("android_binary", "manifest='AndroidManifest.xml',");
ResourceApk resourceApk =
ProcessedAndroidData.processBinaryDataFrom(
ruleContext, getManifest(), false, ImmutableMap.of(), AndroidAaptVersion.AUTO)
.generateRClass(ruleContext, AndroidAaptVersion.AUTO);
assertThat(resourceApk.getResourceProguardConfig()).isNotNull();
assertThat(resourceApk.getMainDexProguardConfig()).isNotNull();
}
/**
* Validates that a parse action was invoked correctly. Returns the {@link ParsedAndroidResources}
* for further validation.
*/
private ParsedAndroidResources assertParse(RuleContext ruleContext) throws Exception {
return assertParse(ruleContext, /* enableDataBinding = */ false);
}
private ParsedAndroidResources assertParse(RuleContext ruleContext, boolean enableDataBinding)
throws Exception {
ImmutableList<Artifact> resources = getResources("values-en/foo.xml", "drawable-hdpi/bar.png");
AndroidResources raw =
new AndroidResources(
resources, AndroidResources.getResourceRoots(ruleContext, resources, "resource_files"));
StampedAndroidManifest manifest = getManifest();
ParsedAndroidResources parsed =
raw.parse(
ruleContext,
manifest,
enableDataBinding,
AndroidAaptVersion.chooseTargetAaptVersion(ruleContext));
// Inherited values should be equal
assertThat(raw).isEqualTo(new AndroidResources(parsed));
// Label should be set from RuleContext
assertThat(parsed.getLabel()).isEqualTo(ruleContext.getLabel());
return parsed;
}
private MergedAndroidResources makeMergedResources(RuleContext ruleContext)
throws RuleErrorException, InterruptedException {
return makeParsedResources(ruleContext)
.merge(
ruleContext,
ResourceDependencies.fromRuleDeps(ruleContext, /* neverlink = */ false),
DataBinding.isEnabled(ruleContext),
AndroidAaptVersion.chooseTargetAaptVersion(ruleContext));
}
private ParsedAndroidResources makeParsedResources(RuleContext ruleContext)
throws RuleErrorException, InterruptedException {
return makeParsedResources(ruleContext, /* enableDataBinding = */ false);
}
private ParsedAndroidResources makeParsedResources(
RuleContext ruleContext, boolean enableDataBinding)
throws RuleErrorException, InterruptedException {
ImmutableList<Artifact> resources = getResources("values-en/foo.xml", "drawable-hdpi/bar.png");
return new AndroidResources(
resources, AndroidResources.getResourceRoots(ruleContext, resources, "resource_files"))
.parse(
ruleContext,
getManifest(),
enableDataBinding,
AndroidAaptVersion.chooseTargetAaptVersion(ruleContext));
}
private ProcessedAndroidManifest getManifest() {
return new ProcessedAndroidManifest(
getResource("some/path/AndroidManifest.xml"), "some.java.pkg", /* exported = */ true);
}
/** Gets a dummy rule context object by creating a dummy target. */
private RuleContext getRuleContext() throws Exception {
return getRuleContext("android_library");
}
/** Gets a dummy rule context object by creating a dummy target. */
private RuleContext getRuleContext(String kind, String... additionalLines) throws Exception {
ConfiguredTarget target =
scratchConfiguredTarget(
"java/foo",
"target",
ImmutableList.<String>builder()
.add(kind + "(name = 'target',")
.add(additionalLines)
.add(")")
.build()
.toArray(new String[0]));
return getRuleContextForActionTesting(target);
}
}