aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/test/java
diff options
context:
space:
mode:
authorGravatar Adam Michael <ajmichael@google.com>2017-03-22 15:33:40 +0000
committerGravatar Yue Gan <yueg@google.com>2017-03-23 09:47:34 +0000
commit2587a6dd010266c749b6234115b9333830a951a7 (patch)
treeb8ccfc24b3fc8a8e9ff9040c3f95a8edb952ed3b /src/test/java
parent832548dd91747cf8cef360c7bcb5a5f1e5ecf28e (diff)
Open source some Android tools' tests.
-- PiperOrigin-RevId: 150881315 MOS_MIGRATED_REVID=150881315
Diffstat (limited to 'src/test/java')
-rw-r--r--src/test/java/com/google/devtools/build/android/AaptCommandBuilderTest.java262
-rw-r--r--src/test/java/com/google/devtools/build/android/AndroidDataBuilder.java202
-rw-r--r--src/test/java/com/google/devtools/build/android/AndroidDataMergerTest.java1300
-rw-r--r--src/test/java/com/google/devtools/build/android/AndroidDataSerializerAndDeserializerTest.java362
-rw-r--r--src/test/java/com/google/devtools/build/android/AndroidDataWriterTest.java397
-rw-r--r--src/test/java/com/google/devtools/build/android/AndroidResourceClassWriterTest.java696
-rw-r--r--src/test/java/com/google/devtools/build/android/BUILD59
-rw-r--r--src/test/java/com/google/devtools/build/android/ClassPathsSubject.java165
-rw-r--r--src/test/java/com/google/devtools/build/android/DataResourceXmlTest.java1325
-rw-r--r--src/test/java/com/google/devtools/build/android/ParsedAndroidDataBuilder.java269
-rw-r--r--src/test/java/com/google/devtools/build/android/PathsSubject.java93
11 files changed, 5130 insertions, 0 deletions
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.<String>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<String> 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(
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>%s</resources>",
+ Joiner.on("\n").join(lines));
+ }
+ },
+ LAYOUT {
+ @Override
+ public String create(String... lines) {
+ return String.format(
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ + "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\""
+ + " android:layout_width=\"fill_parent\""
+ + " android:layout_height=\"fill_parent\">%s</LinearLayout>",
+ 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<Path, String> filesToWrite = new HashMap<>();
+ private Map<Path, Path> 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<String, String> attributes, String... lines) {
+ ImmutableList.Builder<String> attributeBuilder = ImmutableList.builder();
+ for (Entry<String, String> 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("<resources>",
+ String.format("<resources %s>", 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.<String>of(), lines);
+ }
+
+ public AndroidDataBuilder createManifest(
+ String path, String manifestPackage, List<String> namespaces, String... lines) {
+ this.manifest = root.resolve(path);
+ filesToWrite.put(
+ manifest,
+ String.format(
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" %s"
+ + " package=\"%s\">"
+ + "%s</manifest>",
+ 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<Path, String> 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<Path, Path> entry : filesToCopy.entrySet()) {
+ Path target = entry.getKey();
+ Path source = entry.getValue();
+ Files.createDirectories(target.getParent());
+ Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+
+ private <T> Map<Path, T> rerootPaths(Map<Path, T> origMap, Path root, Path newRoot) {
+ Map<Path, T> newMap = new HashMap<>();
+ for (Map.Entry<Path, T> 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.<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")
+ .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, "<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 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<UnwrittenMergedAndroidDataSubject, UnwrittenMergedAndroidData>
+ unwrittenMergedAndroidData =
+ new SubjectFactory<UnwrittenMergedAndroidDataSubject, UnwrittenMergedAndroidData>() {
+ @Override
+ public UnwrittenMergedAndroidDataSubject getSubject(
+ FailureStrategy fs, UnwrittenMergedAndroidData that) {
+ return new UnwrittenMergedAndroidDataSubject(fs, that);
+ }
+ };
+
+ static class UnwrittenMergedAndroidDataSubject
+ extends Subject<UnwrittenMergedAndroidDataSubject, UnwrittenMergedAndroidData> {
+
+ static final SubjectFactory<UnwrittenMergedAndroidDataSubject, UnwrittenMergedAndroidData>
+ FACTORY =
+ new SubjectFactory<UnwrittenMergedAndroidDataSubject, UnwrittenMergedAndroidData>() {
+ @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<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 {}
+ }
+}
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.<String>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<DataAsset> 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<DataResource> 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<DataResource> 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,
+ "<TextView android:id=\"@+id/MyTextView\"",
+ " android:text=\"@string/walrus\"",
+ " android:layout_width=\"wrap_content\"",
+ " android:layout_height=\"wrap_content\" />")
+ // Test what happens if a user accidentally uses the same ID in multiple layouts too.
+ .addResource(
+ "layout/another_layout.xml",
+ AndroidDataBuilder.ResourceType.LAYOUT,
+ "<TextView android:id=\"@+id/MyTextView\"",
+ " android:text=\"@string/walrus\"",
+ " android:layout_width=\"wrap_content\"",
+ " android:layout_height=\"wrap_content\" />")
+ // Also check what happens if a value XML file also contains the same ID.
+ .addResource(
+ "values/ids.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<item name=\"MyTextView\" type=\"id\"/>",
+ "<item name=\"OtherId\" type=\"id\"/>")
+ .addResource(
+ "values/strings.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<string name=\"walrus\">I has a bucket</string>")
+ .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<DataResource> overwriting = TestMapConsumer.ofResources();
+ TestMapConsumer<DataResource> 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("<resources>/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("<resources>/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<T extends DataValue>
+ implements ParsedAndroidData.KeyValueConsumer<DataKey, T>, Map<DataKey, T> {
+
+ Map<DataKey, T> target;
+
+ static TestMapConsumer<DataAsset> ofAssets() {
+ return new TestMapConsumer<>(new HashMap<DataKey, DataAsset>());
+ }
+
+ static TestMapConsumer<DataResource> ofResources() {
+ return new TestMapConsumer<>(new HashMap<DataKey, DataResource>());
+ }
+
+ public TestMapConsumer(Map<DataKey, T> 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<? extends DataKey, ? extends T> m) {
+ target.putAll(m);
+ }
+
+ @Override
+ public void clear() {
+ target.clear();
+ }
+
+ @Override
+ public Set<DataKey> keySet() {
+ return target.keySet();
+ }
+
+ @Override
+ public Collection<T> values() {
+ return target.values();
+ }
+
+ @Override
+ public Set<java.util.Map.Entry<DataKey, T>> 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) + "<resources";
+ private static final String END_RESOURCES = new String(AndroidDataWriter.END_RESOURCES);
+
+ private FileSystem fs;
+
+ @Before
+ public void createCleanEnvironment() {
+ fs = Jimfs.newFileSystem();
+ }
+
+ @Test
+ public void writeResourceFile() throws Exception {
+ Path target = fs.getPath("target");
+ Path source = fs.getPath("source");
+ AndroidDataWriter mergedDataWriter = AndroidDataWriter.createWithDefaults(target);
+ String drawable = "drawable/menu.gif";
+ String layout = "layout/foo.xml";
+ ParsedAndroidData direct =
+ AndroidDataBuilder.of(source)
+ .addResource(layout, AndroidDataBuilder.ResourceType.LAYOUT, "")
+ .addResourceBinary(drawable, Files.createFile(fs.getPath("menu.gif")))
+ .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(drawable)).exists();
+ assertAbout(paths).that(actual.getResourceDir().resolve(layout)).exists();
+ }
+
+ @Test
+ public void writePngInRawAndNotInRaw() throws Exception {
+ Path tmpDir = Files.createTempDirectory(this.toString());
+ final Path target = tmpDir.resolve("target");
+ final Path source = tmpDir.resolve("source");
+ final String drawable = "drawable/crunch.png";
+ final String raw = "raw/nocrunch.png";
+ final String layout = "layout/foo.xml";
+ AndroidDataWriter mergedDataWriter =
+ AndroidDataWriter.createWith(
+ target,
+ target.resolve("res"),
+ target.resolve("assets"),
+ new PngCruncher() {
+ @Override
+ public int start() {
+ return 0;
+ }
+
+ @Override
+ public void end(int key) throws InterruptedException {
+ }
+
+ @Override
+ public void crunchPng(int key, File from, File to)
+ throws PngException {
+ assertThat(from.toString()).doesNotContain(raw);
+ try {
+ Files.copy(from.toPath(), to.toPath());
+ } catch (IOException e) {
+ throw new PngException(e);
+ }
+ }
+ },
+ MoreExecutors.newDirectExecutorService());
+ ParsedAndroidData direct =
+ AndroidDataBuilder.of(source)
+ .addResource(layout, AndroidDataBuilder.ResourceType.LAYOUT, "")
+ .addResourceBinary(drawable, Files.createFile(tmpDir.resolve("crunch.png")))
+ .addResourceBinary(raw, Files.createFile(tmpDir.resolve("nocrunch.png")))
+ .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(drawable)).exists();
+ assertAbout(paths).that(actual.getResourceDir().resolve(raw)).exists();
+ assertAbout(paths).that(actual.getResourceDir().resolve(layout)).exists();
+ }
+
+ @Test
+ public void writeNinepatchResourceFile() throws Exception {
+ Path tmpDir = Files.createTempDirectory(this.toString());
+ Path target = tmpDir.resolve("target");
+ Path source = tmpDir.resolve("source");
+ AndroidDataWriter mergedDataWriter = AndroidDataWriter.createWithDefaults(target);
+ String drawable = "drawable-hdpi-v4/seven_eight.9.png";
+ ParsedAndroidData direct =
+ AndroidDataBuilder.of(source)
+ .addResourceBinary(drawable, Files.createFile(fs.getPath("seven_eight.9.png")))
+ .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(drawable)).exists();
+ }
+
+ @Test
+ public void writeResourceXml() 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,
+ "<item name=\"id1\" type=\"id\"/>",
+ "<item name=\"id\" type=\"id\"/>")
+ .addResource(
+ "values/stubs.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<item name=\"walrus\" type=\"drawable\"/>")
+ .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 + ">",
+ "<!-- source/res/values/stubs.xml --><eat-comment/>",
+ "<item name='walrus' type='drawable'/>",
+ "<!-- source/res/values/ids.xml --><eat-comment/>",
+ "<item name='id' type='id'/>",
+ "<item name='id1' type='id'/>",
+ 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,
+ "<item name=\"id1\" type=\"id\"/>")
+ .addResource(
+ "values-en/ids.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<item name=\"id1\" type=\"id\"/>")
+ .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 + ">",
+ "<!-- source/res/values/ids.xml --><eat-comment/><item name='id1' type='id'/>",
+ 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 + ">",
+ "<!-- source/res/values-en/ids.xml --><eat-comment/><item name='id1' type='id'/>",
+ 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,
+ "<integer name=\"foo\">12345</integer>",
+ "<public name=\"foo\" type=\"integer\" id=\"0x7f040000\"/>",
+ "<integer name=\"zoo\">54321</integer>",
+ "<public name=\"zoo\" type=\"integer\" />")
+ .addResource(
+ "values/strings.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<string name=\"foo\">meow</string>",
+ "<public name=\"foo\" type=\"string\" id=\"0x7f050000\"/>")
+ .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 + ">",
+ "<!-- source/res/values/integers.xml --><eat-comment/>",
+ "<integer name='foo'>12345</integer>",
+ "<integer name='zoo'>54321</integer>",
+ "<!-- source/res/values/strings.xml --><eat-comment/>",
+ "<string name='foo'>meow</string>",
+ "<!-- source/res/values/integers.xml --><eat-comment/>",
+ "<public name='foo' type='integer' id='0x7f040000'/>",
+ "<public name='foo' type='string' id='0x7f050000'/>",
+ "<public name='zoo' type='integer' />",
+ 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,
+ "<TextView android:id=\"@+id/MyTextView\"",
+ " android:text=\"@string/walrus\"",
+ " android:layout_above=\"@+id/AnotherTextView\"",
+ " android:layout_width=\"wrap_content\"",
+ " android:layout_height=\"wrap_content\" />",
+ // Test redundantly having a "+id/MyTextView" in a different attribute.
+ "<TextView android:id=\"@id/AnotherTextView\"",
+ " android:text=\"@string/walrus\"",
+ " android:layout_below=\"@+id/MyTextView\"",
+ " android:layout_width=\"wrap_content\"",
+ " android:layout_height=\"wrap_content\" />")
+ // Test what happens if a user accidentally uses the same ID in multiple layouts too.
+ .addResource(
+ "layout/another_layout.xml",
+ AndroidDataBuilder.ResourceType.LAYOUT,
+ "<TextView android:id=\"@+id/MyTextView\"",
+ " android:text=\"@string/walrus\"",
+ " android:layout_width=\"wrap_content\"",
+ " android:layout_height=\"wrap_content\" />")
+ // Also check what happens if a value XML file also contains the same ID.
+ .addResource(
+ "values/ids.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<item name=\"MyTextView\" type=\"id\"/>",
+ "<item name=\"OtherId\" type=\"id\"/>")
+ .addResource(
+ "values/strings.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<string name=\"walrus\">I has a bucket</string>")
+ .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 + ">",
+ "<!-- source/res/values/ids.xml --><eat-comment/>",
+ "<item name='MyTextView' type='id'/>",
+ "<item name='OtherId' type='id'/>",
+ "<!-- source/res/values/strings.xml --><eat-comment/>",
+ "<string name='walrus'>I has a bucket</string>",
+ 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"),
+ "<item name=\"id1\" type=\"id\"/>",
+ "<item name=\"id\" type=\"id\"/>")
+ .addValuesWithAttributes(
+ "values/stubs.xml",
+ ImmutableMap.of("baz", "bazVal"),
+ "<item name=\"walrus\" type=\"drawable\"/>")
+ .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\">",
+ "<!-- source/res/values/stubs.xml --><eat-comment/>",
+ "<item name='walrus' type='drawable'/>",
+ "<!-- source/res/values/ids.xml --><eat-comment/>",
+ "<item name='id' type='id'/>",
+ "<item name='id1' type='id'/>",
+ 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<PathsSubject, Path> paths =
+ new SubjectFactory<PathsSubject, Path>() {
+ @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.<String, Integer>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,
+ "<TextView android:id=\"@+id/HelloView\"",
+ " android:text=\"Hello World!\"",
+ " android:layout_width=\"wrap_content\"",
+ " android:layout_height=\"wrap_content\" />",
+ "<Button android:id=\"@+id/AdiosButton\"",
+ " android:text=\"Adios!\"",
+ " android:layout_width=\"wrap_content\"",
+ " android:layout_height=\"wrap_content\" />")
+ .createManifest("AndroidManifest.xml", "com.carroll.lewis", "")
+ .buildParsed();
+ UnwrittenMergedAndroidData unwrittenMergedAndroidData =
+ UnwrittenMergedAndroidData.of(
+ source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty());
+ unwrittenMergedAndroidData
+ .writeResourceClass(resourceClassWriter);
+
+ assertAbout(paths)
+ .that(target.resolve("com/carroll/lewis/R.java"))
+ .javaContentsIsEqualTo(
+ "package com.carroll.lewis;",
+ "public final class R {",
+ "public static final class id {",
+ "public static int AdiosButton = 0x7f030000;",
+ "public static int HelloView = 0x7f030001;",
+ "}",
+ "public static final class layout {",
+ "public static int some_layout = 0x7f020000;",
+ "}",
+ "}"
+ );
+ assertAbout(paths)
+ .that(target)
+ .withClass("com.carroll.lewis.R$id")
+ .classContentsIsEqualTo(
+ ImmutableMap.of(
+ "AdiosButton", 0x7f030000,
+ "HelloView", 0x7f030001),
+ ImmutableMap.<String, List<Integer>>of(),
+ false
+ );
+ assertAbout(paths)
+ .that(target)
+ .withClass("com.carroll.lewis.R$layout")
+ .classContentsIsEqualTo(
+ ImmutableMap.of("some_layout", 0x7f020000),
+ ImmutableMap.<String, List<Integer>>of(),
+ false
+ );
+ }
+
+ @Test
+ public void ninePatchFieldNames() throws Exception {
+ Path target = fs.getPath("target");
+ Path source = fs.getPath("source");
+ String drawable = "drawable/light.png";
+ String ninePatch = "drawable/patchface.9.png";
+ AndroidResourceClassWriter resourceClassWriter =
+ AndroidResourceClassWriter.of(mockAndroidFrameworkIds, target, "com.boop");
+ ParsedAndroidData direct =
+ AndroidDataBuilder.of(source)
+ .addResourceBinary(
+ drawable,
+ Files.createFile(fs.getPath("lightbringer.png")))
+ .addResourceBinary(
+ ninePatch,
+ Files.createFile(fs.getPath("patchface.9.png")))
+ .createManifest("AndroidManifest.xml", "com.boop", "")
+ .buildParsed();
+ UnwrittenMergedAndroidData unwrittenMergedAndroidData =
+ UnwrittenMergedAndroidData.of(
+ source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty());
+ unwrittenMergedAndroidData
+ .writeResourceClass(resourceClassWriter);
+ assertAbout(paths)
+ .that(target.resolve("com/boop/R.java"))
+ .javaContentsIsEqualTo(
+ "package com.boop;",
+ "public final class R {",
+ "public static final class drawable {",
+ "public static int light = 0x7f020000;",
+ "public static int patchface = 0x7f020001;",
+ "}",
+ "}"
+ );
+ assertAbout(paths)
+ .that(target)
+ .withClass("com.boop.R$drawable")
+ .classContentsIsEqualTo(
+ ImmutableMap.of(
+ "light", 0x7f020000,
+ "patchface", 0x7f020001),
+ ImmutableMap.<String, List<Integer>>of(),
+ false
+ );
+ }
+
+ @Test
+ public void unionOfResourcesInConfigurations() throws Exception {
+ // See what happens if there are some configuration specific resources
+ // (selection guarded by checks at runtime?).
+ Path target = fs.getPath("target");
+ Path source = fs.getPath("source");
+ String drawable = "drawable/light.png";
+ String drawableV18 = "drawable-v18/light18.png";
+ String drawableV19 = "drawable-xxhdpi-v19/light19.png";
+ String drawableV20 = "drawable-ldltr-v20/light20.png";
+ Path stubImage = Files.createFile(fs.getPath("stub.png"));
+ AndroidResourceClassWriter resourceClassWriter =
+ AndroidResourceClassWriter.of(mockAndroidFrameworkIds, target, "com.boop");
+ ParsedAndroidData direct =
+ AndroidDataBuilder.of(source)
+ .addResourceBinary(drawable, stubImage)
+ .addResourceBinary(drawableV18, stubImage)
+ .addResourceBinary(drawableV19, stubImage)
+ .addResourceBinary(drawableV20, stubImage)
+ .createManifest("AndroidManifest.xml", "com.boop", "")
+ .buildParsed();
+ UnwrittenMergedAndroidData unwrittenMergedAndroidData =
+ UnwrittenMergedAndroidData.of(
+ source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty());
+ unwrittenMergedAndroidData
+ .writeResourceClass(resourceClassWriter);
+ assertAbout(paths)
+ .that(target.resolve("com/boop/R.java"))
+ .javaContentsIsEqualTo(
+ "package com.boop;",
+ "public final class R {",
+ "public static final class drawable {",
+ "public static int light = 0x7f020000;",
+ "public static int light18 = 0x7f020001;",
+ "public static int light19 = 0x7f020002;",
+ "public static int light20 = 0x7f020003;",
+ "}",
+ "}"
+ );
+ assertAbout(paths)
+ .that(target)
+ .withClass("com.boop.R$drawable")
+ .classContentsIsEqualTo(
+ ImmutableMap.of(
+ "light", 0x7f020000,
+ "light18", 0x7f020001,
+ "light19", 0x7f020002,
+ "light20", 0x7f020003),
+ ImmutableMap.<String, List<Integer>>of(),
+ false
+ );
+ }
+
+ @Test
+ public void normalizeStyleAndStyleableNames() throws Exception {
+ // Style and Styleables can have dots in the name. In order for it to be a legal Java
+ // identifier, the dots are converted to underscore.
+ Path target = fs.getPath("target");
+ Path source = fs.getPath("source");
+ Path transitive = fs.getPath("transitive");
+
+ AndroidResourceClassWriter resourceClassWriter =
+ AndroidResourceClassWriter.of(mockAndroidFrameworkIds, target, "com.carroll.lewis");
+ ParsedAndroidData direct =
+ AndroidDataBuilder.of(source)
+ .addResource(
+ "values/attr.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<attr name=\"y_color\" format=\"color\" />",
+ "<attr name=\"z_color\" format=\"color\" />"
+ )
+ .addResource(
+ "values/style.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<style name=\"YStyle\">",
+ " <item name=\"y_color\">#FF00FF00</item>",
+ "</style>",
+ "<style name=\"ZStyle.ABC\" parent=\"YStyle\">",
+ " <item name=\"z_color\">#00FFFF00</item>",
+ "</style>"
+ )
+ .addResource(
+ "values/styleable.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<declare-styleable name=\"com.google.android.Dots\">",
+ " <attr name=\"y_color\"/>",
+ " <attr name=\"z_color\"/>",
+ " <attr name=\"x_color\"/>",
+ "</declare-styleable>"
+ )
+ .createManifest("AndroidManifest.xml", "com.carroll.lewis", "")
+ .buildParsed();
+
+ ParsedAndroidData transitiveDep =
+ AndroidDataBuilder.of(transitive)
+ .addResource(
+ "values/attr.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<attr name=\"x_color\" format=\"color\" />"
+ )
+ .addResource(
+ "values/styleable.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<declare-styleable name=\"com.google.android.Swirls.Fancy\">",
+ " <attr name=\"z_color\"/>",
+ " <attr name=\"x_color\"/>",
+ " <attr name=\"y_color\"/>",
+ "</declare-styleable>"
+ )
+ .createManifest("AndroidManifest.xml", "com.library", "")
+ .buildParsed();
+
+ UnwrittenMergedAndroidData unwrittenMergedAndroidData =
+ UnwrittenMergedAndroidData.of(
+ source.resolve("AndroidManifest.xml"),
+ direct,
+ transitiveDep);
+ unwrittenMergedAndroidData
+ .writeResourceClass(resourceClassWriter);
+
+ assertAbout(paths)
+ .that(target.resolve("com/carroll/lewis/R.java"))
+ .javaContentsIsEqualTo(
+ "package com.carroll.lewis;",
+ "public final class R {",
+ "public static final class attr {",
+ "public static int x_color = 0x7f010000;",
+ "public static int y_color = 0x7f010001;",
+ "public static int z_color = 0x7f010002;",
+ "}",
+ "public static final class style {",
+ "public static int YStyle = 0x7f020000;",
+ "public static int ZStyle_ABC = 0x7f020001;",
+ "}",
+ "public static final class styleable {",
+ "public static int[] com_google_android_Dots = { 0x7f010000, 0x7f010001, 0x7f010002 };",
+ "public static int com_google_android_Dots_x_color = 0x0;",
+ "public static int com_google_android_Dots_y_color = 0x1;",
+ "public static int com_google_android_Dots_z_color = 0x2;",
+ "public static int[] com_google_android_Swirls_Fancy ="
+ + " { 0x7f010000, 0x7f010001, 0x7f010002 };",
+ "public static int com_google_android_Swirls_Fancy_x_color = 0x0;",
+ "public static int com_google_android_Swirls_Fancy_y_color = 0x1;",
+ "public static int com_google_android_Swirls_Fancy_z_color = 0x2;",
+ "}",
+ "}"
+ );
+ assertAbout(paths)
+ .that(target)
+ .withClass("com.carroll.lewis.R$attr")
+ .classContentsIsEqualTo(
+ ImmutableMap.of(
+ "x_color", 0x7f010000,
+ "y_color", 0x7f010001,
+ "z_color", 0x7f010002),
+ ImmutableMap.<String, List<Integer>>of(),
+ false
+ );
+ assertAbout(paths)
+ .that(target)
+ .withClass("com.carroll.lewis.R$style")
+ .classContentsIsEqualTo(
+ ImmutableMap.of(
+ "YStyle", 0x7f020000,
+ "ZStyle_ABC", 0x7f020001),
+ ImmutableMap.<String, List<Integer>>of(),
+ false
+ );
+ assertAbout(paths)
+ .that(target)
+ .withClass("com.carroll.lewis.R$styleable")
+ .classContentsIsEqualTo(
+ ImmutableMap.<String, Integer>builder()
+ .put("com_google_android_Dots_x_color", 0)
+ .put("com_google_android_Dots_y_color", 1)
+ .put("com_google_android_Dots_z_color", 2)
+ .put("com_google_android_Swirls_Fancy_x_color", 0)
+ .put("com_google_android_Swirls_Fancy_y_color", 1)
+ .put("com_google_android_Swirls_Fancy_z_color", 2)
+ .build(),
+ ImmutableMap.<String, List<Integer>>of(
+ "com_google_android_Dots",
+ ImmutableList.of(0x7f010000, 0x7f010001, 0x7f010002),
+ "com_google_android_Swirls_Fancy",
+ ImmutableList.of(0x7f010000, 0x7f010001, 0x7f010002)
+ ),
+ false
+ );
+ }
+
+ @Test
+ public void handleAndroidFrameworkAttributes() throws Exception {
+ // Attributes in the styleable array need to be sorted by integer ID value, so android
+ // framework attributes need to come before application attributes.
+ Path target = fs.getPath("target");
+ Path source = fs.getPath("source");
+ AndroidResourceClassWriter resourceClassWriter =
+ AndroidResourceClassWriter.of(
+ new MockAndroidFrameworkAttrIdProvider(
+ ImmutableMap.of(
+ "textColor", 0x01000000,
+ "textColorSecondary", 0x01000006,
+ "textSize", 0x01000010)),
+ target,
+ "com.carroll.lewis");
+ ParsedAndroidData direct =
+ AndroidDataBuilder.of(source)
+ .addResource(
+ "values/attr.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<attr name=\"aaa\" format=\"boolean\" />",
+ "<attr name=\"zzz\" format=\"boolean\" />"
+ )
+ .addResource(
+ "values/style.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<style name=\"YStyle\">",
+ " <item name=\"android:textSize\">15sp</item>",
+ " <item name=\"android:textColor\">#ffffff</item>",
+ " <item name=\"android:textColorSecondary\">#ffffff</item>",
+ "</style>")
+ .addResource(
+ "values/styleable.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<declare-styleable name=\"com.google.android.Dots\">",
+ " <attr name=\"aaa\"/>",
+ " <attr name=\"zzz\"/>",
+ // the android framework attr should be sorted first, even if it's alphabetically
+ // after the "aaa" attribute.
+ " <attr name=\"android:textSize\"/>",
+ " <attr name=\"android:textColor\"/>",
+ "</declare-styleable>"
+ )
+ .createManifest("AndroidManifest.xml", "com.carroll.lewis", "")
+ .buildParsed();
+ UnwrittenMergedAndroidData unwrittenMergedAndroidData =
+ UnwrittenMergedAndroidData.of(
+ source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty());
+ unwrittenMergedAndroidData
+ .writeResourceClass(resourceClassWriter);
+
+ assertAbout(paths)
+ .that(target.resolve("com/carroll/lewis/R.java"))
+ .javaContentsIsEqualTo(
+ "package com.carroll.lewis;",
+ "public final class R {",
+ "public static final class attr {",
+ "public static int aaa = 0x7f010000;",
+ "public static int zzz = 0x7f010001;",
+ "}",
+ "public static final class style {",
+ "public static int YStyle = 0x7f020000;",
+ "}",
+ "public static final class styleable {",
+ "public static int[] com_google_android_Dots = "
+ + "{ 0x1000000, 0x1000010, 0x7f010000, 0x7f010001 };",
+ "public static int com_google_android_Dots_android_textColor = 0x0;",
+ "public static int com_google_android_Dots_android_textSize = 0x1;",
+ "public static int com_google_android_Dots_aaa = 0x2;",
+ "public static int com_google_android_Dots_zzz = 0x3;",
+ "}",
+ "}"
+ );
+ assertAbout(paths)
+ .that(target)
+ .withClass("com.carroll.lewis.R$attr")
+ .classContentsIsEqualTo(
+ ImmutableMap.of(
+ "aaa", 0x7f010000,
+ "zzz", 0x7f010001),
+ ImmutableMap.<String, List<Integer>>of(),
+ false
+ );
+ assertAbout(paths)
+ .that(target)
+ .withClass("com.carroll.lewis.R$style")
+ .classContentsIsEqualTo(
+ ImmutableMap.of(
+ "YStyle", 0x7f020000),
+ ImmutableMap.<String, List<Integer>>of(),
+ false
+ );
+ assertAbout(paths)
+ .that(target)
+ .withClass("com.carroll.lewis.R$styleable")
+ .classContentsIsEqualTo(
+ ImmutableMap.of(
+ "com_google_android_Dots_android_textColor", 0,
+ "com_google_android_Dots_android_textSize", 1,
+ "com_google_android_Dots_aaa", 2,
+ "com_google_android_Dots_zzz", 3
+ ),
+ ImmutableMap.<String, List<Integer>>of(
+ "com_google_android_Dots",
+ ImmutableList.of(0x01000000, 0x01000010, 0x7f010000, 0x7f010001)
+ ),
+ false
+ );
+ }
+
+ @Test
+ public void missingFrameworkAttribute() throws Exception {
+ Path target = fs.getPath("target");
+ Path source = fs.getPath("source");
+ AndroidResourceClassWriter resourceClassWriter =
+ AndroidResourceClassWriter.of(
+ new MockAndroidFrameworkAttrIdProvider(ImmutableMap.<String, Integer>of()),
+ target,
+ "com.carroll.lewis");
+ ParsedAndroidData direct =
+ AndroidDataBuilder.of(source)
+ .addResource(
+ "values/attr.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<attr name=\"aaazzz\" format=\"boolean\" />"
+ )
+ .addResource(
+ "values/styleable.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<declare-styleable name=\"com.google.android.Dots\">",
+ " <attr name=\"aaazzz\"/>",
+ " <attr name=\"android:aaazzz\"/>",
+ "</declare-styleable>"
+ )
+ .createManifest("AndroidManifest.xml", "com.carroll.lewis", "")
+ .buildParsed();
+ UnwrittenMergedAndroidData unwrittenMergedAndroidData =
+ UnwrittenMergedAndroidData.of(
+ source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty());
+ thrown.expect(IOException.class);
+ thrown.expectMessage("Android attribute not found: aaazzz");
+ unwrittenMergedAndroidData
+ .writeResourceClass(resourceClassWriter);
+ }
+
+ @Test
+ public void missingAppAttribute() throws Exception {
+ Path target = fs.getPath("target");
+ Path source = fs.getPath("source");
+ AndroidResourceClassWriter resourceClassWriter =
+ AndroidResourceClassWriter.of(
+ new MockAndroidFrameworkAttrIdProvider(ImmutableMap.<String, Integer>of()),
+ target,
+ "com.carroll.lewis");
+ ParsedAndroidData direct =
+ AndroidDataBuilder.of(source)
+ .addResource(
+ "values/styleable.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<declare-styleable name=\"com.google.android.Dots\">",
+ " <attr name=\"aaazzz\"/>",
+ "</declare-styleable>"
+ )
+ .createManifest("AndroidManifest.xml", "com.carroll.lewis", "")
+ .buildParsed();
+ UnwrittenMergedAndroidData unwrittenMergedAndroidData =
+ UnwrittenMergedAndroidData.of(
+ source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty());
+ thrown.expect(IOException.class);
+ thrown.expectMessage("App attribute not found: aaazzz");
+ unwrittenMergedAndroidData
+ .writeResourceClass(resourceClassWriter);
+ }
+
+ /**
+ * Test what happens if we try to create a field name that is not a valid Java identifier. Here,
+ * we start the field name with a number, which is not legal according to {@link
+ * Character#isJavaIdentifierStart}.
+ *
+ * See: {@link com.android.ide.common.res2.FileResourceNameValidator}, and {@link
+ * com.android.ide.common.res2.ValueResourceNameValidator}.
+ *
+ * AAPT seems to miss out on checking this case (it only checks for [a-z0-9_.], but isn't
+ * position-sensitive).
+ */
+ @Test
+ public void illegalFileResFieldNamesStart() throws Exception {
+ Path target = fs.getPath("target");
+ Path source = fs.getPath("source");
+ String drawable = "drawable/1.png";
+ assertThat(Character.isJavaIdentifierStart('1')).isFalse();
+ AndroidResourceClassWriter resourceClassWriter =
+ AndroidResourceClassWriter.of(mockAndroidFrameworkIds, target, "com.boop");
+ ParsedAndroidData direct =
+ AndroidDataBuilder.of(source)
+ .addResourceBinary(
+ drawable,
+ Files.createFile(fs.getPath("1.png")))
+ .createManifest("AndroidManifest.xml", "com.boop", "")
+ .buildParsed();
+ UnwrittenMergedAndroidData unwrittenMergedAndroidData =
+ UnwrittenMergedAndroidData.of(
+ source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty());
+ unwrittenMergedAndroidData
+ .writeResourceClass(resourceClassWriter);
+ assertAbout(paths)
+ .that(target.resolve("com/boop/R.java"))
+ .javaContentsIsEqualTo(
+ "package com.boop;",
+ "public final class R {",
+ "public static final class drawable {",
+ "public static int 1 = 0x7f020000;",
+ "}",
+ "}"
+ );
+ assertAbout(paths)
+ .that(target)
+ .withClass("com.boop.R$drawable")
+ .classContentsIsEqualTo(
+ ImmutableMap.of("1", 0x7f020000),
+ ImmutableMap.<String, List<Integer>>of(),
+ false
+ );
+ }
+
+ /**
+ * Test embedding a character that doesn't satisfy Character#isJavaIdentifierPart. Do so in a file
+ * resource. In this case, AAPT will actually complain, so we may not need to do earlier
+ * validation.
+ */
+ @Test
+ public void illegalFileResFieldNamesCharacters() throws Exception {
+ Path target = fs.getPath("target");
+ Path source = fs.getPath("source");
+ String drawable = "drawable/c++.png";
+ assertThat(Character.isJavaIdentifierStart('c')).isTrue();
+ assertThat(Character.isJavaIdentifierPart('+')).isFalse();
+ AndroidResourceClassWriter resourceClassWriter =
+ AndroidResourceClassWriter.of(mockAndroidFrameworkIds, target, "com.boop");
+ ParsedAndroidData direct =
+ AndroidDataBuilder.of(source)
+ .addResourceBinary(
+ drawable,
+ Files.createFile(fs.getPath("phone#.png")))
+ .createManifest("AndroidManifest.xml", "com.boop", "")
+ .buildParsed();
+ UnwrittenMergedAndroidData unwrittenMergedAndroidData =
+ UnwrittenMergedAndroidData.of(
+ source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty());
+ unwrittenMergedAndroidData
+ .writeResourceClass(resourceClassWriter);
+ assertAbout(paths)
+ .that(target.resolve("com/boop/R.java"))
+ .javaContentsIsEqualTo(
+ "package com.boop;",
+ "public final class R {",
+ "public static final class drawable {",
+ "public static int c++ = 0x7f020000;",
+ "}",
+ "}"
+ );
+ assertAbout(paths)
+ .that(target)
+ .withClass("com.boop.R$drawable")
+ .classContentsIsEqualTo(
+ ImmutableMap.of("c++", 0x7f020000),
+ ImmutableMap.<String, List<Integer>>of(),
+ false
+ );
+ }
+
+ /**
+ * Test embedding a character that doesn't satisfy Character#isJavaIdentifierPart. Do so in a
+ * value resource. This is a case that AAPT doesn't validate, so it may pass through to the java
+ * compiler.
+ */
+ @Test
+ public void illegalValueResFieldNamesCharacters() throws Exception {
+ Path target = fs.getPath("target");
+ Path source = fs.getPath("source");
+ assertThat(Character.isJavaIdentifierStart('c')).isTrue();
+ assertThat(Character.isJavaIdentifierPart('+')).isFalse();
+ AndroidResourceClassWriter resourceClassWriter =
+ AndroidResourceClassWriter.of(mockAndroidFrameworkIds, target, "com.boop");
+ ParsedAndroidData direct =
+ AndroidDataBuilder.of(source)
+ .addResource(
+ "values/integers.xml",
+ AndroidDataBuilder.ResourceType.VALUE,
+ "<integer name=\"c++\">0xd</integer>"
+ )
+ .createManifest("AndroidManifest.xml", "com.boop", "")
+ .buildParsed();
+ UnwrittenMergedAndroidData unwrittenMergedAndroidData =
+ UnwrittenMergedAndroidData.of(
+ source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty());
+ unwrittenMergedAndroidData
+ .writeResourceClass(resourceClassWriter);
+ assertAbout(paths)
+ .that(target.resolve("com/boop/R.java"))
+ .javaContentsIsEqualTo(
+ "package com.boop;",
+ "public final class R {",
+ "public static final class integer {",
+ "public static int c++ = 0x7f020000;",
+ "}",
+ "}"
+ );
+ assertAbout(paths)
+ .that(target)
+ .withClass("com.boop.R$integer")
+ .classContentsIsEqualTo(
+ ImmutableMap.of("c++", 0x7f020000),
+ ImmutableMap.<String, List<Integer>>of(),
+ false
+ );
+ }
+
+ private static class MockAndroidFrameworkAttrIdProvider
+ implements AndroidFrameworkAttrIdProvider {
+
+ private final Map<String, Integer> mapToUse;
+
+ MockAndroidFrameworkAttrIdProvider(Map<String, Integer> mapToUse) {
+ this.mapToUse = mapToUse;
+ }
+
+ @Override
+ public int getAttrId(String fieldName) throws AttrLookupException {
+ if (mapToUse.containsKey(fieldName)) {
+ return mapToUse.get(fieldName);
+ }
+ throw new AttrLookupException("Android attribute not found: " + fieldName);
+ }
+ }
+
+ private static final SubjectFactory<ClassPathsSubject, Path> paths =
+ new SubjectFactory<ClassPathsSubject, Path>() {
+ @Override
+ public ClassPathsSubject getSubject(FailureStrategy failureStrategy, Path path) {
+ return new ClassPathsSubject(failureStrategy, path);
+ }
+ };
+}
diff --git a/src/test/java/com/google/devtools/build/android/BUILD b/src/test/java/com/google/devtools/build/android/BUILD
index 411d0f3bb2..11d14b9b75 100644
--- a/src/test/java/com/google/devtools/build/android/BUILD
+++ b/src/test/java/com/google/devtools/build/android/BUILD
@@ -21,6 +21,50 @@ java_test(
)
java_test(
+ name = "AndroidDataMergerTest",
+ srcs = ["AndroidDataMergerTest.java"],
+ tags = ["no_windows"], # Test asserts forward slashes in android data xml files.
+ deps = [
+ ":test_utils",
+ "//src/tools/android/java/com/google/devtools/build/android:android_builder_lib",
+ "//third_party:android_common_25_0_0",
+ "//third_party:guava",
+ "//third_party:jimfs",
+ "//third_party:junit4",
+ "//third_party:truth",
+ ],
+)
+
+java_test(
+ name = "AndroidDataWriterTest",
+ srcs = ["AndroidDataWriterTest.java"],
+ tags = ["no_windows"], # Test asserts forward slashes in android data xml files.
+ deps = [
+ ":test_utils",
+ "//src/tools/android/java/com/google/devtools/build/android:android_builder_lib",
+ "//third_party:android_common_25_0_0",
+ "//third_party:guava",
+ "//third_party:jimfs",
+ "//third_party:junit4",
+ "//third_party:truth",
+ ],
+)
+
+java_test(
+ name = "AndroidResourceClassWriterTest",
+ srcs = ["AndroidResourceClassWriterTest.java"],
+ tags = ["no_windows"], # Test asserts forward slashes in android data xml files.
+ deps = [
+ ":test_utils",
+ "//src/tools/android/java/com/google/devtools/build/android:android_builder_lib",
+ "//third_party:guava",
+ "//third_party:jimfs",
+ "//third_party:junit4",
+ "//third_party:truth",
+ ],
+)
+
+java_test(
name = "ConvertersTest",
size = "small",
srcs = ["ConvertersTest.java"],
@@ -32,3 +76,18 @@ java_test(
"//third_party:truth",
],
)
+
+java_library(
+ name = "test_utils",
+ testonly = 1,
+ srcs = glob(
+ ["*.java"],
+ exclude = ["*Test.java"],
+ ),
+ deps = [
+ "//src/tools/android/java/com/google/devtools/build/android:android_builder_lib",
+ "//third_party:android_common_25_0_0",
+ "//third_party:guava",
+ "//third_party:truth",
+ ],
+)
diff --git a/src/test/java/com/google/devtools/build/android/ClassPathsSubject.java b/src/test/java/com/google/devtools/build/android/ClassPathsSubject.java
new file mode 100644
index 0000000000..3bc266c126
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/ClassPathsSubject.java
@@ -0,0 +1,165 @@
+// 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.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.truth.FailureStrategy;
+import com.google.common.truth.Subject;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/**
+ * A testing utility that allows .java/.class related assertions against Paths.
+ */
+public class ClassPathsSubject extends Subject<ClassPathsSubject, Path> {
+
+ ClassPathsSubject(FailureStrategy failureStrategy, @Nullable Path subject) {
+ super(failureStrategy, subject);
+ }
+
+ void exists() {
+ if (getSubject() == null) {
+ fail("should not be null.");
+ }
+ if (!Files.exists(getSubject())) {
+ fail("exists.");
+ }
+ }
+
+ /**
+ * Check that the contents of the java file at the current path, is equivalent to the given
+ * expected contents (modulo header comments and surrounding whitespace).
+ *
+ * @param contents expected contents
+ */
+ public void javaContentsIsEqualTo(String... contents) {
+ if (getSubject() == null) {
+ fail("should not be null.");
+ }
+ exists();
+ try {
+ assertThat(
+ trimWhitespace(
+ stripJavaHeaderComments(
+ Files.readAllLines(getSubject(), StandardCharsets.UTF_8))))
+ .containsExactly((Object[]) contents)
+ .inOrder();
+ } catch (IOException e) {
+ fail(e.toString());
+ }
+ }
+
+ private List<String> stripJavaHeaderComments(List<String> strings) {
+ List<String> result = new ArrayList<>();
+ boolean inComment = false;
+ for (String string : strings) {
+ if (string.trim().startsWith("/*")) {
+ inComment = true;
+ continue;
+ }
+ if (inComment) {
+ if (string.trim().startsWith("*/")) {
+ inComment = false;
+ continue;
+ }
+ continue;
+ }
+ result.add(string);
+ }
+ return result;
+ }
+
+ private List<String> trimWhitespace(List<String> strings) {
+ return Lists.transform(strings, new Function<String, String>() {
+ @Override
+ public String apply(String s) {
+ return s.trim();
+ }
+ });
+ }
+
+ /**
+ * Check the class file with the given name, assuming the current path is part of the classpath.
+ *
+ * @param className the fully qualified class name
+ */
+ public ClassNameSubject withClass(String className) {
+ if (getSubject() == null) {
+ fail("should not be null.");
+ }
+ exists();
+ return new ClassNameSubject(failureStrategy, getSubject(), className);
+ }
+
+ static final class ClassNameSubject extends Subject<ClassNameSubject, String> {
+
+ private final Path basePath;
+
+ public ClassNameSubject(
+ FailureStrategy failureStrategy, Path basePath, String subject) {
+ super(failureStrategy, subject);
+ this.basePath = basePath;
+ }
+
+ public void classContentsIsEqualTo(
+ ImmutableMap<String, Integer> intFields,
+ ImmutableMap<String, List<Integer>> intArrayFields,
+ boolean areFieldsFinal) throws Exception {
+ String expectedClassName = getSubject();
+ URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{basePath.toUri().toURL()});
+ Class<?> innerClass = urlClassLoader.loadClass(expectedClassName);
+ assertThat(innerClass.getSuperclass()).isEqualTo(Object.class);
+ assertThat(innerClass.getEnclosingClass().toString())
+ .endsWith(expectedClassName.substring(0, expectedClassName.indexOf('$')));
+ ImmutableMap.Builder<String, Integer> actualIntFields = ImmutableMap.builder();
+ ImmutableMap.Builder<String, List<Integer>> actualIntArrayFields = ImmutableMap.builder();
+ for (Field f : innerClass.getFields()) {
+ int fieldModifiers = f.getModifiers();
+ assertThat(Modifier.isFinal(fieldModifiers)).isEqualTo(areFieldsFinal);
+ assertThat(Modifier.isPublic(fieldModifiers)).isTrue();
+ assertThat(Modifier.isStatic(fieldModifiers)).isTrue();
+
+ Class<?> fieldType = f.getType();
+ if (fieldType.isPrimitive()) {
+ assertThat(fieldType).isEqualTo(Integer.TYPE);
+ actualIntFields.put(f.getName(), (Integer) f.get(null));
+ } else {
+ assertThat(fieldType.isArray()).isTrue();
+ int[] asArray = (int[]) f.get(null);
+ ImmutableList.Builder<Integer> list = ImmutableList.builder();
+ for (int i : asArray) {
+ list.add(i);
+ }
+ actualIntArrayFields.put(f.getName(), list.build());
+ }
+ }
+ assertThat(actualIntFields.build()).containsExactlyEntriesIn(intFields).inOrder();
+ assertThat(actualIntArrayFields.build()).containsExactlyEntriesIn(intArrayFields).inOrder();
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/android/DataResourceXmlTest.java b/src/test/java/com/google/devtools/build/android/DataResourceXmlTest.java
new file mode 100644
index 0000000000..653e7b7044
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/DataResourceXmlTest.java
@@ -0,0 +1,1325 @@
+// 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.res2.MergingException;
+import com.android.resources.ResourceType;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.MoreFiles;
+import com.google.common.io.RecursiveDeleteOption;
+import com.google.common.jimfs.Jimfs;
+import com.google.common.truth.FailureStrategy;
+import com.google.common.truth.SubjectFactory;
+import com.google.devtools.build.android.xml.ArrayXmlResourceValue;
+import com.google.devtools.build.android.xml.ArrayXmlResourceValue.ArrayType;
+import com.google.devtools.build.android.xml.AttrXmlResourceValue;
+import com.google.devtools.build.android.xml.AttrXmlResourceValue.BooleanResourceXmlAttrValue;
+import com.google.devtools.build.android.xml.AttrXmlResourceValue.ColorResourceXmlAttrValue;
+import com.google.devtools.build.android.xml.AttrXmlResourceValue.DimensionResourceXmlAttrValue;
+import com.google.devtools.build.android.xml.AttrXmlResourceValue.EnumResourceXmlAttrValue;
+import com.google.devtools.build.android.xml.AttrXmlResourceValue.FlagResourceXmlAttrValue;
+import com.google.devtools.build.android.xml.AttrXmlResourceValue.FloatResourceXmlAttrValue;
+import com.google.devtools.build.android.xml.AttrXmlResourceValue.FractionResourceXmlAttrValue;
+import com.google.devtools.build.android.xml.AttrXmlResourceValue.IntegerResourceXmlAttrValue;
+import com.google.devtools.build.android.xml.AttrXmlResourceValue.ReferenceResourceXmlAttrValue;
+import com.google.devtools.build.android.xml.AttrXmlResourceValue.StringResourceXmlAttrValue;
+import com.google.devtools.build.android.xml.IdXmlResourceValue;
+import com.google.devtools.build.android.xml.PluralXmlResourceValue;
+import com.google.devtools.build.android.xml.PublicXmlResourceValue;
+import com.google.devtools.build.android.xml.SimpleXmlResourceValue;
+import com.google.devtools.build.android.xml.StyleXmlResourceValue;
+import com.google.devtools.build.android.xml.StyleableXmlResourceValue;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.xml.stream.XMLStreamException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link DataResourceXml}. */
+@RunWith(JUnit4.class)
+public class DataResourceXmlTest {
+ static final ImmutableMap<String, String> XLIFF_NAMESPACES =
+ ImmutableMap.of("xliff", "urn:oasis:names:tc:xliff:document:1.2");
+ static final String END_RESOURCES = new String(AndroidDataWriter.END_RESOURCES);
+
+ private FullyQualifiedName.Factory fqnFactory;
+ private FileSystem fs;
+
+ @Before
+ public void createCleanEnvironment() {
+ fs = Jimfs.newFileSystem();
+ fqnFactory = FullyQualifiedName.Factory.from(ImmutableList.<String>of());
+ }
+
+ private Path writeResourceXml(String... xml) throws IOException {
+ return writeResourceXml(ImmutableMap.<String, String>of(), ImmutableMap.<String, String>of(),
+ xml);
+ }
+
+ private Path writeResourceXml(Map<String, String> namespaces, Map<String, String> attributes,
+ String... xml) throws IOException {
+ Path values = fs.getPath("root/values/values.xml");
+ Files.createDirectories(values.getParent());
+ StringBuilder builder = new StringBuilder();
+ builder.append(AndroidDataWriter.PRELUDE).append("<resources");
+ for (Entry<String, String> entry : namespaces.entrySet()) {
+ builder
+ .append(" xmlns:")
+ .append(entry.getKey())
+ .append("=\"")
+ .append(entry.getValue())
+ .append("\"");
+ }
+ for (Entry<String, String> entry : attributes.entrySet()) {
+ builder
+ .append(" ")
+ .append(entry.getKey())
+ .append("=\"")
+ .append(entry.getValue())
+ .append("\"");
+ }
+ builder.append(">");
+ Joiner.on("\n").appendTo(builder, xml);
+ builder.append(END_RESOURCES);
+ Files.write(values, builder.toString().getBytes(StandardCharsets.UTF_8));
+ return values;
+ }
+
+ private void parseResourcesFrom(
+ Path path, Map<DataKey, DataResource> toOverwrite, Map<DataKey, DataResource> toCombine)
+ throws XMLStreamException, IOException {
+ DataResourceXml.parse(
+ XmlResourceValues.getXmlInputFactory(),
+ path,
+ fqnFactory,
+ new FakeConsumer(toOverwrite),
+ new FakeConsumer(toCombine));
+ }
+
+ private FullyQualifiedName fqn(String raw) {
+ return fqnFactory.parse(raw);
+ }
+
+ @Test
+ public void simpleXmlResources() throws Exception {
+ Path path =
+ writeResourceXml(
+ "<string name='exit' description=\"&amp; egress -&gt; &quot;\">way out</string>",
+ "<bool name='canExit'>false</bool>",
+ "<color name='exitColor'>#FF000000</color>",
+ "<dimen name='exitSize'>20sp</dimen>",
+ "<integer name='exitInt'>20</integer>",
+ "<fraction name='exitFraction'>%20</fraction>",
+ "<drawable name='exitBackground'>#99000000</drawable>",
+ "<item name='some_id' type='id'/>",
+ "<item name='reference_id' type='id'>@id/some_id</item>");
+
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite)
+ .containsExactly(
+ fqn("string/exit"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ SimpleXmlResourceValue.of(
+ SimpleXmlResourceValue.Type.STRING,
+ ImmutableMap.of("description", "&amp; egress -&gt; &quot;"),
+ "way out")), // Value
+ fqn("bool/canExit"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ SimpleXmlResourceValue.createWithValue(
+ SimpleXmlResourceValue.Type.BOOL, "false")), // Value
+ fqn("color/exitColor"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ SimpleXmlResourceValue.createWithValue(
+ SimpleXmlResourceValue.Type.COLOR, "#FF000000")), // Value
+ fqn("dimen/exitSize"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ SimpleXmlResourceValue.createWithValue(
+ SimpleXmlResourceValue.Type.DIMEN, "20sp")), // Value
+ fqn("integer/exitInt"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ SimpleXmlResourceValue.createWithValue(
+ SimpleXmlResourceValue.Type.INTEGER, "20")), // Value
+ fqn("fraction/exitFraction"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ SimpleXmlResourceValue.createWithValue(
+ SimpleXmlResourceValue.Type.FRACTION, "%20")), // Value
+ fqn("drawable/exitBackground"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ SimpleXmlResourceValue.createWithValue(
+ SimpleXmlResourceValue.Type.DRAWABLE, "#99000000")) // Value
+ );
+ assertThat(toCombine)
+ .containsExactly(
+ fqn("id/some_id"), // Key
+ DataResourceXml.createWithNoNamespace(path, IdXmlResourceValue.of()), // Value
+ fqn("id/reference_id"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path, IdXmlResourceValue.of("@id/some_id")) // Value
+ );
+ }
+
+ @Test
+ public void skipIgnored() throws Exception {
+ Path path = writeResourceXml("<skip/>");
+
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite).isEmpty();
+ assertThat(toCombine).isEmpty();
+ }
+
+ @Test
+ public void eatCommentIgnored() throws Exception {
+ Path path = writeResourceXml("<eat-comment/>");
+
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite).isEmpty();
+ assertThat(toCombine).isEmpty();
+ }
+
+ @Test
+ public void itemSimpleXmlResources() throws Exception {
+ Path path =
+ writeResourceXml(
+ "<item type='dimen' name='exitSizePercent'>20%</item>",
+ "<item type='dimen' format='float' name='exitSizeFloat'>20.0</item>",
+ "<item type='fraction' name='denom'>%5</item>",
+ "<item name='subtype_id' type='id'>0x6f972360</item>",
+ "<item type='array' name='oboes'/>",
+ "<item type='drawable' name='placeholder'/>");
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite)
+ .containsExactly(
+ fqn("dimen/exitSizePercent"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path, SimpleXmlResourceValue.itemWithValue(ResourceType.DIMEN, "20%")), // Value
+ fqn("dimen/exitSizeFloat"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ SimpleXmlResourceValue.itemWithFormattedValue(
+ ResourceType.DIMEN, "float", "20.0")), // Value
+ fqn("drawable/placeholder"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path, SimpleXmlResourceValue.itemPlaceHolderFor(ResourceType.DRAWABLE)), // Value
+ fqn("array/oboes"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path, SimpleXmlResourceValue.itemPlaceHolderFor(ResourceType.ARRAY)), // Value
+ fqn("fraction/denom"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path, SimpleXmlResourceValue.itemWithValue(ResourceType.FRACTION, "%5")) // Value
+ );
+ }
+
+ @Test
+ public void styleableXmlResourcesEnum() throws Exception {
+ Path path =
+ writeResourceXml(
+ "<declare-styleable name='Theme'>",
+ " <attr name=\"labelPosition\" format=\"enum\">",
+ " <enum name=\"left\" value=\"0\"/>",
+ " <enum name=\"right\" value=\"1\"/>",
+ " </attr>",
+ "</declare-styleable>");
+
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite)
+ .containsExactly(
+ fqn("attr/labelPosition"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ AttrXmlResourceValue.fromFormatEntries(
+ EnumResourceXmlAttrValue.asEntryOf("left", "0", "right", "1"))) // Value
+ );
+ assertThat(toCombine)
+ .containsExactly(
+ fqn("styleable/Theme"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ StyleableXmlResourceValue.createAllAttrAsDefinitions(
+ fqnFactory.parse("attr/labelPosition"))) // Value
+ );
+ }
+
+ @Test
+ public void styleableXmlResourcesString() throws Exception {
+ Path path =
+ writeResourceXml(
+ "<declare-styleable name='UnusedStyleable'>",
+ " <attr name='attribute1'/>",
+ " <attr name='attribute2'/>",
+ "</declare-styleable>",
+ "<attr name='attribute1' format='string'/>",
+ "<attr name='attribute2' format='string'/>");
+
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite)
+ .containsExactly(
+ fqn("attr/attribute1"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ AttrXmlResourceValue.fromFormatEntries(
+ StringResourceXmlAttrValue.asEntry())), // Value
+ fqn("attr/attribute2"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ AttrXmlResourceValue.fromFormatEntries(
+ StringResourceXmlAttrValue.asEntry())) // Value
+ );
+ assertThat(toCombine)
+ .containsExactly(
+ fqn("styleable/UnusedStyleable"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ StyleableXmlResourceValue.createAllAttrAsReferences(
+ fqnFactory.parse("attr/attribute1"),
+ fqnFactory.parse("attr/attribute2"))) // Value
+ );
+ }
+
+ @Test
+ public void pluralXmlResources() throws Exception {
+ Path path =
+ writeResourceXml(
+ "<plurals name='numberOfSongsAvailable'>",
+ " <item quantity='one'>%d song found.</item>",
+ " <item quantity='other'>%d songs found.</item>",
+ "</plurals>");
+
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite)
+ .containsExactly(
+ fqn("plurals/numberOfSongsAvailable"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ PluralXmlResourceValue.createWithoutAttributes(
+ ImmutableMap.of(
+ "one", "%d song found.",
+ "other", "%d songs found."))) // Value
+ );
+ }
+
+ @Test
+ public void pluralXmlResourcesWithTrailingCharacters() throws Exception {
+ Path path =
+ writeResourceXml(
+ "<plurals name='numberOfSongsAvailable'>",
+ " <item quantity='one'>%d song found.</item> // this is an invalid comment.",
+ " <item quantity='other'>%d songs found.</item>typo",
+ "</plurals>");
+
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite)
+ .containsExactly(
+ fqn("plurals/numberOfSongsAvailable"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ PluralXmlResourceValue.createWithoutAttributes(
+ ImmutableMap.of(
+ "one", "%d song found.",
+ "other", "%d songs found."))) // Value
+ );
+ }
+
+ @Test
+ public void styleResources() throws Exception {
+ Path path =
+ writeResourceXml(
+ "<style name=\"candlestick_maker\" parent=\"@style/wax_maker\">\n"
+ + " <item name=\"texture\">waxy</item>\n"
+ + "</style>");
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite)
+ .containsExactly(
+ fqn("style/candlestick_maker"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ StyleXmlResourceValue.of(
+ "@style/wax_maker", ImmutableMap.of("texture", "waxy"))) // Value
+ );
+ }
+
+ @Test
+ public void styleResourcesNoParent() throws Exception {
+ Path path =
+ writeResourceXml(
+ "<style name=\"CandlestickMaker\">\n"
+ + " <item name=\"texture\">waxy</item>\n"
+ + "</style>");
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite)
+ .containsExactly(
+ fqn("style/CandlestickMaker"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path, StyleXmlResourceValue.of(null, ImmutableMap.of("texture", "waxy"))) // Value
+ );
+ }
+
+ @Test
+ public void styleResourcesForceNoParent() throws Exception {
+ Path path =
+ writeResourceXml(
+ // using a '.' implies a parent Candlestick, parent='' corrects to no parent.
+ "<style name=\"Candlestick.Maker\" parent=\"\">\n"
+ + " <item name=\"texture\">waxy</item>\n"
+ + "</style>");
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite)
+ .containsExactly(
+ fqn("style/Candlestick.Maker"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path, StyleXmlResourceValue.of("", ImmutableMap.of("texture", "waxy"))) // Value
+ );
+ }
+
+ @Test
+ public void styleResourcesLazyReference() throws Exception {
+ Path path =
+ writeResourceXml(
+ "<style name=\"candlestick_maker\" parent=\"AppTheme\">\n"
+ + " <item name=\"texture\">waxy</item>\n"
+ + "</style>");
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite)
+ .containsExactly(
+ fqn("style/candlestick_maker"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ StyleXmlResourceValue.of("AppTheme", ImmutableMap.of("texture", "waxy"))) // Value
+ );
+ }
+
+ @Test
+ public void arrayXmlResources() throws Exception {
+ Path path =
+ writeResourceXml(
+ "<array name='icons'>",
+ " <item>@drawable/home</item>",
+ " <item>@drawable/settings</item>",
+ " <item>@drawable/logout</item>",
+ "</array>",
+ "<array name='colors'>",
+ " <item>#FFFF0000</item>",
+ " <item>#FF00FF00</item>",
+ " <item>#FF0000FF</item>",
+ "</array>");
+
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite)
+ .containsExactly(
+ fqn("array/icons"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ ArrayXmlResourceValue.of(
+ ArrayType.ARRAY,
+ "@drawable/home",
+ "@drawable/settings",
+ "@drawable/logout")), // Value
+ fqn("array/colors"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ ArrayXmlResourceValue.of(
+ ArrayType.ARRAY, "#FFFF0000", "#FF00FF00", "#FF0000FF")) // Value
+ );
+ }
+
+ @Test
+ public void stringArrayXmlResources() throws Exception {
+ Path path =
+ writeResourceXml(
+ "<string-array name='characters'>",
+ " <item>boojum</item>",
+ " <item>snark</item>",
+ " <item>bellman</item>",
+ " <item>barrister</item>",
+ " <item>\\\"billiard-marker\\\"</item>",
+ "</string-array>");
+
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite)
+ .containsExactly(
+ fqn("array/characters"),
+ DataResourceXml.createWithNoNamespace(
+ path,
+ ArrayXmlResourceValue.of(
+ ArrayType.STRING_ARRAY,
+ "boojum",
+ "snark",
+ "bellman",
+ "barrister",
+ "\\\"billiard-marker\\\"")));
+ assertThat(toCombine).isEmpty();
+ }
+
+ @Test
+ public void integerArrayXmlResources() throws Exception {
+ Path path =
+ writeResourceXml(
+ "<integer-array name='bits'>",
+ " <item>4</item>",
+ " <item>8</item>",
+ " <item>16</item>",
+ " <item>32</item>",
+ "</integer-array>");
+
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite)
+ .containsExactly(
+ fqn("array/bits"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ ArrayXmlResourceValue.of(ArrayType.INTEGER_ARRAY, "4", "8", "16", "32")) // Value
+ );
+ }
+
+ @Test
+ public void attrFlagXmlResources() throws Exception {
+ Path path =
+ writeResourceXml(
+ " <attr name=\"labelPosition\">",
+ " <flag name=\"left\" value=\"0\"/>",
+ " <flag name=\"right\" value=\"1\"/>",
+ " </attr>");
+
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite)
+ .containsExactly(
+ fqn("attr/labelPosition"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ AttrXmlResourceValue.fromFormatEntries(
+ FlagResourceXmlAttrValue.asEntryOf(
+ "left", "0",
+ "right", "1"))) // Value
+ );
+ }
+
+ @Test
+ public void attrMultiFormatImplicitFlagXmlResources() throws Exception {
+ Path path =
+ writeResourceXml(
+ " <attr name='labelPosition' format='reference'>",
+ " <flag name='left' value='0'/>",
+ " <flag name='right' value='1'/>",
+ " </attr>");
+
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite)
+ .containsExactly(
+ fqn("attr/labelPosition"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ AttrXmlResourceValue.fromFormatEntries(
+ ReferenceResourceXmlAttrValue.asEntry(),
+ FlagResourceXmlAttrValue.asEntryOf(
+ "left", "0",
+ "right", "1"))) // Value
+ );
+ }
+
+ @Test
+ public void attrMultiFormatResources() throws Exception {
+ Path path =
+ writeResourceXml(
+ "<attr name='labelPosition' ",
+ "format='color|boolean|dimension|float|integer|string|fraction|reference' />");
+
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+
+ assertThat(toOverwrite)
+ .containsExactly(
+ fqn("attr/labelPosition"), // Key
+ DataResourceXml.createWithNoNamespace(
+ path,
+ AttrXmlResourceValue.fromFormatEntries(
+ ColorResourceXmlAttrValue.asEntry(),
+ BooleanResourceXmlAttrValue.asEntry(),
+ ReferenceResourceXmlAttrValue.asEntry(),
+ DimensionResourceXmlAttrValue.asEntry(),
+ FloatResourceXmlAttrValue.asEntry(),
+ IntegerResourceXmlAttrValue.asEntry(),
+ StringResourceXmlAttrValue.asEntry(),
+ FractionResourceXmlAttrValue.asEntry())) // Value
+ );
+ }
+
+ @Test
+ public void publicXmlResource() throws Exception {
+ Path path =
+ writeResourceXml(
+ "<string name='exit'>way out</string>",
+ "<public type='string' name='exit' id='0x123'/>");
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+ assertThat(toOverwrite)
+ .containsExactly(
+ fqn("string/exit"),
+ DataResourceXml.createWithNoNamespace(
+ path,
+ SimpleXmlResourceValue.createWithValue(
+ SimpleXmlResourceValue.Type.STRING, "way out")));
+ assertThat(toCombine)
+ .containsExactly(
+ fqn("public/exit"),
+ DataResourceXml.createWithNoNamespace(
+ path, PublicXmlResourceValue.create(ResourceType.STRING, Optional.of(0x123))));
+ }
+
+ @Test
+ public void writeSimpleXmlResources() throws Exception {
+ Path source =
+ writeResourceXml(
+ "<string name='exit'>way <a href=\"out.html\">out</a></string>",
+ "<bool name='canExit'>false</bool>",
+ "<color name='exitColor'>#FF000000</color>",
+ "<integer name='exitInt'>5</integer>",
+ "<drawable name='reference'/>");
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("string/exit")))
+ .xmlContentsIsEqualTo(
+ resourcesXmlFrom(
+ source, "<string name='exit'>way <a href=\"out.html\">out</a></string>"));
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("bool/canExit")))
+ .xmlContentsIsEqualTo(resourcesXmlFrom(source, "<bool name='canExit'>false</bool>"));
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("color/exitColor")))
+ .xmlContentsIsEqualTo(
+ resourcesXmlFrom(source, "<color name='exitColor'>#FF000000</color>"));
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("integer/exitInt")))
+ .xmlContentsIsEqualTo(resourcesXmlFrom(source, "<integer name='exitInt'>5</integer>"));
+ }
+
+ @Test
+ public void writeItemResources() throws Exception {
+ Path source =
+ writeResourceXml(
+ "<item type='dimen' name='exitSizePercent'>20%</item>",
+ "<item type='dimen' format='float' name='exitSizeFloat'>20.0</item>",
+ "<item name='exitId' type='id'/>",
+ "<item name='frac' type='fraction'>5%</item>");
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("dimen/exitSizePercent")))
+ .xmlContentsIsEqualTo(
+ resourcesXmlFrom(source, "<item type='dimen' name='exitSizePercent'>20%</item>"));
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("dimen/exitSizeFloat")))
+ .xmlContentsIsEqualTo(
+ resourcesXmlFrom(
+ source, "<item type='dimen' format='float' name='exitSizeFloat'>20.0</item>"));
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("fraction/frac")))
+ .xmlContentsIsEqualTo(
+ resourcesXmlFrom(source, "<item name='frac' type='fraction'>5%</item>"));
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("id/exitId")))
+ .xmlContentsIsEqualTo(resourcesXmlFrom(source, "<item name='exitId' type='id'/>"));
+ }
+
+ @Test
+ public void writeStringResourceWithXliffNamespace() throws Exception {
+ Path source = fs.getPath("root/values/values.xml");
+ Files.createDirectories(source.getParent());
+ Files.write(
+ source,
+ ("<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">"
+ + "<string name=\"star_rating\">Check out our 5\n"
+ + " <xliff:g id=\"star\">\\u2605</xliff:g>\n"
+ + "</string>"
+ + "</resources>")
+ .getBytes(StandardCharsets.UTF_8));
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("string/star_rating")))
+ .xmlContentsIsEqualTo(
+ resourcesXmlFrom(
+ XLIFF_NAMESPACES,
+ ImmutableMap.<String, String>of(),
+ source,
+ "<string name=\"star_rating\">Check out our 5",
+ " <xliff:g id=\"star\">\\u2605</xliff:g>",
+ "</string>"));
+ }
+
+ @Test
+ public void writeStringResourceCData() throws Exception {
+ String[] xml = {
+ "<string name='cdata'><![CDATA[<b>Jabber, Jabber</b><br><br>\n Wock!]]></string>"
+ };
+ Path source = writeResourceXml(xml);
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("string/cdata")))
+ .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml));
+ }
+
+ @Test
+ public void writeStringResourceWithNamespace() throws Exception {
+ Path source = fs.getPath("root/values/values.xml");
+ Files.createDirectories(source.getParent());
+ Files.write(
+ source,
+ ("<resources xmlns:ns1=\"urn:oasis:names:tc:xliff:document:1.2\">"
+ + "<string name=\"star_rating\">Check out our 5\n"
+ + " <ns1:g xmlns:foglebert=\"defogle\" foglebert:id=\"star\">\\u2605</ns1:g>\n"
+ + "</string>"
+ + "</resources>")
+ .getBytes(StandardCharsets.UTF_8));
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("string/star_rating")))
+ .xmlContentsIsEqualTo(
+ resourcesXmlFrom(
+ ImmutableMap.of("ns1", "urn:oasis:names:tc:xliff:document:1.2"),
+ ImmutableMap.<String, String>of(),
+ source,
+ "<string name=\"star_rating\">Check out our 5 ",
+ "<ns1:g xmlns:foglebert=\"defogle\" " + "foglebert:id=\"star\">\\u2605</ns1:g>",
+ "</string>"));
+ }
+
+ @Test
+ public void writeStringResourceWithUnusedNamespace() throws Exception {
+ Path source = fs.getPath("root/values/values.xml");
+ Files.createDirectories(source.getParent());
+ Files.write(
+ source,
+ ("<resources xmlns:ns1=\"urn:oasis:names:tc:xliff:document:1.2\">"
+ + "<string name=\"star_rating\">"
+ + "not yet implemented\n"
+ + "</string>"
+ + "</resources>")
+ .getBytes(StandardCharsets.UTF_8));
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("string/star_rating")))
+ .xmlContentsIsEqualTo(
+ resourcesXmlFrom(
+ source, "<string name=\"star_rating\">", "not yet implemented\n", "</string>"));
+ }
+
+ @Test
+ public void writeStringResourceWithEscapedValues() throws Exception {
+ String[] xml = {
+ "<string name=\"AAP_SUGGEST_ACCEPT_SUGGESTION\">",
+ " &lt;b&gt;<xliff:g id=\"name\" example=\"Pizza hut\">%1$s</xliff:g>&lt;/b&gt; "
+ + "already exists at &lt;b&gt;<xliff:g id=\"address\" "
+ + "example=\"123 main street\">%2$s</xliff:g>&lt;/b&gt;&lt;br&gt;",
+ " &lt;br&gt;",
+ " Is this the place you\\'re trying to add?\n",
+ "</string>"
+ };
+
+ Path source = writeResourceXml(XLIFF_NAMESPACES, ImmutableMap.<String, String>of(), xml);
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("string/AAP_SUGGEST_ACCEPT_SUGGESTION")))
+ .xmlContentsIsEqualTo(
+ resourcesXmlFrom(XLIFF_NAMESPACES, ImmutableMap.<String, String>of(), source, xml));
+ }
+
+ @Test
+ public void writeStyleableXmlResource() throws Exception {
+ String[] xml = {
+ "<declare-styleable name='Theme'>",
+ " <attr name=\"labelPosition\" />",
+ "</declare-styleable>"
+ };
+
+ Path source = writeResourceXml(xml);
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("styleable/Theme")))
+ .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml));
+ }
+
+ @Test
+ public void writeStyleableXmlResourceReference() throws Exception {
+ String[] xml = {
+ "<declare-styleable name='Theme'>",
+ " <attr name=\"labelColor\" format=\"color\" />",
+ "</declare-styleable>"
+ };
+
+ Path source = writeResourceXml(xml);
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("styleable/Theme"), fqn("attr/labelColor")))
+ .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml));
+ }
+
+ @Test
+ public void writePluralXmlResources() throws Exception {
+ String[] xml = {
+ "<plurals name='numberOfSongsAvailable' format='none'>",
+ " <item quantity='one'>%d song found.</item>",
+ " <item quantity='other'>%d songs found.</item>",
+ "</plurals>"
+ };
+ Path source = writeResourceXml(xml);
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("plurals/numberOfSongsAvailable")))
+ .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml));
+ }
+
+ @Test
+ public void writePublicXmlResources() throws Exception {
+ String[] xml = {
+ "<public name='bar' type='dimen' id='0x7f030003' />",
+ "<public name='foo' type='dimen' />",
+ };
+ Path source = writeResourceXml(xml);
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("public/bar"), fqn("public/foo")))
+ .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml));
+ }
+
+ @Test
+ public void writeArrayXmlResources() throws Exception {
+ String[] xml = {
+ "<array name='icons'>",
+ " <item>@drawable/home</item>",
+ " <item>@drawable/settings</item>",
+ " <item>@drawable/logout</item>",
+ "</array>",
+ };
+
+ Path source = writeResourceXml(xml);
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("array/icons")))
+ .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml));
+ }
+
+ @Test
+ public void writeStringArrayXmlResources() throws Exception {
+ String[] xml = {
+ "<string-array name='rosebud' translatable='false'>",
+ " <item>Howard Hughes</item>",
+ " <item>Randolph Hurst</item>",
+ "</string-array>",
+ };
+
+ Path source = writeResourceXml(xml);
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("array/rosebud")))
+ .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml));
+ }
+
+ @Test
+ public void writeIntegerArrayXmlResources() throws Exception {
+ String[] xml = {
+ "<integer-array name='bits'>",
+ " <item>4</item>",
+ " <item>8</item>",
+ " <item>16</item>",
+ " <item>32</item>",
+ "</integer-array>"
+ };
+ Path source = writeResourceXml(xml);
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("array/bits")))
+ .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml));
+ }
+
+ @Test
+ public void writeAttrFlagXmlResources() throws Exception {
+ String[] xml = {
+ " <attr name=\"labelPosition\">",
+ " <flag name=\"left\" value=\"0\"/>",
+ " <flag name=\"right\" value=\"1\"/>",
+ " </attr>"
+ };
+ Path source = writeResourceXml(xml);
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("attr/labelPosition")))
+ .xmlContentsIsEqualTo(
+ resourcesXmlFrom(
+ source,
+ " <attr name=\"labelPosition\">",
+ " <flag name=\"left\" value=\"0\"/>",
+ " <flag name=\"right\" value=\"1\"/>",
+ " </attr>"));
+ }
+
+ @Test
+ public void writeAttrMultiFormatImplicitFlagXmlResources() throws Exception {
+ String[] xml = {
+ " <attr name='labelPosition' format='reference'>",
+ " <flag name='left' value='0'/>",
+ " <flag name='right' value='1'/>",
+ " </attr>"
+ };
+ Path source = writeResourceXml(xml);
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("attr/labelPosition")))
+ .xmlContentsIsEqualTo(
+ resourcesXmlFrom(
+ source,
+ " <attr name='labelPosition' format='reference'>",
+ " <flag name='left' value='0'/>",
+ " <flag name='right' value='1'/>",
+ " </attr>"));
+ }
+
+ @Test
+ public void writeAttrMultiFormatResources() throws Exception {
+ String[] xml = {
+ "<attr name='labelPosition' ",
+ "format='boolean|color|dimension|float|fraction|integer|reference|string'/>"
+ };
+ Path source = writeResourceXml(xml);
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("attr/labelPosition")))
+ .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml));
+ }
+
+ @Test
+ public void writeStyle() throws Exception {
+ String[] xml = {
+ "<style name='candlestick_maker' parent='@style/wax_maker'>\n"
+ + " <item name='texture'>waxy</item>\n"
+ + "</style>"
+ };
+
+ Path source = writeResourceXml(xml);
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("style/candlestick_maker")))
+ .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml));
+ }
+
+ @Test
+ public void writeForceNoParentStyle() throws Exception {
+ String[] xml = {
+ "<style name='candlestick_maker' parent=''>\n",
+ " <item name='texture'>waxy</item>\n",
+ "</style>"
+ };
+
+ Path source = writeResourceXml(xml);
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("style/candlestick_maker")))
+ .xmlContentsIsEqualTo(resourcesXmlFrom(source, xml));
+ }
+
+ @Test
+ public void writeResourceAttributes() throws Exception {
+ Path source = writeResourceXml(
+ ImmutableMap.of("tools", "http://schemas.android.com/tools"),
+ ImmutableMap.of("tools:foo", "fooVal"));
+ assertAbout(resourcePaths)
+ .that(parsedAndWritten(source, fqn("<resources>/tools:foo")))
+ .xmlContentsIsEqualTo(
+ resourcesXmlFrom(
+ ImmutableMap.of("tools", "http://schemas.android.com/tools"),
+ ImmutableMap.of("tools:foo", "fooVal"),
+ null));
+ }
+
+ @Test
+ public void serializeMultipleSimpleXmlResources() throws Exception {
+ Path serialized = fs.getPath("out/out.bin");
+ Path source = fs.getPath("res/values/values.xml");
+ FullyQualifiedName stringKey = fqn("string/exit");
+ DataResourceXml stringValue =
+ DataResourceXml.createWithNoNamespace(
+ source,
+ SimpleXmlResourceValue.createWithValue(SimpleXmlResourceValue.Type.STRING, "way out"));
+ FullyQualifiedName nullStringKey = fqn("string/nullexit");
+ DataResourceXml nullStringValue =
+ DataResourceXml.createWithNoNamespace(
+ source,
+ SimpleXmlResourceValue.createWithValue(SimpleXmlResourceValue.Type.STRING, null));
+ FullyQualifiedName boolKey = fqn("bool/canExit");
+ DataResourceXml boolValue =
+ DataResourceXml.createWithNoNamespace(
+ source,
+ SimpleXmlResourceValue.createWithValue(SimpleXmlResourceValue.Type.BOOL, "false"));
+ FullyQualifiedName colorKey = fqn("color/exitColor");
+ DataResourceXml colorValue =
+ DataResourceXml.createWithNoNamespace(
+ source,
+ SimpleXmlResourceValue.createWithValue(SimpleXmlResourceValue.Type.COLOR, "#FF000000"));
+ FullyQualifiedName dimenKey = fqn("dimen/exitSize");
+ DataResourceXml dimenValue =
+ DataResourceXml.createWithNoNamespace(
+ source,
+ SimpleXmlResourceValue.createWithValue(SimpleXmlResourceValue.Type.DIMEN, "20sp"));
+
+ AndroidDataSerializer serializer = AndroidDataSerializer.create();
+ serializer.queueForSerialization(stringKey, stringValue);
+ serializer.queueForSerialization(nullStringKey, nullStringValue);
+ serializer.queueForSerialization(boolKey, boolValue);
+ serializer.queueForSerialization(colorKey, colorValue);
+ serializer.queueForSerialization(dimenKey, dimenValue);
+ serializer.flushTo(serialized);
+
+ AndroidDataDeserializer deserializer = AndroidDataDeserializer.create();
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ deserializer.read(
+ serialized,
+ KeyValueConsumers.of(new FakeConsumer(toOverwrite), new FakeConsumer(toCombine), null));
+ assertThat(toOverwrite)
+ .containsExactly(
+ stringKey, stringValue,
+ boolKey, boolValue,
+ colorKey, colorValue,
+ nullStringKey, nullStringValue,
+ dimenKey, dimenValue);
+ assertThat(toCombine).isEmpty();
+ }
+
+ @Test
+ public void serializeItemXmlResources() throws Exception {
+ Path source = fs.getPath("res/values/values.xml");
+ assertSerialization(
+ fqn("dimen/exitSizePercent"),
+ DataResourceXml.createWithNoNamespace(
+ source, SimpleXmlResourceValue.itemWithValue(ResourceType.DIMEN, "20%")));
+ assertSerialization(
+ fqn("dimen/exitSizeFloat"),
+ DataResourceXml.createWithNoNamespace(
+ source,
+ SimpleXmlResourceValue.itemWithFormattedValue(ResourceType.DIMEN, "float", "20.0")));
+ assertSerialization(
+ fqn("fraction/denom"),
+ DataResourceXml.createWithNoNamespace(
+ source,
+ SimpleXmlResourceValue.createWithValue(SimpleXmlResourceValue.Type.COLOR, "5%")));
+ assertSerialization(
+ fqn("id/subtype_afrikaans"),
+ DataResourceXml.createWithNoNamespace(
+ source, SimpleXmlResourceValue.itemWithValue(ResourceType.ID, "0x6f972360")));
+ }
+
+ @Test
+ public void serializeStyleableXmlResource() throws Exception {
+ Path serialized = fs.getPath("out/out.bin");
+ Path source = fs.getPath("res/values/values.xml");
+ FullyQualifiedName attrKey = fqn("attr/labelPosition");
+ DataResourceXml attrValue =
+ DataResourceXml.createWithNoNamespace(
+ source,
+ AttrXmlResourceValue.fromFormatEntries(
+ EnumResourceXmlAttrValue.asEntryOf("left", "0", "right", "1")));
+ FullyQualifiedName themeKey = fqn("styleable/Theme");
+ DataResourceXml themeValue =
+ DataResourceXml.createWithNoNamespace(
+ source,
+ StyleableXmlResourceValue.createAllAttrAsReferences(
+ fqnFactory.parse("attr/labelPosition")));
+
+ AndroidDataSerializer serializer = AndroidDataSerializer.create();
+ serializer.queueForSerialization(attrKey, attrValue);
+ serializer.queueForSerialization(themeKey, themeValue);
+ serializer.flushTo(serialized);
+
+ AndroidDataDeserializer deserializer = AndroidDataDeserializer.create();
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ deserializer.read(
+ serialized,
+ KeyValueConsumers.of(new FakeConsumer(toOverwrite), new FakeConsumer(toCombine), null));
+ assertThat(toOverwrite).containsEntry(attrKey, attrValue);
+ assertThat(toCombine).containsEntry(themeKey, themeValue);
+ }
+
+ @Test
+ public void serializePlurals() throws Exception {
+ Path path = fs.getPath("res/values/values.xml");
+ assertSerialization(
+ fqn("plurals/numberOfSongsAvailable"),
+ DataResourceXml.createWithNoNamespace(
+ path,
+ PluralXmlResourceValue.createWithoutAttributes(
+ ImmutableMap.of(
+ "one", "%d song found.",
+ "other", "%d songs found."))));
+ }
+
+ @Test
+ public void serializeArrays() throws Exception {
+ Path path = fs.getPath("res/values/values.xml");
+ assertSerialization(
+ fqn("plurals/numberOfSongsAvailable"),
+ DataResourceXml.createWithNoNamespace(
+ path,
+ PluralXmlResourceValue.createWithoutAttributes(
+ ImmutableMap.of(
+ "one", "%d song found.",
+ "other", "%d songs found."))));
+ assertSerialization(
+ fqn("array/icons"),
+ DataResourceXml.createWithNoNamespace(
+ path,
+ ArrayXmlResourceValue.of(
+ ArrayType.ARRAY, "@drawable/home", "@drawable/settings", "@drawable/logout")));
+ assertSerialization(
+ fqn("array/colors"),
+ DataResourceXml.createWithNoNamespace(
+ path,
+ ArrayXmlResourceValue.of(ArrayType.ARRAY, "#FFFF0000", "#FF00FF00", "#FF0000FF")));
+ assertSerialization(
+ fqn("array/characters"),
+ DataResourceXml.createWithNoNamespace(
+ path,
+ ArrayXmlResourceValue.of(
+ ArrayType.STRING_ARRAY,
+ "boojum",
+ "snark",
+ "bellman",
+ "barrister",
+ "\\\"billiard-marker\\\"")));
+ }
+
+ @Test
+ public void serializeAttrFlag() throws Exception {
+ assertSerialization(
+ fqn("attr/labelPosition"),
+ DataResourceXml.createWithNoNamespace(
+ fs.getPath("res/values/values.xml"),
+ AttrXmlResourceValue.fromFormatEntries(
+ FlagResourceXmlAttrValue.asEntryOf(
+ "left", "0",
+ "right", "1"))));
+ }
+
+ @Test
+ public void serializeId() throws Exception {
+ assertSerialization(
+ fqn("id/squark"),
+ DataResourceXml.createWithNoNamespace(
+ fs.getPath("res/values/values.xml"), IdXmlResourceValue.of()));
+ }
+
+ @Test
+ public void serializePublic() throws Exception {
+ assertSerialization(
+ fqn("public/park"),
+ DataResourceXml.createWithNoNamespace(
+ fs.getPath("res/values/public.xml"), PublicXmlResourceValue.of(
+ ImmutableMap.of(
+ ResourceType.DIMEN, Optional.of(0x7f040000),
+ ResourceType.STRING, Optional.of(0x7f050000))
+ )));
+ }
+
+ @Test
+ public void serializeStyle() throws Exception {
+ assertSerialization(
+ fqn("style/snark"),
+ DataResourceXml.createWithNoNamespace(
+ fs.getPath("res/values/styles.xml"),
+ StyleXmlResourceValue.of(null, ImmutableMap.of("look", "boojum"))));
+ }
+
+ @Test
+ public void assertMultiFormatAttr() throws Exception {
+ assertSerialization(
+ fqn("attr/labelPosition"),
+ DataResourceXml.createWithNoNamespace(
+ fs.getPath("res/values/values.xml"),
+ AttrXmlResourceValue.fromFormatEntries(
+ ColorResourceXmlAttrValue.asEntry(),
+ BooleanResourceXmlAttrValue.asEntry(),
+ ReferenceResourceXmlAttrValue.asEntry(),
+ DimensionResourceXmlAttrValue.asEntry(),
+ FloatResourceXmlAttrValue.asEntry(),
+ IntegerResourceXmlAttrValue.asEntry(),
+ StringResourceXmlAttrValue.asEntry(),
+ FractionResourceXmlAttrValue.asEntry())));
+ }
+
+ private void assertSerialization(FullyQualifiedName key, DataValue value) throws Exception {
+ Path serialized = fs.getPath("out/out.bin");
+ Files.deleteIfExists(serialized);
+ Path manifestPath = fs.getPath("AndroidManifest.xml");
+ Files.deleteIfExists(manifestPath);
+ Files.createFile(manifestPath);
+ AndroidDataSerializer serializer = AndroidDataSerializer.create();
+ serializer.queueForSerialization(key, value);
+ serializer.flushTo(serialized);
+
+ AndroidDataDeserializer deserializer = AndroidDataDeserializer.create();
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ deserializer.read(
+ serialized,
+ KeyValueConsumers.of(new FakeConsumer(toOverwrite), new FakeConsumer(toCombine), null));
+ if (key.isOverwritable()) {
+ assertThat(toOverwrite).containsEntry(key, value);
+ assertThat(toCombine).isEmpty();
+ } else {
+ assertThat(toCombine).containsEntry(key, value);
+ assertThat(toOverwrite).isEmpty();
+ }
+ }
+
+ private String[] resourcesXmlFrom(Path source, String... lines) {
+ return resourcesXmlFrom(ImmutableMap.<String, String>of(), ImmutableMap.<String, String>of(),
+ source, lines);
+ }
+
+ private String[] resourcesXmlFrom(Map<String, String> namespaces, Map<String, String> attributes,
+ Path source, String... lines) {
+ FluentIterable<String> xml = FluentIterable.of(new String(AndroidDataWriter.PRELUDE))
+ .append("<resources")
+ .append(
+ FluentIterable.from(namespaces.entrySet())
+ .transform(
+ new Function<Entry<String, String>, String>() {
+ @Override
+ public String apply(Entry<String, String> input) {
+ return String.format(" xmlns:%s=\"%s\"", input.getKey(), input.getValue());
+ }
+ })
+ .join(Joiner.on("")))
+ .append(
+ FluentIterable.from(attributes.entrySet())
+ .transform(
+ new Function<Entry<String, String>, String>() {
+ @Override
+ public String apply(Entry<String, String> input) {
+ return String.format(" %s=\"%s\"", input.getKey(), input.getValue());
+ }
+ })
+ .join(Joiner.on("")));
+ if (source == null && (lines == null || lines.length == 0)) {
+ xml = xml.append("/>");
+ } else {
+ xml = xml.append(">");
+ if (source != null) {
+ xml = xml.append(String.format("<!-- %s --> <eat-comment/>", source));
+ }
+ if (lines != null) {
+ xml = xml.append(lines);
+ }
+ xml = xml.append(END_RESOURCES);
+ }
+ return xml.toArray(String.class);
+ }
+
+ private Path parsedAndWritten(Path path, FullyQualifiedName... fqns)
+ throws XMLStreamException, IOException, MergingException {
+ final Map<DataKey, DataResource> toOverwrite = new HashMap<>();
+ final Map<DataKey, DataResource> toCombine = new HashMap<>();
+ parseResourcesFrom(path, toOverwrite, toCombine);
+ Path out = fs.getPath("out");
+ if (Files.exists(out)) {
+ MoreFiles.deleteRecursively(out, RecursiveDeleteOption.ALLOW_INSECURE);
+ }
+ // find and write the resource -- the categorization is tested during parsing.
+ AndroidDataWriter mergedDataWriter = AndroidDataWriter.createWithDefaults(out);
+ for (FullyQualifiedName fqn : fqns) {
+ if (toOverwrite.containsKey(fqn)) {
+ toOverwrite.get(fqn).writeResource(fqn, mergedDataWriter);
+ } else if (toCombine.containsKey(fqn)) {
+ toCombine.get(fqn).writeResource(fqn, mergedDataWriter);
+ }
+ }
+ mergedDataWriter.flush();
+ return mergedDataWriter.resourceDirectory().resolve("values/values.xml");
+ }
+
+ private static final SubjectFactory<PathsSubject, Path> resourcePaths =
+ new SubjectFactory<PathsSubject, Path>() {
+ @Override
+ public PathsSubject getSubject(FailureStrategy failureStrategy, Path path) {
+ return new PathsSubject(failureStrategy, path);
+ }
+ };
+
+ private static class FakeConsumer
+ implements ParsedAndroidData.KeyValueConsumer<DataKey, DataResource> {
+ private final Map<DataKey, DataResource> target;
+
+ FakeConsumer(Map<DataKey, DataResource> target) {
+ this.target = target;
+ }
+
+ @Override
+ public void consume(DataKey key, DataResource value) {
+ target.put(key, value);
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/android/ParsedAndroidDataBuilder.java b/src/test/java/com/google/devtools/build/android/ParsedAndroidDataBuilder.java
new file mode 100644
index 0000000000..19710f7e35
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/ParsedAndroidDataBuilder.java
@@ -0,0 +1,269 @@
+// 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.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.android.FullyQualifiedName.Factory;
+import com.google.devtools.build.android.ParsedAndroidData.CombiningConsumer;
+import com.google.devtools.build.android.ParsedAndroidData.KeyValueConsumer;
+import com.google.devtools.build.android.ParsedAndroidData.OverwritableConsumer;
+import com.google.devtools.build.android.xml.Namespaces;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/**
+ * Build for ParsedAndroidData instance.
+ */
+public class ParsedAndroidDataBuilder {
+
+ private final Path defaultRoot;
+ private final FullyQualifiedName.Factory fqnFactory;
+ private final Map<DataKey, DataResource> overwrite = new HashMap<>();
+ private final Map<DataKey, DataResource> combine = new HashMap<>();
+ private final Map<DataKey, DataAsset> assets = new HashMap<>();
+ private final Set<MergeConflict> conflicts = new HashSet<>();
+
+ public ParsedAndroidDataBuilder(
+ @Nullable Path root, @Nullable FullyQualifiedName.Factory fqnFactory) {
+ this.defaultRoot = root;
+ this.fqnFactory = fqnFactory;
+ }
+
+ public static ParsedAndroidData empty() {
+ return ParsedAndroidData.of(
+ ImmutableSet.<MergeConflict>of(),
+ ImmutableMap.<DataKey, DataResource>of(),
+ ImmutableMap.<DataKey, DataResource>of(),
+ ImmutableMap.<DataKey, DataAsset>of());
+ }
+
+ public static ParsedAndroidDataBuilder buildOn(
+ Path defaultRoot, FullyQualifiedName.Factory fqnFactory) {
+ return new ParsedAndroidDataBuilder(defaultRoot, fqnFactory);
+ }
+
+ public static ParsedAndroidDataBuilder buildOn(FullyQualifiedName.Factory fqnFactory) {
+ return buildOn(null, fqnFactory);
+ }
+
+ public static ParsedAndroidDataBuilder buildOn(Path defaultRoot) {
+ return buildOn(defaultRoot, null);
+ }
+
+ public static ParsedAndroidDataBuilder builder() {
+ return buildOn(null, null);
+ }
+
+ public ParsedAndroidDataBuilder overwritable(DataEntry... resourceBuilders) {
+ OverwritableConsumer<DataKey, DataResource> consumer =
+ new OverwritableConsumer<>(overwrite, conflicts);
+ for (DataEntry resourceBuilder : resourceBuilders) {
+ resourceBuilder.accept(fqnFactory, defaultRoot, consumer);
+ }
+ return this;
+ }
+
+ public ParsedAndroidDataBuilder combining(DataEntry... resourceBuilders) {
+ CombiningConsumer consumer = new CombiningConsumer(combine);
+ for (DataEntry resourceBuilder : resourceBuilders) {
+ resourceBuilder.accept(fqnFactory, defaultRoot, consumer);
+ }
+ return this;
+ }
+
+ public ParsedAndroidDataBuilder assets(DataEntry... assetBuilders) {
+ OverwritableConsumer<DataKey, DataAsset> consumer =
+ new OverwritableConsumer<>(assets, conflicts);
+ for (DataEntry assetBuilder : assetBuilders) {
+ assetBuilder.accept(defaultRoot, consumer);
+ }
+ return this;
+ }
+
+ public static FileResourceBuilder file(String rawKey) {
+ return new FileResourceBuilder(rawKey);
+ }
+
+ public static FileResourceBuilder file() {
+ return new FileResourceBuilder(null);
+ }
+
+ public static XmlResourceBuilder xml(String rawKey) {
+ return new XmlResourceBuilder(rawKey);
+ }
+
+ public ParsedAndroidData build() {
+ return ParsedAndroidData.of(
+ ImmutableSet.copyOf(conflicts),
+ ImmutableMap.copyOf(overwrite),
+ ImmutableMap.copyOf(combine),
+ ImmutableMap.copyOf(assets));
+ }
+
+ static class FileResourceBuilder {
+ private String rawKey;
+ private Path root;
+
+ FileResourceBuilder(@Nullable String rawKey) {
+ this.rawKey = rawKey;
+ }
+
+ FileResourceBuilder root(Path root) {
+ this.root = root;
+ return this;
+ }
+
+ Path chooseRoot(Path defaultRoot) {
+ if (defaultRoot != null) {
+ return defaultRoot;
+ }
+ if (root != null) {
+ return root;
+ }
+ throw new IllegalStateException(
+ "the default root and asset root are null! A root is required!");
+ }
+
+ DataEntry source(final DataSource source) {
+ return new DataEntry() {
+ @Override
+ void accept(
+ @Nullable Factory factory,
+ @Nullable Path root,
+ KeyValueConsumer<DataKey, DataResource> consumer) {
+ consumer.consume(factory.parse(rawKey), DataValueFile.of(source));
+ }
+
+ @Override
+ void accept(@Nullable Path defaultRoot, KeyValueConsumer<DataKey, DataAsset> target) {
+ target.consume(
+ RelativeAssetPath.Factory.of(chooseRoot(defaultRoot).resolve("assets"))
+ .create(source.getPath()),
+ DataValueFile.of(source));
+ }
+ };
+ }
+
+ DataEntry source(final String path) {
+ return new DataEntry() {
+ @Override
+ public void accept(
+ FullyQualifiedName.Factory factory,
+ Path defaultRoot,
+ KeyValueConsumer<DataKey, DataResource> consumer) {
+ Path res = chooseRoot(defaultRoot).resolve("res");
+ consumer.consume(factory.parse(rawKey), DataValueFile.of(res.resolve(path)));
+ }
+
+ @Override
+ public void accept(
+ @Nullable Path defaultRoot, KeyValueConsumer<DataKey, DataAsset> consumer) {
+ Path assets = chooseRoot(defaultRoot).resolve("assets");
+ Path fullPath = assets.resolve(path);
+ consumer.consume(
+ RelativeAssetPath.Factory.of(assets).create(fullPath), DataValueFile.of(fullPath));
+ }
+ };
+ }
+ }
+
+ static class XmlResourceBuilder {
+ private final String rawFqn;
+ private Path root;
+ private final Map<String, String> prefixToUri = new LinkedHashMap<>();
+
+ XmlResourceBuilder(String rawFqn) {
+ this(rawFqn, null);
+ }
+
+ XmlResourceBuilder(String rawFqn, Path root) {
+ this.rawFqn = rawFqn;
+ this.root = root;
+ }
+
+ XmlResourceBuilder source(final String path) {
+ return new XmlResourceBuilder(rawFqn, root) {
+ @Override
+ public DataEntry value(final XmlResourceValue value) {
+ return new DataEntry() {
+ @Override
+ public void accept(
+ FullyQualifiedName.Factory factory,
+ Path defaultRoot,
+ KeyValueConsumer<DataKey, DataResource> consumer) {
+ Path res = (root == null ? defaultRoot : root).resolve("res");
+ consumer.consume(
+ factory.parse(rawFqn),
+ DataResourceXml.createWithNamespaces(
+ res.resolve(path), value, Namespaces.from(prefixToUri)));
+ }
+ };
+ }
+ };
+ }
+
+ XmlResourceBuilder source(final DataSource dataSource) {
+ return new XmlResourceBuilder(rawFqn, root) {
+ @Override
+ public DataEntry value(final XmlResourceValue value) {
+ return new DataEntry() {
+ @Override
+ public void accept(
+ FullyQualifiedName.Factory factory,
+ Path defaultRoot,
+ KeyValueConsumer<DataKey, DataResource> consumer) {
+ consumer.consume(
+ factory.parse(rawFqn),
+ DataResourceXml.createWithNamespaces(
+ dataSource, value, Namespaces.from(prefixToUri)));
+ }
+ };
+ }
+ };
+ }
+
+ XmlResourceBuilder root(Path root) {
+ this.root = root;
+ return this;
+ }
+
+ XmlResourceBuilder namespace(String prefix, String uri) {
+ prefixToUri.put(prefix, uri);
+ return this;
+ }
+
+ DataEntry value(final XmlResourceValue value) {
+ throw new UnsupportedOperationException("A source must be declared!");
+ }
+ }
+
+ abstract static class DataEntry {
+ void accept(
+ @Nullable FullyQualifiedName.Factory factory,
+ @Nullable Path root,
+ KeyValueConsumer<DataKey, DataResource> consumer) {
+ throw new UnsupportedOperationException("assets cannot be resources!");
+ }
+
+ void accept(@Nullable Path root, KeyValueConsumer<DataKey, DataAsset> target) {
+ throw new UnsupportedOperationException("xml resources cannot be assets!");
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/android/PathsSubject.java b/src/test/java/com/google/devtools/build/android/PathsSubject.java
new file mode 100644
index 0000000000..54ffec371f
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/PathsSubject.java
@@ -0,0 +1,93 @@
+// 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.google.common.base.Joiner;
+import com.google.common.truth.FailureStrategy;
+import com.google.common.truth.Subject;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+/**
+ * A testing utility that allows assertions against Paths.
+ */
+class PathsSubject extends Subject<PathsSubject, Path> {
+
+ PathsSubject(FailureStrategy failureStrategy, @Nullable Path subject) {
+ super(failureStrategy, subject);
+ }
+
+ void exists() {
+ if (getSubject() == null) {
+ fail("should not be null.");
+ }
+ if (!Files.exists(getSubject())) {
+ fail("exists.");
+ }
+ }
+
+ void xmlContentsIsEqualTo(String... contents) {
+ if (getSubject() == null) {
+ fail("should not be null.");
+ }
+ exists();
+ try {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ Transformer transformer = TransformerFactory.newInstance().newTransformer();
+ transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+ transformer.setOutputProperty(OutputKeys.INDENT, "no");
+
+ assertThat(
+ normalizeXml(
+ newXmlDocument(factory, Files.readAllLines(getSubject(), StandardCharsets.UTF_8)),
+ transformer))
+ .isEqualTo(normalizeXml(newXmlDocument(factory, Arrays.asList(contents)), transformer));
+ } catch (IOException | SAXException | ParserConfigurationException | TransformerException e) {
+ fail(e.toString());
+ }
+ }
+
+ private Document newXmlDocument(DocumentBuilderFactory factory, List<String> contents)
+ throws SAXException, IOException, ParserConfigurationException {
+ return factory
+ .newDocumentBuilder()
+ .parse(new InputSource(new StringReader(Joiner.on("").join(contents))));
+ }
+
+ private String normalizeXml(Document doc, Transformer transformer) throws TransformerException {
+ StringWriter writer = new StringWriter();
+ transformer.transform(new DOMSource(doc), new StreamResult(writer));
+ return writer.toString().replaceAll("\n|\r", "").replaceAll(">\\s+<", "><");
+ }
+}