// 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.cmdline.Label; 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.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 getResourceContainers(ImmutableList... resources) throws Exception { NestedSetBuilder builder = NestedSetBuilder.naiveLinkOrder(); for (ImmutableList resourceList : resources) { builder.add(getResourceContainer(resourceList)); } return builder.build(); } private ResourceContainer getResourceContainer(ImmutableList resources) throws Exception { // Get dummy objects for irrelevant values required by the builder. Artifact manifest = getResource("manifest"); // Include a hashCode of the resources in the label. A hack in ResourceContainer currently means // that class has a limited hashCode method that doesn't take resources into account. // TODO(bazel-team): Remove this hack once that one no longer exists. Label label = Label.create( manifest.getOwnerLabel().getPackageName(), "resourceContainer_" + resources.hashCode()); return ResourceContainer.builder() .setResources(resources) .setResourcesRoots( LocalResourceContainer.getResourceRoots(errorConsumer, resources, "resource_files")) .setLabel(label) .setManifestExported(false) .setManifest(manifest) .build(); } @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.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 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 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 resources) throws Exception { testFilter( resourceConfigurationFilters, densities, filterInAnalysis, resources, ImmutableList.of()); } private void testFilter( String resourceConfigurationFilters, String densities, boolean filterInAnalysis, List resourcesToKeep, List resourcesToDiscard) throws Exception { List unexpectedResources = new ArrayList<>(); for (String resource : resourcesToDiscard) { unexpectedResources.add(getResource(resource)); } List expectedResources = new ArrayList<>(); for (String resource : resourcesToKeep) { expectedResources.add(getResource(resource)); } ImmutableList allArtifacts = ImmutableList.copyOf(Iterables.concat(expectedResources, unexpectedResources)); ResourceFilterFactory resourceFilterFactory = makeResourceFilter(resourceConfigurationFilters, densities, filterInAnalysis); ImmutableList 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"); LocalResourceContainer localResources = LocalResourceContainer.forResources( errorConsumer, ImmutableList.of(localResourceToKeep, localResourceToDiscard)); ResourceDependencies resourceDependencies = ResourceDependencies.empty() .withResources( getResourceContainers( ImmutableList.of(transitiveResourceToDiscard), ImmutableList.of(transitiveResourceToKeep)), getResourceContainers( ImmutableList.of(directResourceToDiscard), ImmutableList.of(directResourceToKeep)), new NestedSetBuilder(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.filter(errorConsumer, filter).getResources()) .containsExactly(localResourceToKeep); ResourceDependencies filteredResourceDeps = resourceDependencies.filter(filter); // TODO: Remove - assert was same order before assertThat(resourceDependencies.getTransitiveResources()) .containsAllOf(directResourceToKeep, transitiveResourceToKeep) .inOrder(); assertThat(filteredResourceDeps.getTransitiveResources()) .containsExactly(directResourceToKeep, transitiveResourceToKeep) .inOrder(); List directContainers = filteredResourceDeps.getDirectResourceContainers().toList(); assertThat(directContainers).hasSize(2); ResourceContainer directToDiscard = directContainers.get(0); assertThat(directToDiscard.getResources()).isEmpty(); assertThat(directToDiscard.getResourcesRoots()).isEmpty(); ResourceContainer directToKeep = directContainers.get(1); assertThat(directToKeep.getResources()).containsExactly(directResourceToKeep); assertThat(directToKeep.getResourcesRoots()) .containsExactly( directResourceToKeep.getExecPath().getParentDirectory().getParentDirectory()); List transitiveContainers = filteredResourceDeps.getTransitiveResourceContainers().toList(); assertThat(transitiveContainers).hasSize(2); ResourceContainer transitiveToDiscard = transitiveContainers.get(0); assertThat(transitiveToDiscard.getResources()).isEmpty(); assertThat(transitiveToDiscard.getResourcesRoots()).isEmpty(); ResourceContainer transitiveToKeep = transitiveContainers.get(1); assertThat(transitiveToKeep.getResources()).containsExactly(transitiveResourceToKeep); assertThat(transitiveToKeep.getResourcesRoots()) .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 resourceConfigurationFilters, ImmutableList densities, boolean filterInAnalysis) { return ResourceFilterFactory.from( filterInAnalysis, getAttributeMap(resourceConfigurationFilters, densities)); } private AttributeMap getAttributeMap( ImmutableList resourceConfigurationFilters, ImmutableList densities) { return FakeAttributeMapper.builder() .withStringList( ResourceFilterFactory.RESOURCE_CONFIGURATION_FILTERS_NAME, resourceConfigurationFilters) .withStringList(ResourceFilterFactory.DENSITIES_NAME, densities) .build(); } private ImmutableList doFilter( ResourceFilterFactory resourceFilterFactory, ImmutableList artifacts) throws RuleErrorException { LocalResourceContainer localResourceContainer = LocalResourceContainer.forResources(errorConsumer, artifacts); ResourceDependencies resourceDeps = ResourceDependencies.empty(); ResourceFilter filter = resourceFilterFactory.getResourceFilter( errorConsumer, resourceDeps, localResourceContainer); assertThat(resourceDeps.filter(filter)).isSameAs(resourceDeps); return localResourceContainer.filter(errorConsumer, filter).getResources(); } }