| /* |
| * 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 requied 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. |
| * |
| */ |
| |
| #define LOG_TAG "BowTest" |
| |
| #include <fstream> |
| #include <string> |
| |
| #include <assert.h> |
| #include <errno.h> |
| #include <inttypes.h> |
| #include <linux/fs.h> |
| #include <linux/loop.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/types.h> |
| #include <unistd.h> |
| |
| #include <android-base/stringprintf.h> |
| #include <android-base/unique_fd.h> |
| #include <gtest/gtest.h> |
| #include <libdm/dm.h> |
| #include <utils/Log.h> |
| |
| namespace android { |
| |
| using base::unique_fd; |
| using namespace dm; |
| |
| bool blockCheckpointsSupported() { |
| static bool supported = false; |
| static bool evaluated = false; |
| |
| if (evaluated) return supported; |
| |
| pid_t pid = fork(); |
| EXPECT_NE(pid, -1); |
| |
| if (pid == 0) { |
| static const char* args[] = {"/system/bin/vdc", "checkpoint", |
| "supportsBlockCheckpoint"}; |
| EXPECT_NE(execv(args[0], const_cast<char* const*>(args)), -1); |
| } |
| |
| int status; |
| EXPECT_NE(waitpid(pid, &status, 0), -1); |
| |
| supported = status == 1; |
| evaluated = true; |
| return supported; |
| } |
| |
| template <void (*Prepare)(std::string)> |
| class LoopbackTestFixture : public ::testing::Test { |
| protected: |
| void SetUp() { |
| Prepare(loop_file_); |
| |
| // Get free loop device name |
| unique_fd cfd(open("/dev/loop-control", O_RDWR)); |
| ASSERT_NE(cfd.get(), -1); |
| int i = ioctl(cfd, 0x4C82); // LOOP_CTL_GET_FREE |
| ASSERT_GE(i, 0); |
| loop_device_ = std::string("/dev/block/loop") + std::to_string(i); |
| |
| // Associate loop device with file |
| unique_fd lfd(open(loop_device_.c_str(), O_RDWR)); |
| ASSERT_NE(lfd.get(), -1); |
| unique_fd ffd(open(loop_file_.c_str(), O_RDWR)); |
| ASSERT_NE(ffd.get(), -1); |
| ASSERT_EQ(ioctl(lfd.get(), LOOP_SET_FD, ffd.get()), 0); |
| } |
| |
| void TearDown() { |
| unique_fd lfd(open(loop_device_.c_str(), O_RDWR)); |
| EXPECT_NE(lfd.get(), -1); |
| EXPECT_EQ(ioctl(lfd, LOOP_CLR_FD, 0), 0); |
| EXPECT_EQ(remove(loop_file_.c_str()), 0); |
| } |
| |
| const static std::string loop_file_; |
| |
| public: |
| const static size_t sector_size_ = 512; |
| const static size_t loop_size_ = 4096 * sector_size_; |
| std::string loop_device_; |
| }; |
| |
| template <void (*Prepare)(std::string)> |
| const std::string LoopbackTestFixture<Prepare>::loop_file_ = |
| "/data/local/tmp/bow_loop"; |
| |
| void PrepareBowDefault(std::string) {} |
| |
| template <void (*PrepareLoop)(std::string), |
| void (*PrepareBow)(std::string) = PrepareBowDefault> |
| class BowTestFixture : public LoopbackTestFixture<PrepareLoop> { |
| std::string GetTableStatus() { |
| std::vector<DeviceMapper::TargetInfo> targets; |
| DeviceMapper& dm = DeviceMapper::Instance(); |
| EXPECT_TRUE(dm.GetTableInfo("bow1", &targets)); |
| EXPECT_EQ(targets.size(), 1); |
| return targets[0].data; |
| } |
| |
| bool torn_down_; |
| |
| protected: |
| void SetUp() { |
| if (!blockCheckpointsSupported()) return; |
| |
| LoopbackTestFixture<PrepareLoop>::SetUp(); |
| PrepareBow(loop_device_); |
| |
| torn_down_ = false; |
| |
| DmTable table; |
| table.AddTarget(std::make_unique<DmTargetBow>(0, loop_size_ / 512, |
| loop_device_.c_str())); |
| |
| DeviceMapper& dm = DeviceMapper::Instance(); |
| ASSERT_TRUE(dm.CreateDevice("bow1", table)); |
| ASSERT_TRUE(dm.GetDmDevicePathByName("bow1", &bow_device_)); |
| } |
| |
| void TearDown() { |
| if (!blockCheckpointsSupported()) return; |
| BowTearDown(); |
| LoopbackTestFixture<PrepareLoop>::TearDown(); |
| } |
| |
| bool TornDown() const { return torn_down_; } |
| |
| public: |
| using LoopbackTestFixture<PrepareLoop>::loop_size_; |
| using LoopbackTestFixture<PrepareLoop>::sector_size_; |
| using LoopbackTestFixture<PrepareLoop>::loop_device_; |
| |
| void BowTearDown() { |
| if (torn_down_) return; |
| torn_down_ = true; |
| EXPECT_TRUE(DeviceMapper::Instance().DeleteDevice("bow1")); |
| } |
| |
| void SetState(int i) { |
| std::string state_file = "/sys" + bow_device_.substr(4) + "/bow/state"; |
| std::ofstream(state_file) << i; |
| |
| int j; |
| std::ifstream(state_file) >> j; |
| EXPECT_EQ(i, j); |
| } |
| |
| enum SectorTypes { |
| INVALID, |
| SECTOR0, |
| SECTOR0_CURRENT, |
| UNCHANGED, |
| BACKUP, |
| FREE, |
| CHANGED, |
| TOP |
| }; |
| |
| struct TableEntry { |
| SectorTypes type; |
| uint64_t offset; |
| |
| bool operator==(const TableEntry& te) const { |
| return type == te.type && offset == te.offset; |
| } |
| }; |
| |
| std::vector<TableEntry> GetTable() { |
| std::string status = GetTableStatus(); |
| std::istringstream i(status); |
| std::vector<TableEntry> table; |
| while (true) { |
| TableEntry te = {}; |
| std::string s; |
| i >> s >> te.offset; |
| if (!i) { |
| EXPECT_EQ(s, ""); |
| break; |
| } |
| |
| if (s == "Sector0:") |
| te.type = SECTOR0; |
| else if (s == "Sector0_current:") |
| te.type = SECTOR0_CURRENT; |
| else if (s == "Unchanged:") |
| te.type = UNCHANGED; |
| else if (s == "Backup:") |
| te.type = BACKUP; |
| else if (s == "Free:") |
| te.type = FREE; |
| else if (s == "Changed:") |
| te.type = CHANGED; |
| else if (s == "Top:") |
| te.type = TOP; |
| else |
| ADD_FAILURE(); |
| |
| te.offset /= sector_size_ / 512; |
| |
| table.push_back(te); |
| } |
| return table; |
| } |
| |
| std::string bow_device_; |
| }; |
| |
| void PrepareFile(std::string loop_file) { |
| auto const& sector_size_ = LoopbackTestFixture<PrepareFile>::sector_size_; |
| auto const& loop_size_ = LoopbackTestFixture<PrepareFile>::loop_size_; |
| |
| unique_fd fd( |
| open(loop_file.c_str(), O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR)); |
| ASSERT_NE(fd.get(), -1); |
| for (int i = 0; i < loop_size_ / sector_size_; ++i) { |
| char buffer[sector_size_] = {}; |
| snprintf(buffer, sizeof(buffer), "Sector %d", i); |
| write(fd.get(), buffer, sizeof(buffer)); |
| } |
| } |
| |
| class FileBowTestFixture : public BowTestFixture<&PrepareFile> { |
| void SetUp() { |
| if (!blockCheckpointsSupported()) return; |
| BowTestFixture<&PrepareFile>::SetUp(); |
| fd_ = unique_fd(open(bow_device_.c_str(), O_RDWR)); |
| ASSERT_NE(fd_.get(), -1); |
| } |
| |
| void TearDown() { |
| fd_ = unique_fd(); |
| BowTestFixture<&PrepareFile>::TearDown(); |
| } |
| |
| unique_fd fd_; |
| |
| public: |
| void Discard(uint64_t offset, uint64_t length) { |
| uint64_t range[2] = {offset * sector_size_, length * sector_size_}; |
| EXPECT_EQ(ioctl(fd_.get(), BLKDISCARD, range), 0); |
| } |
| |
| int Write(SectorTypes type) { |
| for (auto i : GetTable()) |
| if (i.type == type) { |
| EXPECT_NE(lseek(fd_, i.offset * sector_size_, SEEK_SET), -1); |
| EXPECT_EQ(write(fd_, "Changed", 8), 8); |
| return i.offset; |
| } |
| EXPECT_TRUE(false); |
| return -1; |
| } |
| |
| void FindChanged(std::vector<TableEntry> const& free, int expected_changed) { |
| unique_fd fd; |
| if (TornDown()) |
| fd = unique_fd(open(loop_device_.c_str(), O_RDONLY)); |
| else |
| fd = unique_fd(open(bow_device_.c_str(), O_RDONLY)); |
| EXPECT_NE(fd.get(), -1); |
| if (fd.get() == -1) return; |
| |
| int changed = -1; |
| for (int i = 0; i < loop_size_ / sector_size_; ++i) { |
| if (i != expected_changed) { |
| auto type = SECTOR0; |
| for (auto j : free) |
| if (j.offset > i) |
| break; |
| else |
| type = j.type; |
| if (type == FREE) continue; |
| } |
| |
| char buffer[sector_size_]; |
| EXPECT_NE(lseek(fd.get(), i * sector_size_, SEEK_SET), -1); |
| EXPECT_EQ(read(fd.get(), buffer, sizeof(buffer)), sizeof(buffer)); |
| if (strcmp(buffer, "Changed") == 0) { |
| EXPECT_EQ(changed, -1); |
| changed = i; |
| } else { |
| std::string expected = "Sector " + std::to_string(i); |
| EXPECT_STREQ(buffer, expected.c_str()); |
| } |
| } |
| EXPECT_EQ(changed, expected_changed); |
| } |
| |
| void DumpSector0() { |
| char buffer[sector_size_]; |
| lseek(fd_.get(), 0, SEEK_SET); |
| read(fd_.get(), buffer, sizeof(buffer)); |
| for (int i = 0; i < sector_size_ * 2; ++i) { |
| std::cout << std::hex << std::setw(2) << std::setfill('0') |
| << (int)buffer[i]; |
| if (i % 32 == 31) |
| std::cout << std::endl; |
| else |
| std::cout << " "; |
| } |
| } |
| |
| void WriteSubmit(SectorTypes type) { |
| if (!blockCheckpointsSupported()) return; |
| Discard(1, 1); |
| Discard(3, 2); |
| auto free = GetTable(); |
| |
| SetState(1); |
| auto changed = Write(type); |
| |
| SetState(2); |
| FindChanged(free, changed); |
| } |
| |
| void WriteRestore(SectorTypes type) { |
| if (!blockCheckpointsSupported()) return; |
| Discard(1, 1); |
| Discard(3, 2); |
| auto free = GetTable(); |
| SetState(1); |
| Write(type); |
| BowTearDown(); |
| system(std::string("vdc checkpoint restoreCheckpoint " + loop_device_) |
| .c_str()); |
| FindChanged(free, -1); |
| } |
| }; |
| |
| TEST_F(FileBowTestFixture, discardVisible) { |
| if (!blockCheckpointsSupported()) return; |
| Discard(8, 1); |
| Discard(16, 1); |
| Discard(12, 1); |
| Discard(4, 1); |
| |
| std::vector<TableEntry> table = { |
| {UNCHANGED, 0}, {FREE, 4}, |
| {UNCHANGED, 5}, {FREE, 8}, |
| {UNCHANGED, 9}, {FREE, 12}, |
| {UNCHANGED, 13}, {FREE, 16}, |
| {UNCHANGED, 17}, {TOP, loop_size_ / sector_size_}, |
| }; |
| |
| EXPECT_EQ(GetTable(), table); |
| } |
| |
| TEST_F(FileBowTestFixture, writeSector0Submit) { |
| SCOPED_TRACE("write submit SECTOR0"); |
| WriteSubmit(SECTOR0); |
| } |
| |
| TEST_F(FileBowTestFixture, writeSector0Revert) { |
| SCOPED_TRACE("write restore SECTOR0"); |
| WriteRestore(SECTOR0); |
| } |
| |
| TEST_F(FileBowTestFixture, writeSector0_CurrentSubmit) { |
| SCOPED_TRACE("write submit SECTOR0_CURRENT"); |
| WriteSubmit(SECTOR0_CURRENT); |
| } |
| |
| TEST_F(FileBowTestFixture, writeSector0_CurrentRevert) { |
| SCOPED_TRACE("write restore SECTOR0_CURRENT"); |
| WriteRestore(SECTOR0_CURRENT); |
| } |
| |
| TEST_F(FileBowTestFixture, writeUnchangedSubmit) { |
| SCOPED_TRACE("write submit UNCHANGED"); |
| WriteSubmit(UNCHANGED); |
| } |
| |
| TEST_F(FileBowTestFixture, writeUnchangedRevert) { |
| SCOPED_TRACE("write restore UNCHANGED"); |
| WriteRestore(UNCHANGED); |
| } |
| |
| TEST_F(FileBowTestFixture, writeBackupSubmit) { |
| SCOPED_TRACE("write submit BACKUP"); |
| WriteSubmit(BACKUP); |
| } |
| |
| TEST_F(FileBowTestFixture, writeBackupRevert) { |
| SCOPED_TRACE("write restore BACKUP"); |
| WriteRestore(BACKUP); |
| } |
| |
| TEST_F(FileBowTestFixture, writeFreeSubmit) { |
| SCOPED_TRACE("write submit FREE"); |
| WriteSubmit(FREE); |
| } |
| |
| TEST_F(FileBowTestFixture, writeFreeRevert) { |
| SCOPED_TRACE("write restore FREE"); |
| WriteRestore(FREE); |
| } |
| |
| /* There are no changed sectors at start, so these can't work as is |
| TEST_F(BowTestFixture, writeChangedSubmit) { |
| SCOPED_TRACE("write submit CHANGED"); |
| WriteSubmit(CHANGED); |
| } |
| |
| TEST_F(BowTestFixture, writeChangedRevert) { |
| SCOPED_TRACE("write restore CHANGED"); |
| WriteRestore(CHANGED); |
| } |
| */ |
| |
| void PrepareFileSystem(std::string loop_file) { |
| EXPECT_EQ( |
| system((std::string("dd if=/dev/zero bs=512 count=4096 of=") + loop_file) |
| .c_str()), |
| 0); |
| } |
| |
| #define MOUNT_POINT "/data/local/tmp/mount" |
| |
| void SetupFileSystem(std::string loop_device) { |
| EXPECT_EQ(system((std::string("mke2fs ") + loop_device).c_str()), 0); |
| EXPECT_EQ(system("mkdir " MOUNT_POINT), 0); |
| EXPECT_EQ( |
| system((std::string("mount ") + loop_device + " " MOUNT_POINT).c_str()), |
| 0); |
| EXPECT_EQ(system("echo Original > " MOUNT_POINT "/file"), 0); |
| EXPECT_EQ(system("umount -D " MOUNT_POINT), 0); |
| EXPECT_EQ(system("rmdir " MOUNT_POINT), 0); |
| } |
| |
| void Trim() { |
| unique_fd fd(open(MOUNT_POINT, O_RDONLY)); |
| EXPECT_NE(fd.get(), -1); |
| struct fstrim_range range = {}; |
| range.len = ULLONG_MAX; |
| EXPECT_EQ(ioctl(fd, FITRIM, &range), 0); |
| } |
| |
| typedef BowTestFixture<&PrepareFileSystem, &SetupFileSystem> |
| FileSystemBowTestFixture; |
| |
| TEST_F(FileSystemBowTestFixture, filesystemSubmit) { |
| if (!blockCheckpointsSupported()) return; |
| EXPECT_EQ(system("mkdir " MOUNT_POINT), 0); |
| EXPECT_EQ( |
| system((std::string("mount ") + bow_device_ + " " MOUNT_POINT).c_str()), |
| 0); |
| Trim(); |
| SetState(1); |
| EXPECT_EQ(system("echo Changed > " MOUNT_POINT "/file"), 0); |
| SetState(2); |
| EXPECT_EQ(system("umount -D " MOUNT_POINT), 0); |
| BowTearDown(); |
| EXPECT_EQ( |
| system((std::string("mount ") + loop_device_ + " " MOUNT_POINT).c_str()), |
| 0); |
| std::string contents; |
| std::ifstream(MOUNT_POINT "/file") >> contents; |
| EXPECT_EQ(contents, std::string("Changed")); |
| EXPECT_EQ(system("umount -D " MOUNT_POINT), 0); |
| EXPECT_EQ(system("rmdir " MOUNT_POINT), 0); |
| } |
| |
| TEST_F(FileSystemBowTestFixture, filesystemRevert) { |
| if (!blockCheckpointsSupported()) return; |
| EXPECT_EQ(system("mkdir " MOUNT_POINT), 0); |
| EXPECT_EQ( |
| system((std::string("mount ") + bow_device_ + " " MOUNT_POINT).c_str()), |
| 0); |
| Trim(); |
| SetState(1); |
| EXPECT_EQ(system("echo Changed > " MOUNT_POINT "/file"), 0); |
| EXPECT_EQ(system("umount -D " MOUNT_POINT), 0); |
| BowTearDown(); |
| system((std::string("vdc checkpoint restoreCheckpoint ") + loop_device_) |
| .c_str()); |
| EXPECT_EQ( |
| system((std::string("mount ") + loop_device_ + " " MOUNT_POINT).c_str()), |
| 0); |
| std::string contents; |
| std::ifstream(MOUNT_POINT "/file") >> contents; |
| EXPECT_EQ(contents, std::string("Original")); |
| EXPECT_EQ(system("umount -D " MOUNT_POINT), 0); |
| EXPECT_EQ(system("rmdir " MOUNT_POINT), 0); |
| } |
| |
| } // namespace android |