#include #include #include #include #include #include #include #include #include #include using namespace yams::wal; TEST_CASE("WALEntry::Header::isValid accepts OpTypes 1-8", "[wal][header][catch2]") { WALEntry::Header h{}; h.sequenceNum = 1; h.version = 1; CHECK(WALEntry::Header::size() == sizeof(WALEntry::Header)); for (uint8_t opv = 1; opv <= 8; ++opv) { h.operation = static_cast(opv); CHECK(h.isValid()); } } TEST_CASE("WALEntry::Header::isValid rejects bad magic", "[wal][header][catch2]") { WALEntry::Header h{}; h.sequenceNum = 1; h.version = 1; h.operation = WALEntry::OpType::StoreBlock; h.magic = 0; CHECK_FALSE(h.isValid()); } TEST_CASE("WALEntry::Header::isValid rejects bad version (not 1 or 2)", "[wal][header][catch2]") { WALEntry::Header h{}; h.sequenceNum = 1; h.operation = WALEntry::OpType::StoreBlock; h.version = 0; CHECK_FALSE(h.isValid()); h.version = 3; CHECK_FALSE(h.isValid()); } TEST_CASE("WALEntry::Header::isValid accepts version 2 (dual-read compat)", "[wal][header][catch2]") { WALEntry::Header h{}; h.sequenceNum = 1; h.version = 2; h.operation = WALEntry::OpType::StoreBlock; CHECK(h.isValid()); } TEST_CASE("WALEntry::Header::isValid rejects zero sequenceNum", "[wal][header][catch2]") { WALEntry::Header h{}; h.sequenceNum = 0; h.version = 1; h.operation = WALEntry::OpType::StoreBlock; CHECK_FALSE(h.isValid()); } TEST_CASE("WALEntry::Header::isValid rejects out-of-range OpType (0 and 9)", "[wal][header][catch2]") { WALEntry::Header h{}; h.sequenceNum = 1; h.version = 1; h.operation = static_cast(0); CHECK_FALSE(h.isValid()); h.operation = static_cast(9); CHECK_FALSE(h.isValid()); } TEST_CASE("WALEntry constructor sets header fields", "[wal][ctor][catch2]") { std::vector payload; payload.resize(3); payload[0] = std::byte{0x01}; payload[1] = std::byte{0x02}; payload[2] = std::byte{0x03}; const auto op = WALEntry::OpType::UpdateReference; const uint64_t seq = 42; const uint64_t txn = 99; WALEntry e(op, seq, txn, std::span(payload.data(), payload.size())); CHECK(e.header.magic == 0x57414C31); CHECK(e.header.version == 1); CHECK(e.header.sequenceNum == seq); CHECK(e.header.transactionId == txn); CHECK(e.header.operation == op); CHECK(e.header.flags == 0); CHECK(e.header.reserved == 0); CHECK(e.header.dataSize == static_cast(payload.size())); CHECK(e.data == payload); CHECK(e.header.timestamp != 0); CHECK(e.verifyChecksum()); } TEST_CASE("WALEntry::isTransactional true for Begin/Commit/Rollback", "[wal][ctor][catch2]") { WALEntry begin(WALEntry::OpType::BeginTransaction, 1, 123); WALEntry commit(WALEntry::OpType::CommitTransaction, 2, 123); WALEntry rollback(WALEntry::OpType::Rollback, 3, 123); CHECK(begin.isTransactional()); CHECK(commit.isTransactional()); CHECK(rollback.isTransactional()); WALEntry store(WALEntry::OpType::StoreBlock, 4, 123); WALEntry del(WALEntry::OpType::DeleteBlock, 5, 123); WALEntry ref(WALEntry::OpType::UpdateReference, 6, 123); WALEntry meta(WALEntry::OpType::UpdateMetadata, 7, 123); WALEntry cp(WALEntry::OpType::Checkpoint, 8, 123); CHECK_FALSE(store.isTransactional()); CHECK_FALSE(del.isTransactional()); CHECK_FALSE(ref.isTransactional()); CHECK_FALSE(meta.isTransactional()); CHECK_FALSE(cp.isTransactional()); } TEST_CASE("WALEntry serialize/deserialize round-trip with empty payload", "[wal][serde][catch2]") { WALEntry e(WALEntry::OpType::StoreBlock, 1, 7); REQUIRE(e.data.empty()); auto buf = e.serialize(); auto out = WALEntry::deserialize(std::span(buf.data(), buf.size())); REQUIRE(out.has_value()); CHECK(out->header.magic == e.header.magic); CHECK(out->header.version == e.header.version); CHECK(out->header.sequenceNum == e.header.sequenceNum); CHECK(out->header.transactionId == e.header.transactionId); CHECK(out->header.operation == e.header.operation); CHECK(out->header.dataSize == 0); CHECK(out->data.empty()); CHECK(out->verifyChecksum()); } TEST_CASE("WALEntry serialize/deserialize round-trip with 128-byte payload", "[wal][serde][catch2]") { std::vector payload; payload.resize(128); for (size_t i = 0; i < payload.size(); ++i) { payload[i] = std::byte{static_cast(i & 0xFF)}; } WALEntry e(WALEntry::OpType::UpdateMetadata, 2, 99, std::span(payload.data(), payload.size())); auto buf = e.serialize(); auto out = WALEntry::deserialize(std::span(buf.data(), buf.size())); REQUIRE(out.has_value()); CHECK(out->header.operation == e.header.operation); CHECK(out->header.sequenceNum == e.header.sequenceNum); CHECK(out->header.transactionId == e.header.transactionId); CHECK(out->header.dataSize == static_cast(payload.size())); CHECK(out->data == payload); CHECK(out->verifyChecksum()); } TEST_CASE("WALEntry serialize/deserialize round-trip for every OpType (1-8)", "[wal][serde][catch2]") { for (uint8_t opv = 1; opv <= 8; ++opv) { auto op = static_cast(opv); WALEntry e(op, 100 + opv, 5000 + opv); auto buf = e.serialize(); auto out = WALEntry::deserialize(std::span(buf.data(), buf.size())); REQUIRE(out.has_value()); CHECK(out->header.operation == op); CHECK(out->header.sequenceNum == e.header.sequenceNum); CHECK(out->header.transactionId == e.header.transactionId); CHECK(out->verifyChecksum()); } } TEST_CASE("WALEntry::verifyChecksum succeeds on valid entry", "[wal][checksum][catch2]") { std::vector payload; payload.resize(4); payload[0] = std::byte{0xDE}; payload[1] = std::byte{0xAD}; payload[2] = std::byte{0xBE}; payload[3] = std::byte{0xEF}; WALEntry e(WALEntry::OpType::StoreBlock, 1, 1, std::span(payload.data(), payload.size())); CHECK(e.verifyChecksum()); } TEST_CASE("WALEntry::verifyChecksum fails after corrupting data[0]", "[wal][checksum][catch2]") { std::vector payload; payload.resize(1); payload[0] = std::byte{0xAA}; WALEntry e(WALEntry::OpType::StoreBlock, 10, 77, std::span(payload.data(), payload.size())); REQUIRE_FALSE(e.data.empty()); CHECK(e.verifyChecksum()); e.data[0] = std::byte{0xAB}; CHECK_FALSE(e.verifyChecksum()); } TEST_CASE("WALEntry updateChecksum then verify after mutation", "[wal][checksum][catch2]") { std::vector payload; payload.resize(2); payload[0] = std::byte{0x10}; payload[1] = std::byte{0x20}; WALEntry e(WALEntry::OpType::DeleteBlock, 11, 88, std::span(payload.data(), payload.size())); CHECK(e.verifyChecksum()); e.data[1] = std::byte{0x21}; CHECK_FALSE(e.verifyChecksum()); e.updateChecksum(); CHECK(e.verifyChecksum()); } TEST_CASE("WALEntry::deserialize rejects buffer smaller than header", "[wal][serde][error][catch2]") { std::vector buf; buf.resize(WALEntry::Header::size() - 1); auto out = WALEntry::deserialize(std::span(buf.data(), buf.size())); CHECK_FALSE(out.has_value()); } TEST_CASE("WALEntry::deserialize rejects invalid header (corrupt magic)", "[wal][serde][error][catch2]") { WALEntry e(WALEntry::OpType::StoreBlock, 1, 1); auto buf = e.serialize(); REQUIRE(buf.size() >= 1); buf[0] ^= std::byte{0xFF}; auto out = WALEntry::deserialize(std::span(buf.data(), buf.size())); CHECK_FALSE(out.has_value()); } TEST_CASE("WALEntry::deserialize rejects truncated data region", "[wal][serde][error][catch2]") { std::vector payload; payload.resize(16); for (size_t i = 0; i < payload.size(); ++i) { payload[i] = std::byte{0x5A}; } WALEntry e(WALEntry::OpType::UpdateReference, 2, 3, std::span(payload.data(), payload.size())); auto buf = e.serialize(); REQUIRE(buf.size() == WALEntry::Header::size() + payload.size()); buf.resize(buf.size() - 1); auto out = WALEntry::deserialize(std::span(buf.data(), buf.size())); CHECK_FALSE(out.has_value()); } TEST_CASE("WALEntry::deserialize rejects corrupted checksum (flip a byte)", "[wal][serde][error][catch2]") { std::vector payload; payload.resize(8); for (size_t i = 0; i < payload.size(); ++i) { payload[i] = std::byte{static_cast(i)}; } WALEntry e(WALEntry::OpType::UpdateMetadata, 3, 4, std::span(payload.data(), payload.size())); auto buf = e.serialize(); REQUIRE(buf.size() > WALEntry::Header::size()); buf.back() ^= std::byte{0xFF}; auto out = WALEntry::deserialize(std::span(buf.data(), buf.size())); CHECK_FALSE(out.has_value()); } namespace { // tests/fixtures/wal/store_block_entry_clang_v1.bin: one StoreBlock entry serialized by the // pre-fix wal_entry.cpp built with Clang 21 (which zero-fills header padding). Its logical // content is reproduced by makeFixtureEntry(). std::vector readWalFixture(const char* name) { const std::filesystem::path path = std::filesystem::path(YAMS_WAL_FIXTURE_DIR) / name; std::ifstream in(path, std::ios::binary); REQUIRE(in.good()); std::vector raw((std::istreambuf_iterator(in)), std::istreambuf_iterator()); std::vector bytes(raw.size()); std::memcpy(bytes.data(), raw.data(), raw.size()); return bytes; } constexpr uint32_t kFixtureChecksum = 0x032B862E; WALEntry makeFixtureEntry() { auto payload = WALEntry::StoreBlockData::encode("0123456789abcdef0123456789abcdef", 4096, 1); WALEntry e(WALEntry::OpType::StoreBlock, 0x1122334455667788ULL, 0x99, std::span(payload.data(), payload.size())); e.header.timestamp = 1700000000000000ULL; e.updateChecksum(); return e; } } // namespace TEST_CASE("WALEntry decodes and verifies a Clang-written v1 entry fixture", "[wal][serde][compat][catch2]") { const auto buf = readWalFixture("store_block_entry_clang_v1.bin"); REQUIRE(buf.size() == WALEntry::Header::size() + sizeof(WALEntry::StoreBlockData)); auto out = WALEntry::deserialize(std::span(buf.data(), buf.size())); REQUIRE(out.has_value()); CHECK(out->header.magic == 0x57414C31); CHECK(out->header.version == 1); CHECK(out->header.sequenceNum == 0x1122334455667788ULL); CHECK(out->header.timestamp == 1700000000000000ULL); CHECK(out->header.transactionId == 0x99); CHECK(out->header.operation == WALEntry::OpType::StoreBlock); CHECK(out->header.flags == 0); CHECK(out->header.reserved == 0); CHECK(out->header.dataSize == sizeof(WALEntry::StoreBlockData)); CHECK(out->header.checksum == kFixtureChecksum); CHECK(out->verifyChecksum()); auto block = WALEntry::StoreBlockData::decode( std::span(out->data.data(), out->data.size())); REQUIRE(block.has_value()); CHECK(std::string(block->hash, sizeof(block->hash)) == "0123456789abcdef0123456789abcdef"); CHECK(block->size == 4096); CHECK(block->refCount == 1); } TEST_CASE("WALEntry serialize reproduces the Clang-written v1 fixture byte for byte", "[wal][serde][compat][catch2]") { const auto expected = readWalFixture("store_block_entry_clang_v1.bin"); // Repeat so that differing stack contents between calls cannot hide nondeterminism. for (int i = 0; i < 4; ++i) { auto e = makeFixtureEntry(); CHECK(e.header.checksum == kFixtureChecksum); CHECK(e.verifyChecksum()); const auto buf = e.serialize(); REQUIRE(buf.size() == expected.size()); // Bytes 44..47 are the Header's trailing alignment padding: always zero on disk. for (size_t off = 44; off < WALEntry::Header::size(); ++off) { INFO("header byte " << off); CHECK(buf[off] == std::byte{0}); } CHECK(buf == expected); } } TEST_CASE("WALEntry fresh entries survive serialize/deserialize/verifyChecksum", "[wal][serde][checksum][catch2]") { for (uint64_t seq = 1; seq <= 64; ++seq) { std::vector payload(seq % 17, std::byte{static_cast(seq)}); WALEntry e(static_cast(1 + seq % 8), seq, seq * 3, std::span(payload.data(), payload.size())); INFO("seq " << seq); REQUIRE(e.verifyChecksum()); const auto buf = e.serialize(); auto out = WALEntry::deserialize(std::span(buf.data(), buf.size())); REQUIRE(out.has_value()); CHECK(out->header.checksum == e.header.checksum); CHECK(out->verifyChecksum()); CHECK(out->serialize() == buf); } } TEST_CASE("WALEntry::deserialize accepts legacy entries with nonzero header padding", "[wal][serde][compat][catch2]") { // Older GCC builds left Header padding uninitialized, wrote it to disk and included it in // the CRC. Such an entry must still recover; it is normalized to the canonical checksum. auto buf = readWalFixture("store_block_entry_clang_v1.bin"); for (size_t off = 44; off < WALEntry::Header::size(); ++off) { buf[off] = std::byte{static_cast(0x40 + off)}; } std::memset(buf.data() + 40, 0, sizeof(uint32_t)); const uint32_t legacyCrc = yams::common::crc32(std::span(buf)); std::memcpy(buf.data() + 40, &legacyCrc, sizeof(legacyCrc)); REQUIRE(legacyCrc != kFixtureChecksum); auto out = WALEntry::deserialize(std::span(buf.data(), buf.size())); REQUIRE(out.has_value()); CHECK(out->header.sequenceNum == 0x1122334455667788ULL); CHECK(out->header.checksum == kFixtureChecksum); CHECK(out->verifyChecksum()); CHECK(out->serialize() == readWalFixture("store_block_entry_clang_v1.bin")); } TEST_CASE("WALEntry::deserialize rejects corrupted header fields under both checksum forms", "[wal][serde][error][catch2]") { // Offsets 16 (timestamp), 24 (transactionId), 33 (flags), 34 (reserved). for (size_t off : {size_t{16}, size_t{24}, size_t{33}, size_t{34}}) { auto buf = readWalFixture("store_block_entry_clang_v1.bin"); buf[off] ^= std::byte{0x01}; INFO("header byte " << off); auto out = WALEntry::deserialize(std::span(buf.data(), buf.size())); CHECK_FALSE(out.has_value()); } } TEST_CASE("WALEntry checksum ignores header padding contents", "[wal][serde][catch2]") { // Padding carries no data; the canonical checksum treats it as zero. auto buf = readWalFixture("store_block_entry_clang_v1.bin"); buf[45] = std::byte{0x01}; auto out = WALEntry::deserialize(std::span(buf.data(), buf.size())); REQUIRE(out.has_value()); CHECK(out->header.checksum == kFixtureChecksum); CHECK(out->serialize() == readWalFixture("store_block_entry_clang_v1.bin")); } TEST_CASE("WALEntry::decodeHeader reads the fixed on-disk layout", "[wal][header][catch2]") { const auto buf = readWalFixture("store_block_entry_clang_v1.bin"); CHECK_FALSE(WALEntry::decodeHeader(std::span(buf.data(), 47)).has_value()); auto h = WALEntry::decodeHeader(std::span(buf.data(), buf.size())); REQUIRE(h.has_value()); CHECK(h->isValid()); CHECK(h->sequenceNum == 0x1122334455667788ULL); CHECK(h->timestamp == 1700000000000000ULL); CHECK(h->transactionId == 0x99); CHECK(h->operation == WALEntry::OpType::StoreBlock); CHECK(h->dataSize == sizeof(WALEntry::StoreBlockData)); CHECK(h->checksum == kFixtureChecksum); } TEST_CASE("TransactionData encodes 16 bytes with zeroed padding", "[wal][txn][catch2]") { const auto enc = WALEntry::TransactionData::encode(0x0102030405060708ULL, 0x0A0B0C0DU); REQUIRE(enc.size() == 16); uint64_t id = 0; uint32_t count = 0; std::memcpy(&id, enc.data(), sizeof(id)); std::memcpy(&count, enc.data() + 8, sizeof(count)); CHECK(id == 0x0102030405060708ULL); CHECK(count == 0x0A0B0C0DU); for (size_t off = 12; off < enc.size(); ++off) { INFO("byte " << off); CHECK(enc[off] == std::byte{0}); } } TEST_CASE("StoreBlockData encode/decode round-trip", "[wal][store][catch2]") { const std::string hash = "0123456789abcdef0123456789abcdef"; // 32 bytes auto enc = WALEntry::StoreBlockData::encode(hash, 4096, 2); auto dec = WALEntry::StoreBlockData::decode(std::span(enc.data(), enc.size())); REQUIRE(dec.has_value()); CHECK(std::memcmp(dec->hash, hash.data(), yams::HASH_SIZE) == 0); CHECK(dec->size == 4096); CHECK(dec->refCount == 2); } TEST_CASE("StoreBlockData short hash is zero-padded", "[wal][store][catch2]") { const std::string hash = "abc"; auto enc = WALEntry::StoreBlockData::encode(hash, 1, 1); auto dec = WALEntry::StoreBlockData::decode(std::span(enc.data(), enc.size())); REQUIRE(dec.has_value()); CHECK(dec->hash[0] == 'a'); CHECK(dec->hash[1] == 'b'); CHECK(dec->hash[2] == 'c'); for (size_t i = 3; i < yams::HASH_SIZE; ++i) { CHECK(dec->hash[i] == 0); } } TEST_CASE("StoreBlockData default refCount is 1", "[wal][store][catch2]") { const std::string hash = "0123456789abcdef0123456789abcdef"; auto enc = WALEntry::StoreBlockData::encode(hash, 4096); auto dec = WALEntry::StoreBlockData::decode(std::span(enc.data(), enc.size())); REQUIRE(dec.has_value()); CHECK(dec->refCount == 1); } TEST_CASE("StoreBlockData decode rejects too-small buffer", "[wal][store][catch2]") { std::vector buf; buf.resize(sizeof(WALEntry::StoreBlockData) - 1); auto dec = WALEntry::StoreBlockData::decode(std::span(buf.data(), buf.size())); CHECK_FALSE(dec.has_value()); } TEST_CASE("DeleteBlockData encode/decode round-trip", "[wal][delete][catch2]") { const std::string hash = "0123456789abcdef0123456789abcdef"; auto enc = WALEntry::DeleteBlockData::encode(hash); auto dec = WALEntry::DeleteBlockData::decode(std::span(enc.data(), enc.size())); REQUIRE(dec.has_value()); CHECK(std::memcmp(dec->hash, hash.data(), yams::HASH_SIZE) == 0); } TEST_CASE("DeleteBlockData decode rejects too-small buffer", "[wal][delete][catch2]") { std::vector buf; buf.resize(sizeof(WALEntry::DeleteBlockData) - 1); auto dec = WALEntry::DeleteBlockData::decode(std::span(buf.data(), buf.size())); CHECK_FALSE(dec.has_value()); } TEST_CASE("UpdateReferenceData encode/decode round-trip with positive delta", "[wal][ref][catch2]") { const std::string hash = "0123456789abcdef0123456789abcdef"; auto enc = WALEntry::UpdateReferenceData::encode(hash, 5); auto dec = WALEntry::UpdateReferenceData::decode(std::span(enc.data(), enc.size())); REQUIRE(dec.has_value()); CHECK(std::memcmp(dec->hash, hash.data(), yams::HASH_SIZE) == 0); CHECK(dec->delta == 5); } TEST_CASE("UpdateReferenceData encode/decode round-trip with negative delta", "[wal][ref][catch2]") { const std::string hash = "0123456789abcdef0123456789abcdef"; auto enc = WALEntry::UpdateReferenceData::encode(hash, -3); auto dec = WALEntry::UpdateReferenceData::decode(std::span(enc.data(), enc.size())); REQUIRE(dec.has_value()); CHECK(std::memcmp(dec->hash, hash.data(), yams::HASH_SIZE) == 0); CHECK(dec->delta == -3); } TEST_CASE("UpdateReferenceData decode rejects too-small buffer", "[wal][ref][catch2]") { std::vector buf; buf.resize(sizeof(WALEntry::UpdateReferenceData) - 1); auto dec = WALEntry::UpdateReferenceData::decode(std::span(buf.data(), buf.size())); CHECK_FALSE(dec.has_value()); } TEST_CASE("UpdateMetadataData encode/decode round-trip with key/value sizes", "[wal][meta][catch2]") { const std::string hash = "0123456789abcdef0123456789abcdef"; const std::string key = "author"; const std::string value = "alice"; auto enc = WALEntry::UpdateMetadataData::encode(hash, key, value); auto dec = WALEntry::UpdateMetadataData::decode(std::span(enc.data(), enc.size())); REQUIRE(dec.has_value()); CHECK(std::memcmp(dec->hash, hash.data(), yams::HASH_SIZE) == 0); CHECK(dec->keySize == static_cast(key.size())); CHECK(dec->valueSize == static_cast(value.size())); CHECK(enc.size() == sizeof(WALEntry::UpdateMetadataData) + key.size() + value.size()); const auto* keyPtr = enc.data() + sizeof(WALEntry::UpdateMetadataData); const auto* valPtr = keyPtr + key.size(); CHECK(std::memcmp(keyPtr, key.data(), key.size()) == 0); CHECK(std::memcmp(valPtr, value.data(), value.size()) == 0); } TEST_CASE("UpdateMetadataData decode rejects truncated key/value region", "[wal][meta][catch2]") { const std::string hash = "0123456789abcdef0123456789abcdef"; const std::string key = "author"; const std::string value = "alice"; auto enc = WALEntry::UpdateMetadataData::encode(hash, key, value); REQUIRE(enc.size() > sizeof(WALEntry::UpdateMetadataData)); enc.resize(enc.size() - 1); auto dec = WALEntry::UpdateMetadataData::decode(std::span(enc.data(), enc.size())); CHECK_FALSE(dec.has_value()); } TEST_CASE("UpdateMetadataData decode rejects too-small buffer", "[wal][meta][catch2]") { std::vector buf; buf.resize(sizeof(WALEntry::UpdateMetadataData) - 1); auto dec = WALEntry::UpdateMetadataData::decode(std::span(buf.data(), buf.size())); CHECK_FALSE(dec.has_value()); } TEST_CASE("TransactionData encode/decode round-trip", "[wal][txn][catch2]") { auto enc = WALEntry::TransactionData::encode(12345, 10); auto dec = WALEntry::TransactionData::decode(std::span(enc.data(), enc.size())); REQUIRE(dec.has_value()); CHECK(dec->transactionId == 12345); CHECK(dec->participantCount == 10); } TEST_CASE("TransactionData default count is 0", "[wal][txn][catch2]") { auto enc = WALEntry::TransactionData::encode(12345); auto dec = WALEntry::TransactionData::decode(std::span(enc.data(), enc.size())); REQUIRE(dec.has_value()); CHECK(dec->transactionId == 12345); CHECK(dec->participantCount == 0); } TEST_CASE("TransactionData decode rejects too-small buffer", "[wal][txn][catch2]") { std::vector buf; buf.resize(sizeof(WALEntry::TransactionData) - 1); auto dec = WALEntry::TransactionData::decode(std::span(buf.data(), buf.size())); CHECK_FALSE(dec.has_value()); } TEST_CASE("CheckpointData encode/decode round-trip", "[wal][checkpoint][catch2]") { auto enc = WALEntry::CheckpointData::encode(500, 1700000000); auto dec = WALEntry::CheckpointData::decode(std::span(enc.data(), enc.size())); REQUIRE(dec.has_value()); CHECK(dec->sequenceNum == 500); CHECK(dec->timestamp == 1700000000); } TEST_CASE("CheckpointData decode rejects too-small buffer", "[wal][checkpoint][catch2]") { std::vector buf; buf.resize(sizeof(WALEntry::CheckpointData) - 1); auto dec = WALEntry::CheckpointData::decode(std::span(buf.data(), buf.size())); CHECK_FALSE(dec.has_value()); } TEST_CASE("OpType stream operator prints all named values and Unknown", "[wal][stream][catch2]") { struct Expected { WALEntry::OpType op; const char* s; }; const Expected expected[] = { {WALEntry::OpType::BeginTransaction, "BeginTransaction"}, {WALEntry::OpType::StoreBlock, "StoreBlock"}, {WALEntry::OpType::DeleteBlock, "DeleteBlock"}, {WALEntry::OpType::UpdateReference, "UpdateReference"}, {WALEntry::OpType::UpdateMetadata, "UpdateMetadata"}, {WALEntry::OpType::CommitTransaction, "CommitTransaction"}, {WALEntry::OpType::Rollback, "Rollback"}, {WALEntry::OpType::Checkpoint, "Checkpoint"}, }; for (const auto& e : expected) { std::ostringstream oss; oss << e.op; CHECK(oss.str() == e.s); } { std::ostringstream oss; oss << static_cast(9); CHECK(oss.str() == "Unknown(9)"); } } TEST_CASE("WALEntry stream includes key fields", "[wal][stream][catch2]") { WALEntry e(WALEntry::OpType::CommitTransaction, 123, 456); std::ostringstream oss; oss << e; const auto s = oss.str(); CHECK(s.find("WALEntry{") != std::string::npos); CHECK(s.find("seq=") != std::string::npos); CHECK(s.find("txn=") != std::string::npos); }