/* * Copyright (C) 2015 The Android Open Source Project * * 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. */ #include "link/TableMerger.h" #include "filter/ConfigFilter.h" #include "io/FileSystem.h" #include "test/Test.h" using ::aapt::test::ValueEq; using ::testing::Contains; using ::testing::Eq; using ::testing::Field; using ::testing::NotNull; using ::testing::Pointee; using ::testing::StrEq; using ::testing::UnorderedElementsAreArray; using PolicyFlags = android::ResTable_overlayable_policy_header::PolicyFlags; namespace aapt { struct TableMergerTest : public ::testing::Test { std::unique_ptr context_; void SetUp() override { context_ = test::ContextBuilder() // We are compiling this package. .SetCompilationPackage("com.app.a") // Merge all packages that have this package ID. .SetPackageId(0x7f) // Mangle all packages that do not have this package name. .SetNameManglerPolicy(NameManglerPolicy{"com.app.a", {"com.app.b"}}) .Build(); } }; TEST_F(TableMergerTest, SimpleMerge) { std::unique_ptr table_a = test::ResourceTableBuilder() .AddReference("com.app.a:id/foo", "com.app.a:id/bar") .AddReference("com.app.a:id/bar", "com.app.b:id/foo") .AddValue("com.app.a:styleable/view", test::StyleableBuilder().AddItem("com.app.b:id/foo").Build()) .Build(); std::unique_ptr table_b = test::ResourceTableBuilder() .AddSimple("com.app.b:id/foo") .Build(); ResourceTable final_table; TableMerger merger(context_.get(), &final_table, TableMergerOptions{}); ASSERT_TRUE(merger.Merge({}, table_a.get(), false /*overlay*/)); ASSERT_TRUE(merger.MergeAndMangle({}, "com.app.b", table_b.get())); EXPECT_TRUE(merger.merged_packages().count("com.app.b") != 0); // Entries from com.app.a should not be mangled. EXPECT_TRUE(final_table.FindResource(test::ParseNameOrDie("com.app.a:id/foo"))); EXPECT_TRUE(final_table.FindResource(test::ParseNameOrDie("com.app.a:id/bar"))); EXPECT_TRUE(final_table.FindResource(test::ParseNameOrDie("com.app.a:styleable/view"))); // The unmangled name should not be present. EXPECT_FALSE(final_table.FindResource(test::ParseNameOrDie("com.app.b:id/foo"))); // Look for the mangled name. EXPECT_TRUE(final_table.FindResource(test::ParseNameOrDie("com.app.a:id/com.app.b$foo"))); } TEST_F(TableMergerTest, MergeFile) { ResourceTable final_table; TableMergerOptions options; options.auto_add_overlay = false; TableMerger merger(context_.get(), &final_table, options); ResourceFile file_desc; file_desc.config = test::ParseConfigOrDie("hdpi-v4"); file_desc.name = test::ParseNameOrDie("layout/main"); file_desc.source = Source("res/layout-hdpi/main.xml"); test::TestFile test_file("path/to/res/layout-hdpi/main.xml.flat"); ASSERT_TRUE(merger.MergeFile(file_desc, false /*overlay*/, &test_file)); FileReference* file = test::GetValueForConfig( &final_table, "com.app.a:layout/main", test::ParseConfigOrDie("hdpi-v4")); ASSERT_THAT(file, NotNull()); EXPECT_EQ(std::string("res/layout-hdpi-v4/main.xml"), *file->path); } TEST_F(TableMergerTest, MergeFileOverlay) { ResourceTable final_table; TableMergerOptions options; options.auto_add_overlay = false; TableMerger merger(context_.get(), &final_table, options); ResourceFile file_desc; file_desc.name = test::ParseNameOrDie("xml/foo"); test::TestFile file_a("path/to/fileA.xml.flat"); test::TestFile file_b("path/to/fileB.xml.flat"); ASSERT_TRUE(merger.MergeFile(file_desc, false /*overlay*/, &file_a)); ASSERT_TRUE(merger.MergeFile(file_desc, true /*overlay*/, &file_b)); } TEST_F(TableMergerTest, MergeFileReferences) { test::TestFile file_a("res/xml/file.xml"); test::TestFile file_b("res/xml/file.xml"); std::unique_ptr table_a = test::ResourceTableBuilder() .AddFileReference("com.app.a:xml/file", "res/xml/file.xml", &file_a) .Build(); std::unique_ptr table_b = test::ResourceTableBuilder() .AddFileReference("com.app.b:xml/file", "res/xml/file.xml", &file_b) .Build(); ResourceTable final_table; TableMerger merger(context_.get(), &final_table, TableMergerOptions{}); ASSERT_TRUE(merger.Merge({}, table_a.get(), false /*overlay*/)); ASSERT_TRUE(merger.MergeAndMangle({}, "com.app.b", table_b.get())); FileReference* f = test::GetValue(&final_table, "com.app.a:xml/file"); ASSERT_THAT(f, NotNull()); EXPECT_THAT(*f->path, StrEq("res/xml/file.xml")); EXPECT_THAT(f->file, Eq(&file_a)); f = test::GetValue(&final_table, "com.app.a:xml/com.app.b$file"); ASSERT_THAT(f, NotNull()); EXPECT_THAT(*f->path, StrEq("res/xml/com.app.b$file.xml")); EXPECT_THAT(f->file, Eq(&file_b)); } TEST_F(TableMergerTest, OverrideResourceWithOverlay) { std::unique_ptr base = test::ResourceTableBuilder() .AddValue("bool/foo", ResourceUtils::TryParseBool("true")) .Build(); std::unique_ptr overlay = test::ResourceTableBuilder() .AddValue("bool/foo", ResourceUtils::TryParseBool("false")) .Build(); ResourceTable final_table; TableMergerOptions options; options.auto_add_overlay = false; TableMerger merger(context_.get(), &final_table, options); ASSERT_TRUE(merger.Merge({}, base.get(), false /*overlay*/)); ASSERT_TRUE(merger.Merge({}, overlay.get(), true /*overlay*/)); BinaryPrimitive* foo = test::GetValue(&final_table, "com.app.a:bool/foo"); ASSERT_THAT(foo, Pointee(Field(&BinaryPrimitive::value, Field(&android::Res_value::data, Eq(0u))))); } TEST_F(TableMergerTest, DoNotOverrideResourceComment) { std::unique_ptr foo_original = ResourceUtils::TryParseBool("true"); foo_original->SetComment(android::StringPiece("Original foo comment")); std::unique_ptr bar_original = ResourceUtils::TryParseBool("true"); std::unique_ptr foo_overlay = ResourceUtils::TryParseBool("false"); foo_overlay->SetComment(android::StringPiece("Overlay foo comment")); std::unique_ptr bar_overlay = ResourceUtils::TryParseBool("false"); bar_overlay->SetComment(android::StringPiece("Overlay bar comment")); std::unique_ptr baz_overlay = ResourceUtils::TryParseBool("false"); baz_overlay->SetComment(android::StringPiece("Overlay baz comment")); std::unique_ptr base = test::ResourceTableBuilder() .AddValue("bool/foo", std::move(foo_original)) .AddValue("bool/bar", std::move(bar_original)) .Build(); std::unique_ptr overlay = test::ResourceTableBuilder() .AddValue("bool/foo", std::move(foo_overlay)) .AddValue("bool/bar", std::move(bar_overlay)) .AddValue("bool/baz", std::move(baz_overlay)) .Build(); ResourceTable final_table; TableMergerOptions options; options.auto_add_overlay = true; TableMerger merger(context_.get(), &final_table, options); ASSERT_TRUE(merger.Merge({}, base.get(), false /*overlay*/)); ASSERT_TRUE(merger.Merge({}, overlay.get(), true /*overlay*/)); BinaryPrimitive* foo = test::GetValue(&final_table, "com.app.a:bool/foo"); EXPECT_THAT(foo, Pointee(Property(&BinaryPrimitive::GetComment, StrEq("Original foo comment")))); BinaryPrimitive* bar = test::GetValue(&final_table, "com.app.a:bool/bar"); EXPECT_THAT(bar, Pointee(Property(&BinaryPrimitive::GetComment, StrEq("")))); BinaryPrimitive* baz = test::GetValue(&final_table, "com.app.a:bool/baz"); EXPECT_THAT(baz, Pointee(Property(&BinaryPrimitive::GetComment, StrEq("Overlay baz comment")))); } TEST_F(TableMergerTest, OverrideSameResourceIdsWithOverlay) { std::unique_ptr base = test::ResourceTableBuilder() .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), Visibility::Level::kPublic) .Build(); std::unique_ptr overlay = test::ResourceTableBuilder() .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), Visibility::Level::kPublic) .Build(); ResourceTable final_table; TableMergerOptions options; options.auto_add_overlay = false; TableMerger merger(context_.get(), &final_table, options); ASSERT_TRUE(merger.Merge({}, base.get(), false /*overlay*/)); ASSERT_TRUE(merger.Merge({}, overlay.get(), true /*overlay*/)); } TEST_F(TableMergerTest, FailToOverrideConflictingTypeIdsWithOverlay) { std::unique_ptr base = test::ResourceTableBuilder() .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), Visibility::Level::kPublic) .Build(); std::unique_ptr overlay = test::ResourceTableBuilder() .SetSymbolState("bool/foo", ResourceId(0x7f, 0x02, 0x0001), Visibility::Level::kPublic) .Build(); ResourceTable final_table; TableMergerOptions options; options.auto_add_overlay = false; TableMerger merger(context_.get(), &final_table, options); ASSERT_TRUE(merger.Merge({}, base.get(), false /*overlay*/)); ASSERT_FALSE(merger.Merge({}, overlay.get(), true /*overlay*/)); } TEST_F(TableMergerTest, FailToOverrideConflictingEntryIdsWithOverlay) { std::unique_ptr base = test::ResourceTableBuilder() .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), Visibility::Level::kPublic) .Build(); std::unique_ptr overlay = test::ResourceTableBuilder() .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0002), Visibility::Level::kPublic) .Build(); ResourceTable final_table; TableMergerOptions options; options.auto_add_overlay = false; TableMerger merger(context_.get(), &final_table, options); ASSERT_TRUE(merger.Merge({}, base.get(), false /*overlay*/)); ASSERT_FALSE(merger.Merge({}, overlay.get(), true /*overlay*/)); } TEST_F(TableMergerTest, FailConflictingVisibility) { std::unique_ptr base = test::ResourceTableBuilder() .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), Visibility::Level::kPublic) .Build(); std::unique_ptr overlay = test::ResourceTableBuilder() .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), Visibility::Level::kPrivate) .Build(); // It should fail if the "--strict-visibility" flag is set. ResourceTable final_table; TableMergerOptions options; options.auto_add_overlay = false; options.strict_visibility = true; TableMerger merger(context_.get(), &final_table, options); ASSERT_TRUE(merger.Merge({}, base.get(), false /*overlay*/)); ASSERT_FALSE(merger.Merge({}, overlay.get(), true /*overlay*/)); // But it should still pass if the flag is not set. ResourceTable final_table2; options.strict_visibility = false; TableMerger merger2(context_.get(), &final_table2, options); ASSERT_TRUE(merger2.Merge({}, base.get(), false /*overlay*/)); ASSERT_TRUE(merger2.Merge({}, overlay.get(), true /*overlay*/)); } TEST_F(TableMergerTest, MergeAddResourceFromOverlay) { std::unique_ptr table_a = test::ResourceTableBuilder().Build(); std::unique_ptr table_b = test::ResourceTableBuilder() .SetSymbolState("bool/foo", {}, Visibility::Level::kUndefined, true /*allow new overlay*/) .AddValue("bool/foo", ResourceUtils::TryParseBool("true")) .Build(); ResourceTable final_table; TableMergerOptions options; options.auto_add_overlay = false; TableMerger merger(context_.get(), &final_table, options); ASSERT_TRUE(merger.Merge({}, table_a.get(), false /*overlay*/)); ASSERT_TRUE(merger.Merge({}, table_b.get(), false /*overlay*/)); } TEST_F(TableMergerTest, MergeAddResourceFromOverlayWithAutoAddOverlay) { std::unique_ptr table_a = test::ResourceTableBuilder().Build(); std::unique_ptr table_b = test::ResourceTableBuilder() .AddValue("bool/foo", ResourceUtils::TryParseBool("true")) .Build(); ResourceTable final_table; TableMergerOptions options; options.auto_add_overlay = true; TableMerger merger(context_.get(), &final_table, options); ASSERT_TRUE(merger.Merge({}, table_a.get(), false /*overlay*/)); ASSERT_TRUE(merger.Merge({}, table_b.get(), false /*overlay*/)); } TEST_F(TableMergerTest, FailToMergeNewResourceWithoutAutoAddOverlay) { std::unique_ptr table_a = test::ResourceTableBuilder().Build(); std::unique_ptr table_b = test::ResourceTableBuilder() .AddValue("bool/foo", ResourceUtils::TryParseBool("true")) .Build(); ResourceTable final_table; TableMergerOptions options; options.auto_add_overlay = false; TableMerger merger(context_.get(), &final_table, options); ASSERT_TRUE(merger.Merge({}, table_a.get(), false /*overlay*/)); ASSERT_FALSE(merger.Merge({}, table_b.get(), true /*overlay*/)); } TEST_F(TableMergerTest, OverlaidStyleablesAndStylesShouldBeMerged) { std::unique_ptr table_a = test::ResourceTableBuilder() .AddValue("com.app.a:styleable/Foo", test::StyleableBuilder() .AddItem("com.app.a:attr/bar") .AddItem("com.app.a:attr/foo", ResourceId(0x01010000)) .Build()) .AddValue("com.app.a:style/Theme", test::StyleBuilder() .SetParent("com.app.a:style/Parent") .AddItem("com.app.a:attr/bar", util::make_unique()) .AddItem("com.app.a:attr/foo", ResourceUtils::MakeBool(false)) .Build()) .Build(); std::unique_ptr table_b = test::ResourceTableBuilder() .AddValue("com.app.a:styleable/Foo", test::StyleableBuilder() .AddItem("com.app.a:attr/bat") .AddItem("com.app.a:attr/foo") .Build()) .AddValue("com.app.a:style/Theme", test::StyleBuilder() .SetParent("com.app.a:style/OverlayParent") .AddItem("com.app.a:attr/bat", util::make_unique()) .AddItem("com.app.a:attr/foo", ResourceId(0x01010000), ResourceUtils::MakeBool(true)) .Build()) .Build(); ResourceTable final_table; TableMergerOptions options; options.auto_add_overlay = true; TableMerger merger(context_.get(), &final_table, options); ASSERT_TRUE(merger.Merge({}, table_a.get(), false /*overlay*/)); ASSERT_TRUE(merger.Merge({}, table_b.get(), true /*overlay*/)); Styleable* styleable = test::GetValue(&final_table, "com.app.a:styleable/Foo"); ASSERT_THAT(styleable, NotNull()); std::vector expected_refs = { Reference(test::ParseNameOrDie("com.app.a:attr/bar")), Reference(test::ParseNameOrDie("com.app.a:attr/bat")), Reference(test::ParseNameOrDie("com.app.a:attr/foo"), ResourceId(0x01010000)), }; EXPECT_THAT(styleable->entries, UnorderedElementsAreArray(expected_refs)); Style* style = test::GetValue