// 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 static org.junit.Assert.fail; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.jimfs.Jimfs; import com.google.common.truth.Subject; import com.google.common.truth.Truth; import com.google.devtools.build.android.AndroidResourceMerger.MergingException; import com.google.devtools.build.android.FullyQualifiedName.Factory; import com.google.devtools.build.android.xml.AttrXmlResourceValue; import com.google.devtools.build.android.xml.IdXmlResourceValue; import com.google.devtools.build.android.xml.ResourcesAttribute; import com.google.devtools.build.android.xml.SimpleXmlResourceValue; import com.google.devtools.build.android.xml.StyleableXmlResourceValue; 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 ParsedAndroidData */ @RunWith(JUnit4.class) public class ParsedAndroidDataTest { private FileSystem fs; private Factory fqnFactory; @Before public void createCleanEnvironment() { fs = Jimfs.newFileSystem(); fqnFactory = FullyQualifiedName.Factory.from(ImmutableList.of()); } @Test public void assets() throws Exception { Path root = fs.getPath("transDep"); ParsedAndroidData dataSet = ParsedAndroidData.from( ImmutableList.of( AndroidDataBuilder.of(root) .addAsset("bin/boojum") .createManifest("AndroidManifest.xml", "com.google.foo", "") .buildDependency())); Path assetPath = root.resolve("assets/bin/boojum"); RelativeAssetPath key = RelativeAssetPath.Factory.of(root.resolve("assets")).create(assetPath); assertThat(dataSet.getAssets()).containsEntry(key, DataValueFile.of(assetPath)); assertThat(dataSet.getCombiningResources()).isEmpty(); assertThat(dataSet.getOverwritingResources()).isEmpty(); } @Test public void assetsConflict() throws Exception { Path root = fs.getPath("root"); Path otherRoot = fs.getPath("otherRoot"); ParsedAndroidData dataSet = ParsedAndroidData.from( ImmutableList.of( AndroidDataBuilder.of(root) .addAsset("bin/boojum") .createManifest("AndroidManifest.xml", "com.google.foo", "") .buildDependency(), AndroidDataBuilder.of(otherRoot) .addAsset("bin/boojum") .createManifest("AndroidManifest.xml", "com.google.foo", "") .buildDependency())); DataSource assetSource = DataSource.of(root.resolve("assets/bin/boojum")); DataSource otherAssetSource = DataSource.of(otherRoot.resolve("assets/bin/boojum")); RelativeAssetPath key = RelativeAssetPath.Factory.of(root.resolve("assets")).create(assetSource.getPath()); Truth.assertAbout(parsedAndroidData) .that(dataSet) .isEqualTo( ParsedAndroidData.of( ImmutableSet.of( MergeConflict.of( key, DataValueFile.of(assetSource), DataValueFile.of(otherAssetSource))), ImmutableMap.of(), ImmutableMap.of(), ImmutableMap.of( key, DataValueFile.of(otherAssetSource.overwrite(assetSource))))); } @Test public void fileResources() throws Exception { Path root = fs.getPath("transDep"); ParsedAndroidData dataSet = ParsedAndroidData.from( ImmutableList.of( AndroidDataBuilder.of(root) .addResource("layout/foo.xml", AndroidDataBuilder.ResourceType.LAYOUT, "") .addResourceBinary( "drawable/menu.png", Files.createFile(fs.getPath("menu.png"))) .createManifest("AndroidManifest.xml", "com.google.foo", "") .buildDependency())); FullyQualifiedName layoutFoo = fqnFactory.parse("layout/foo"); FullyQualifiedName drawableMenu = fqnFactory.parse("drawable/menu"); assertThat(dataSet.getOverwritingResources()) .containsExactly( layoutFoo, DataValueFile.of(root.resolve("res/layout/foo.xml")), drawableMenu, DataValueFile.of(root.resolve("res/drawable/menu.png"))); assertThat(dataSet.getCombiningResources()).isEmpty(); } @Test public void ninePatchResourceName() throws Exception { // drawable/foo.9.png should give a resource named "R.drawable.foo" Path root = fs.getPath("transDep"); ParsedAndroidData dataSet = ParsedAndroidData.from( ImmutableList.of( AndroidDataBuilder.of(root) .addResourceBinary( "drawable-hdpi/seven_eight.9.png", Files.createFile(fs.getPath("seven_eight.9.png"))) .createManifest("AndroidManifest.xml", "com.google.foo", "") .buildDependency())); FullyQualifiedName.Factory fqnFactory = FullyQualifiedName.Factory.from(ImmutableList.of("hdpi", "v4")); FullyQualifiedName drawable = fqnFactory.parse("drawable/seven_eight"); assertThat(dataSet.getOverwritingResources()) .containsExactly( drawable, DataValueFile.of(root.resolve("res/drawable-hdpi/seven_eight.9.png"))); assertThat(dataSet.getCombiningResources()).isEmpty(); } @Test public void fileResourcesSkipInvalidQualifier() throws Exception { Path root = fs.getPath("transDep"); ParsedAndroidData dataSet = ParsedAndroidData.from( ImmutableList.of( AndroidDataBuilder.of(root) .addResource("layout/foo.xml", AndroidDataBuilder.ResourceType.LAYOUT, "") .addResourceBinary( "drawable-scooby-doo/menu.png", Files.createFile(fs.getPath("menu.png"))) .createManifest("AndroidManifest.xml", "com.google.foo", "") .buildDependency())); FullyQualifiedName layoutFoo = fqnFactory.parse("layout/foo"); assertThat(dataSet.getOverwritingResources()) .containsExactly(layoutFoo, DataValueFile.of(root.resolve("res/layout/foo.xml"))); assertThat(dataSet.getCombiningResources()).isEmpty(); } @Test public void nestedFileResources() throws Exception { Path parent = fs.getPath("parent"); Path root = parent.resolve("transDep"); AndroidDataBuilder.of(root) .addResource("layout/foo.xml", AndroidDataBuilder.ResourceType.LAYOUT, "") .addResourceBinary("drawable/menu.png", Files.createFile(fs.getPath("menu.png"))) .createManifest("AndroidManifest.xml", "com.google.foo", "") .buildDependency(); ParsedAndroidData dataSet = ParsedAndroidData.from( new UnvalidatedAndroidData( ImmutableList.of(root), ImmutableList.of(), root.resolve("AndroidManifest.xml"))); FullyQualifiedName layoutFoo = fqnFactory.parse("layout/foo"); FullyQualifiedName drawableMenu = fqnFactory.parse("drawable/menu"); assertThat(dataSet.getOverwritingResources()) .containsExactly( layoutFoo, DataValueFile.of(root.resolve("res/layout/foo.xml")), drawableMenu, DataValueFile.of(root.resolve("res/drawable/menu.png"))); assertThat(dataSet.getCombiningResources()).isEmpty(); } @Test public void xmlAndFileResources() throws Exception { Path root = fs.getPath("root"); DependencyAndroidData dep = AndroidDataBuilder.of(root) .addResource("layout/foo.xml", AndroidDataBuilder.ResourceType.LAYOUT, "") .addResourceBinary("drawable/menu.png", Files.createFile(fs.getPath("menu.png"))) .addValuesWithAttributes( "values/attr.xml", ImmutableMap.of("foo", "fooVal"), "way out", "", " ", " ", " ", " ", "", "") .createManifest("AndroidManifest.xml", "com.google.foo", "") .buildDependency(); ParsedAndroidData dataSet = ParsedAndroidData.from(ImmutableList.of(dep)); FullyQualifiedName layoutFoo = fqnFactory.parse("layout/foo"); FullyQualifiedName drawableMenu = fqnFactory.parse("drawable/menu"); FullyQualifiedName attributeFoo = fqnFactory.parse("/foo"); FullyQualifiedName stringExit = fqnFactory.parse("string/exit"); FullyQualifiedName attrLabelPosition = fqnFactory.parse("attr/labelPosition"); assertThat(dataSet.getOverwritingResources()) .containsExactly( layoutFoo, // key DataValueFile.of(root.resolve("res/layout/foo.xml")), // value drawableMenu, // key DataValueFile.of(root.resolve("res/drawable/menu.png")), // value attributeFoo, // key DataResourceXml.createWithNoNamespace( root.resolve("res/values/attr.xml"), ResourcesAttribute.of(attributeFoo, "foo", "fooVal")), // value stringExit, // key DataResourceXml.createWithNoNamespace( root.resolve("res/values/attr.xml"), SimpleXmlResourceValue.createWithValue( SimpleXmlResourceValue.Type.STRING, "way out")), // value attrLabelPosition, // key DataResourceXml.createWithNoNamespace( root.resolve("res/values/attr.xml"), // value AttrXmlResourceValue.of( ImmutableMap.of( "enum", AttrXmlResourceValue.EnumResourceXmlAttrValue.of( ImmutableMap.of("left", "0", "right", "1")))))); FullyQualifiedName styleableTheme = fqnFactory.parse("styleable/Theme"); FullyQualifiedName idSomeId = fqnFactory.parse("id/some_id"); assertThat(dataSet.getCombiningResources()) .containsExactly( styleableTheme, // key DataResourceXml.createWithNoNamespace( root.resolve("res/values/attr.xml"), StyleableXmlResourceValue.createAllAttrAsDefinitions( fqnFactory.parse("attr/labelPosition"))), // value idSomeId, // key DataResourceXml.createWithNoNamespace( root.resolve("res/values/attr.xml"), IdXmlResourceValue.of())); // value assertThat(dataSet.getAssets()).isEmpty(); } @Test public void combiningResources() throws Exception { Path root = fs.getPath("root"); DependencyAndroidData dep = AndroidDataBuilder.of(root) .addResource( "values/attr.xml", AndroidDataBuilder.ResourceType.VALUE, "", " ", "", "", " ", "", "") .createManifest("AndroidManifest.xml", "com.google.foo", "") .buildDependency(); ParsedAndroidData dataSet = ParsedAndroidData.from(ImmutableList.of(dep)); assertThat(dataSet.getOverwritingResources()).isEmpty(); FullyQualifiedName styleableDoors = fqnFactory.parse("styleable/Doors"); FullyQualifiedName idExitId = fqnFactory.parse("id/exit_id"); assertThat(dataSet.getCombiningResources()) .containsExactly( styleableDoors, // key DataResourceXml.createWithNoNamespace( root.resolve("res/values/attr.xml"), StyleableXmlResourceValue.createAllAttrAsReferences( fqnFactory.parse("attr/egress"), fqnFactory.parse("attr/exit"))), // value idExitId, // key DataResourceXml.createWithNoNamespace( root.resolve("res/values/attr.xml"), IdXmlResourceValue.of())); // value assertThat(dataSet.getAssets()).isEmpty(); } @Test public void xmlAndFileResourcesConflict() throws Exception { Path root = fs.getPath("root"); Path otherRoot = fs.getPath("otherRoot"); DependencyAndroidData dep = AndroidDataBuilder.of(root) .addResourceBinary("drawable/menu.png", Files.createFile(fs.getPath("menu1.png"))) .addValuesWithAttributes( "values/attr.xml", ImmutableMap.of("foo", "fooVal"), "way out", "") .createManifest("AndroidManifest.xml", "com.google.foo", "") .buildDependency(); DependencyAndroidData otherDep = AndroidDataBuilder.of(otherRoot) .addResourceBinary("drawable/menu.png", Files.createFile(fs.getPath("menu2.png"))) .addValuesWithAttributes( "values/attr.xml", ImmutableMap.of("foo", "fooVal"), "way out", "") .createManifest("AndroidManifest.xml", "com.google.foo", "") .buildDependency(); ParsedAndroidData dataSet = ParsedAndroidData.from(ImmutableList.of(dep, otherDep)); FullyQualifiedName drawableMenu = fqnFactory.parse("drawable/menu"); FullyQualifiedName stringExit = fqnFactory.parse("string/exit"); FullyQualifiedName attributeFoo = fqnFactory.parse("/foo"); DataSource rootDrawableMenuPath = DataSource.of(root.resolve("res/drawable/menu.png")); DataSource otherRootDrawableMenuPath = DataSource.of(otherRoot.resolve("res/drawable/menu.png")); DataSource rootValuesPath = DataSource.of(root.resolve("res/values/attr.xml")); DataSource otherRootValuesPath = DataSource.of(otherRoot.resolve("res/values/attr.xml")); FullyQualifiedName idSomeId = fqnFactory.parse("id/some_id"); Truth.assertAbout(parsedAndroidData) .that(dataSet) .isEqualTo( ParsedAndroidData.of( ImmutableSet.of( MergeConflict.of( drawableMenu, DataValueFile.of(rootDrawableMenuPath), DataValueFile.of(otherRootDrawableMenuPath)), MergeConflict.of( stringExit, DataResourceXml.createWithNoNamespace( rootValuesPath, SimpleXmlResourceValue.createWithValue( SimpleXmlResourceValue.Type.STRING, "way out")), DataResourceXml.createWithNoNamespace( otherRootValuesPath, SimpleXmlResourceValue.createWithValue( SimpleXmlResourceValue.Type.STRING, "way out"))), MergeConflict.of( attributeFoo, DataResourceXml.createWithNoNamespace( rootValuesPath, ResourcesAttribute.of(attributeFoo, "foo", "fooVal")), DataResourceXml.createWithNoNamespace( otherRootValuesPath, ResourcesAttribute.of(attributeFoo, "foo", "fooVal")))), ImmutableMap.of( drawableMenu, // key DataValueFile.of( otherRootDrawableMenuPath.overwrite(rootDrawableMenuPath)), // value attributeFoo, // key DataResourceXml.createWithNoNamespace( otherRootValuesPath.overwrite(rootValuesPath), ResourcesAttribute.of(attributeFoo, "foo", "fooVal")), // value stringExit, // key DataResourceXml.createWithNoNamespace( otherRootValuesPath.overwrite(rootValuesPath), SimpleXmlResourceValue.createWithValue( SimpleXmlResourceValue.Type.STRING, "way out")) // value ), ImmutableMap.of( idSomeId, // key DataResourceXml.createWithNoNamespace( rootValuesPath, IdXmlResourceValue.of()) // value ), ImmutableMap.of())); } @Test public void layoutWithIDsForwardDeclared() throws Exception { Path root = fs.getPath("root"); ParsedAndroidData dataSet = AndroidDataBuilder.of(root) .addResource( "layout/some_layout.xml", AndroidDataBuilder.ResourceType.LAYOUT, "", "") .addResource( "values/strings.xml", AndroidDataBuilder.ResourceType.VALUE, "I has a bucket") .createManifest("AndroidManifest.xml", "com.carroll.lewis", "") .buildParsed(); FullyQualifiedName layoutKey = fqnFactory.parse("layout/some_layout"); FullyQualifiedName stringKey = fqnFactory.parse("string/walrus"); Path layoutPath = root.resolve("res/layout/some_layout.xml"); Path valuesPath = root.resolve("res/values/strings.xml"); assertThat(dataSet.getOverwritingResources()) .containsExactly( layoutKey, DataValueFile.of(layoutPath), stringKey, DataResourceXml.createWithNoNamespace( valuesPath, SimpleXmlResourceValue.createWithValue( SimpleXmlResourceValue.Type.STRING, "I has a bucket"))); FullyQualifiedName idTextView = fqnFactory.parse("id/MyTextView"); FullyQualifiedName idTextView2 = fqnFactory.parse("id/AnotherTextView"); assertThat(dataSet.getCombiningResources()) .containsExactly( idTextView, DataResourceXml.createWithNoNamespace(layoutPath, IdXmlResourceValue.of()), idTextView2, DataResourceXml.createWithNoNamespace(layoutPath, IdXmlResourceValue.of())); } @Test public void databindingWithIDs() throws Exception { Path root = fs.getPath("root"); ParsedAndroidData dataSet = AndroidDataBuilder.of(root) .addResource( "layout/databinding_layout.xml", AndroidDataBuilder.ResourceType.UNFORMATTED, "", "", " ", " ", " ", " ", " ", " ", " ", " ", "") .createManifest("AndroidManifest.xml", "com.xyz", "") .buildParsed(); FullyQualifiedName layoutKey = fqnFactory.parse("layout/databinding_layout"); Path layoutPath = root.resolve("res/layout/databinding_layout.xml"); assertThat(dataSet.getOverwritingResources()) .containsExactly(layoutKey, DataValueFile.of(layoutPath)); FullyQualifiedName idTextView = fqnFactory.parse("id/MyTextView"); FullyQualifiedName idTextView2 = fqnFactory.parse("id/AnotherTextView"); assertThat(dataSet.getCombiningResources()) .containsExactly( idTextView, DataResourceXml.createWithNoNamespace(layoutPath, IdXmlResourceValue.of()), idTextView2, DataResourceXml.createWithNoNamespace(layoutPath, IdXmlResourceValue.of())); } @Test public void menuWithByteOrderMark() throws Exception { Path root = fs.getPath("root"); ParsedAndroidData dataSet = AndroidDataBuilder.of(root) .addResource( "menu/some_menu.xml", AndroidDataBuilder.ResourceType.UNFORMATTED, "\uFEFF", "", " ", " ", " ", "") .createManifest("AndroidManifest.xml", "com.xyz", "") .buildParsed(); FullyQualifiedName menuKey = fqnFactory.parse("menu/some_menu"); Path menuPath = root.resolve("res/menu/some_menu.xml"); assertThat(dataSet.getOverwritingResources()) .containsExactly(menuKey, DataValueFile.of(menuPath)); FullyQualifiedName groupIdKey = fqnFactory.parse("id/some_group"); FullyQualifiedName itemIdKey = fqnFactory.parse("id/action_settings"); assertThat(dataSet.getCombiningResources()) .containsExactly( groupIdKey, DataResourceXml.createWithNoNamespace(menuPath, IdXmlResourceValue.of()), itemIdKey, DataResourceXml.createWithNoNamespace(menuPath, IdXmlResourceValue.of())); } @Test public void drawableWithIDs() throws Exception { Path root = fs.getPath("root"); ParsedAndroidData dataSet = AndroidDataBuilder.of(root) .addResource( "drawable-v21/some_drawable.xml", AndroidDataBuilder.ResourceType.UNFORMATTED, "", "", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "") .createManifest("AndroidManifest.xml", "com.carroll.lewis", "") .buildParsed(); FullyQualifiedName.Factory fqnV21Factory = FullyQualifiedName.Factory.from(ImmutableList.of("v21")); FullyQualifiedName drawableKey = fqnV21Factory.parse("drawable/some_drawable"); Path drawablePath = root.resolve("res/drawable-v21/some_drawable.xml"); assertThat(dataSet.getOverwritingResources()) .containsExactly(drawableKey, DataValueFile.of(drawablePath)); FullyQualifiedName focusedState = fqnV21Factory.parse("id/focused_state"); FullyQualifiedName defaultState = fqnV21Factory.parse("id/default_state"); FullyQualifiedName pressedState = fqnV21Factory.parse("id/pressed_state"); assertThat(dataSet.getCombiningResources()) .containsExactly( focusedState, DataResourceXml.createWithNoNamespace(drawablePath, IdXmlResourceValue.of()), defaultState, DataResourceXml.createWithNoNamespace(drawablePath, IdXmlResourceValue.of()), pressedState, DataResourceXml.createWithNoNamespace(drawablePath, IdXmlResourceValue.of())); } @Test public void testParseErrorsHaveContext() throws Exception { Path root = fs.getPath("root"); AndroidDataBuilder builder = AndroidDataBuilder.of(root) .addResource( "values/unique_strings.xml", AndroidDataBuilder.ResourceType.VALUE, "Hello", "Unrecognized tag") .addResource( "layout/unique_layout.xml", AndroidDataBuilder.ResourceType.UNFORMATTED, "", "", "") .addResource( "menu/unique_menu.xml", AndroidDataBuilder.ResourceType.UNFORMATTED, "", "", "") .createManifest("AndroidManifest.xml", "com.xyz", ""); try { builder.buildParsed(); fail("expected MergingException"); } catch (MergingException e) { assertThat(e) .hasMessageThat() .isEqualTo(MergingException.withMessage("3 Parse Error(s)").getMessage()); String combinedSuberrors = Joiner.on('\n').join(e.getSuppressed()); assertThat(combinedSuberrors) .contains(fs.getPath("values/unique_strings.xml") + ": ParseError at [row,col]:[3,35]"); assertThat(combinedSuberrors) .contains("unrecognized resource type: "); assertThat(combinedSuberrors) .contains(fs.getPath("layout/unique_layout.xml") + ": ParseError at [row,col]:[6,3]"); assertThat(combinedSuberrors).contains("must be terminated by the matching end-tag"); assertThat(combinedSuberrors) .contains(fs.getPath("menu/unique_menu.xml") + ": ParseError at [row,col]:[1,30]"); assertThat(combinedSuberrors) .contains("XML version \"not_a_version\" is not supported, only XML 1.0 is supported"); } } @Test public void publicResourceValidation() throws Exception { Path root = fs.getPath("root"); AndroidDataBuilder builder = AndroidDataBuilder.of(root) .addResource( "values/missing_name.xml", AndroidDataBuilder.ResourceType.VALUE, "") .addResource( "values/missing_type.xml", AndroidDataBuilder.ResourceType.VALUE, "") .addResource( "values/bad_type.xml", AndroidDataBuilder.ResourceType.VALUE, "") .addResource( "values/invalid_id.xml", AndroidDataBuilder.ResourceType.VALUE, "") .addResource( "values/overflow_id.xml", AndroidDataBuilder.ResourceType.VALUE, "") .createManifest("AndroidManifest.xml", "com.carroll.lewis", ""); try { builder.buildParsed(); fail("expected MergingException"); } catch (MergingException e) { assertThat(e) .hasMessageThat() .isEqualTo(MergingException.withMessage("5 Parse Error(s)").getMessage()); String combinedSuberrors = Joiner.on('\n').join(e.getSuppressed()); assertThat(combinedSuberrors).contains(fs.getPath("values/missing_name.xml").toString()); assertThat(combinedSuberrors).contains("resource name is required for public"); assertThat(combinedSuberrors).contains(fs.getPath("values/missing_type.xml").toString()); assertThat(combinedSuberrors).contains("missing type attribute"); assertThat(combinedSuberrors).contains(fs.getPath("values/bad_type.xml").toString()); assertThat(combinedSuberrors).contains("has invalid type attribute"); assertThat(combinedSuberrors).contains(fs.getPath("values/invalid_id.xml").toString()); assertThat(combinedSuberrors).contains("has invalid id number"); assertThat(combinedSuberrors).contains(fs.getPath("values/overflow_id.xml").toString()); assertThat(combinedSuberrors).contains("has invalid id number"); } } final Subject.Factory parsedAndroidData = ParsedAndroidDataSubject::new; }