// 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.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.file; import static com.google.devtools.build.android.ParsedAndroidDataBuilder.xml; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.jimfs.Jimfs; import com.google.common.truth.FailureStrategy; import com.google.common.truth.SubjectFactory; import com.google.devtools.build.android.AndroidDataBuilder.ResourceType; import com.google.devtools.build.android.AndroidDataMerger.SourceChecker; import com.google.devtools.build.android.xml.IdXmlResourceValue; import com.google.devtools.build.android.xml.PublicXmlResourceValue; import com.google.devtools.build.android.xml.SimpleXmlResourceValue; import com.google.devtools.build.android.xml.SimpleXmlResourceValue.Type; import com.google.devtools.build.android.xml.StyleableXmlResourceValue; import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; 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.JUnit4; /** Tests for {@link AndroidDataMerger}. */ @RunWith(JUnit4.class) public class AndroidDataMergerTest { static final String XLIFF_NAMESPACE = "urn:oasis:names:tc:xliff:document:1.2"; static final String XLIFF_PREFIX = "xliff"; private FileSystem fileSystem; private FullyQualifiedName.Factory fqnFactory; private TestLoggingHandler loggingHandler; private Logger mergerLogger; @Before public void setUp() throws Exception { fileSystem = Jimfs.newFileSystem(); fqnFactory = FullyQualifiedName.Factory.from(ImmutableList.<String>of()); mergerLogger = Logger.getLogger(AndroidDataMerger.class.getCanonicalName()); loggingHandler = new TestLoggingHandler(); mergerLogger.addHandler(loggingHandler); } @After public void removeLoggingHandler() { mergerLogger.removeHandler(loggingHandler); } @Test public void mergeDirectDeps() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); DataSource primaryStrings = DataSource.of(primaryRoot.resolve("res/values/resources.xml")); DataSource directStrings = DataSource.of(directRoot.resolve("res/values/strings.xml")); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.empty(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory) .overwritable( file("layout/exit").source("res/layout/exit.xml"), xml("string/exit") .source(directStrings) .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out"))) .combining(xml("id/exit").source("values/ids.xml").value(IdXmlResourceValue.of())) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .addResource( "values/resources.xml", ResourceType.VALUE, "<string name='exit'>way out</string>") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); UnwrittenMergedAndroidData data = merger.merge(transitiveDependency, directDependency, primary, false); UnwrittenMergedAndroidData expected = UnwrittenMergedAndroidData.of( primary.getManifest(), ParsedAndroidDataBuilder.buildOn(fqnFactory) .overwritable( xml("string/exit") .source(primaryStrings.overwrite(directStrings)) .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out"))) .build(), ParsedAndroidDataBuilder.buildOn(fqnFactory) .overwritable(file("layout/exit").root(directRoot).source("res/layout/exit.xml")) .combining( xml("id/exit") .root(directRoot) .source("values/ids.xml") .value(IdXmlResourceValue.of())) .build()); assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected); } @Test public void mergeDirectAndTransitiveDeps() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); Path transitiveRoot = fileSystem.getPath("transitive"); DataSource directString = DataSource.of(directRoot.resolve("res/values/resources.xml")); DataSource primaryString = DataSource.of(primaryRoot.resolve("res").resolve("values/resources.xml")); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory) .overwritable(file("layout/enter").source("res/layout/enter.xml")) .combining(xml("id/exit").source("values/ids.xml").value(IdXmlResourceValue.of())) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory) .overwritable( file("layout/exit").source("res/layout/exit.xml"), xml("string/exit") .source(directString) .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out"))) .combining(xml("id/exit").source("values/ids.xml").value(IdXmlResourceValue.of())) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .addResource( "values/resources.xml", ResourceType.VALUE, "<string name='exit'>way out</string>") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); UnwrittenMergedAndroidData data = merger.merge(transitiveDependency, directDependency, primary, false); UnwrittenMergedAndroidData expected = UnwrittenMergedAndroidData.of( primary.getManifest(), ParsedAndroidDataBuilder.buildOn(fqnFactory) .overwritable( xml("string/exit") .root(primaryRoot) .source(primaryString.overwrite(directString)) .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out"))) .build(), ParsedAndroidDataBuilder.buildOn(fqnFactory) .overwritable( file("layout/enter").root(transitiveRoot).source("res/layout/enter.xml"), file("layout/exit").root(directRoot).source("res/layout/exit.xml")) .combining( xml("id/exit") .root(directRoot) .source("values/ids.xml") .value(IdXmlResourceValue.of())) .build()); assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected); } @Test public void mergeWithOverwriteInTransitiveDeps() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); Path transitiveRoot = fileSystem.getPath("transitive"); Path descendentRoot = fileSystem.getPath("descendent"); DataSource descendentLayout = DataSource.of(descendentRoot.resolve("res/layout/enter.xml")); DataSource transitiveLayout = DataSource.of(transitiveRoot.resolve("res/layout/enter.xml")); DataSource primaryString = DataSource.of(primaryRoot.resolve("res/values/resources.xml")); DataSource directStrings = DataSource.of(directRoot.resolve("res/values/strings.xml")); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory) .overwritable(file("layout/enter").source(descendentLayout)) .overwritable(file("layout/enter").source(transitiveLayout.overwrite(descendentLayout))) .combining(xml("id/exit").source("values/ids.xml").value(IdXmlResourceValue.of())) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory) .overwritable( file("layout/exit").source("res/layout/exit.xml"), xml("string/exit") .source(directStrings) .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out"))) .combining(xml("id/exit").source("values/ids.xml").value(IdXmlResourceValue.of())) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .addResource( "values/resources.xml", ResourceType.VALUE, "<string name='exit'>way out</string>") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); UnwrittenMergedAndroidData transitive = merger.merge(transitiveDependency, directDependency, primary, false); UnwrittenMergedAndroidData expected = UnwrittenMergedAndroidData.of( primary.getManifest(), ParsedAndroidDataBuilder.buildOn(fqnFactory) .overwritable( xml("string/exit") .source(primaryString.overwrite(directStrings)) .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out"))) .build(), ParsedAndroidDataBuilder.buildOn(fqnFactory) .overwritable( file("layout/enter").source(transitiveLayout.overwrite(descendentLayout)), file("layout/exit").root(directRoot).source("res/layout/exit.xml")) .combining( xml("id/exit") .root(directRoot) .source("values/ids.xml") .value(IdXmlResourceValue.of())) .build()); assertAbout(unwrittenMergedAndroidData).that(transitive).isEqualTo(expected); } @Test public void mergeDirectConflict() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.empty(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory) // Two string/exit will create conflict. .overwritable( xml("string/exit") .source("values/strings.xml") .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out")), xml("string/exit") .source("values/strings.xml") .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out"))) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); merger.merge(transitiveDependency, directDependency, primary, false); assertThat(loggingHandler.warnings) .containsExactly( MergeConflict.of( fqnFactory.parse("string/exit"), DataResourceXml.createWithNoNamespace( directRoot.resolve("res/values/strings.xml"), SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")), DataResourceXml.createWithNoNamespace( directRoot.resolve("res/values/strings.xml"), SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out"))) .toConflictMessage()); } @Test public void mergeDirectConflictDuplicated() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.empty(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory) // Two string/exit will create conflict. .overwritable( xml("string/exit") .source("values/strings.xml") .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out")), xml("string/exit") .source("values/strings.xml") .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out"))) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaultThreadPool( new SourceChecker() { @Override public boolean checkEquality(DataSource one, DataSource two) throws IOException { return one.equals(two); } }); assertAbout(unwrittenMergedAndroidData) .that(merger.merge(transitiveDependency, directDependency, primary, false)) .isEqualTo( UnwrittenMergedAndroidData.of( primary.getManifest(), ParsedAndroidDataBuilder.empty(), ParsedAndroidDataBuilder.buildOn(fqnFactory) .overwritable( xml("string/exit") .root(directRoot) .source("values/strings.xml") .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out"))) .build())); } @Test public void mergeDirectConflictWithPrimaryOverride() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); DataSource primaryStrings = DataSource.of(primaryRoot.resolve("res/values/strings.xml")); DataSource directStrings = DataSource.of(directRoot.resolve("res/values/strings.xml")); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.empty(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory) // Two string/exit will create conflict. .overwritable( xml("string/exit") .source(directStrings) .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out")), xml("string/exit") .source(directStrings) .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out"))) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .addResource( "values/strings.xml", ResourceType.VALUE, "<string name='exit'>way out</string>") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); UnwrittenMergedAndroidData data = merger.merge(transitiveDependency, directDependency, primary, true); UnwrittenMergedAndroidData expected = UnwrittenMergedAndroidData.of( primary.getManifest(), ParsedAndroidDataBuilder.buildOn(fqnFactory) .overwritable( xml("string/exit") .source(primaryStrings.overwrite(directStrings)) .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out"))) .build(), ParsedAndroidDataBuilder.empty()); assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected); } @Test public void mergeTransitiveConflict() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path transitiveRoot = fileSystem.getPath("transitive"); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory) // Two string/exit will create conflict. .overwritable( xml("string/exit") .source("values/strings.xml") .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out")), xml("string/exit") .source("values/strings.xml") .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out"))) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.empty(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); merger.merge(transitiveDependency, directDependency, primary, false); assertThat(loggingHandler.warnings) .containsExactly( MergeConflict.of( fqnFactory.parse("string/exit"), DataResourceXml.createWithNoNamespace( transitiveRoot.resolve("res/values/strings.xml"), SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")), DataResourceXml.createWithNoNamespace( transitiveRoot.resolve("res/values/strings.xml"), SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out"))) .toConflictMessage()); } @Test public void mergeTransitiveConflictWithPrimaryOverride() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); Path transitiveRoot = fileSystem.getPath("transitive"); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory) // Two string/exit will create conflict. .overwritable( xml("string/exit") .root(transitiveRoot.resolve("1")) .source("values/strings.xml") .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out")), xml("string/exit") .root(transitiveRoot.resolve("2")) .source("values/strings.xml") .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out"))) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory).build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .addResource( "values/strings.xml", ResourceType.VALUE, "<string name='exit'>way out</string>") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); UnwrittenMergedAndroidData data = merger.merge(transitiveDependency, directDependency, primary, true); UnwrittenMergedAndroidData expected = UnwrittenMergedAndroidData.of( primary.getManifest(), ParsedAndroidDataBuilder.buildOn(fqnFactory) .overwritable( xml("string/exit") .root(primaryRoot) .source("values/strings.xml") .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out"))) .build(), ParsedAndroidDataBuilder.empty()); assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected); } @Test public void mergeDirectAndTransitiveConflict() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); Path transitiveRoot = fileSystem.getPath("transitive"); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory) .overwritable( xml("string/exit") .source("values/strings.xml") .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out"))) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory) .overwritable( xml("string/exit") .source("values/strings.xml") .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out"))) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); merger.merge(transitiveDependency, directDependency, primary, false); assertThat(loggingHandler.warnings) .containsExactly( MergeConflict.of( fqnFactory.parse("string/exit"), DataResourceXml.createWithNoNamespace( directRoot.resolve("res/values/strings.xml"), SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")), DataResourceXml.createWithNoNamespace( transitiveRoot.resolve("res/values/strings.xml"), SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out"))) .toConflictMessage()); } @Test public void mergeDirectTransitivePrimaryConflictWithoutPrimaryOverride() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); Path transitiveRoot = fileSystem.getPath("transitive"); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory) .overwritable( xml("string/exit") .source("values/strings.xml") .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out"))) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory) .overwritable( xml("string/exit") .source("values/strings.xml") .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out"))) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .addResource( "values/strings.xml", ResourceType.VALUE, "<string name='exit'>way out</string>") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); merger.merge(transitiveDependency, directDependency, primary, false); FullyQualifiedName fullyQualifiedName = fqnFactory.parse("string/exit"); assertThat(loggingHandler.warnings) .containsExactly( MergeConflict.of( fullyQualifiedName, DataResourceXml.createWithNoNamespace( directRoot.resolve("res/values/strings.xml"), SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")), DataResourceXml.createWithNoNamespace( transitiveRoot.resolve("res/values/strings.xml"), SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out"))) .toConflictMessage()); } @Test public void mergeDirectTransitivePrimaryConflictWithPrimaryOverride() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); Path transitiveRoot = fileSystem.getPath("transitive"); DataSource primaryStrings = DataSource.of(primaryRoot.resolve("res/values/strings.xml")); DataSource directStrings = DataSource.of(directRoot.resolve("res/values/strings.xml")); DataSource transitiveStrings = DataSource.of(transitiveRoot.resolve("res/values/strings.xml")); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory) .overwritable( xml("string/exit") .source(transitiveStrings) .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out"))) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory) .overwritable( xml("string/exit") .source(directStrings) .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out"))) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .addResource( "values/strings.xml", ResourceType.VALUE, "<string name='exit'>way out</string>") .buildUnvalidated(); UnwrittenMergedAndroidData data = AndroidDataMerger.createWithDefaults() .merge(transitiveDependency, directDependency, primary, true); UnwrittenMergedAndroidData expected = UnwrittenMergedAndroidData.of( primary.getManifest(), ParsedAndroidDataBuilder.buildOn(fqnFactory) .overwritable( xml("string/exit") .source(primaryStrings.overwrite(directStrings)) .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out"))) .build(), ParsedAndroidDataBuilder.empty()); assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected); } @Test public void mergeDirectAndTransitiveNinepatchConflict() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); Path transitiveRoot = fileSystem.getPath("transitive"); // A drawable nine-patch png and plain png with the same base filename will conflict. ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory) .overwritable(file("drawable/rounded_corners").source("drawable/rounded_corners.9.png")) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory) .overwritable(file("drawable/rounded_corners").source("drawable/rounded_corners.png")) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); merger.merge(transitiveDependency, directDependency, primary, false); assertThat(loggingHandler.warnings) .containsExactly( MergeConflict.of( fqnFactory.parse("drawable/rounded_corners"), DataValueFile.of(directRoot.resolve("res/drawable/rounded_corners.png")), DataValueFile.of(transitiveRoot.resolve("res/drawable/rounded_corners.9.png"))) .toConflictMessage()); } @Test public void mergeAssetsDirectDeps() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.empty(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot) .assets(file().source("hunting/of/the/snark.txt")) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .addAsset("bin/boojum") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); UnwrittenMergedAndroidData data = merger.merge(transitiveDependency, directDependency, primary, false); UnwrittenMergedAndroidData expected = UnwrittenMergedAndroidData.of( primary.getManifest(), ParsedAndroidDataBuilder.builder() .assets(file().root(primaryRoot).source("bin/boojum")) .build(), ParsedAndroidDataBuilder.builder() .assets(file().root(directRoot).source("hunting/of/the/snark.txt")) .build()); assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected); } @Test public void mergeCombiningResources() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); Path transitiveRoot = fileSystem.getPath("transitive"); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory) .combining( xml("styleable/RubADubDub") .source("values/attrs.xml") .value( StyleableXmlResourceValue.createAllAttrAsReferences( fqnFactory.parse("attr/baker")))) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory) .combining( xml("styleable/RubADubDub") .source("values/attrs.xml") .value( StyleableXmlResourceValue.createAllAttrAsReferences( fqnFactory.parse("attr/candlestickmaker")))) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .addResource( "values/attrs.xml", ResourceType.VALUE, "<declare-styleable name='RubADubDub'>", " <attr name='butcher'/>", "</declare-styleable>") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); UnwrittenMergedAndroidData data = merger.merge(transitiveDependency, directDependency, primary, false); UnwrittenMergedAndroidData expected = UnwrittenMergedAndroidData.of( primary.getManifest(), ParsedAndroidDataBuilder.buildOn(primaryRoot, fqnFactory) .combining( xml("styleable/RubADubDub") .source("values/attrs.xml") .value( StyleableXmlResourceValue.createAllAttrAsReferences( fqnFactory.parse("attr/baker"), fqnFactory.parse("attr/butcher"), fqnFactory.parse("attr/candlestickmaker")))) .build(), ParsedAndroidDataBuilder.empty()); assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected); } @Test public void mergeCombiningResourcesWithNamespaces() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); Path transitiveRoot = fileSystem.getPath("transitive"); // TODO(corysmith): Make conflict uris for a single prefix and error ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory) .combining( xml("styleable/RubADubDub") .source("values/attrs.xml") .namespace(XLIFF_PREFIX, XLIFF_NAMESPACE) .namespace("tools", "http://schemas.android.com/tools") .value( StyleableXmlResourceValue.createAllAttrAsReferences( fqnFactory.parse("attr/baker")))) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory) .combining( xml("styleable/RubADubDub") .source("values/attrs.xml") .namespace(XLIFF_PREFIX, "wrong uri") .value( StyleableXmlResourceValue.createAllAttrAsReferences( fqnFactory.parse("attr/candlestickmaker")))) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .addResource( "values/attrs.xml", ResourceType.VALUE, "<declare-styleable name='RubADubDub'>", " <attr name='butcher'/>", "</declare-styleable>") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); UnwrittenMergedAndroidData data = merger.merge(transitiveDependency, directDependency, primary, false); UnwrittenMergedAndroidData expected = UnwrittenMergedAndroidData.of( primary.getManifest(), ParsedAndroidDataBuilder.buildOn(primaryRoot, fqnFactory) .combining( xml("styleable/RubADubDub") .source("values/attrs.xml") .namespace(XLIFF_PREFIX, "wrong uri") .namespace("tools", "http://schemas.android.com/tools") .value( StyleableXmlResourceValue.createAllAttrAsReferences( fqnFactory.parse("attr/baker"), fqnFactory.parse("attr/butcher"), fqnFactory.parse("attr/candlestickmaker")))) .build(), ParsedAndroidDataBuilder.empty()); assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected); } @Test public void mergeCombiningLayoutIDResources() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); Path transitiveRoot = fileSystem.getPath("transitive"); DataSource transitiveLayout = DataSource.of(transitiveRoot.resolve("res/layout/transitive.xml")); DataSource directLayout = DataSource.of(directRoot.resolve("res/layout/direct.xml")); DataSource primaryLayout = DataSource.of(primaryRoot.resolve("res/layout/primary.xml")); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory) .overwritable(file("layout/transitive").source(transitiveLayout)) .combining( xml("id/back_door").source(transitiveLayout).value(IdXmlResourceValue.of()), xml("id/door").source("values/ids.xml").value(IdXmlResourceValue.of()), xml("id/slide").source(transitiveLayout).value(IdXmlResourceValue.of())) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory) .overwritable(file("layout/zzDirect").source(directLayout)) .combining( xml("id/door").source(directLayout).value(IdXmlResourceValue.of()), xml("id/slide").source(directLayout).value(IdXmlResourceValue.of()), xml("id/window").source(directLayout).value(IdXmlResourceValue.of())) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .addResource( "layout/primary.xml", ResourceType.LAYOUT, "<TextView android:id=\"@+id/door\"", " android:text=\"this way ---> \"", " android:layout_width=\"wrap_content\"", " android:layout_height=\"wrap_content\" />", "<TextView android:id=\"@+id/window\"", " android:text=\"no, not that way\"", " android:layout_width=\"wrap_content\"", " android:layout_height=\"wrap_content\" />") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); UnwrittenMergedAndroidData data = merger.merge(transitiveDependency, directDependency, primary, false); UnwrittenMergedAndroidData expected = UnwrittenMergedAndroidData.of( primary.getManifest(), ParsedAndroidDataBuilder.buildOn(primaryRoot, fqnFactory) .overwritable(file("layout/primary").source(primaryLayout)) .combining( xml("id/window").source("layout/primary.xml").value(IdXmlResourceValue.of()), xml("id/door") .root(transitiveRoot) .source("values/ids.xml") .value(IdXmlResourceValue.of())) .build(), ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory) .overwritable( file("layout/transitive").source(transitiveLayout), file("layout/zzDirect").source(directLayout)) .combining( xml("id/back_door").source(transitiveLayout).value(IdXmlResourceValue.of()), xml("id/slide").source(directLayout).value(IdXmlResourceValue.of())) .build()); assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected); } @Test public void mergeCombiningPublicResources() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); Path transitiveRoot = fileSystem.getPath("transitive"); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory) .combining( xml("public/RubADubDub") .source("values/public.xml") .value( PublicXmlResourceValue.create( com.android.resources.ResourceType.STRING, Optional.of(0x7f040000)))) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory) .combining( xml("public/RubADubDub") .source("values/public.xml") .value( PublicXmlResourceValue.create( com.android.resources.ResourceType.DIMEN, Optional.of(0x7f020000)))) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .addResource( "values/public.xml", ResourceType.VALUE, "<public name='RubADubDub' type='color' id='0x7f030000' />") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); UnwrittenMergedAndroidData data = merger.merge(transitiveDependency, directDependency, primary, false); UnwrittenMergedAndroidData expected = UnwrittenMergedAndroidData.of( primary.getManifest(), ParsedAndroidDataBuilder.buildOn(primaryRoot, fqnFactory) .combining( xml("public/RubADubDub") .source("values/public.xml") .value( PublicXmlResourceValue.of( ImmutableMap.of( com.android.resources.ResourceType.COLOR, Optional.of(0x7f030000), com.android.resources.ResourceType.DIMEN, Optional.of(0x7f020000), com.android.resources.ResourceType.STRING, Optional.of(0x7f040000))))) .build(), ParsedAndroidDataBuilder.empty()); assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected); } @Test public void mergeAssetsDirectAndTransitiveDeps() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); Path transitiveRoot = fileSystem.getPath("transitive"); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.buildOn(transitiveRoot) .assets(file().source("hunting/of/the/jubjub.bird")) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot) .assets(file().source("hunting/of/the/snark.txt")) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .addAsset("bin/boojum") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); UnwrittenMergedAndroidData data = merger.merge(transitiveDependency, directDependency, primary, false); UnwrittenMergedAndroidData expected = UnwrittenMergedAndroidData.of( primary.getManifest(), ParsedAndroidDataBuilder.builder() .assets(file().root(primaryRoot).source("bin/boojum")) .build(), ParsedAndroidDataBuilder.builder() .assets( file().root(directRoot).source("hunting/of/the/snark.txt"), file().root(transitiveRoot).source("hunting/of/the/jubjub.bird")) .build()); assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected); } @Test public void mergeAssetsDirectConflict() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRootOne = fileSystem.getPath("directOne"); Path directRootTwo = fileSystem.getPath("directTwo"); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.empty(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.builder() .assets( file().root(directRootOne).source("hunting/of/the/snark.txt"), file().root(directRootTwo).source("hunting/of/the/snark.txt")) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); merger.merge(transitiveDependency, directDependency, primary, false); assertThat(loggingHandler.warnings) .containsExactly( MergeConflict.of( RelativeAssetPath.Factory.of(directRootOne.resolve("assets")) .create(directRootOne.resolve("assets/hunting/of/the/snark.txt")), DataValueFile.of(directRootOne.resolve("assets/hunting/of/the/snark.txt")), DataValueFile.of(directRootTwo.resolve("assets/hunting/of/the/snark.txt"))) .toConflictMessage()); } @Test public void mergeAssetsDirectConflictWithPrimaryOverride() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRootOne = fileSystem.getPath("directOne"); Path directRootTwo = fileSystem.getPath("directTwo"); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.empty(); String assetFile = "hunting/of/the/snark.txt"; DataSource primarySource = DataSource.of(primaryRoot.resolve("assets/" + assetFile)); DataSource directSourceOne = DataSource.of(directRootOne.resolve("assets/" + assetFile)); DataSource directSourceTwo = DataSource.of(directRootTwo.resolve("assets/" + assetFile)); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.builder() .assets( file().root(directRootOne).source(directSourceOne), file().root(directRootTwo).source(directSourceTwo)) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .addAsset(assetFile) .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); UnwrittenMergedAndroidData data = merger.merge(transitiveDependency, directDependency, primary, true); UnwrittenMergedAndroidData expected = UnwrittenMergedAndroidData.of( primary.getManifest(), ParsedAndroidDataBuilder.builder() .assets( file() .root(primaryRoot) .source(primarySource.overwrite(directSourceOne, directSourceTwo))) .build(), ParsedAndroidDataBuilder.empty()); assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected); } @Test public void mergeAssetsTransitiveConflict() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path transitiveRootOne = fileSystem.getPath("transitiveOne"); Path transitiveRootTwo = fileSystem.getPath("transitiveTwo"); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.builder() .assets( file().root(transitiveRootOne).source("hunting/of/the/snark.txt"), file().root(transitiveRootTwo).source("hunting/of/the/snark.txt")) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.empty(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); merger.merge(transitiveDependency, directDependency, primary, false); assertThat(loggingHandler.warnings) .containsExactly( MergeConflict.of( RelativeAssetPath.Factory.of(transitiveRootOne.resolve("assets")) .create(transitiveRootOne.resolve("assets/hunting/of/the/snark.txt")), DataValueFile.of(transitiveRootOne.resolve("assets/hunting/of/the/snark.txt")), DataValueFile.of(transitiveRootTwo.resolve("assets/hunting/of/the/snark.txt"))) .toConflictMessage()); } @Test public void mergeAssetsTransitiveConflictWithPrimaryOverride() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path transitiveRoot = fileSystem.getPath("transitive"); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.buildOn(transitiveRoot) .assets(file().source("hunting/of/the/snark.txt")) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.empty(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .addAsset("hunting/of/the/snark.txt") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); UnwrittenMergedAndroidData data = merger.merge(transitiveDependency, directDependency, primary, true); UnwrittenMergedAndroidData expected = UnwrittenMergedAndroidData.of( primary.getManifest(), ParsedAndroidDataBuilder.buildOn(primaryRoot) .assets(file().source("hunting/of/the/snark.txt")) .build(), ParsedAndroidDataBuilder.empty()); assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected); } @Test public void mergeAssetsDirectAndTransitiveConflict() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); Path transitiveRoot = fileSystem.getPath("transitive"); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.buildOn(transitiveRoot) .assets(file().source("hunting/of/the/snark.txt")) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot) .assets(file().source("hunting/of/the/snark.txt")) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); merger.merge(transitiveDependency, directDependency, primary, false); assertThat(loggingHandler.warnings) .containsExactly( MergeConflict.of( RelativeAssetPath.Factory.of(directRoot.resolve("assets")) .create(directRoot.resolve("assets/hunting/of/the/snark.txt")), DataValueFile.of(directRoot.resolve("assets/hunting/of/the/snark.txt")), DataValueFile.of(transitiveRoot.resolve("assets/hunting/of/the/snark.txt"))) .toConflictMessage()); } @Test public void mergeAssetsDirectTransitivePrimaryConflictWithoutPrimaryOverride() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); Path transitiveRoot = fileSystem.getPath("transitive"); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.buildOn(transitiveRoot) .assets(file().source("hunting/of/the/snark.txt")) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot) .assets(file().source("hunting/of/the/snark.txt")) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .addAsset("hunting/of/the/snark.txt") .buildUnvalidated(); AndroidDataMerger merger = AndroidDataMerger.createWithDefaults(); merger.merge(transitiveDependency, directDependency, primary, false); assertThat(loggingHandler.warnings) .containsExactly( MergeConflict.of( RelativeAssetPath.Factory.of(directRoot.resolve("assets")) .create(directRoot.resolve("assets/hunting/of/the/snark.txt")), DataValueFile.of(directRoot.resolve("assets/hunting/of/the/snark.txt")), DataValueFile.of(transitiveRoot.resolve("assets/hunting/of/the/snark.txt"))) .toConflictMessage()); } @Test public void mergeAssetsDirectTransitivePrimaryConflictWithPrimaryOverride() throws Exception { Path primaryRoot = fileSystem.getPath("primary"); Path directRoot = fileSystem.getPath("direct"); Path transitiveRoot = fileSystem.getPath("transitive"); DataSource primarySource = DataSource.of(primaryRoot.resolve("assets/hunting/of/the/snark.txt")); DataSource directSource = DataSource.of(directRoot.resolve("assets/hunting/of/the/snark.txt")); ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.buildOn(transitiveRoot) .assets(file().source("hunting/of/the/snark.txt")) .build(); ParsedAndroidData directDependency = ParsedAndroidDataBuilder.buildOn(directRoot) .assets(file().source("hunting/of/the/snark.txt")) .build(); UnvalidatedAndroidData primary = AndroidDataBuilder.of(primaryRoot) .createManifest("AndroidManifest.xml", "com.google.mergetest") .addAsset("hunting/of/the/snark.txt") .buildUnvalidated(); UnwrittenMergedAndroidData data = AndroidDataMerger.createWithDefaults() .merge(transitiveDependency, directDependency, primary, true); UnwrittenMergedAndroidData expected = UnwrittenMergedAndroidData.of( primary.getManifest(), ParsedAndroidDataBuilder.buildOn(primaryRoot) .assets(file().source(primarySource.overwrite(directSource))) .build(), ParsedAndroidDataBuilder.empty()); assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected); } final SubjectFactory<UnwrittenMergedAndroidDataSubject, UnwrittenMergedAndroidData> unwrittenMergedAndroidData = new SubjectFactory<UnwrittenMergedAndroidDataSubject, UnwrittenMergedAndroidData>() { @Override public UnwrittenMergedAndroidDataSubject getSubject( FailureStrategy fs, UnwrittenMergedAndroidData that) { return new UnwrittenMergedAndroidDataSubject(fs, that); } }; private static final class TestLoggingHandler extends Handler { public final List<String> warnings = new ArrayList<String>(); @Override public void publish(LogRecord record) { if (record.getLevel().equals(Level.WARNING)) { warnings.add(record.getMessage()); } } @Override public void flush() {} @Override public void close() throws SecurityException {} } }