From 2587a6dd010266c749b6234115b9333830a951a7 Mon Sep 17 00:00:00 2001 From: Adam Michael Date: Wed, 22 Mar 2017 15:33:40 +0000 Subject: Open source some Android tools' tests. -- PiperOrigin-RevId: 150881315 MOS_MIGRATED_REVID=150881315 --- .../build/android/AaptCommandBuilderTest.java | 262 ++++ .../devtools/build/android/AndroidDataBuilder.java | 202 +++ .../build/android/AndroidDataMergerTest.java | 1300 +++++++++++++++++++ .../AndroidDataSerializerAndDeserializerTest.java | 362 ++++++ .../build/android/AndroidDataWriterTest.java | 397 ++++++ .../android/AndroidResourceClassWriterTest.java | 696 ++++++++++ .../java/com/google/devtools/build/android/BUILD | 59 + .../devtools/build/android/ClassPathsSubject.java | 165 +++ .../build/android/DataResourceXmlTest.java | 1325 ++++++++++++++++++++ .../build/android/ParsedAndroidDataBuilder.java | 269 ++++ .../devtools/build/android/PathsSubject.java | 93 ++ 11 files changed, 5130 insertions(+) create mode 100644 src/test/java/com/google/devtools/build/android/AaptCommandBuilderTest.java create mode 100644 src/test/java/com/google/devtools/build/android/AndroidDataBuilder.java create mode 100644 src/test/java/com/google/devtools/build/android/AndroidDataMergerTest.java create mode 100644 src/test/java/com/google/devtools/build/android/AndroidDataSerializerAndDeserializerTest.java create mode 100644 src/test/java/com/google/devtools/build/android/AndroidDataWriterTest.java create mode 100644 src/test/java/com/google/devtools/build/android/AndroidResourceClassWriterTest.java create mode 100644 src/test/java/com/google/devtools/build/android/ClassPathsSubject.java create mode 100644 src/test/java/com/google/devtools/build/android/DataResourceXmlTest.java create mode 100644 src/test/java/com/google/devtools/build/android/ParsedAndroidDataBuilder.java create mode 100644 src/test/java/com/google/devtools/build/android/PathsSubject.java (limited to 'src/test/java') diff --git a/src/test/java/com/google/devtools/build/android/AaptCommandBuilderTest.java b/src/test/java/com/google/devtools/build/android/AaptCommandBuilderTest.java new file mode 100644 index 0000000000..f6762a3b93 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/AaptCommandBuilderTest.java @@ -0,0 +1,262 @@ +// 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.assertThat; + +import com.android.builder.core.VariantType; +import com.android.repository.Revision; +import com.google.common.collect.ImmutableList; +import com.google.common.testing.NullPointerTester; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.util.ArrayList; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link AaptCommandBuilder}. */ +@RunWith(JUnit4.class) +public class AaptCommandBuilderTest { + private Path aapt; + private Path manifest; + + @Before + public void createPaths() { + FileSystem fs = FileSystems.getDefault(); + aapt = fs.getPath("aapt"); + manifest = fs.getPath("AndroidManifest.xml"); + } + + @Test + public void testPassesNullPointerTester() throws Exception { + NullPointerTester tester = new NullPointerTester().setDefault(Path.class, aapt); + + tester.testConstructor(AaptCommandBuilder.class.getConstructor(Path.class)); + tester.testAllPublicInstanceMethods(new AaptCommandBuilder(aapt)); + tester.testAllPublicInstanceMethods(new AaptCommandBuilder(aapt).when(true)); + tester.testAllPublicInstanceMethods(new AaptCommandBuilder(aapt).when(false)); + } + + @Test + public void testAaptPathAddedAsFirstArgument() { + assertThat(new AaptCommandBuilder(aapt).build()).containsExactly(aapt.toString()); + } + + @Test + public void testAddArgumentAddedToList() { + assertThat(new AaptCommandBuilder(aapt).add("package").build()).contains("package"); + } + + @Test + public void testAddCallsAddedToEndOfList() { + assertThat(new AaptCommandBuilder(aapt).add("package").add("-f").build()) + .containsExactly(aapt.toString(), "package", "-f") + .inOrder(); + } + + @Test + public void testAddWithStringValueAddsFlagThenValueAsSeparateWords() { + assertThat(new AaptCommandBuilder(aapt).add("-0", "gif").build()) + .containsExactly(aapt.toString(), "-0", "gif") + .inOrder(); + } + + @Test + public void testAddWithEmptyValueAddsNothing() { + assertThat(new AaptCommandBuilder(aapt).add("-0", "").build()).doesNotContain("-0"); + } + + @Test + public void testAddWithNullStringValueAddsNothing() { + assertThat(new AaptCommandBuilder(aapt).add("-0", (String) null).build()).doesNotContain("-0"); + } + + @Test + public void testAddWithPathValueAddsFlagThenStringValueAsSeparateWords() { + assertThat(new AaptCommandBuilder(aapt).add("-M", manifest).build()) + .containsExactly(aapt.toString(), "-M", manifest.toString()) + .inOrder(); + } + + @Test + public void testAddWithNullPathValueAddsNothing() { + assertThat(new AaptCommandBuilder(aapt).add("-M", (Path) null).build()).doesNotContain("-M"); + } + + @Test + public void testAddRepeatedWithEmptyValuesAddsNothing() { + assertThat(new AaptCommandBuilder(aapt).addRepeated("-0", ImmutableList.of()).build()) + .doesNotContain("-0"); + } + + @Test + public void testAddRepeatedWithSingleValueAddsOneFlagOneValue() { + assertThat(new AaptCommandBuilder(aapt).addRepeated("-0", ImmutableList.of("gif")).build()) + .containsExactly(aapt.toString(), "-0", "gif") + .inOrder(); + } + + @Test + public void testAddRepeatedWithMultipleValuesAddsFlagBeforeEachValue() { + assertThat( + new AaptCommandBuilder(aapt).addRepeated("-0", ImmutableList.of("gif", "png")).build()) + .containsExactly(aapt.toString(), "-0", "gif", "-0", "png") + .inOrder(); + } + + @Test + public void testAddRepeatedSkipsNullValues() { + ArrayList list = new ArrayList<>(3); + list.add("gif"); + list.add(null); + list.add("png"); + assertThat( + new AaptCommandBuilder(aapt).addRepeated("-0", list).build()) + .containsExactly(aapt.toString(), "-0", "gif", "-0", "png") + .inOrder(); + } + + + @Test + public void testThenAddFlagForwardsCallAfterWhenTrue() { + assertThat( + new AaptCommandBuilder(aapt).when(true).thenAdd("--addthisflag").build()) + .contains("--addthisflag"); + } + + @Test + public void testThenAddFlagWithValueForwardsCallAfterWhenTrue() { + assertThat( + new AaptCommandBuilder(aapt) + .when(true).thenAdd("--addthisflag", "andthisvalue").build()) + .contains("--addthisflag"); + } + + @Test + public void testThenAddFlagWithPathForwardsCallAfterWhenTrue() { + assertThat( + new AaptCommandBuilder(aapt) + .when(true).thenAdd("--addthisflag", manifest).build()) + .contains("--addthisflag"); + } + + @Test + public void testThenAddRepeatedForwardsCallAfterWhenTrue() { + assertThat( + new AaptCommandBuilder(aapt) + .when(true).thenAddRepeated("--addthisflag", ImmutableList.of("andthesevalues")) + .build()) + .contains("--addthisflag"); + } + + @Test + public void testThenAddFlagDoesNothingAfterWhenFalse() { + assertThat( + new AaptCommandBuilder(aapt).when(false).thenAdd("--dontaddthisflag").build()) + .doesNotContain("--dontaddthisflag"); + } + + @Test + public void testThenAddFlagWithValueDoesNothingAfterWhenFalse() { + assertThat( + new AaptCommandBuilder(aapt) + .when(false).thenAdd("--dontaddthisflag", "orthisvalue").build()) + .doesNotContain("--dontaddthisflag"); + } + + @Test + public void testThenAddFlagWithPathDoesNothingAfterWhenFalse() { + assertThat( + new AaptCommandBuilder(aapt) + .when(false).thenAdd("--dontaddthisflag", manifest).build()) + .doesNotContain("--dontaddthisflag"); + } + + @Test + public void testThenAddRepeatedDoesNothingAfterWhenFalse() { + assertThat( + new AaptCommandBuilder(aapt) + .when(false).thenAddRepeated("--dontaddthisflag", ImmutableList.of("orthesevalues")) + .build()) + .doesNotContain("--dontaddthisflag"); + } + + @Test + public void testWhenVersionIsAtLeastAddsFlagsForEqualVersion() { + assertThat( + new AaptCommandBuilder(aapt).forBuildToolsVersion(new Revision(23)) + .whenVersionIsAtLeast(new Revision(23)).thenAdd("--addthisflag") + .build()) + .contains("--addthisflag"); + } + + @Test + public void testWhenVersionIsAtLeastAddsFlagsForGreaterVersion() { + assertThat( + new AaptCommandBuilder(aapt).forBuildToolsVersion(new Revision(24)) + .whenVersionIsAtLeast(new Revision(23)).thenAdd("--addthisflag") + .build()) + + .contains("--addthisflag"); + } + + @Test + public void testWhenVersionIsAtLeastAddsFlagsForUnspecifiedVersion() { + assertThat( + new AaptCommandBuilder(aapt) + .whenVersionIsAtLeast(new Revision(23)).thenAdd("--addthisflag") + .build()) + .contains("--addthisflag"); + } + + @Test + public void testWhenVersionIsAtLeastDoesNotAddFlagsForLesserVersion() { + assertThat( + new AaptCommandBuilder(aapt).forBuildToolsVersion(new Revision(22)) + .whenVersionIsAtLeast(new Revision(23)).thenAdd("--dontaddthisflag") + .build()) + .doesNotContain("--dontaddthisflag"); + } + + @Test + public void testWhenVariantIsAddsFlagsForEqualVariantType() { + assertThat( + new AaptCommandBuilder(aapt).forVariantType(VariantType.LIBRARY) + .whenVariantIs(VariantType.LIBRARY).thenAdd("--addthisflag") + .build()) + .contains("--addthisflag"); + } + + @Test + public void testWhenVariantIsDoesNotAddFlagsForUnequalVariantType() { + assertThat( + new AaptCommandBuilder(aapt).forVariantType(VariantType.DEFAULT) + .whenVariantIs(VariantType.LIBRARY).thenAdd("--dontaddthisflag") + .build()) + .doesNotContain("--dontaddthisflag"); + } + + @Test + public void testWhenVariantIsDoesNotAddFlagsForUnspecifiedVariantType() { + assertThat( + new AaptCommandBuilder(aapt) + .whenVariantIs(VariantType.LIBRARY).thenAdd("--dontaddthisflag") + .build()) + .doesNotContain("--dontaddthisflag"); + } + +} diff --git a/src/test/java/com/google/devtools/build/android/AndroidDataBuilder.java b/src/test/java/com/google/devtools/build/android/AndroidDataBuilder.java new file mode 100644 index 0000000000..3e92eb64a4 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/AndroidDataBuilder.java @@ -0,0 +1,202 @@ +// 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 com.android.ide.common.res2.MergingException; +import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Utility for building {@link UnvalidatedAndroidData}, {@link ParsedAndroidData}, + * {@link DependencyAndroidData} and {@link MergedAndroidData}. + */ +public class AndroidDataBuilder { + /** Templates for resource files generation. */ + public enum ResourceType { + VALUE { + @Override + public String create(String... lines) { + return String.format( + "\n%s", + Joiner.on("\n").join(lines)); + } + }, + LAYOUT { + @Override + public String create(String... lines) { + return String.format( + "" + + "%s", + Joiner.on("\n").join(lines)); + } + }, + UNFORMATTED { + @Override + public String create(String... lines) { + return String.format(Joiner.on("\n").join(lines)); + } + }; + + public abstract String create(String... lines); + } + + public static AndroidDataBuilder of(Path root) { + return new AndroidDataBuilder(root); + } + + private final Path root; + private final Path assetDir; + private final Path resourceDir; + private Map filesToWrite = new HashMap<>(); + private Map filesToCopy = new HashMap<>(); + private Path manifest; + private Path rTxt; + + private AndroidDataBuilder(Path root) { + this.root = root; + assetDir = root.resolve("assets"); + resourceDir = root.resolve("res"); + } + + AndroidDataBuilder copyOf(Path newRoot) { + AndroidDataBuilder result = new AndroidDataBuilder(newRoot); + result.filesToWrite = rerootPaths(this.filesToWrite, this.root, newRoot); + result.filesToCopy = rerootPaths(this.filesToCopy, this.root, newRoot); + result.manifest = this.manifest; + result.rTxt = this.rTxt; + return result; + } + + public AndroidDataBuilder addResource( + String path, AndroidDataBuilder.ResourceType template, String... lines) { + filesToWrite.put(resourceDir.resolve(path), template.create(lines)); + return this; + } + + public AndroidDataBuilder addValuesWithAttributes( + String path, Map attributes, String... lines) { + ImmutableList.Builder attributeBuilder = ImmutableList.builder(); + for (Entry attribute : attributes.entrySet()) { + if (attribute.getKey() != null && attribute.getValue() != null) { + attributeBuilder.add(String.format("%s=\"%s\"", attribute.getKey(), attribute.getValue())); + } + } + String fileContents = ResourceType.VALUE.create(lines); + fileContents = fileContents.replace("", + String.format("", Joiner.on(" ").join(attributeBuilder.build()))); + filesToWrite.put(resourceDir.resolve(path), fileContents); + return this; + } + + public AndroidDataBuilder addResourceBinary(String path, Path source) { + final Path target = resourceDir.resolve(path); + filesToCopy.put(target, source); + return this; + } + + public AndroidDataBuilder addAsset(String path, String... lines) { + filesToWrite.put(assetDir.resolve(path), Joiner.on("\n").join(lines)); + return this; + } + + public AndroidDataBuilder createManifest(String path, String manifestPackage, String... lines) { + return createManifest(path, manifestPackage, ImmutableList.of(), lines); + } + + public AndroidDataBuilder createManifest( + String path, String manifestPackage, List namespaces, String... lines) { + this.manifest = root.resolve(path); + filesToWrite.put( + manifest, + String.format( + "" + + "" + + "%s", + Joiner.on(" ").join(namespaces), + manifestPackage, + Joiner.on("\n").join(lines))); + return this; + } + + public AndroidDataBuilder createRTxt(String path, String... lines) { + this.rTxt = root.resolve(path); + filesToWrite.put(rTxt, Joiner.on("\n").join(lines)); + return this; + } + + public UnvalidatedAndroidData buildUnvalidated() throws IOException { + writeFiles(); + return new UnvalidatedAndroidData( + ImmutableList.of(resourceDir), ImmutableList.of(assetDir), manifest); + } + + public ParsedAndroidData buildParsed() throws IOException, MergingException { + return ParsedAndroidData.from(buildUnvalidated()); + } + + public DependencyAndroidData buildDependency() throws IOException { + writeFiles(); + return new DependencyAndroidData( + ImmutableList.of(resourceDir), + ImmutableList.of(assetDir), + manifest, + rTxt, + null); + } + + public MergedAndroidData buildMerged() throws IOException { + writeFiles(); + return new MergedAndroidData(resourceDir, assetDir, manifest); + } + + private void writeFiles() throws IOException { + Files.createDirectories(assetDir); + Files.createDirectories(resourceDir); + Preconditions.checkNotNull(manifest, "A manifest is required."); + for (Entry entry : filesToWrite.entrySet()) { + Files.createDirectories(entry.getKey().getParent()); + Files.write(entry.getKey(), entry.getValue().getBytes(StandardCharsets.UTF_8)); + Preconditions.checkArgument(Files.exists(entry.getKey())); + } + for (Entry entry : filesToCopy.entrySet()) { + Path target = entry.getKey(); + Path source = entry.getValue(); + Files.createDirectories(target.getParent()); + Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); + } + } + + private Map rerootPaths(Map origMap, Path root, Path newRoot) { + Map newMap = new HashMap<>(); + for (Map.Entry origEntry : origMap.entrySet()) { + Path relPath = root.relativize(origEntry.getKey()); + newMap.put(newRoot.resolve(relPath), origEntry.getValue()); + } + return newMap; + } + +} diff --git a/src/test/java/com/google/devtools/build/android/AndroidDataMergerTest.java b/src/test/java/com/google/devtools/build/android/AndroidDataMergerTest.java new file mode 100644 index 0000000000..3e5ff0cee4 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/AndroidDataMergerTest.java @@ -0,0 +1,1300 @@ +// 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.Objects; +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.Subject; +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 javax.annotation.Nullable; +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.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, "way out") + .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, "way out") + .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, "way out") + .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, "way out") + .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") + .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.buildOn(directRoot, fqnFactory).build(); + + UnvalidatedAndroidData primary = + AndroidDataBuilder.of(primaryRoot) + .createManifest("AndroidManifest.xml", "com.google.mergetest") + .addResource( + "values/strings.xml", ResourceType.VALUE, "way out") + .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, "way out") + .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, "way out") + .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, + "", + " ", + "") + .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, + "", + " ", + "") + .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, + " \"", + " 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, + "") + .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 directSource = DataSource.of(directRootTwo.resolve("assets/" + assetFile)); + + ParsedAndroidData directDependency = + ParsedAndroidDataBuilder.builder() + .assets( + file().root(directRootOne).source(assetFile), + file().root(directRootTwo).source(assetFile)) + .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(directSource))) + .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 + unwrittenMergedAndroidData = + new SubjectFactory() { + @Override + public UnwrittenMergedAndroidDataSubject getSubject( + FailureStrategy fs, UnwrittenMergedAndroidData that) { + return new UnwrittenMergedAndroidDataSubject(fs, that); + } + }; + + static class UnwrittenMergedAndroidDataSubject + extends Subject { + + static final SubjectFactory + FACTORY = + new SubjectFactory() { + @Override + public UnwrittenMergedAndroidDataSubject getSubject( + FailureStrategy fs, UnwrittenMergedAndroidData that) { + return new UnwrittenMergedAndroidDataSubject(fs, that); + } + }; + + public UnwrittenMergedAndroidDataSubject( + FailureStrategy failureStrategy, @Nullable UnwrittenMergedAndroidData subject) { + super(failureStrategy, subject); + } + + public void isEqualTo(UnwrittenMergedAndroidData expected) { + UnwrittenMergedAndroidData subject = getSubject(); + if (!Objects.equal(subject, expected)) { + if (subject == null) { + assertThat(subject).isEqualTo(expected); + } + assertThat(subject.getManifest().toString()) + .named("manifest") + .isEqualTo(expected.getManifest().toString()); + + compareDataSets("resources", subject.getPrimary(), expected.getPrimary()); + compareDataSets("deps", subject.getTransitive(), expected.getTransitive()); + } + } + + private void compareDataSets( + String identifier, ParsedAndroidData subject, ParsedAndroidData expected) { + assertThat(subject.getOverwritingResources()) + .named("Overwriting " + identifier) + .containsExactlyEntriesIn(expected.getOverwritingResources()); + assertThat(subject.getCombiningResources()) + .named("Combining " + identifier) + .containsExactlyEntriesIn(expected.getCombiningResources()); + assertThat(subject.getAssets()) + .named("Assets " + identifier) + .containsExactlyEntriesIn(expected.getAssets()); + } + } + + private static final class TestLoggingHandler extends Handler { + public final List 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() throws SecurityException {} + } +} diff --git a/src/test/java/com/google/devtools/build/android/AndroidDataSerializerAndDeserializerTest.java b/src/test/java/com/google/devtools/build/android/AndroidDataSerializerAndDeserializerTest.java new file mode 100644 index 0000000000..c7ac512854 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/AndroidDataSerializerAndDeserializerTest.java @@ -0,0 +1,362 @@ +// 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.devtools.build.android.ParsedAndroidDataBuilder.file; +import static com.google.devtools.build.android.ParsedAndroidDataBuilder.xml; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.common.jimfs.Jimfs; +import com.google.common.truth.Truth; +import com.google.devtools.build.android.xml.IdXmlResourceValue; +import com.google.devtools.build.android.xml.ResourcesAttribute; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for the AndroidDataSerializer and AndroidDataDeserializer. */ +@RunWith(JUnit4.class) +public class AndroidDataSerializerAndDeserializerTest { + + private FileSystem fs; + private FullyQualifiedName.Factory fqnFactory; + private Path source; + private Path manifest; + + @Before + public void createCleanEnvironment() throws Exception { + fs = Jimfs.newFileSystem(); + fqnFactory = FullyQualifiedName.Factory.from(ImmutableList.of()); + source = Files.createDirectory(fs.getPath("source")); + manifest = Files.createFile(source.resolve("AndroidManifest.xml")); + } + + @Test + public void serializeAssets() throws Exception { + Path binaryPath = fs.getPath("out.bin"); + AndroidDataSerializer serializer = AndroidDataSerializer.create(); + UnwrittenMergedAndroidData expected = + UnwrittenMergedAndroidData.of( + manifest, + ParsedAndroidDataBuilder.buildOn(source) + .assets(file().source("hunting/of/the/boojum")) + .build(), + ParsedAndroidDataBuilder.empty()); + expected.serializeTo(serializer); + serializer.flushTo(binaryPath); + + AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); + TestMapConsumer assets = TestMapConsumer.ofAssets(); + deserializer.read(binaryPath, KeyValueConsumers.of(null, null, assets)); + Truth.assertThat(assets).isEqualTo(expected.getPrimary().getAssets()); + } + + @Test + public void serializeCombiningResource() throws Exception { + Path binaryPath = fs.getPath("out.bin"); + AndroidDataSerializer serializer = AndroidDataSerializer.create(); + UnwrittenMergedAndroidData expected = + UnwrittenMergedAndroidData.of( + manifest, + ParsedAndroidDataBuilder.buildOn(source, fqnFactory) + .combining( + xml("id/snark").source("values/ids.xml").value(IdXmlResourceValue.of())) + .build(), + ParsedAndroidDataBuilder.empty()); + expected.serializeTo(serializer); + serializer.flushTo(binaryPath); + + AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); + TestMapConsumer resources = TestMapConsumer.ofResources(); + deserializer.read( + binaryPath, + KeyValueConsumers.of( + null, // overwriting + resources, // combining + null // assets + )); + Truth.assertThat(resources).isEqualTo(expected.getPrimary().getCombiningResources()); + } + + @Test + public void serializeOverwritingResource() throws Exception { + Path binaryPath = fs.getPath("out.bin"); + AndroidDataSerializer serializer = AndroidDataSerializer.create(); + UnwrittenMergedAndroidData expected = + UnwrittenMergedAndroidData.of( + manifest, + ParsedAndroidDataBuilder.buildOn(source, fqnFactory) + .overwritable(file("layout/banker").source("layout/banker.xml")) + .build(), + ParsedAndroidDataBuilder.empty()); + expected.serializeTo(serializer); + serializer.flushTo(binaryPath); + + AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); + TestMapConsumer resources = TestMapConsumer.ofResources(); + deserializer.read( + binaryPath, + KeyValueConsumers.of( + resources, // overwriting + null, // combining + null // assets + )); + Truth.assertThat(resources).isEqualTo(expected.getPrimary().getOverwritingResources()); + } + + @Test + public void serializeFileWithIds() throws Exception { + Path binaryPath = fs.getPath("out.bin"); + AndroidDataSerializer serializer = AndroidDataSerializer.create(); + ParsedAndroidData direct = + AndroidDataBuilder.of(source) + .addResource( + "layout/some_layout.xml", + AndroidDataBuilder.ResourceType.LAYOUT, + "") + // Test what happens if a user accidentally uses the same ID in multiple layouts too. + .addResource( + "layout/another_layout.xml", + AndroidDataBuilder.ResourceType.LAYOUT, + "") + // Also check what happens if a value XML file also contains the same ID. + .addResource( + "values/ids.xml", + AndroidDataBuilder.ResourceType.VALUE, + "", + "") + .addResource( + "values/strings.xml", + AndroidDataBuilder.ResourceType.VALUE, + "I has a bucket") + .createManifest("AndroidManifest.xml", "com.carroll.lewis", "") + .buildParsed(); + UnwrittenMergedAndroidData expected = + UnwrittenMergedAndroidData.of( + manifest, + direct, + ParsedAndroidDataBuilder.empty()); + expected.serializeTo(serializer); + serializer.flushTo(binaryPath); + + AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); + TestMapConsumer overwriting = TestMapConsumer.ofResources(); + TestMapConsumer combining = TestMapConsumer.ofResources(); + deserializer.read( + binaryPath, + KeyValueConsumers.of( + overwriting, + combining, + null // assets + )); + Truth.assertThat(overwriting).isEqualTo(expected.getPrimary().getOverwritingResources()); + Truth.assertThat(combining).isEqualTo(expected.getPrimary().getCombiningResources()); + } + + @Test + public void serialize() throws Exception { + Path binaryPath = fs.getPath("out.bin"); + AndroidDataSerializer serializer = AndroidDataSerializer.create(); + UnwrittenMergedAndroidData expected = + UnwrittenMergedAndroidData.of( + manifest, + ParsedAndroidDataBuilder.buildOn(source, fqnFactory) + .overwritable( + file("layout/banker").source("layout/banker.xml"), + xml("/foo").source("values/ids.xml") + .value(ResourcesAttribute.of("foo", "fooVal"))) + .combining( + xml("id/snark").source("values/ids.xml").value(IdXmlResourceValue.of())) + .assets(file().source("hunting/of/the/boojum")) + .build(), + ParsedAndroidDataBuilder.buildOn(source, fqnFactory) + .overwritable(file("layout/butcher").source("layout/butcher.xml")) + .combining( + xml("id/snark").source("values/ids.xml").value(IdXmlResourceValue.of())) + .assets(file().source("hunting/of/the/snark")) + .build()); + expected.serializeTo(serializer); + serializer.flushTo(binaryPath); + + KeyValueConsumers primary = + KeyValueConsumers.of( + TestMapConsumer.ofResources(), // overwriting + TestMapConsumer.ofResources(), // combining + TestMapConsumer.ofAssets() // assets + ); + + AndroidDataDeserializer deserializer = AndroidDataDeserializer.create(); + deserializer.read(binaryPath, primary); + Truth.assertThat(primary.overwritingConsumer) + .isEqualTo(expected.getPrimary().getOverwritingResources()); + Truth.assertThat(primary.combiningConsumer) + .isEqualTo(expected.getPrimary().getCombiningResources()); + Truth.assertThat(primary.assetConsumer).isEqualTo(expected.getPrimary().getAssets()); + } + + @Test + public void testDeserializeMissing() throws Exception { + Path binaryPath = fs.getPath("out.bin"); + AndroidDataSerializer serializer = AndroidDataSerializer.create(); + UnwrittenMergedAndroidData expected = + UnwrittenMergedAndroidData.of( + manifest, + ParsedAndroidDataBuilder.buildOn(source, fqnFactory) + .overwritable( + file("layout/banker").source("layout/banker.xml"), + xml("/foo").source("values/ids.xml") + .value(ResourcesAttribute.of("foo", "fooVal"))) + .combining( + xml("id/snark").source("values/ids.xml").value(IdXmlResourceValue.of())) + .assets(file().source("hunting/of/the/boojum")) + .build(), + ParsedAndroidDataBuilder.buildOn(source, fqnFactory) + .overwritable(file("layout/butcher").source("layout/butcher.xml")) + .combining( + xml("id/snark").source("values/ids.xml").value(IdXmlResourceValue.of())) + .assets(file().source("hunting/of/the/snark")) + .build()); + expected.serializeTo(serializer); + serializer.flushTo(binaryPath); + + AndroidDataDeserializer deserializer = + AndroidDataDeserializer.withFilteredResources( + ImmutableList.of("the/boojum", "values/ids.xml", "layout/banker.xml")); + + KeyValueConsumers primary = + KeyValueConsumers.of( + TestMapConsumer.ofResources(), // overwriting + TestMapConsumer.ofResources(), // combining + null // assets + ); + + deserializer.read(binaryPath, primary); + Truth.assertThat(primary.overwritingConsumer).isEqualTo(Collections.emptyMap()); + Truth.assertThat(primary.combiningConsumer).isEqualTo(Collections.emptyMap()); + } + + private static class TestMapConsumer + implements ParsedAndroidData.KeyValueConsumer, Map { + + Map target; + + static TestMapConsumer ofAssets() { + return new TestMapConsumer<>(new HashMap()); + } + + static TestMapConsumer ofResources() { + return new TestMapConsumer<>(new HashMap()); + } + + public TestMapConsumer(Map target) { + this.target = target; + } + + @Override + public void consume(DataKey key, T value) { + target.put(key, value); + } + + @Override + public int size() { + return target.size(); + } + + @Override + public boolean isEmpty() { + return target.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return target.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return target.containsValue(value); + } + + @Override + public T get(Object key) { + return target.get(key); + } + + @Override + public T put(DataKey key, T value) { + return target.put(key, value); + } + + @Override + public T remove(Object key) { + return target.remove(key); + } + + @Override + public void putAll(Map m) { + target.putAll(m); + } + + @Override + public void clear() { + target.clear(); + } + + @Override + public Set keySet() { + return target.keySet(); + } + + @Override + public Collection values() { + return target.values(); + } + + @Override + public Set> entrySet() { + return target.entrySet(); + } + + @Override + public boolean equals(Object o) { + return target.equals(o); + } + + @Override + public int hashCode() { + return target.hashCode(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("target", target).toString(); + } + } +} diff --git a/src/test/java/com/google/devtools/build/android/AndroidDataWriterTest.java b/src/test/java/com/google/devtools/build/android/AndroidDataWriterTest.java new file mode 100644 index 0000000000..e9ee8dcb93 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/AndroidDataWriterTest.java @@ -0,0 +1,397 @@ +// 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 com.android.ide.common.internal.PngCruncher; +import com.android.ide.common.internal.PngException; +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.common.util.concurrent.MoreExecutors; +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for the AndroidDataWriter. */ +@RunWith(JUnit4.class) +public class AndroidDataWriterTest { + + private static final String START_RESOURCES = + new String(AndroidDataWriter.PRELUDE) + "", + "") + .addResource( + "values/stubs.xml", + AndroidDataBuilder.ResourceType.VALUE, + "") + .createManifest("AndroidManifest.xml", "com.carroll.lewis", "") + .buildParsed(); + MergedAndroidData actual = + UnwrittenMergedAndroidData.of( + source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty()) + .write(mergedDataWriter); + + assertAbout(paths).that(actual.getManifest()).exists(); + assertAbout(paths).that(actual.getResourceDir().resolve("values/values.xml")).exists(); + assertAbout(paths) + .that(actual.getResourceDir().resolve("values/values.xml")) + .xmlContentsIsEqualTo( + START_RESOURCES + ">", + "", + "", + "", + "", + "", + END_RESOURCES); + } + + @Test + public void writeResourceXmlWithQualfiers() throws Exception { + Path target = fs.getPath("target"); + Path source = fs.getPath("source"); + AndroidDataWriter mergedDataWriter = AndroidDataWriter.createWithDefaults(target); + ParsedAndroidData direct = + AndroidDataBuilder.of(source) + .addResource( + "values/ids.xml", + AndroidDataBuilder.ResourceType.VALUE, + "") + .addResource( + "values-en/ids.xml", + AndroidDataBuilder.ResourceType.VALUE, + "") + .createManifest("AndroidManifest.xml", "com.carroll.lewis", "") + .buildParsed(); + MergedAndroidData actual = + UnwrittenMergedAndroidData.of( + source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty()) + .write(mergedDataWriter); + + assertAbout(paths).that(actual.getManifest()).exists(); + assertAbout(paths).that(actual.getResourceDir().resolve("values/values.xml")).exists(); + assertAbout(paths) + .that(actual.getResourceDir().resolve("values/values.xml")) + .xmlContentsIsEqualTo( + START_RESOURCES + ">", + "", + END_RESOURCES); + assertAbout(paths).that(actual.getResourceDir().resolve("values-en/values.xml")).exists(); + assertAbout(paths) + .that(actual.getResourceDir().resolve("values-en/values.xml")) + .xmlContentsIsEqualTo( + START_RESOURCES + ">", + "", + END_RESOURCES); + } + + @Test + public void writePublicResourceSameNameDifferentType() throws Exception { + Path target = fs.getPath("target"); + Path source = fs.getPath("source"); + AndroidDataWriter mergedDataWriter = AndroidDataWriter.createWithDefaults(target); + ParsedAndroidData direct = + AndroidDataBuilder.of(source) + .addResource( + "values/integers.xml", + AndroidDataBuilder.ResourceType.VALUE, + "12345", + "", + "54321", + "") + .addResource( + "values/strings.xml", + AndroidDataBuilder.ResourceType.VALUE, + "meow", + "") + .createManifest("AndroidManifest.xml", "com.carroll.lewis", "") + .buildParsed(); + MergedAndroidData actual = + UnwrittenMergedAndroidData.of( + source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty()) + .write(mergedDataWriter); + + assertAbout(paths).that(actual.getManifest()).exists(); + assertAbout(paths).that(actual.getResourceDir().resolve("values/values.xml")).exists(); + assertAbout(paths) + .that(actual.getResourceDir().resolve("values/values.xml")) + .xmlContentsIsEqualTo( + START_RESOURCES + ">", + "", + "12345", + "54321", + "", + "meow", + "", + "", + "", + "", + END_RESOURCES); + } + + @Test + public void writeWithIDDuplicates() throws Exception { + // We parse IDs from layout, etc. XML. We can include it in the merged values.xml redundantly + // (redundant because we also give aapt the original layout xml, which it can parse for IDs + // too), but make sure we don't accidentally put multiple copies in the merged values.xml file. + // Otherwise, aapt will throw an error if there are duplicates in the same values.xml file. + Path target = fs.getPath("target"); + Path source = fs.getPath("source"); + AndroidDataWriter mergedDataWriter = AndroidDataWriter.createWithDefaults(target); + ParsedAndroidData direct = + AndroidDataBuilder.of(source) + .addResource( + "layout/some_layout.xml", + AndroidDataBuilder.ResourceType.LAYOUT, + "", + // Test redundantly having a "+id/MyTextView" in a different attribute. + "") + // Test what happens if a user accidentally uses the same ID in multiple layouts too. + .addResource( + "layout/another_layout.xml", + AndroidDataBuilder.ResourceType.LAYOUT, + "") + // Also check what happens if a value XML file also contains the same ID. + .addResource( + "values/ids.xml", + AndroidDataBuilder.ResourceType.VALUE, + "", + "") + .addResource( + "values/strings.xml", + AndroidDataBuilder.ResourceType.VALUE, + "I has a bucket") + .createManifest("AndroidManifest.xml", "com.carroll.lewis", "") + .buildParsed(); + MergedAndroidData actual = + UnwrittenMergedAndroidData.of( + source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty()) + .write(mergedDataWriter); + + assertAbout(paths).that(actual.getManifest()).exists(); + assertAbout(paths).that(actual.getResourceDir().resolve("layout/some_layout.xml")).exists(); + assertAbout(paths).that(actual.getResourceDir().resolve("values/values.xml")).exists(); + assertAbout(paths) + .that(actual.getResourceDir().resolve("values/values.xml")) + .xmlContentsIsEqualTo( + START_RESOURCES + ">", + "", + "", + "", + "", + "I has a bucket", + END_RESOURCES); + } + + @Test + public void writeResourceXmlWithAttributes() throws Exception { + Path target = fs.getPath("target"); + Path source = fs.getPath("source"); + AndroidDataWriter mergedDataWriter = AndroidDataWriter.createWithDefaults(target); + ParsedAndroidData direct = + AndroidDataBuilder.of(source) + .addValuesWithAttributes( + "values/ids.xml", + ImmutableMap.of("foo", "fooVal", "bar", "barVal"), + "", + "") + .addValuesWithAttributes( + "values/stubs.xml", + ImmutableMap.of("baz", "bazVal"), + "") + .createManifest("AndroidManifest.xml", "com.carroll.lewis", "") + .buildParsed(); + MergedAndroidData actual = + UnwrittenMergedAndroidData.of( + source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty()) + .write(mergedDataWriter); + + assertAbout(paths).that(actual.getManifest()).exists(); + assertAbout(paths).that(actual.getResourceDir().resolve("values/values.xml")).exists(); + assertAbout(paths) + .that(actual.getResourceDir().resolve("values/values.xml")) + .xmlContentsIsEqualTo( + START_RESOURCES + " foo=\"fooVal\" bar=\"barVal\" baz=\"bazVal\">", + "", + "", + "", + "", + "", + END_RESOURCES); + } + + @Test + public void writeAssetFile() throws Exception { + Path target = fs.getPath("target"); + Path source = fs.getPath("source"); + AndroidDataWriter mergedDataWriter = AndroidDataWriter.createWithDefaults(target); + String asset = "hunting/of/the/boojum"; + ParsedAndroidData direct = + AndroidDataBuilder.of(source) + .addAsset(asset, "not a snark!") + .createManifest("AndroidManifest.xml", "com.carroll.lewis", "") + .buildParsed(); + MergedAndroidData actual = + UnwrittenMergedAndroidData.of( + source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty()) + .write(mergedDataWriter); + + assertAbout(paths).that(actual.getManifest()).exists(); + assertAbout(paths).that(actual.getAssetDir().resolve(asset)).exists(); + } + + private static final SubjectFactory paths = + new SubjectFactory() { + @Override + public PathsSubject getSubject(FailureStrategy failureStrategy, Path path) { + return new PathsSubject(failureStrategy, path); + } + }; +} diff --git a/src/test/java/com/google/devtools/build/android/AndroidResourceClassWriterTest.java b/src/test/java/com/google/devtools/build/android/AndroidResourceClassWriterTest.java new file mode 100644 index 0000000000..72eac32a7f --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/AndroidResourceClassWriterTest.java @@ -0,0 +1,696 @@ +// 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 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 java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests for {@link AndroidResourceClassWriter}. + */ +@RunWith(JUnit4.class) +public class AndroidResourceClassWriterTest { + + private FileSystem fs; + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private static final AndroidFrameworkAttrIdProvider mockAndroidFrameworkIds = + new MockAndroidFrameworkAttrIdProvider(ImmutableMap.of()); + + @Before + public void createCleanEnvironment() { + fs = Jimfs.newFileSystem(); + } + + @Test + public void simpleIdFromLayout() throws Exception { + Path target = fs.getPath("target"); + Path source = fs.getPath("source"); + AndroidResourceClassWriter resourceClassWriter = + AndroidResourceClassWriter.of(mockAndroidFrameworkIds, target, "com.carroll.lewis"); + ParsedAndroidData direct = + AndroidDataBuilder.of(source) + .addResource( + "layout/some_layout.xml", + AndroidDataBuilder.ResourceType.LAYOUT, + "", + "