| // Copyright (C) 2018 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 <optional> |
| #include <tuple> |
| |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| #include <libdm/dm.h> |
| #include <liblp/builder.h> |
| #include <liblp/property_fetcher.h> |
| |
| #include <libsnapshot/test_helpers.h> |
| |
| #include "dm_snapshot_internals.h" |
| #include "partition_cow_creator.h" |
| #include "utility.h" |
| |
| using namespace android::fs_mgr; |
| |
| using chromeos_update_engine::InstallOperation; |
| using UeExtent = chromeos_update_engine::Extent; |
| using google::protobuf::RepeatedPtrField; |
| using ::testing::Matches; |
| using ::testing::Pointwise; |
| using ::testing::Truly; |
| |
| namespace android { |
| namespace snapshot { |
| |
| class PartitionCowCreatorTest : public ::testing::Test { |
| public: |
| void SetUp() override { |
| SKIP_IF_NON_VIRTUAL_AB(); |
| SnapshotTestPropertyFetcher::SetUp(); |
| } |
| void TearDown() override { |
| RETURN_IF_NON_VIRTUAL_AB(); |
| SnapshotTestPropertyFetcher::TearDown(); |
| } |
| }; |
| |
| TEST_F(PartitionCowCreatorTest, IntersectSelf) { |
| constexpr uint64_t super_size = 1_MiB; |
| constexpr uint64_t partition_size = 40_KiB; |
| |
| auto builder_a = MetadataBuilder::New(super_size, 1_KiB, 2); |
| ASSERT_NE(builder_a, nullptr); |
| auto system_a = builder_a->AddPartition("system_a", LP_PARTITION_ATTR_READONLY); |
| ASSERT_NE(system_a, nullptr); |
| ASSERT_TRUE(builder_a->ResizePartition(system_a, partition_size)); |
| |
| auto builder_b = MetadataBuilder::New(super_size, 1_KiB, 2); |
| ASSERT_NE(builder_b, nullptr); |
| auto system_b = builder_b->AddPartition("system_b", LP_PARTITION_ATTR_READONLY); |
| ASSERT_NE(system_b, nullptr); |
| ASSERT_TRUE(builder_b->ResizePartition(system_b, partition_size)); |
| |
| PartitionCowCreator creator{.target_metadata = builder_b.get(), |
| .target_suffix = "_b", |
| .target_partition = system_b, |
| .current_metadata = builder_a.get(), |
| .current_suffix = "_a"}; |
| auto ret = creator.Run(); |
| ASSERT_TRUE(ret.has_value()); |
| ASSERT_EQ(partition_size, ret->snapshot_status.device_size()); |
| ASSERT_EQ(partition_size, ret->snapshot_status.snapshot_size()); |
| } |
| |
| TEST_F(PartitionCowCreatorTest, Holes) { |
| const auto& opener = test_device->GetPartitionOpener(); |
| |
| constexpr auto slack_space = 1_MiB; |
| constexpr auto big_size = (kSuperSize - slack_space) / 2; |
| constexpr auto small_size = big_size / 2; |
| |
| BlockDeviceInfo super_device("super", kSuperSize, 0, 0, 4_KiB); |
| std::vector<BlockDeviceInfo> devices = {super_device}; |
| auto source = MetadataBuilder::New(devices, "super", 1_KiB, 2); |
| auto system = source->AddPartition("system_a", 0); |
| ASSERT_NE(nullptr, system); |
| ASSERT_TRUE(source->ResizePartition(system, big_size)); |
| auto vendor = source->AddPartition("vendor_a", 0); |
| ASSERT_NE(nullptr, vendor); |
| ASSERT_TRUE(source->ResizePartition(vendor, big_size)); |
| // Create a hole between system and vendor |
| ASSERT_TRUE(source->ResizePartition(system, small_size)); |
| auto source_metadata = source->Export(); |
| ASSERT_NE(nullptr, source_metadata); |
| ASSERT_TRUE(FlashPartitionTable(opener, fake_super, *source_metadata.get())); |
| |
| auto target = MetadataBuilder::NewForUpdate(opener, "super", 0, 1); |
| // Shrink vendor |
| vendor = target->FindPartition("vendor_b"); |
| ASSERT_NE(nullptr, vendor); |
| ASSERT_TRUE(target->ResizePartition(vendor, small_size)); |
| // Grow system to take hole & saved space from vendor |
| system = target->FindPartition("system_b"); |
| ASSERT_NE(nullptr, system); |
| ASSERT_TRUE(target->ResizePartition(system, big_size * 2 - small_size)); |
| |
| PartitionCowCreator creator{.target_metadata = target.get(), |
| .target_suffix = "_b", |
| .target_partition = system, |
| .current_metadata = source.get(), |
| .current_suffix = "_a"}; |
| auto ret = creator.Run(); |
| ASSERT_TRUE(ret.has_value()); |
| } |
| |
| TEST_F(PartitionCowCreatorTest, CowSize) { |
| using InstallOperation = chromeos_update_engine::InstallOperation; |
| using RepeatedInstallOperationPtr = google::protobuf::RepeatedPtrField<InstallOperation>; |
| using Extent = chromeos_update_engine::Extent; |
| |
| constexpr uint64_t super_size = 50_MiB; |
| constexpr uint64_t partition_size = 40_MiB; |
| |
| auto builder_a = MetadataBuilder::New(super_size, 1_KiB, 2); |
| ASSERT_NE(builder_a, nullptr); |
| auto system_a = builder_a->AddPartition("system_a", LP_PARTITION_ATTR_READONLY); |
| ASSERT_NE(system_a, nullptr); |
| ASSERT_TRUE(builder_a->ResizePartition(system_a, partition_size)); |
| |
| auto builder_b = MetadataBuilder::New(super_size, 1_KiB, 2); |
| ASSERT_NE(builder_b, nullptr); |
| auto system_b = builder_b->AddPartition("system_b", LP_PARTITION_ATTR_READONLY); |
| ASSERT_NE(system_b, nullptr); |
| ASSERT_TRUE(builder_b->ResizePartition(system_b, partition_size)); |
| |
| const uint64_t block_size = builder_b->logical_block_size(); |
| const uint64_t chunk_size = kSnapshotChunkSize * dm::kSectorSize; |
| ASSERT_EQ(chunk_size, block_size); |
| |
| auto cow_device_size = [](const std::vector<InstallOperation>& iopv, MetadataBuilder* builder_a, |
| MetadataBuilder* builder_b, Partition* system_b) { |
| PartitionUpdate update; |
| *update.mutable_operations() = RepeatedInstallOperationPtr(iopv.begin(), iopv.end()); |
| |
| PartitionCowCreator creator{.target_metadata = builder_b, |
| .target_suffix = "_b", |
| .target_partition = system_b, |
| .current_metadata = builder_a, |
| .current_suffix = "_a", |
| .update = &update}; |
| |
| auto ret = creator.Run(); |
| |
| if (ret.has_value()) { |
| return ret->snapshot_status.cow_file_size() + ret->snapshot_status.cow_partition_size(); |
| } |
| return std::numeric_limits<uint64_t>::max(); |
| }; |
| |
| std::vector<InstallOperation> iopv; |
| InstallOperation iop; |
| Extent* e; |
| |
| // No data written, no operations performed |
| ASSERT_EQ(2 * chunk_size, cow_device_size(iopv, builder_a.get(), builder_b.get(), system_b)); |
| |
| // No data written |
| e = iop.add_dst_extents(); |
| e->set_start_block(0); |
| e->set_num_blocks(0); |
| iopv.push_back(iop); |
| ASSERT_EQ(2 * chunk_size, cow_device_size(iopv, builder_a.get(), builder_b.get(), system_b)); |
| |
| e = iop.add_dst_extents(); |
| e->set_start_block(1); |
| e->set_num_blocks(0); |
| iopv.push_back(iop); |
| ASSERT_EQ(2 * chunk_size, cow_device_size(iopv, builder_a.get(), builder_b.get(), system_b)); |
| |
| // Fill the first block |
| e = iop.add_dst_extents(); |
| e->set_start_block(0); |
| e->set_num_blocks(1); |
| iopv.push_back(iop); |
| ASSERT_EQ(3 * chunk_size, cow_device_size(iopv, builder_a.get(), builder_b.get(), system_b)); |
| |
| // Fill the second block |
| e = iop.add_dst_extents(); |
| e->set_start_block(1); |
| e->set_num_blocks(1); |
| iopv.push_back(iop); |
| ASSERT_EQ(4 * chunk_size, cow_device_size(iopv, builder_a.get(), builder_b.get(), system_b)); |
| |
| // Jump to 5th block and write 2 |
| e = iop.add_dst_extents(); |
| e->set_start_block(5); |
| e->set_num_blocks(2); |
| iopv.push_back(iop); |
| ASSERT_EQ(6 * chunk_size, cow_device_size(iopv, builder_a.get(), builder_b.get(), system_b)); |
| } |
| |
| TEST_F(PartitionCowCreatorTest, Zero) { |
| constexpr uint64_t super_size = 1_MiB; |
| auto builder_a = MetadataBuilder::New(super_size, 1_KiB, 2); |
| ASSERT_NE(builder_a, nullptr); |
| |
| auto builder_b = MetadataBuilder::New(super_size, 1_KiB, 2); |
| ASSERT_NE(builder_b, nullptr); |
| auto system_b = builder_b->AddPartition("system_b", LP_PARTITION_ATTR_READONLY); |
| ASSERT_NE(system_b, nullptr); |
| |
| PartitionCowCreator creator{.target_metadata = builder_b.get(), |
| .target_suffix = "_b", |
| .target_partition = system_b, |
| .current_metadata = builder_a.get(), |
| .current_suffix = "_a", |
| .update = nullptr}; |
| |
| auto ret = creator.Run(); |
| |
| ASSERT_EQ(0u, ret->snapshot_status.device_size()); |
| ASSERT_EQ(0u, ret->snapshot_status.snapshot_size()); |
| ASSERT_EQ(0u, ret->snapshot_status.cow_file_size()); |
| ASSERT_EQ(0u, ret->snapshot_status.cow_partition_size()); |
| } |
| |
| TEST_F(PartitionCowCreatorTest, CompressionEnabled) { |
| constexpr uint64_t super_size = 1_MiB; |
| auto builder_a = MetadataBuilder::New(super_size, 1_KiB, 2); |
| ASSERT_NE(builder_a, nullptr); |
| |
| auto builder_b = MetadataBuilder::New(super_size, 1_KiB, 2); |
| ASSERT_NE(builder_b, nullptr); |
| auto system_b = builder_b->AddPartition("system_b", LP_PARTITION_ATTR_READONLY); |
| ASSERT_NE(system_b, nullptr); |
| ASSERT_TRUE(builder_b->ResizePartition(system_b, 128_KiB)); |
| |
| PartitionUpdate update; |
| update.set_estimate_cow_size(256_KiB); |
| |
| PartitionCowCreator creator{.target_metadata = builder_b.get(), |
| .target_suffix = "_b", |
| .target_partition = system_b, |
| .current_metadata = builder_a.get(), |
| .current_suffix = "_a", |
| .using_snapuserd = true, |
| .update = &update}; |
| |
| auto ret = creator.Run(); |
| ASSERT_TRUE(ret.has_value()); |
| ASSERT_EQ(ret->snapshot_status.cow_file_size(), 1458176); |
| } |
| |
| TEST_F(PartitionCowCreatorTest, CompressionWithNoManifest) { |
| constexpr uint64_t super_size = 1_MiB; |
| auto builder_a = MetadataBuilder::New(super_size, 1_KiB, 2); |
| ASSERT_NE(builder_a, nullptr); |
| |
| auto builder_b = MetadataBuilder::New(super_size, 1_KiB, 2); |
| ASSERT_NE(builder_b, nullptr); |
| auto system_b = builder_b->AddPartition("system_b", LP_PARTITION_ATTR_READONLY); |
| ASSERT_NE(system_b, nullptr); |
| ASSERT_TRUE(builder_b->ResizePartition(system_b, 128_KiB)); |
| |
| PartitionUpdate update; |
| |
| PartitionCowCreator creator{.target_metadata = builder_b.get(), |
| .target_suffix = "_b", |
| .target_partition = system_b, |
| .current_metadata = builder_a.get(), |
| .current_suffix = "_a", |
| .using_snapuserd = true, |
| .update = nullptr}; |
| |
| auto ret = creator.Run(); |
| ASSERT_FALSE(ret.has_value()); |
| } |
| |
| TEST(DmSnapshotInternals, CowSizeCalculator) { |
| SKIP_IF_NON_VIRTUAL_AB(); |
| |
| DmSnapCowSizeCalculator cc(512, 8); |
| unsigned long int b; |
| |
| // Empty COW |
| ASSERT_EQ(cc.cow_size_sectors(), 16); |
| |
| // First chunk written |
| for (b = 0; b < 4_KiB; ++b) { |
| cc.WriteByte(b); |
| ASSERT_EQ(cc.cow_size_sectors(), 24); |
| } |
| |
| // Second chunk written |
| for (b = 4_KiB; b < 8_KiB; ++b) { |
| cc.WriteByte(b); |
| ASSERT_EQ(cc.cow_size_sectors(), 32); |
| } |
| |
| // Leave a hole and write 5th chunk |
| for (b = 16_KiB; b < 20_KiB; ++b) { |
| cc.WriteByte(b); |
| ASSERT_EQ(cc.cow_size_sectors(), 40); |
| } |
| |
| // Write a byte that would surely overflow the counter |
| cc.WriteChunk(std::numeric_limits<uint64_t>::max()); |
| ASSERT_FALSE(cc.cow_size_sectors().has_value()); |
| } |
| |
| void BlocksToExtents(const std::vector<uint64_t>& blocks, |
| google::protobuf::RepeatedPtrField<UeExtent>* extents) { |
| for (uint64_t block : blocks) { |
| AppendExtent(extents, block, 1); |
| } |
| } |
| |
| template <typename T> |
| std::vector<uint64_t> ExtentsToBlocks(const T& extents) { |
| std::vector<uint64_t> blocks; |
| for (const auto& extent : extents) { |
| for (uint64_t offset = 0; offset < extent.num_blocks(); ++offset) { |
| blocks.push_back(extent.start_block() + offset); |
| } |
| } |
| return blocks; |
| } |
| |
| InstallOperation CreateCopyOp(const std::vector<uint64_t>& src_blocks, |
| const std::vector<uint64_t>& dst_blocks) { |
| InstallOperation op; |
| op.set_type(InstallOperation::SOURCE_COPY); |
| BlocksToExtents(src_blocks, op.mutable_src_extents()); |
| BlocksToExtents(dst_blocks, op.mutable_dst_extents()); |
| return op; |
| } |
| |
| // ExtentEqual(tuple<UeExtent, UeExtent>) |
| MATCHER(ExtentEqual, "") { |
| auto&& [a, b] = arg; |
| return a.start_block() == b.start_block() && a.num_blocks() == b.num_blocks(); |
| } |
| |
| struct OptimizeOperationTestParam { |
| InstallOperation input; |
| std::optional<InstallOperation> expected_output; |
| }; |
| |
| class OptimizeOperationTest : public ::testing::TestWithParam<OptimizeOperationTestParam> { |
| void SetUp() override { SKIP_IF_NON_VIRTUAL_AB(); } |
| }; |
| TEST_P(OptimizeOperationTest, Test) { |
| InstallOperation actual_output; |
| EXPECT_EQ(GetParam().expected_output.has_value(), |
| OptimizeSourceCopyOperation(GetParam().input, &actual_output)) |
| << "OptimizeSourceCopyOperation should " |
| << (GetParam().expected_output.has_value() ? "succeed" : "fail"); |
| if (!GetParam().expected_output.has_value()) return; |
| EXPECT_THAT(actual_output.src_extents(), |
| Pointwise(ExtentEqual(), GetParam().expected_output->src_extents())); |
| EXPECT_THAT(actual_output.dst_extents(), |
| Pointwise(ExtentEqual(), GetParam().expected_output->dst_extents())); |
| } |
| |
| std::vector<OptimizeOperationTestParam> GetOptimizeOperationTestParams() { |
| return { |
| {CreateCopyOp({}, {}), CreateCopyOp({}, {})}, |
| {CreateCopyOp({1, 2, 4}, {1, 2, 4}), CreateCopyOp({}, {})}, |
| {CreateCopyOp({1, 2, 3}, {4, 5, 6}), std::nullopt}, |
| {CreateCopyOp({3, 2}, {1, 2}), CreateCopyOp({3}, {1})}, |
| {CreateCopyOp({5, 6, 3, 4, 1, 2}, {1, 2, 3, 4, 5, 6}), |
| CreateCopyOp({5, 6, 1, 2}, {1, 2, 5, 6})}, |
| {CreateCopyOp({1, 2, 3, 5, 5, 6}, {5, 6, 3, 4, 1, 2}), |
| CreateCopyOp({1, 2, 5, 5, 6}, {5, 6, 4, 1, 2})}, |
| {CreateCopyOp({1, 2, 5, 6, 9, 10}, {1, 4, 5, 6, 7, 8}), |
| CreateCopyOp({2, 9, 10}, {4, 7, 8})}, |
| {CreateCopyOp({2, 3, 3, 4, 4}, {1, 2, 3, 4, 5}), CreateCopyOp({2, 3, 4}, {1, 2, 5})}, |
| }; |
| } |
| |
| INSTANTIATE_TEST_CASE_P(Snapshot, OptimizeOperationTest, |
| ::testing::ValuesIn(GetOptimizeOperationTestParams())); |
| |
| } // namespace snapshot |
| } // namespace android |