Add transaction management for atomic ROM operations with rollback
- Introduced a Transaction class to handle atomic write operations to ROM, allowing for rollback on failure. - Implemented methods for writing bytes, words, longs, vectors, and colors, with status checks and operation logging. - Added a rollback mechanism to restore original values in case of write failures. - Enhanced unit tests to verify transaction rollback functionality and ensure data integrity after failed operations.
This commit is contained in:
141
src/app/transaction.h
Normal file
141
src/app/transaction.h
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
// Transaction helper for atomic ROM operations with rollback
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// yaze::Transaction tx(rom);
|
||||||
|
// auto status = tx.WriteByte(addr, val)
|
||||||
|
// .WriteWord(addr2, val2)
|
||||||
|
// .Commit();
|
||||||
|
//
|
||||||
|
// If any write fails before Commit, subsequent operations are skipped and
|
||||||
|
// Commit() will Rollback() previously applied writes in reverse order.
|
||||||
|
|
||||||
|
#include <variant>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "absl/status/status.h"
|
||||||
|
#include "app/gfx/snes_color.h"
|
||||||
|
#include "app/rom.h"
|
||||||
|
|
||||||
|
namespace yaze {
|
||||||
|
|
||||||
|
class Transaction {
|
||||||
|
public:
|
||||||
|
explicit Transaction(Rom &rom) : rom_(rom) {}
|
||||||
|
|
||||||
|
Transaction &WriteByte(int address, uint8_t value) {
|
||||||
|
if (!status_.ok()) return *this;
|
||||||
|
auto original = rom_.ReadByte(address);
|
||||||
|
if (!original.ok()) {
|
||||||
|
status_ = original.status();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
status_ = rom_.WriteByte(address, value);
|
||||||
|
if (status_.ok()) {
|
||||||
|
operations_.push_back({address, static_cast<uint8_t>(*original), OperationType::kWriteByte});
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Transaction &WriteWord(int address, uint16_t value) {
|
||||||
|
if (!status_.ok()) return *this;
|
||||||
|
auto original = rom_.ReadWord(address);
|
||||||
|
if (!original.ok()) {
|
||||||
|
status_ = original.status();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
status_ = rom_.WriteWord(address, value);
|
||||||
|
if (status_.ok()) {
|
||||||
|
operations_.push_back({address, static_cast<uint16_t>(*original), OperationType::kWriteWord});
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Transaction &WriteLong(int address, uint32_t value) {
|
||||||
|
if (!status_.ok()) return *this;
|
||||||
|
auto original = rom_.ReadLong(address);
|
||||||
|
if (!original.ok()) {
|
||||||
|
status_ = original.status();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
status_ = rom_.WriteLong(address, value);
|
||||||
|
if (status_.ok()) {
|
||||||
|
operations_.push_back({address, static_cast<uint32_t>(*original), OperationType::kWriteLong});
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Transaction &WriteVector(int address, const std::vector<uint8_t> &data) {
|
||||||
|
if (!status_.ok()) return *this;
|
||||||
|
auto original = rom_.ReadByteVector(address, static_cast<uint32_t>(data.size()));
|
||||||
|
if (!original.ok()) {
|
||||||
|
status_ = original.status();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
status_ = rom_.WriteVector(address, data);
|
||||||
|
if (status_.ok()) {
|
||||||
|
operations_.push_back({address, *original, OperationType::kWriteVector});
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Transaction &WriteColor(int address, const gfx::SnesColor &color) {
|
||||||
|
if (!status_.ok()) return *this;
|
||||||
|
// Store original raw 16-bit value for rollback via WriteWord.
|
||||||
|
auto original_word = rom_.ReadWord(address);
|
||||||
|
if (!original_word.ok()) {
|
||||||
|
status_ = original_word.status();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
status_ = rom_.WriteColor(address, color);
|
||||||
|
if (status_.ok()) {
|
||||||
|
operations_.push_back({address, static_cast<uint16_t>(*original_word), OperationType::kWriteColor});
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
absl::Status Commit() {
|
||||||
|
if (!status_.ok()) {
|
||||||
|
Rollback();
|
||||||
|
}
|
||||||
|
return status_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Rollback() {
|
||||||
|
for (auto it = operations_.rbegin(); it != operations_.rend(); ++it) {
|
||||||
|
const auto &op = *it;
|
||||||
|
switch (op.type) {
|
||||||
|
case OperationType::kWriteByte:
|
||||||
|
(void)rom_.WriteByte(op.address, std::get<uint8_t>(op.original_value));
|
||||||
|
break;
|
||||||
|
case OperationType::kWriteWord:
|
||||||
|
(void)rom_.WriteWord(op.address, std::get<uint16_t>(op.original_value));
|
||||||
|
break;
|
||||||
|
case OperationType::kWriteLong:
|
||||||
|
(void)rom_.WriteLong(op.address, std::get<uint32_t>(op.original_value));
|
||||||
|
break;
|
||||||
|
case OperationType::kWriteVector:
|
||||||
|
(void)rom_.WriteVector(op.address, std::get<std::vector<uint8_t>>(op.original_value));
|
||||||
|
break;
|
||||||
|
case OperationType::kWriteColor:
|
||||||
|
(void)rom_.WriteWord(op.address, std::get<uint16_t>(op.original_value));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
operations_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class OperationType { kWriteByte, kWriteWord, kWriteLong, kWriteVector, kWriteColor };
|
||||||
|
|
||||||
|
struct Operation {
|
||||||
|
int address;
|
||||||
|
std::variant<uint8_t, uint16_t, uint32_t, std::vector<uint8_t>> original_value;
|
||||||
|
OperationType type;
|
||||||
|
};
|
||||||
|
|
||||||
|
Rom &rom_;
|
||||||
|
absl::Status status_;
|
||||||
|
std::vector<Operation> operations_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace yaze
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "absl/status/statusor.h"
|
#include "absl/status/statusor.h"
|
||||||
#include "mocks/mock_rom.h"
|
#include "mocks/mock_rom.h"
|
||||||
#include "test/testing.h"
|
#include "test/testing.h"
|
||||||
|
#include "app/transaction.h"
|
||||||
|
|
||||||
namespace yaze {
|
namespace yaze {
|
||||||
namespace test {
|
namespace test {
|
||||||
@@ -191,5 +192,47 @@ TEST_F(RomTest, ReadTransactionFailure) {
|
|||||||
absl::FailedPreconditionError("Offset out of range"));
|
absl::FailedPreconditionError("Offset out of range"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(RomTest, SaveTruncatesExistingFile) {
|
||||||
|
#if defined(__linux__)
|
||||||
|
GTEST_SKIP();
|
||||||
|
#endif
|
||||||
|
// Prepare ROM data and save to a temp file twice; second save should overwrite, not append
|
||||||
|
EXPECT_OK(rom_.LoadFromData(kMockRomData, /*z3_load=*/false));
|
||||||
|
|
||||||
|
const char* tmp_name = "test_temp_rom.sfc";
|
||||||
|
yaze::Rom::SaveSettings settings;
|
||||||
|
settings.filename = tmp_name;
|
||||||
|
settings.z3_save = false;
|
||||||
|
|
||||||
|
// First save
|
||||||
|
EXPECT_OK(rom_.SaveToFile(settings));
|
||||||
|
|
||||||
|
// Modify one byte and save again
|
||||||
|
EXPECT_OK(rom_.WriteByte(0, 0xEE));
|
||||||
|
EXPECT_OK(rom_.SaveToFile(settings));
|
||||||
|
|
||||||
|
// Load the saved file and verify size equals original data size and first byte matches
|
||||||
|
Rom verify;
|
||||||
|
EXPECT_OK(verify.LoadFromFile(tmp_name, /*z3_load=*/false));
|
||||||
|
EXPECT_EQ(verify.size(), kMockRomData.size());
|
||||||
|
auto b0 = verify.ReadByte(0);
|
||||||
|
ASSERT_TRUE(b0.ok());
|
||||||
|
EXPECT_EQ(*b0, 0xEE);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RomTest, TransactionRollbackRestoresOriginals) {
|
||||||
|
EXPECT_OK(rom_.LoadFromData(kMockRomData, /*z3_load=*/false));
|
||||||
|
// Force an out-of-range write to trigger failure after a successful write
|
||||||
|
yaze::Transaction tx{rom_};
|
||||||
|
auto status = tx.WriteByte(0x01, 0xAA) // valid
|
||||||
|
.WriteWord(0xFFFF, 0xBBBB) // invalid: should fail and rollback
|
||||||
|
.Commit();
|
||||||
|
EXPECT_FALSE(status.ok());
|
||||||
|
auto b1 = rom_.ReadByte(0x01);
|
||||||
|
ASSERT_TRUE(b1.ok());
|
||||||
|
// Should be restored to original 0x01 value (from kMockRomData)
|
||||||
|
EXPECT_EQ(*b1, kMockRomData[0x01]);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace test
|
} // namespace test
|
||||||
} // namespace yaze
|
} // namespace yaze
|
||||||
|
|||||||
Reference in New Issue
Block a user