blob: d09214c3e0ecf347a0845fb14c46279db8f31a42 [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 com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.truth.BooleanSubject;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.packages.AttributeMap;
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.testutil.FakeAttributeMapper;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests {@link ResourceFilterFactory}. */
// TODO(asteinb): Test behavior not already covered in this test, and, when practical, move unit
// tests currently located in {@link AndroidBinaryTest} to this class instead.
@RunWith(JUnit4.class)
public class ResourceFilterFactoryTest extends ResourceTestBase {
private NestedSet<ValidatedAndroidResources> getResources(ImmutableList<Artifact>... resources)
throws Exception {
NestedSetBuilder<ValidatedAndroidResources> builder = NestedSetBuilder.naiveLinkOrder();
for (ImmutableList<Artifact> resourceList : resources) {
builder.add(getResources(resourceList));
}
return builder.build();
}
private ValidatedAndroidResources getResources(ImmutableList<Artifact> resources)
throws Exception {
return makeValidatedResourcesFor(
resources,
/* includeAapt2Outs = */ true,
new ProcessedAndroidManifest(getOutput("manifest"), "com.some.pkg", /* exported = */ false),
ResourceDependencies.empty());
}
@Test
public void testFilterInExecution() throws Exception {
testNoopFilter(
"en",
"hdpi",
/* filterInAnalysis = */ false,
ImmutableList.of(
"values-en/foo.xml", "values/foo.xml", "values-hdpi/foo.png", "values-ldpi/foo.png"));
}
@Test
public void testFilterEmpty() throws Exception {
testNoopFilter("", "", /* filterInAnalysis = */ true, ImmutableList.<String>of());
}
@Test
public void testFilterDefaultAndNonDefault() throws Exception {
testNoopFilter(
"en",
"xhdpi,xxhdpi",
/* filterInAnalysis = */ true,
ImmutableList.of("drawable/ic_clear.xml", "drawable-v21/ic_clear.xml"));
}
/**
* Tests that version qualifiers are ignored for both resource qualifier and density filtering.
*/
@Test
public void testFilterVersionIgnored() throws Exception {
testNoopFilter(
"v4",
"hdpi",
/* filterInAnalysis = */ true,
ImmutableList.of("drawable-hdpi-v4/foo.png", "drawable-hdpi-v11/foo.png"));
}
@Test
public void testFilterByDensityPersistsOrdering() throws Exception {
testFilter(
"",
"hdpi,ldpi",
/* filterInAnalysis = */ true,
// If we add resources to the output list in density order, these resources will be
// rearranged.
ImmutableList.of(
"drawable-hdpi/foo.png",
"drawable-ldpi/foo.png",
"drawable-ldpi/bar.png",
"drawable-hdpi/bar.png"),
// Filter out some resources to make sure the original list isn't just returned because the
// filtering was a no-op.
ImmutableList.of("drawable-mdpi/foo.png", "drawable-mdpi/bar.png"));
}
/** Tests handling of Aapt's old region format */
@Test
public void testFilterOldLanguageFormat() throws Exception {
testFilter(
"en_US",
"",
/* filterInAnalysis = */ true,
ImmutableList.of("values-en/foo.xml", "values-en-rUS/foo.xml"),
ImmutableList.of("values-fr/foo.xml", "values-en-rCA/foo.xml"));
errorConsumer.assertNoAttributeWarnings(
ResourceFilterFactory.RESOURCE_CONFIGURATION_FILTERS_NAME);
}
@Test
public void testFilterOldLanguageFormatWithAdditionalQualifiers() throws Exception {
testFilter(
"en_US-ldrtl",
"",
/* filterInAnalysis = */ true,
ImmutableList.of("values-en/foo.xml", "values-en-rUS/foo.xml"),
ImmutableList.of("values-fr/foo.xml", "values-en-rCA/foo.xml"));
errorConsumer.assertNoAttributeWarnings(
ResourceFilterFactory.RESOURCE_CONFIGURATION_FILTERS_NAME);
}
@Test
public void testFilterOldLanguageFormatWithMcc() throws Exception {
testFilter(
"mcc111-en_US",
"",
/* filterInAnalysis = */ true,
ImmutableList.of("values-en/foo.xml", "values-en-rUS/foo.xml"),
ImmutableList.of("values-fr/foo.xml", "values-en-rCA/foo.xml"));
errorConsumer.assertNoAttributeWarnings(
ResourceFilterFactory.RESOURCE_CONFIGURATION_FILTERS_NAME);
}
@Test
public void testFilterOldLanguageFormatWithMccAndMnc() throws Exception {
testFilter(
"mcc111-mnc111-en_US",
"",
/* filterInAnalysis = */ true,
ImmutableList.of("values-en/foo.xml", "values-en-rUS/foo.xml"),
ImmutableList.of("values-fr/foo.xml", "values-en-rCA/foo.xml"));
errorConsumer.assertNoAttributeWarnings(
ResourceFilterFactory.RESOURCE_CONFIGURATION_FILTERS_NAME);
}
@Test
public void testFilterSerbianLatinCharacters() throws Exception {
testFilter(
"sr-Latn,sr-rLatn,sr_Latn,b+sr+Latn",
"",
/* filterInAnalysis = */ true,
ImmutableList.of(
"values-sr/foo.xml",
"values-b+sr+Latn/foo.xml",
"values-sr-Latn/foo.xml",
"values-sr-rLatn/foo.xml"),
// Serbian in explicitly Cyrillic characters should be filtered out.
ImmutableList.of("values-b+sr+Cyrl/foo.xml"));
errorConsumer.assertNoAttributeWarnings(
ResourceFilterFactory.RESOURCE_CONFIGURATION_FILTERS_NAME);
}
@Test
public void testFilterSerbianCharactersNotSpecified() throws Exception {
testNoopFilter(
"sr",
"",
/* filterInAnalysis = */ true,
ImmutableList.of(
"values-sr/foo.xml",
"values-b+sr+Latn/foo.xml",
"values-b+sr+Cyrl/foo.xml",
"values-sr-Latn/foo.xml",
"values-sr-rLatn/foo.xml"));
errorConsumer.assertNoAttributeWarnings(
ResourceFilterFactory.RESOURCE_CONFIGURATION_FILTERS_NAME);
}
@Test
public void testFilterSpanishLatinAmericaAndCaribbean() throws Exception {
testFilter(
"es-419,es_419,b+es+419",
"",
/* filterInAnalysis = */ true,
ImmutableList.of("values-es/foo.xml", "values-b+es+419/foo.xml", "values-es-419/foo.xml"),
// Spanish with another region specified should be filtered out.
ImmutableList.of("values-es-rUS/foo.xml"));
errorConsumer.assertNoAttributeWarnings(
ResourceFilterFactory.RESOURCE_CONFIGURATION_FILTERS_NAME);
}
@Test
public void testFilterSpanishRegionNotSpecified() throws Exception {
testNoopFilter(
"es",
"",
/* filterInAnalysis = */ true,
ImmutableList.of(
"values-es/foo.xml",
"values-b+es+419/foo.xml",
"values-es-rUS/foo.xml",
"values-es-419/foo.xml"));
errorConsumer.assertNoAttributeWarnings(
ResourceFilterFactory.RESOURCE_CONFIGURATION_FILTERS_NAME);
}
@Test
public void testFilterDeprecatedQualifierFormatsRuleWarnings() throws RuleErrorException {
ImmutableList<Artifact> badResources =
getResources(
"values-es_US/foo.xml",
"drawable-sr-Latn/foo.xml",
"layout-es-419/foo.xml",
"values-mcc310-es_US/foo.xml",
"values-sr_rLatn/foo.xml",
"drawable-es_419/foo.xml");
ImmutableList<String> expectedWarnings =
ImmutableList.of(
"For resource folder drawable-sr-Latn, when referring to Serbian in Latin characters, "
+ "use of qualifier 'sr-Latn' is deprecated. Use 'b+sr+Latn' instead.",
"For resource folder layout-es-419, when referring to Spanish for Latin America and "
+ "the Caribbean, use of qualifier 'es-419' is deprecated. Use 'b+es+419' instead.",
"For resource folder values-es_US, when referring to locale qualifiers with regions, "
+ "use of qualifier 'es_US' is deprecated. Use 'es-rUS' instead.");
ResourceFilterFactory filter =
new ResourceFilterFactory(
ImmutableList.of("en"), ImmutableList.of(), /* filterInAnalysis = */ true);
doFilter(filter, badResources);
assertThat(errorConsumer.getAndClearRuleWarnings()).containsExactlyElementsIn(expectedWarnings);
errorConsumer.assertNoAttributeWarnings(
ResourceFilterFactory.RESOURCE_CONFIGURATION_FILTERS_NAME);
// Filtering again with this filter should not produce additional warnings
doFilter(filter, badResources);
errorConsumer.assertNoRuleWarnings();
errorConsumer.assertNoAttributeWarnings(
ResourceFilterFactory.RESOURCE_CONFIGURATION_FILTERS_NAME);
// Filtering with a new filter should produce warnings again, since it is working on a different
// target
filter =
new ResourceFilterFactory(
ImmutableList.of("en"), ImmutableList.of(), /* filterInAnalysis = */ true);
doFilter(filter, badResources);
assertThat(errorConsumer.getAndClearRuleWarnings()).containsExactlyElementsIn(expectedWarnings);
errorConsumer.assertNoAttributeWarnings(
ResourceFilterFactory.RESOURCE_CONFIGURATION_FILTERS_NAME);
}
@Test
public void testFilterResourceConflict() throws Exception {
testNoopFilter(
"en",
"hdpi",
/* filterInAnalysis = */ true,
ImmutableList.of(
"first-subdir/res/drawable-en-hdpi/foo.png",
"second-subdir/res/drawable-en-hdpi/foo.png"));
}
private void testNoopFilter(
String resourceConfigurationFilters,
String densities,
boolean filterInAnalysis,
List<String> resources)
throws Exception {
testFilter(
resourceConfigurationFilters,
densities,
filterInAnalysis,
resources,
ImmutableList.<String>of());
}
private void testFilter(
String resourceConfigurationFilters,
String densities,
boolean filterInAnalysis,
List<String> resourcesToKeep,
List<String> resourcesToDiscard)
throws Exception {
List<Artifact> unexpectedResources = new ArrayList<>();
for (String resource : resourcesToDiscard) {
unexpectedResources.add(getResource(resource));
}
List<Artifact> expectedResources = new ArrayList<>();
for (String resource : resourcesToKeep) {
expectedResources.add(getResource(resource));
}
ImmutableList<Artifact> allArtifacts =
ImmutableList.copyOf(Iterables.concat(expectedResources, unexpectedResources));
ResourceFilterFactory resourceFilterFactory =
makeResourceFilter(resourceConfigurationFilters, densities, filterInAnalysis);
ImmutableList<Artifact> filtered = doFilter(resourceFilterFactory, allArtifacts);
assertThat(filtered).containsExactlyElementsIn(expectedResources).inOrder();
// Only dependencies need to be tracked for ignoring in execution
assertThat(resourceFilterFactory.getResourcesToIgnoreInExecution()).isEmpty();
}
@Test
public void testFilterLocalAndTransitive() throws Exception {
Artifact localResourceToKeep = getResource("drawable-en-hdpi/local.png");
Artifact localResourceToDiscard = getResource("drawable-en-ldpi/local.png");
// These resources go in different ResourceContainers to ensure we are filter across all
// resources.
Artifact directResourceToKeep = getResource("direct/drawable-en-hdpi/direct.png");
Artifact directResourceToDiscard = getResource("direct/drawable-en-ldpi/direct.png");
Artifact transitiveResourceToKeep = getResource("transitive/drawable-en-hdpi/transitive.png");
Artifact transitiveResourceToDiscard =
getResource("transitive/drawable-en-ldpi/transitive.png");
AndroidResources localResources =
AndroidResources.forResources(
errorConsumer,
ImmutableList.of(localResourceToKeep, localResourceToDiscard),
"resource_files");
ResourceDependencies resourceDependencies =
ResourceDependencies.empty()
.withResources(
getResources(
ImmutableList.of(transitiveResourceToDiscard),
ImmutableList.of(transitiveResourceToKeep)),
getResources(
ImmutableList.of(directResourceToDiscard),
ImmutableList.of(directResourceToKeep)),
new NestedSetBuilder<Artifact>(Order.NAIVE_LINK_ORDER)
.add(directResourceToDiscard)
.add(directResourceToKeep)
.addTransitive(
NestedSetBuilder.create(
Order.NAIVE_LINK_ORDER,
transitiveResourceToDiscard,
transitiveResourceToKeep))
.build());
ResourceFilterFactory resourceFilterFactory =
makeResourceFilter("en", "hdpi", /* filterInAnalysis = */ true);
ResourceFilter filter =
resourceFilterFactory.getResourceFilter(
errorConsumer, resourceDependencies, localResources);
assertThat(localResources.filterLocalResources(errorConsumer, filter).getResources())
.containsExactly(localResourceToKeep);
ResourceDependencies filteredResourceDeps = resourceDependencies.filter(errorConsumer, filter);
// TODO: Remove - assert was same order before
assertThat(resourceDependencies.getTransitiveResources())
.containsAllOf(directResourceToKeep, transitiveResourceToKeep)
.inOrder();
assertThat(filteredResourceDeps.getTransitiveResources())
.containsExactly(directResourceToKeep, transitiveResourceToKeep)
.inOrder();
List<ValidatedAndroidResources> directContainers =
filteredResourceDeps.getDirectResourceContainers().toList();
assertThat(directContainers).hasSize(2);
ValidatedAndroidResources directToDiscard = directContainers.get(0);
assertThat(directToDiscard.getResources()).isEmpty();
assertThat(directToDiscard.getResourceRoots()).isEmpty();
ValidatedAndroidResources directToKeep = directContainers.get(1);
assertThat(directToKeep.getResources()).containsExactly(directResourceToKeep);
assertThat(directToKeep.getResourceRoots())
.containsExactly(
directResourceToKeep.getExecPath().getParentDirectory().getParentDirectory());
List<ValidatedAndroidResources> transitiveContainers =
filteredResourceDeps.getTransitiveResourceContainers().toList();
assertThat(transitiveContainers).hasSize(2);
ValidatedAndroidResources transitiveToDiscard = transitiveContainers.get(0);
assertThat(transitiveToDiscard.getResources()).isEmpty();
assertThat(transitiveToDiscard.getResourceRoots()).isEmpty();
ValidatedAndroidResources transitiveToKeep = transitiveContainers.get(1);
assertThat(transitiveToKeep.getResources()).containsExactly(transitiveResourceToKeep);
assertThat(transitiveToKeep.getResourceRoots())
.containsExactly(
transitiveResourceToKeep.getExecPath().getParentDirectory().getParentDirectory());
// We tell the resource processing actions to ignore references to filtered resources from
// dependencies.
assertThat(resourceFilterFactory.getResourcesToIgnoreInExecution())
.containsExactly("drawable-en-ldpi/direct.png", "drawable-en-ldpi/transitive.png");
}
@Test
public void testIsPrefilteringFilterInExecution() throws Exception {
assertIsPrefiltering(/* filterInAnalysis = */ false, false);
}
@Test
public void testIsPrefilteringFilterInAnalysis() throws Exception {
assertIsPrefiltering(/* filterInAnalysis = */ true, true);
}
private void assertIsPrefiltering(boolean filterInAnalysis, boolean expectWhenNonEmpty)
throws Exception {
// Empty filters should never prefilter
assertIsPrefiltering(false, false, filterInAnalysis).isFalse();
// Prefiltering behavior should be the same regardless of which setting is set
assertIsPrefiltering(true, false, filterInAnalysis).isEqualTo(expectWhenNonEmpty);
assertIsPrefiltering(false, true, filterInAnalysis).isEqualTo(expectWhenNonEmpty);
assertIsPrefiltering(true, true, filterInAnalysis).isEqualTo(expectWhenNonEmpty);
}
private BooleanSubject assertIsPrefiltering(
boolean hasConfigurationFilters, boolean hasDensities, boolean filterInAnalysis)
throws Exception {
return assertThat(
makeResourceFilter(
hasConfigurationFilters ? "en" : "", hasDensities ? "hdpi" : "", filterInAnalysis)
.isPrefiltering());
}
private ResourceFilterFactory makeResourceFilter(
String resourceConfigurationFilters, String densities, boolean filterInAnalysis) {
return makeResourceFilter(
resourceConfigurationFilters.isEmpty()
? ImmutableList.of()
: ImmutableList.of(resourceConfigurationFilters),
densities.isEmpty() ? ImmutableList.of() : ImmutableList.of(densities),
filterInAnalysis);
}
private ResourceFilterFactory makeResourceFilter(
ImmutableList<String> resourceConfigurationFilters,
ImmutableList<String> densities,
boolean filterInAnalysis) {
return ResourceFilterFactory.fromAttrs(
filterInAnalysis ? AndroidAaptVersion.AAPT : AndroidAaptVersion.AAPT2,
getAttributeMap(resourceConfigurationFilters, densities));
}
private AttributeMap getAttributeMap(
ImmutableList<String> resourceConfigurationFilters, ImmutableList<String> densities) {
return FakeAttributeMapper.builder()
.withStringList(
ResourceFilterFactory.RESOURCE_CONFIGURATION_FILTERS_NAME, resourceConfigurationFilters)
.withStringList(ResourceFilterFactory.DENSITIES_NAME, densities)
.build();
}
private ImmutableList<Artifact> doFilter(
ResourceFilterFactory resourceFilterFactory, ImmutableList<Artifact> artifacts)
throws RuleErrorException {
AndroidResources localResources =
AndroidResources.forResources(errorConsumer, artifacts, "resource_files");
ResourceDependencies resourceDeps = ResourceDependencies.empty();
ResourceFilter filter =
resourceFilterFactory.getResourceFilter(errorConsumer, resourceDeps, localResources);
assertThat(resourceDeps.filter(errorConsumer, filter)).isSameAs(resourceDeps);
return localResources.filterLocalResources(errorConsumer, filter).getResources();
}
}