| // Copyright 2019 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.android; | |
| import static com.google.common.truth.Truth.assertAbout; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static com.google.devtools.build.android.ParsedAndroidDataBuilder.xml; | |
| import com.android.aapt.Resources.Attribute; | |
| import com.android.aapt.Resources.CompoundValue; | |
| import com.android.aapt.Resources.Value; | |
| import com.google.auto.value.AutoValue; | |
| import com.google.common.base.Joiner; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.jimfs.Jimfs; | |
| import com.google.common.truth.Subject; | |
| import com.google.devtools.build.android.resources.Visibility; | |
| import com.google.devtools.build.android.xml.AttrXmlResourceValue; | |
| import com.google.devtools.build.android.xml.AttrXmlResourceValue.ReferenceResourceXmlAttrValue; | |
| import com.google.devtools.build.android.xml.AttrXmlResourceValue.StringResourceXmlAttrValue; | |
| import java.nio.file.FileSystem; | |
| import java.nio.file.Path; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collection; | |
| import java.util.List; | |
| import java.util.Optional; | |
| import java.util.function.Function; | |
| import java.util.logging.Handler; | |
| import java.util.logging.Level; | |
| import java.util.logging.LogRecord; | |
| import java.util.logging.Logger; | |
| import org.junit.After; | |
| import org.junit.Before; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.Parameterized; | |
| import org.junit.runners.Parameterized.Parameter; | |
| import org.junit.runners.Parameterized.Parameters; | |
| /** Parameterized tests for {@link AndroidDataMerger} focusing on merging attribute resoutces. */ | |
| @RunWith(Parameterized.class) | |
| public final class AttributeResourcesAndroidDataMergerTest { | |
| /** For test data readability, represent strength as a 2-state enum rather than a boolean. */ | |
| enum Strength { | |
| STRONG, | |
| WEAK | |
| } | |
| @Parameters(name = "{index}: {0}") | |
| public static Collection<Object[]> data() { | |
| return Arrays.asList( | |
| new Object[][] { | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.STRONG, 1) | |
| .set2(Strength.STRONG, 0xFFFF) | |
| .setExpectedMergeConflict( | |
| ctx -> | |
| MergeConflict.of( | |
| ctx.fqnFactory.parse("attr/ambiguousName"), | |
| DataResourceXml.createWithNoNamespace( | |
| ctx.transitiveRoot1.resolve("res/values/attrs.xml"), | |
| AttrXmlResourceValue.fromFormatEntries( | |
| ReferenceResourceXmlAttrValue.asEntry())), | |
| DataResourceXml.createWithNoNamespace( | |
| ctx.transitiveRoot2.resolve("res/values/attrs.xml"), | |
| AttrXmlResourceValue.fromFormatEntries()))) | |
| .build() | |
| }, | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.WEAK, 1) | |
| .set2(Strength.WEAK, 0xFFFF) | |
| .setExpectedMergedAndroidData( | |
| ctx -> | |
| UnwrittenMergedAndroidData.of( | |
| ctx.primary.getManifest(), | |
| ParsedAndroidDataBuilder.empty(), | |
| ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory) | |
| .overwritable( | |
| xml("attr/ambiguousName") | |
| .root(ctx.primaryRoot) | |
| .source(ctx.transitiveAttr1.overwrite(ctx.transitiveAttr2)) | |
| .value( | |
| AttrXmlResourceValue.weakFromFormatEntries( | |
| ReferenceResourceXmlAttrValue.asEntry()))) | |
| .build())) | |
| .build() | |
| }, | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.WEAK, 0xFFFF) | |
| .set2(Strength.WEAK, 1) | |
| .setExpectedMergedAndroidData( | |
| ctx -> | |
| UnwrittenMergedAndroidData.of( | |
| ctx.primary.getManifest(), | |
| ParsedAndroidDataBuilder.empty(), | |
| ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory) | |
| .overwritable( | |
| xml("attr/ambiguousName") | |
| .root(ctx.primaryRoot) | |
| .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1)) | |
| .value( | |
| AttrXmlResourceValue.weakFromFormatEntries( | |
| ReferenceResourceXmlAttrValue.asEntry()))) | |
| .build())) | |
| .build() | |
| }, | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.STRONG, 1) | |
| .set2(Strength.STRONG, 1) | |
| .setExpectedMergedAndroidData( | |
| ctx -> | |
| UnwrittenMergedAndroidData.of( | |
| ctx.primary.getManifest(), | |
| ParsedAndroidDataBuilder.empty(), | |
| ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory) | |
| .overwritable( | |
| xml("attr/ambiguousName") | |
| .root(ctx.primaryRoot) | |
| .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1)) | |
| .value( | |
| AttrXmlResourceValue.fromFormatEntries( | |
| ReferenceResourceXmlAttrValue.asEntry()))) | |
| .build())) | |
| .build() | |
| }, | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.WEAK, 1) | |
| .set2(Strength.WEAK, 1) | |
| .setExpectedMergedAndroidData( | |
| ctx -> | |
| UnwrittenMergedAndroidData.of( | |
| ctx.primary.getManifest(), | |
| ParsedAndroidDataBuilder.empty(), | |
| ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory) | |
| .overwritable( | |
| xml("attr/ambiguousName") | |
| .root(ctx.primaryRoot) | |
| .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1)) | |
| .value( | |
| AttrXmlResourceValue.weakFromFormatEntries( | |
| ReferenceResourceXmlAttrValue.asEntry()))) | |
| .build())) | |
| .build() | |
| }, | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.STRONG, 1) | |
| .set2(Strength.WEAK, 0xFFFF) | |
| .setExpectedMergedAndroidData( | |
| ctx -> | |
| UnwrittenMergedAndroidData.of( | |
| ctx.primary.getManifest(), | |
| ParsedAndroidDataBuilder.empty(), | |
| ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory) | |
| .overwritable( | |
| xml("attr/ambiguousName") | |
| .root(ctx.primaryRoot) | |
| .source(ctx.transitiveAttr1.overwrite(ctx.transitiveAttr2)) | |
| .value( | |
| AttrXmlResourceValue.fromFormatEntries( | |
| ReferenceResourceXmlAttrValue.asEntry()))) | |
| .build())) | |
| .build() | |
| }, | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.WEAK, 0xFFFF) | |
| .set2(Strength.STRONG, 2) | |
| .setExpectedMergedAndroidData( | |
| ctx -> | |
| UnwrittenMergedAndroidData.of( | |
| ctx.primary.getManifest(), | |
| ParsedAndroidDataBuilder.empty(), | |
| ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory) | |
| .overwritable( | |
| xml("attr/ambiguousName") | |
| .root(ctx.primaryRoot) | |
| .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1)) | |
| .value( | |
| AttrXmlResourceValue.fromFormatEntries( | |
| StringResourceXmlAttrValue.asEntry()))) | |
| .build())) | |
| .build() | |
| }, | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.STRONG, 1) | |
| .set2(Strength.WEAK, 2) | |
| .setExpectedMergeConflict( | |
| ctx -> | |
| MergeConflict.of( | |
| ctx.fqnFactory.parse("attr/ambiguousName"), | |
| DataResourceXml.createWithNoNamespace( | |
| ctx.transitiveRoot1.resolve("res/values/attrs.xml"), | |
| AttrXmlResourceValue.fromFormatEntries( | |
| ReferenceResourceXmlAttrValue.asEntry())), | |
| DataResourceXml.createWithNoNamespace( | |
| ctx.transitiveRoot2.resolve("res/values/attrs.xml"), | |
| AttrXmlResourceValue.weakFromFormatEntries( | |
| StringResourceXmlAttrValue.asEntry())))) | |
| .build() | |
| }, | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.WEAK, 1) | |
| .set2(Strength.STRONG, 2) | |
| .setExpectedMergeConflict( | |
| ctx -> | |
| MergeConflict.of( | |
| ctx.fqnFactory.parse("attr/ambiguousName"), | |
| DataResourceXml.createWithNoNamespace( | |
| ctx.transitiveRoot1.resolve("res/values/attrs.xml"), | |
| AttrXmlResourceValue.weakFromFormatEntries( | |
| ReferenceResourceXmlAttrValue.asEntry())), | |
| DataResourceXml.createWithNoNamespace( | |
| ctx.transitiveRoot2.resolve("res/values/attrs.xml"), | |
| AttrXmlResourceValue.fromFormatEntries( | |
| StringResourceXmlAttrValue.asEntry())))) | |
| .build() | |
| }, | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.WEAK, 1) | |
| .set2(Strength.WEAK, 2) | |
| .setExpectedMergeConflict( | |
| ctx -> | |
| MergeConflict.of( | |
| ctx.fqnFactory.parse("attr/ambiguousName"), | |
| DataResourceXml.createWithNoNamespace( | |
| ctx.transitiveRoot1.resolve("res/values/attrs.xml"), | |
| AttrXmlResourceValue.weakFromFormatEntries( | |
| ReferenceResourceXmlAttrValue.asEntry())), | |
| DataResourceXml.createWithNoNamespace( | |
| ctx.transitiveRoot2.resolve("res/values/attrs.xml"), | |
| AttrXmlResourceValue.weakFromFormatEntries( | |
| StringResourceXmlAttrValue.asEntry())))) | |
| .build() | |
| }, | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.STRONG, 1) | |
| .set2(Strength.WEAK, 1) | |
| .setExpectedMergedAndroidData( | |
| ctx -> | |
| UnwrittenMergedAndroidData.of( | |
| ctx.primary.getManifest(), | |
| ParsedAndroidDataBuilder.empty(), | |
| ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory) | |
| .overwritable( | |
| xml("attr/ambiguousName") | |
| .root(ctx.primaryRoot) | |
| .source(ctx.transitiveAttr1.overwrite(ctx.transitiveAttr2)) | |
| .value( | |
| AttrXmlResourceValue.fromFormatEntries( | |
| ReferenceResourceXmlAttrValue.asEntry()))) | |
| .build())) | |
| .build() | |
| }, | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.WEAK, 1) | |
| .set2(Strength.STRONG, 1) | |
| .setExpectedMergedAndroidData( | |
| ctx -> | |
| UnwrittenMergedAndroidData.of( | |
| ctx.primary.getManifest(), | |
| ParsedAndroidDataBuilder.empty(), | |
| ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory) | |
| .overwritable( | |
| xml("attr/ambiguousName") | |
| .root(ctx.primaryRoot) | |
| .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1)) | |
| .value( | |
| AttrXmlResourceValue.fromFormatEntries( | |
| ReferenceResourceXmlAttrValue.asEntry()))) | |
| .build())) | |
| .build() | |
| }, | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.STRONG, 0xFFFF) | |
| .set2(Strength.STRONG, 0xFFFF) | |
| .setExpectedMergedAndroidData( | |
| ctx -> | |
| UnwrittenMergedAndroidData.of( | |
| ctx.primary.getManifest(), | |
| ParsedAndroidDataBuilder.empty(), | |
| ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory) | |
| .overwritable( | |
| xml("attr/ambiguousName") | |
| .root(ctx.primaryRoot) | |
| .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1)) | |
| .value(AttrXmlResourceValue.fromFormatEntries())) | |
| .build())) | |
| .build() | |
| }, | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.WEAK, 0xFFFF) | |
| .set2(Strength.WEAK, 0xFFFF) | |
| .setExpectedMergedAndroidData( | |
| ctx -> | |
| UnwrittenMergedAndroidData.of( | |
| ctx.primary.getManifest(), | |
| ParsedAndroidDataBuilder.empty(), | |
| ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory) | |
| .overwritable( | |
| xml("attr/ambiguousName") | |
| .root(ctx.primaryRoot) | |
| .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1)) | |
| .value(AttrXmlResourceValue.weakFromFormatEntries())) | |
| .build())) | |
| .build() | |
| }, | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.STRONG, 0xFFFF) | |
| .set2(Strength.WEAK, 0xFFFF) | |
| .setExpectedMergedAndroidData( | |
| ctx -> | |
| UnwrittenMergedAndroidData.of( | |
| ctx.primary.getManifest(), | |
| ParsedAndroidDataBuilder.empty(), | |
| ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory) | |
| .overwritable( | |
| xml("attr/ambiguousName") | |
| .root(ctx.primaryRoot) | |
| .source(ctx.transitiveAttr1.overwrite(ctx.transitiveAttr2)) | |
| .value(AttrXmlResourceValue.fromFormatEntries())) | |
| .build())) | |
| .build() | |
| }, | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.WEAK, 0xFFFF) | |
| .set2(Strength.STRONG, 0xFFFF) | |
| .setExpectedMergedAndroidData( | |
| ctx -> | |
| UnwrittenMergedAndroidData.of( | |
| ctx.primary.getManifest(), | |
| ParsedAndroidDataBuilder.empty(), | |
| ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory) | |
| .overwritable( | |
| xml("attr/ambiguousName") | |
| .root(ctx.primaryRoot) | |
| .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1)) | |
| .value(AttrXmlResourceValue.fromFormatEntries())) | |
| .build())) | |
| .build() | |
| }, | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.STRONG, 0xFFFF) | |
| .set2(Strength.WEAK, 1) | |
| .setExpectedMergeConflict( | |
| ctx -> | |
| MergeConflict.of( | |
| ctx.fqnFactory.parse("attr/ambiguousName"), | |
| DataResourceXml.createWithNoNamespace( | |
| ctx.transitiveRoot1.resolve("res/values/attrs.xml"), | |
| AttrXmlResourceValue.fromFormatEntries()), | |
| DataResourceXml.createWithNoNamespace( | |
| ctx.transitiveRoot2.resolve("res/values/attrs.xml"), | |
| AttrXmlResourceValue.weakFromFormatEntries( | |
| ReferenceResourceXmlAttrValue.asEntry())))) | |
| .build() | |
| }, | |
| { | |
| TestParameters.builder() | |
| .set1(Strength.WEAK, 1) | |
| .set2(Strength.STRONG, 0xFFFF) | |
| .setExpectedMergeConflict( | |
| ctx -> | |
| MergeConflict.of( | |
| ctx.fqnFactory.parse("attr/ambiguousName"), | |
| DataResourceXml.createWithNoNamespace( | |
| ctx.transitiveRoot1.resolve("res/values/attrs.xml"), | |
| AttrXmlResourceValue.weakFromFormatEntries( | |
| ReferenceResourceXmlAttrValue.asEntry())), | |
| DataResourceXml.createWithNoNamespace( | |
| ctx.transitiveRoot2.resolve("res/values/attrs.xml"), | |
| AttrXmlResourceValue.fromFormatEntries()))) | |
| .build() | |
| } | |
| }); | |
| } | |
| @Parameter public TestParameters testParameters; | |
| private FullyQualifiedName.Factory fqnFactory; | |
| private TestLoggingHandler loggingHandler; | |
| private Logger mergerLogger; | |
| private Path primaryRoot; | |
| private Path transitiveRoot1; | |
| private Path transitiveRoot2; | |
| private DataSource transitiveAttr1; | |
| private DataSource transitiveAttr2; | |
| private UnvalidatedAndroidData primary; | |
| @Before | |
| public void setUp() throws Exception { | |
| FileSystem fileSystem = Jimfs.newFileSystem(); | |
| fqnFactory = FullyQualifiedName.Factory.from(ImmutableList.of()); | |
| mergerLogger = Logger.getLogger(AndroidDataMerger.class.getCanonicalName()); | |
| loggingHandler = new TestLoggingHandler(); | |
| mergerLogger.addHandler(loggingHandler); | |
| primaryRoot = fileSystem.getPath("primary"); | |
| transitiveRoot1 = fileSystem.getPath("transitive1"); | |
| transitiveRoot2 = fileSystem.getPath("transitive2"); | |
| transitiveAttr1 = | |
| DataSource.of( | |
| DependencyInfo.UNKNOWN, transitiveRoot1.resolve("res").resolve("values/attrs.xml")); | |
| transitiveAttr2 = | |
| DataSource.of( | |
| DependencyInfo.UNKNOWN, transitiveRoot2.resolve("res").resolve("values/attrs.xml")); | |
| primary = | |
| AndroidDataBuilder.of(primaryRoot) | |
| .createManifest("AndroidManifest.xml", "com.google.mergetest") | |
| .buildUnvalidated(); | |
| } | |
| @After | |
| public void removeLoggingHandler() { | |
| mergerLogger.removeHandler(loggingHandler); | |
| } | |
| @Test | |
| public void test() throws Exception { | |
| ParsedAndroidData transitiveDependency = | |
| ParsedAndroidDataBuilder.buildOn(fqnFactory) | |
| .overwritable( | |
| xml("attr/ambiguousName") | |
| .root(transitiveRoot1) | |
| .source("values/attrs.xml") | |
| .value( | |
| AttrXmlResourceValue.from( | |
| Value.newBuilder() | |
| .setCompoundValue( | |
| CompoundValue.newBuilder() | |
| .setAttr( | |
| Attribute.newBuilder() | |
| .setFormatFlags(testParameters.formatFlags1()))) | |
| .setWeak(testParameters.strength1() == Strength.WEAK) | |
| .build(), | |
| Visibility.UNKNOWN))) | |
| .overwritable( | |
| xml("attr/ambiguousName") | |
| .root(transitiveRoot2) | |
| .source("values/attrs.xml") | |
| .value( | |
| AttrXmlResourceValue.from( | |
| Value.newBuilder() | |
| .setCompoundValue( | |
| CompoundValue.newBuilder() | |
| .setAttr( | |
| Attribute.newBuilder() | |
| .setFormatFlags(testParameters.formatFlags2()))) | |
| .setWeak(testParameters.strength2() == Strength.WEAK) | |
| .build(), | |
| Visibility.UNKNOWN))) | |
| .build(); | |
| ParsedAndroidData directDependency = ParsedAndroidDataBuilder.empty(); | |
| AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); | |
| UnwrittenMergedAndroidData data = | |
| merger.merge(transitiveDependency, directDependency, primary, false, false); | |
| if (testParameters.expectedMergedAndroidData().isPresent()) { | |
| assertAbout(unwrittenMergedAndroidData) | |
| .that(data) | |
| .isEqualTo(testParameters.expectedMergedAndroidData().get().apply(this)); | |
| } | |
| if (testParameters.expectedMergeConflict().isPresent()) { | |
| assertThat(loggingHandler.warnings) | |
| .containsExactly( | |
| testParameters.expectedMergeConflict().get().apply(this).toConflictMessage()); | |
| } else { | |
| assertThat(loggingHandler.warnings).isEmpty(); | |
| } | |
| } | |
| private final Subject.Factory<UnwrittenMergedAndroidDataSubject, UnwrittenMergedAndroidData> | |
| unwrittenMergedAndroidData = UnwrittenMergedAndroidDataSubject::new; | |
| private static final class TestLoggingHandler extends Handler { | |
| public final List<String> warnings = new ArrayList<>(); | |
| @Override | |
| public void publish(LogRecord record) { | |
| if (record.getLevel().equals(Level.WARNING)) { | |
| warnings.add(record.getMessage()); | |
| } | |
| } | |
| @Override | |
| public void flush() {} | |
| @Override | |
| public void close() {} | |
| } | |
| @AutoValue | |
| abstract static class TestParameters { | |
| abstract Strength strength1(); | |
| abstract int formatFlags1(); | |
| abstract Strength strength2(); | |
| abstract int formatFlags2(); | |
| abstract Optional<Function<AttributeResourcesAndroidDataMergerTest, UnwrittenMergedAndroidData>> | |
| expectedMergedAndroidData(); | |
| abstract Optional<Function<AttributeResourcesAndroidDataMergerTest, MergeConflict>> | |
| expectedMergeConflict(); | |
| @Override | |
| public final String toString() { | |
| return Joiner.on(", ") | |
| .join( | |
| strength1(), | |
| formatFlags1(), | |
| strength2(), | |
| formatFlags2(), | |
| expectedMergeConflict().isPresent() | |
| ? "conflict expected" | |
| : "successful merge expected"); | |
| } | |
| static Builder builder() { | |
| return new AutoValue_AttributeResourcesAndroidDataMergerTest_TestParameters.Builder(); | |
| } | |
| @AutoValue.Builder | |
| abstract static class Builder { | |
| Builder set1(Strength strength, int formatFlags) { | |
| return setStrength1(strength).setFormatFlags1(formatFlags); | |
| } | |
| Builder set2(Strength strength, int formatFlags) { | |
| return setStrength2(strength).setFormatFlags2(formatFlags); | |
| } | |
| abstract Builder setStrength1(Strength value); | |
| abstract Builder setFormatFlags1(int value); | |
| abstract Builder setStrength2(Strength value); | |
| abstract Builder setFormatFlags2(int value); | |
| abstract Builder setExpectedMergedAndroidData( | |
| Function<AttributeResourcesAndroidDataMergerTest, UnwrittenMergedAndroidData> value); | |
| abstract Builder setExpectedMergeConflict( | |
| Function<AttributeResourcesAndroidDataMergerTest, MergeConflict> value); | |
| abstract TestParameters build(); | |
| } | |
| } | |
| } |