blob: 2825d3a8e9aef0773f7ed55cae46748b03f88e36 [file] [log] [blame]
// Copyright 2023 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <map>
#include <string>
#include <vector>
#include <base/files/file_util.h>
#include <base/files/scoped_temp_dir.h>
#include <base/process/launch.h>
#include <base/test/scoped_chromeos_version_info.h>
#include <brillo/files/file_util.h>
#include <gtest/gtest.h>
#include "installer/inst_util.h"
#include "installer/mock_cgpt_manager.h"
#include "installer/mock_metrics.h"
#include "installer/reven_partition_migration.h"
#include "installer/reven_partition_migration_private.h"
using ::testing::_;
using ::testing::Eq;
using ::testing::Invoke;
using ::testing::Return;
using ::testing::ReturnRef;
using ::testing::StrictMock;
namespace {
uint64_t MibToBytes(uint64_t mib) {
return mib * 1024 * 1024;
}
// Use truncate to create a sparse file.
void TruncateFile(const base::FilePath& path, const std::string& size) {
std::string output;
EXPECT_TRUE(base::GetAppOutput(
base::CommandLine({"truncate", "--size=" + size, path.value()}),
&output));
}
struct ComparePartitionNum {
bool operator()(const PartitionNum& a, const PartitionNum& b) const {
return a.Value() < b.Value();
}
};
class MockEnvironment : public base::Environment {
public:
MOCK_METHOD(bool,
GetVar,
(base::StringPiece variable_name, std::string* result),
(override));
MOCK_METHOD(bool, HasVar, (base::StringPiece variable_name), (override));
MOCK_METHOD(bool,
SetVar,
(base::StringPiece variable_name, const std::string& new_value),
(override));
MOCK_METHOD(bool, UnSetVar, (base::StringPiece variable_name), (override));
};
class PartitionMigrationTest : public ::testing::Test {
protected:
void SetUp() {
EXPECT_TRUE(tmp_dir_.CreateUniqueTempDir());
disk_path_ = tmp_dir_.GetPath().Append("disk");
TruncateFile(disk_path_, "10GiB");
EXPECT_CALL(cgpt_manager_, DeviceName())
.WillRepeatedly(ReturnRef(disk_path_));
// Numbers from a reven 113 install (`cgpt show`).
orig_partitions_[PartitionNum::KERN_A] = {69, 32768};
orig_partitions_[PartitionNum::ROOT_A] = {8884224, 8388608};
orig_partitions_[PartitionNum::KERN_B] = {32837, 32768};
orig_partitions_[PartitionNum::ROOT_B] = {495616, 8388608};
// Write some to the kernel partitions to validate it gets copied
// properly.
InitializeKernelData();
WriteKernelData(orig_partitions_[PartitionNum::KERN_A].start);
WriteKernelData(orig_partitions_[PartitionNum::KERN_B].start);
EXPECT_CALL(cgpt_manager_, GetSectorRange(_, _))
.WillRepeatedly(Invoke(this, &PartitionMigrationTest::GetSectorRange));
}
// Write the 16MiB test kernel data to the fake disk, beginning at
// `start_sector`.
void WriteKernelData(uint64_t start_sector) {
base::File disk_file(disk_path_,
base::File::FLAG_OPEN | base::File::FLAG_WRITE);
EXPECT_TRUE(
disk_file.WriteAndCheck(SectorsToBytes(start_sector), kernel_data_));
}
// Check that the new kernel partition's data was properly initialized.
void CheckNewKernelData(PartitionNum kern_num) {
PartitionNum root_num(kern_num.Value() + 1);
base::File disk_file(disk_path_,
base::File::FLAG_OPEN | base::File::FLAG_READ);
std::vector<uint8_t> new_kernel_data;
new_kernel_data.resize(MibToBytes(64));
EXPECT_TRUE(disk_file.ReadAndCheck(
SectorsToBytes(orig_partitions_[root_num].start +
orig_partitions_[root_num].count - MibToSectors(64)),
new_kernel_data));
std::vector<uint8_t> expected_kernel_data = kernel_data_;
expected_kernel_data.resize(new_kernel_data.size(), 0);
EXPECT_EQ(new_kernel_data, expected_kernel_data);
}
void ExpectMetric(PartitionMigrationResult result) {
EXPECT_CALL(metrics_, SendEnumMetric(_, static_cast<int>(result), _))
.WillOnce(Return(true));
}
void SetIsInstall(bool is_install) {
EXPECT_CALL(env_, HasVar(base::StringPiece(kEnvIsInstall)))
.WillOnce(Return(is_install));
}
void ExpectSlotAMigration() {
SectorRange root_a = orig_partitions_[PartitionNum::ROOT_A];
// Kernel is now the last 64MiB of the original root partition.
EXPECT_CALL(cgpt_manager_, SetSectorRange(PartitionNum::KERN_A,
Eq(root_a.start + root_a.count -
MibToSectors(64)),
Eq(MibToSectors(64))))
.WillOnce(Return(CgptErrorCode::kSuccess));
EXPECT_CALL(cgpt_manager_,
SetSectorRange(PartitionNum::ROOT_A, Eq(std::nullopt),
Eq(root_a.count - MibToSectors(64))))
.WillOnce(Return(CgptErrorCode::kSuccess));
}
void ExpectSlotBMigration() {
SectorRange root_b = orig_partitions_[PartitionNum::ROOT_B];
// Kernel is now the last 64MiB of the original root partition.
EXPECT_CALL(cgpt_manager_, SetSectorRange(PartitionNum::KERN_B,
Eq(root_b.start + root_b.count -
MibToSectors(64)),
Eq(MibToSectors(64))))
.WillOnce(Return(CgptErrorCode::kSuccess));
EXPECT_CALL(cgpt_manager_,
SetSectorRange(PartitionNum::ROOT_B, Eq(std::nullopt),
Eq(root_b.count - MibToSectors(64))))
.WillOnce(Return(CgptErrorCode::kSuccess));
}
base::ScopedTempDir tmp_dir_;
base::FilePath disk_path_;
std::map<PartitionNum, SectorRange, ComparePartitionNum> orig_partitions_;
// Use StrictMock for cgpt_manager because we care about not having
// any unexpected calls that modify the disk. Also use StrictMock for
// the other interfaces, since they are very minimal and unlikely to
// break.
StrictMock<MockCgptManager> cgpt_manager_;
StrictMock<MockEnvironment> env_;
StrictMock<MockMetrics> metrics_;
private:
void InitializeKernelData() {
kernel_data_.resize(MibToBytes(16)); // 16MiB
uint8_t val = 1;
for (uint8_t& byte : kernel_data_) {
byte = val;
val++;
}
}
CgptErrorCode GetSectorRange(PartitionNum num, SectorRange& sectors) {
const auto it = orig_partitions_.find(num);
if (it == orig_partitions_.end()) {
return CgptErrorCode::kUnknownError;
}
sectors = it->second;
return CgptErrorCode::kSuccess;
}
std::vector<uint8_t> kernel_data_;
};
} // namespace
// Tests for errors in creating a `SlotPlan`:
TEST_F(PartitionMigrationTest, SlotPlanInitializeGptReadKernError) {
orig_partitions_.erase(PartitionNum::KERN_A);
SlotPlan slot_plan = SlotPlan::ForSlotA(cgpt_manager_);
EXPECT_EQ(slot_plan.Initialize(),
PartitionMigrationResult::kGptReadKernError);
}
TEST_F(PartitionMigrationTest, SlotPlanInitializeGptReadRootError) {
orig_partitions_.erase(PartitionNum::ROOT_A);
SlotPlan slot_plan = SlotPlan::ForSlotA(cgpt_manager_);
EXPECT_EQ(slot_plan.Initialize(),
PartitionMigrationResult::kGptReadRootError);
}
TEST_F(PartitionMigrationTest, SlotPlanInitializeRootUnexpectedSize) {
orig_partitions_[PartitionNum::ROOT_A].count = MibToSectors(3047);
SlotPlan slot_plan = SlotPlan::ForSlotA(cgpt_manager_);
EXPECT_EQ(slot_plan.Initialize(),
PartitionMigrationResult::kRootPartitionUnexpectedSize);
}
TEST_F(PartitionMigrationTest, SlotPlanInitializeNoMigrationNeeded) {
orig_partitions_[PartitionNum::KERN_A].count = MibToSectors(64);
SlotPlan slot_plan = SlotPlan::ForSlotA(cgpt_manager_);
EXPECT_EQ(slot_plan.Initialize(),
PartitionMigrationResult::kNoMigrationNeeded);
}
// Tests for errors when running a migration on one slot:
TEST_F(PartitionMigrationTest, SlotPlanRunDiskOpenError) {
EXPECT_TRUE(brillo::DeleteFile(disk_path_));
SlotPlan slot_plan = SlotPlan::ForSlotA(cgpt_manager_);
EXPECT_EQ(slot_plan.Initialize(), PartitionMigrationResult::kSuccess);
EXPECT_EQ(slot_plan.Run(), PartitionMigrationResult::kDiskOpenError);
}
TEST_F(PartitionMigrationTest, SlotPlanRunDiskReadError) {
EXPECT_TRUE(base::WriteFile(disk_path_, ""));
SlotPlan slot_plan = SlotPlan::ForSlotA(cgpt_manager_);
EXPECT_EQ(slot_plan.Initialize(), PartitionMigrationResult::kSuccess);
EXPECT_EQ(slot_plan.Run(), PartitionMigrationResult::kDiskReadError);
}
TEST_F(PartitionMigrationTest, SlotPlanRunGptWriteRootError) {
EXPECT_CALL(cgpt_manager_, SetSectorRange(PartitionNum::ROOT_A, _, _))
.WillOnce(Return(CgptErrorCode::kUnknownError));
SlotPlan slot_plan = SlotPlan::ForSlotA(cgpt_manager_);
EXPECT_EQ(slot_plan.Initialize(), PartitionMigrationResult::kSuccess);
EXPECT_EQ(slot_plan.Run(), PartitionMigrationResult::kGptWriteRootError);
}
TEST_F(PartitionMigrationTest, SlotPlanRunGptWriteKernError) {
EXPECT_CALL(cgpt_manager_, SetSectorRange(PartitionNum::ROOT_A, _, _))
.WillOnce(Return(CgptErrorCode::kSuccess));
EXPECT_CALL(cgpt_manager_, SetSectorRange(PartitionNum::KERN_A, _, _))
.WillOnce(Return(CgptErrorCode::kUnknownError));
SlotPlan slot_plan = SlotPlan::ForSlotA(cgpt_manager_);
EXPECT_EQ(slot_plan.Initialize(), PartitionMigrationResult::kSuccess);
EXPECT_EQ(slot_plan.Run(), PartitionMigrationResult::kGptWriteKernError);
}
// Note: no test for kDiskWriteError, since it's difficult to force a
// write error to occur in a test without also making the file open
// fail.
// Tests for a successful migration of one or both slots:
TEST_F(PartitionMigrationTest, RunSuccess) {
SetIsInstall(true);
ExpectSlotAMigration();
ExpectSlotBMigration();
ExpectMetric(PartitionMigrationResult::kSuccess);
EXPECT_TRUE(RunRevenPartitionMigration(cgpt_manager_, metrics_, env_));
CheckNewKernelData(PartitionNum::KERN_A);
CheckNewKernelData(PartitionNum::KERN_B);
}
TEST_F(PartitionMigrationTest, RunSuccessCloudReady96) {
SetIsInstall(true);
// Numbers from a beerover 96.4 install (`cgpt show`).
orig_partitions_[PartitionNum::KERN_A] = {20480, 32768};
orig_partitions_[PartitionNum::ROOT_A] = {6623232, 6242304};
orig_partitions_[PartitionNum::KERN_B] = {53248, 32768};
orig_partitions_[PartitionNum::ROOT_B] = {380928, 6242304};
WriteKernelData(orig_partitions_[PartitionNum::KERN_A].start);
WriteKernelData(orig_partitions_[PartitionNum::KERN_B].start);
ExpectSlotAMigration();
ExpectSlotBMigration();
ExpectMetric(PartitionMigrationResult::kSuccess);
EXPECT_TRUE(RunRevenPartitionMigration(cgpt_manager_, metrics_, env_));
CheckNewKernelData(PartitionNum::KERN_A);
CheckNewKernelData(PartitionNum::KERN_B);
}
TEST_F(PartitionMigrationTest, RunNoMigrationNeeded) {
SetIsInstall(true);
orig_partitions_[PartitionNum::KERN_A].count = MibToSectors(64);
orig_partitions_[PartitionNum::KERN_B].count = MibToSectors(64);
ExpectMetric(PartitionMigrationResult::kNoMigrationNeeded);
EXPECT_TRUE(RunRevenPartitionMigration(cgpt_manager_, metrics_, env_));
}
TEST_F(PartitionMigrationTest, RunSlotANoMigrationNeeded) {
SetIsInstall(true);
orig_partitions_[PartitionNum::KERN_A].count = MibToSectors(64);
ExpectSlotBMigration();
ExpectMetric(PartitionMigrationResult::kSuccess);
EXPECT_TRUE(RunRevenPartitionMigration(cgpt_manager_, metrics_, env_));
}
TEST_F(PartitionMigrationTest, RunSlotBNoMigrationNeeded) {
SetIsInstall(true);
orig_partitions_[PartitionNum::KERN_B].count = MibToSectors(64);
ExpectSlotAMigration();
ExpectMetric(PartitionMigrationResult::kSuccess);
EXPECT_TRUE(RunRevenPartitionMigration(cgpt_manager_, metrics_, env_));
}
// Tests for how errors are handled if either slot plan fails to
// initialize. An error metric should be sent, but postinstall should be
// allowed to proceed (RunRevenPartitionMigration returns true).
TEST_F(PartitionMigrationTest, RunSlotAPlanError) {
SetIsInstall(true);
orig_partitions_.erase(PartitionNum::KERN_A);
ExpectMetric(PartitionMigrationResult::kGptReadKernError);
EXPECT_TRUE(RunRevenPartitionMigration(cgpt_manager_, metrics_, env_));
}
TEST_F(PartitionMigrationTest, RunSlotBPlanError) {
SetIsInstall(true);
orig_partitions_.erase(PartitionNum::KERN_B);
ExpectMetric(PartitionMigrationResult::kGptReadKernError);
EXPECT_TRUE(RunRevenPartitionMigration(cgpt_manager_, metrics_, env_));
}
// Tests for propagating errors if either slot migration fails:
TEST_F(PartitionMigrationTest, RunSlotAMigrationError) {
SetIsInstall(true);
// Arbitrary choice of failure in the slot A migration.
EXPECT_CALL(cgpt_manager_, SetSectorRange(PartitionNum::ROOT_A, _, _))
.WillOnce(Return(CgptErrorCode::kUnknownError));
ExpectMetric(PartitionMigrationResult::kGptWriteRootError);
EXPECT_FALSE(RunRevenPartitionMigration(cgpt_manager_, metrics_, env_));
}
TEST_F(PartitionMigrationTest, RunSlotBMigrationError) {
SetIsInstall(true);
// Arbitrary choice of failure in the slot B migration.
EXPECT_CALL(cgpt_manager_, SetSectorRange(PartitionNum::ROOT_B, _, _))
.WillOnce(Return(CgptErrorCode::kUnknownError));
// The Slot A migration has already happened by the time we get to the
// failure on the B slot.
ExpectSlotAMigration();
ExpectMetric(PartitionMigrationResult::kGptWriteRootError);
EXPECT_FALSE(RunRevenPartitionMigration(cgpt_manager_, metrics_, env_));
}
// Test that no migration occurs during updates (except on particular
// channels). This behavior will change in the future, but for now we
// only run the migration on install.
TEST_F(PartitionMigrationTest, NotRunningFromInstaller) {
SetIsInstall(false);
EXPECT_TRUE(RunRevenPartitionMigration(cgpt_manager_, metrics_, env_));
}
// Test that migration planning does occur during updates, even if the
// migration doesn't run.
TEST_F(PartitionMigrationTest, UpdatePlanningOccurs) {
SetIsInstall(false);
orig_partitions_[PartitionNum::KERN_A].count = MibToSectors(64);
orig_partitions_[PartitionNum::KERN_B].count = MibToSectors(64);
ExpectMetric(PartitionMigrationResult::kNoMigrationNeeded);
EXPECT_TRUE(RunRevenPartitionMigration(cgpt_manager_, metrics_, env_));
}
// Test that migration runs during updates when the payload is a test image.
TEST_F(PartitionMigrationTest, UpdateMigrationOnTest) {
base::test::ScopedChromeOSVersionInfo scoped_info(
"CHROMEOS_RELEASE_TRACK=testimage-channel\n", base::Time::Now());
SetIsInstall(false);
ExpectSlotAMigration();
ExpectSlotBMigration();
ExpectMetric(PartitionMigrationResult::kSuccess);
EXPECT_TRUE(RunRevenPartitionMigration(cgpt_manager_, metrics_, env_));
CheckNewKernelData(PartitionNum::KERN_A);
CheckNewKernelData(PartitionNum::KERN_B);
}
// Test that migration runs during updates when the payload is on canary.
TEST_F(PartitionMigrationTest, UpdateMigrationOnCanary) {
base::test::ScopedChromeOSVersionInfo scoped_info(
"CHROMEOS_RELEASE_TRACK=canary-channel\n", base::Time::Now());
SetIsInstall(false);
ExpectSlotAMigration();
ExpectSlotBMigration();
ExpectMetric(PartitionMigrationResult::kSuccess);
EXPECT_TRUE(RunRevenPartitionMigration(cgpt_manager_, metrics_, env_));
CheckNewKernelData(PartitionNum::KERN_A);
CheckNewKernelData(PartitionNum::KERN_B);
}
TEST(PartitionMigration, MibToSectors) {
EXPECT_EQ(MibToSectors(0), 0);
EXPECT_EQ(MibToSectors(1), 2048);
EXPECT_EQ(MibToSectors(4096), 8388608);
}
TEST(PartitionMigration, SectorsToBytes) {
EXPECT_EQ(SectorsToBytes(0), 0);
EXPECT_EQ(SectorsToBytes(1), 512);
EXPECT_EQ(SectorsToBytes(4096), 2097152);
}