Improve exception robustness.
- Introduced `test_swap_edge_cases.cc` with extensive tests for minimum payload sizes, truncated payloads, data overflows, unsupported encoding versions, CRC mismatches, and mixed valid/invalid records to ensure reliability under complex scenarios. - Enhanced `main.cc` with a top-level exception handler to prevent data loss and ensure cleanup during unexpected failures.
This commit is contained in:
23
Buffer.cc
23
Buffer.cc
@@ -417,11 +417,34 @@ Buffer::OpenFromFile(const std::string &path, std::string &err)
|
|||||||
// Read entire file into PieceTable as-is
|
// Read entire file into PieceTable as-is
|
||||||
std::string data;
|
std::string data;
|
||||||
in.seekg(0, std::ios::end);
|
in.seekg(0, std::ios::end);
|
||||||
|
if (!in) {
|
||||||
|
err = "Failed to seek to end of file: " + norm;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
auto sz = in.tellg();
|
auto sz = in.tellg();
|
||||||
|
if (sz < 0) {
|
||||||
|
err = "Failed to get file size: " + norm;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (sz > 0) {
|
if (sz > 0) {
|
||||||
data.resize(static_cast<std::size_t>(sz));
|
data.resize(static_cast<std::size_t>(sz));
|
||||||
in.seekg(0, std::ios::beg);
|
in.seekg(0, std::ios::beg);
|
||||||
|
if (!in) {
|
||||||
|
err = "Failed to seek to beginning of file: " + norm;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
in.read(data.data(), static_cast<std::streamsize>(data.size()));
|
in.read(data.data(), static_cast<std::streamsize>(data.size()));
|
||||||
|
if (!in && !in.eof()) {
|
||||||
|
err = "Failed to read file: " + norm;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Validate we read the expected number of bytes
|
||||||
|
const std::streamsize bytes_read = in.gcount();
|
||||||
|
if (bytes_read != static_cast<std::streamsize>(data.size())) {
|
||||||
|
err = "Partial read of file (expected " + std::to_string(data.size()) +
|
||||||
|
" bytes, got " + std::to_string(bytes_read) + "): " + norm;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
content_.Clear();
|
content_.Clear();
|
||||||
if (!data.empty())
|
if (!data.empty())
|
||||||
|
|||||||
@@ -316,6 +316,7 @@ if (BUILD_TESTS)
|
|||||||
tests/test_swap_recorder.cc
|
tests/test_swap_recorder.cc
|
||||||
tests/test_swap_writer.cc
|
tests/test_swap_writer.cc
|
||||||
tests/test_swap_replay.cc
|
tests/test_swap_replay.cc
|
||||||
|
tests/test_swap_edge_cases.cc
|
||||||
tests/test_swap_recovery_prompt.cc
|
tests/test_swap_recovery_prompt.cc
|
||||||
tests/test_swap_cleanup.cc
|
tests/test_swap_cleanup.cc
|
||||||
tests/test_swap_git_editor.cc
|
tests/test_swap_git_editor.cc
|
||||||
|
|||||||
309
Swap.cc
309
Swap.cc
@@ -598,24 +598,32 @@ SwapManager::write_header(int fd)
|
|||||||
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
SwapManager::open_ctx(JournalCtx &ctx, const std::string &path)
|
SwapManager::open_ctx(JournalCtx &ctx, const std::string &path, std::string &err)
|
||||||
{
|
{
|
||||||
|
err.clear();
|
||||||
if (ctx.fd >= 0)
|
if (ctx.fd >= 0)
|
||||||
return true;
|
return true;
|
||||||
if (!ensure_parent_dir(path))
|
if (!ensure_parent_dir(path)) {
|
||||||
|
err = "Failed to create parent directory for swap file: " + path;
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
int flags = O_CREAT | O_WRONLY | O_APPEND;
|
int flags = O_CREAT | O_WRONLY | O_APPEND;
|
||||||
#ifdef O_CLOEXEC
|
#ifdef O_CLOEXEC
|
||||||
flags |= O_CLOEXEC;
|
flags |= O_CLOEXEC;
|
||||||
#endif
|
#endif
|
||||||
int fd = ::open(path.c_str(), flags, 0600);
|
int fd = ::open(path.c_str(), flags, 0600);
|
||||||
if (fd < 0)
|
if (fd < 0) {
|
||||||
|
int saved_errno = errno;
|
||||||
|
err = "Failed to open swap file '" + path + "': " + std::strerror(saved_errno);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
// Ensure permissions even if file already existed.
|
// Ensure permissions even if file already existed.
|
||||||
(void) ::fchmod(fd, 0600);
|
(void) ::fchmod(fd, 0600);
|
||||||
struct stat st{};
|
struct stat st{};
|
||||||
if (fstat(fd, &st) != 0) {
|
if (fstat(fd, &st) != 0) {
|
||||||
|
int saved_errno = errno;
|
||||||
::close(fd);
|
::close(fd);
|
||||||
|
err = "Failed to fstat swap file '" + path + "': " + std::strerror(saved_errno);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// If an existing file is too small to contain the fixed header, truncate
|
// If an existing file is too small to contain the fixed header, truncate
|
||||||
@@ -627,8 +635,11 @@ SwapManager::open_ctx(JournalCtx &ctx, const std::string &path)
|
|||||||
tflags |= O_CLOEXEC;
|
tflags |= O_CLOEXEC;
|
||||||
#endif
|
#endif
|
||||||
fd = ::open(path.c_str(), tflags, 0600);
|
fd = ::open(path.c_str(), tflags, 0600);
|
||||||
if (fd < 0)
|
if (fd < 0) {
|
||||||
|
int saved_errno = errno;
|
||||||
|
err = "Failed to reopen swap file for truncation '" + path + "': " + std::strerror(saved_errno);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
(void) ::fchmod(fd, 0600);
|
(void) ::fchmod(fd, 0600);
|
||||||
st.st_size = 0;
|
st.st_size = 0;
|
||||||
}
|
}
|
||||||
@@ -637,6 +648,9 @@ SwapManager::open_ctx(JournalCtx &ctx, const std::string &path)
|
|||||||
if (st.st_size == 0) {
|
if (st.st_size == 0) {
|
||||||
ctx.header_ok = write_header(fd);
|
ctx.header_ok = write_header(fd);
|
||||||
ctx.approx_size_bytes = ctx.header_ok ? 64 : 0;
|
ctx.approx_size_bytes = ctx.header_ok ? 64 : 0;
|
||||||
|
if (!ctx.header_ok) {
|
||||||
|
err = "Failed to write swap file header: " + path;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.header_ok = true; // stage 1: trust existing header
|
ctx.header_ok = true; // stage 1: trust existing header
|
||||||
ctx.approx_size_bytes = static_cast<std::uint64_t>(st.st_size);
|
ctx.approx_size_bytes = static_cast<std::uint64_t>(st.st_size);
|
||||||
@@ -658,12 +672,17 @@ SwapManager::close_ctx(JournalCtx &ctx)
|
|||||||
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
SwapManager::compact_to_checkpoint(JournalCtx &ctx, const std::vector<std::uint8_t> &chkpt_record)
|
SwapManager::compact_to_checkpoint(JournalCtx &ctx, const std::vector<std::uint8_t> &chkpt_record, std::string &err)
|
||||||
{
|
{
|
||||||
if (ctx.path.empty())
|
err.clear();
|
||||||
|
if (ctx.path.empty()) {
|
||||||
|
err = "Compact failed: empty path";
|
||||||
return false;
|
return false;
|
||||||
if (chkpt_record.empty())
|
}
|
||||||
|
if (chkpt_record.empty()) {
|
||||||
|
err = "Compact failed: empty checkpoint record";
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Close existing file before rename.
|
// Close existing file before rename.
|
||||||
if (ctx.fd >= 0) {
|
if (ctx.fd >= 0) {
|
||||||
@@ -675,30 +694,46 @@ SwapManager::compact_to_checkpoint(JournalCtx &ctx, const std::vector<std::uint8
|
|||||||
|
|
||||||
const std::string tmp_path = ctx.path + ".tmp";
|
const std::string tmp_path = ctx.path + ".tmp";
|
||||||
// Create the compacted file: header + checkpoint record.
|
// Create the compacted file: header + checkpoint record.
|
||||||
if (!ensure_parent_dir(tmp_path))
|
if (!ensure_parent_dir(tmp_path)) {
|
||||||
|
err = "Failed to create parent directory for temp swap file: " + tmp_path;
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
int flags = O_CREAT | O_WRONLY | O_TRUNC;
|
int flags = O_CREAT | O_WRONLY | O_TRUNC;
|
||||||
#ifdef O_CLOEXEC
|
#ifdef O_CLOEXEC
|
||||||
flags |= O_CLOEXEC;
|
flags |= O_CLOEXEC;
|
||||||
#endif
|
#endif
|
||||||
int tfd = ::open(tmp_path.c_str(), flags, 0600);
|
int tfd = ::open(tmp_path.c_str(), flags, 0600);
|
||||||
if (tfd < 0)
|
if (tfd < 0) {
|
||||||
|
int saved_errno = errno;
|
||||||
|
err = "Failed to open temp swap file '" + tmp_path + "': " + std::strerror(saved_errno);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
(void) ::fchmod(tfd, 0600);
|
(void) ::fchmod(tfd, 0600);
|
||||||
bool ok = write_header(tfd);
|
bool ok = write_header(tfd);
|
||||||
if (ok)
|
if (ok)
|
||||||
ok = write_full(tfd, chkpt_record.data(), chkpt_record.size());
|
ok = write_full(tfd, chkpt_record.data(), chkpt_record.size());
|
||||||
if (ok)
|
if (ok) {
|
||||||
ok = (::fsync(tfd) == 0);
|
if (::fsync(tfd) != 0) {
|
||||||
|
int saved_errno = errno;
|
||||||
|
err = "Failed to fsync temp swap file '" + tmp_path + "': " + std::strerror(saved_errno);
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
::close(tfd);
|
::close(tfd);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
|
if (err.empty()) {
|
||||||
|
err = "Failed to write temp swap file: " + tmp_path;
|
||||||
|
}
|
||||||
std::remove(tmp_path.c_str());
|
std::remove(tmp_path.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atomic replace.
|
// Atomic replace.
|
||||||
if (::rename(tmp_path.c_str(), ctx.path.c_str()) != 0) {
|
if (::rename(tmp_path.c_str(), ctx.path.c_str()) != 0) {
|
||||||
|
int saved_errno = errno;
|
||||||
|
err = "Failed to rename temp swap file '" + tmp_path + "' to '" + ctx.path + "': " + std::strerror(
|
||||||
|
saved_errno);
|
||||||
std::remove(tmp_path.c_str());
|
std::remove(tmp_path.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -723,8 +758,10 @@ SwapManager::compact_to_checkpoint(JournalCtx &ctx, const std::vector<std::uint8
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Re-open for further appends.
|
// Re-open for further appends.
|
||||||
if (!open_ctx(ctx, ctx.path))
|
if (!open_ctx(ctx, ctx.path, err)) {
|
||||||
|
// err already set by open_ctx
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
ctx.approx_size_bytes = 64 + static_cast<std::uint64_t>(chkpt_record.size());
|
ctx.approx_size_bytes = 64 + static_cast<std::uint64_t>(chkpt_record.size());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -969,7 +1006,13 @@ SwapManager::writer_loop()
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
for (const Pending &p: batch) {
|
for (const Pending &p: batch) {
|
||||||
process_one(p);
|
try {
|
||||||
|
process_one(p);
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
report_error(std::string("Exception in process_one: ") + e.what(), p.buf);
|
||||||
|
} catch (...) {
|
||||||
|
report_error("Unknown exception in process_one", p.buf);
|
||||||
|
}
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lg(mtx_);
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
if (p.seq > last_processed_)
|
if (p.seq > last_processed_)
|
||||||
@@ -981,23 +1024,29 @@ SwapManager::writer_loop()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Throttled fsync: best-effort (grouped)
|
// Throttled fsync: best-effort (grouped)
|
||||||
std::vector<int> to_sync;
|
try {
|
||||||
std::uint64_t now = now_ns();
|
std::vector<int> to_sync;
|
||||||
{
|
std::uint64_t now = now_ns();
|
||||||
std::lock_guard<std::mutex> lg(mtx_);
|
{
|
||||||
for (auto &kv: journals_) {
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
JournalCtx &ctx = kv.second;
|
for (auto &kv: journals_) {
|
||||||
if (ctx.fd >= 0) {
|
JournalCtx &ctx = kv.second;
|
||||||
if (ctx.last_fsync_ns == 0 || (now - ctx.last_fsync_ns) / 1000000ULL >=
|
if (ctx.fd >= 0) {
|
||||||
cfg_.fsync_interval_ms) {
|
if (ctx.last_fsync_ns == 0 || (now - ctx.last_fsync_ns) / 1000000ULL >=
|
||||||
ctx.last_fsync_ns = now;
|
cfg_.fsync_interval_ms) {
|
||||||
to_sync.push_back(ctx.fd);
|
ctx.last_fsync_ns = now;
|
||||||
|
to_sync.push_back(ctx.fd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
for (int fd: to_sync) {
|
||||||
for (int fd: to_sync) {
|
(void) ::fsync(fd);
|
||||||
(void) ::fsync(fd);
|
}
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
report_error(std::string("Exception in fsync operations: ") + e.what());
|
||||||
|
} catch (...) {
|
||||||
|
report_error("Unknown exception in fsync operations");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Wake any waiters.
|
// Wake any waiters.
|
||||||
@@ -1010,70 +1059,90 @@ SwapManager::process_one(const Pending &p)
|
|||||||
{
|
{
|
||||||
if (!p.buf)
|
if (!p.buf)
|
||||||
return;
|
return;
|
||||||
Buffer &buf = *p.buf;
|
|
||||||
|
|
||||||
JournalCtx *ctxp = nullptr;
|
try {
|
||||||
std::string path;
|
Buffer &buf = *p.buf;
|
||||||
std::size_t compact_bytes = 0;
|
|
||||||
{
|
JournalCtx *ctxp = nullptr;
|
||||||
std::lock_guard<std::mutex> lg(mtx_);
|
std::string path;
|
||||||
auto it = journals_.find(p.buf);
|
std::size_t compact_bytes = 0;
|
||||||
if (it == journals_.end())
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
auto it = journals_.find(p.buf);
|
||||||
|
if (it == journals_.end())
|
||||||
|
return;
|
||||||
|
if (it->second.path.empty())
|
||||||
|
it->second.path = ComputeSidecarPath(buf);
|
||||||
|
path = it->second.path;
|
||||||
|
ctxp = &it->second;
|
||||||
|
compact_bytes = cfg_.compact_bytes;
|
||||||
|
}
|
||||||
|
if (!ctxp)
|
||||||
return;
|
return;
|
||||||
if (it->second.path.empty())
|
std::string open_err;
|
||||||
it->second.path = ComputeSidecarPath(buf);
|
if (!open_ctx(*ctxp, path, open_err)) {
|
||||||
path = it->second.path;
|
report_error(open_err, p.buf);
|
||||||
ctxp = &it->second;
|
return;
|
||||||
compact_bytes = cfg_.compact_bytes;
|
}
|
||||||
}
|
if (p.payload.size() > 0xFFFFFFu) {
|
||||||
if (!ctxp)
|
report_error("Payload too large: " + std::to_string(p.payload.size()) + " bytes", p.buf);
|
||||||
return;
|
return;
|
||||||
if (!open_ctx(*ctxp, path))
|
}
|
||||||
return;
|
|
||||||
if (p.payload.size() > 0xFFFFFFu)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Build record: [type u8][len u24][payload][crc32 u32]
|
// Build record: [type u8][len u24][payload][crc32 u32]
|
||||||
std::uint8_t len3[3];
|
std::uint8_t len3[3];
|
||||||
put_u24_le(len3, static_cast<std::uint32_t>(p.payload.size()));
|
put_u24_le(len3, static_cast<std::uint32_t>(p.payload.size()));
|
||||||
|
|
||||||
std::uint8_t head[4];
|
std::uint8_t head[4];
|
||||||
head[0] = static_cast<std::uint8_t>(p.type);
|
head[0] = static_cast<std::uint8_t>(p.type);
|
||||||
head[1] = len3[0];
|
head[1] = len3[0];
|
||||||
head[2] = len3[1];
|
head[2] = len3[1];
|
||||||
head[3] = len3[2];
|
head[3] = len3[2];
|
||||||
|
|
||||||
std::uint32_t c = 0;
|
std::uint32_t c = 0;
|
||||||
c = crc32(head, sizeof(head), c);
|
c = crc32(head, sizeof(head), c);
|
||||||
if (!p.payload.empty())
|
if (!p.payload.empty())
|
||||||
c = crc32(p.payload.data(), p.payload.size(), c);
|
c = crc32(p.payload.data(), p.payload.size(), c);
|
||||||
std::uint8_t crcbytes[4];
|
std::uint8_t crcbytes[4];
|
||||||
crcbytes[0] = static_cast<std::uint8_t>(c & 0xFFu);
|
crcbytes[0] = static_cast<std::uint8_t>(c & 0xFFu);
|
||||||
crcbytes[1] = static_cast<std::uint8_t>((c >> 8) & 0xFFu);
|
crcbytes[1] = static_cast<std::uint8_t>((c >> 8) & 0xFFu);
|
||||||
crcbytes[2] = static_cast<std::uint8_t>((c >> 16) & 0xFFu);
|
crcbytes[2] = static_cast<std::uint8_t>((c >> 16) & 0xFFu);
|
||||||
crcbytes[3] = static_cast<std::uint8_t>((c >> 24) & 0xFFu);
|
crcbytes[3] = static_cast<std::uint8_t>((c >> 24) & 0xFFu);
|
||||||
|
|
||||||
std::vector<std::uint8_t> rec;
|
std::vector<std::uint8_t> rec;
|
||||||
rec.reserve(sizeof(head) + p.payload.size() + sizeof(crcbytes));
|
rec.reserve(sizeof(head) + p.payload.size() + sizeof(crcbytes));
|
||||||
rec.insert(rec.end(), head, head + sizeof(head));
|
rec.insert(rec.end(), head, head + sizeof(head));
|
||||||
if (!p.payload.empty())
|
if (!p.payload.empty())
|
||||||
rec.insert(rec.end(), p.payload.begin(), p.payload.end());
|
rec.insert(rec.end(), p.payload.begin(), p.payload.end());
|
||||||
rec.insert(rec.end(), crcbytes, crcbytes + sizeof(crcbytes));
|
rec.insert(rec.end(), crcbytes, crcbytes + sizeof(crcbytes));
|
||||||
|
|
||||||
// Write (handle partial writes and check results)
|
// Write (handle partial writes and check results)
|
||||||
bool ok = write_full(ctxp->fd, rec.data(), rec.size());
|
bool ok = write_full(ctxp->fd, rec.data(), rec.size());
|
||||||
if (ok) {
|
if (!ok) {
|
||||||
|
int err = errno;
|
||||||
|
report_error("Failed to write swap record to '" + path + "': " + std::strerror(err), p.buf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
ctxp->approx_size_bytes += static_cast<std::uint64_t>(rec.size());
|
ctxp->approx_size_bytes += static_cast<std::uint64_t>(rec.size());
|
||||||
if (p.urgent_flush) {
|
if (p.urgent_flush) {
|
||||||
(void) ::fsync(ctxp->fd);
|
if (::fsync(ctxp->fd) != 0) {
|
||||||
|
int err = errno;
|
||||||
|
report_error("Failed to fsync swap file '" + path + "': " + std::strerror(err), p.buf);
|
||||||
|
}
|
||||||
ctxp->last_fsync_ns = now_ns();
|
ctxp->last_fsync_ns = now_ns();
|
||||||
}
|
}
|
||||||
if (p.type == SwapRecType::CHKPT && compact_bytes > 0 &&
|
if (p.type == SwapRecType::CHKPT && compact_bytes > 0 &&
|
||||||
ctxp->approx_size_bytes >= static_cast<std::uint64_t>(compact_bytes)) {
|
ctxp->approx_size_bytes >= static_cast<std::uint64_t>(compact_bytes)) {
|
||||||
(void) compact_to_checkpoint(*ctxp, rec);
|
std::string compact_err;
|
||||||
|
if (!compact_to_checkpoint(*ctxp, rec, compact_err)) {
|
||||||
|
report_error(compact_err, p.buf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
report_error(std::string("Exception in process_one: ") + e.what(), p.buf);
|
||||||
|
} catch (...) {
|
||||||
|
report_error("Unknown exception in process_one", p.buf);
|
||||||
}
|
}
|
||||||
(void) ok; // best-effort; future work could mark ctx error state
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1184,8 +1253,10 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case SwapRecType::INS: {
|
case SwapRecType::INS: {
|
||||||
std::size_t off = 0;
|
std::size_t off = 0;
|
||||||
if (payload.empty()) {
|
// INS payload: encver(1) + row(4) + col(4) + nbytes(4) + data(nbytes)
|
||||||
err = "Swap record missing INS payload";
|
// Minimum: 1 + 4 + 4 + 4 = 13 bytes
|
||||||
|
if (payload.size() < 13) {
|
||||||
|
err = "INS payload too short (need at least 13 bytes)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const std::uint8_t encver = payload[off++];
|
const std::uint8_t encver = payload[off++];
|
||||||
@@ -1196,7 +1267,7 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
std::uint32_t row = 0, col = 0, nbytes = 0;
|
std::uint32_t row = 0, col = 0, nbytes = 0;
|
||||||
if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col) || !parse_u32_le(
|
if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col) || !parse_u32_le(
|
||||||
payload, off, nbytes)) {
|
payload, off, nbytes)) {
|
||||||
err = "Malformed INS payload";
|
err = "Malformed INS payload (failed to parse row/col/nbytes)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (off + nbytes > payload.size()) {
|
if (off + nbytes > payload.size()) {
|
||||||
@@ -1209,8 +1280,10 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
}
|
}
|
||||||
case SwapRecType::DEL: {
|
case SwapRecType::DEL: {
|
||||||
std::size_t off = 0;
|
std::size_t off = 0;
|
||||||
if (payload.empty()) {
|
// DEL payload: encver(1) + row(4) + col(4) + dlen(4)
|
||||||
err = "Swap record missing DEL payload";
|
// Minimum: 1 + 4 + 4 + 4 = 13 bytes
|
||||||
|
if (payload.size() < 13) {
|
||||||
|
err = "DEL payload too short (need at least 13 bytes)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const std::uint8_t encver = payload[off++];
|
const std::uint8_t encver = payload[off++];
|
||||||
@@ -1221,7 +1294,7 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
std::uint32_t row = 0, col = 0, dlen = 0;
|
std::uint32_t row = 0, col = 0, dlen = 0;
|
||||||
if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col) || !parse_u32_le(
|
if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col) || !parse_u32_le(
|
||||||
payload, off, dlen)) {
|
payload, off, dlen)) {
|
||||||
err = "Malformed DEL payload";
|
err = "Malformed DEL payload (failed to parse row/col/dlen)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
buf.delete_text((int) row, (int) col, (std::size_t) dlen);
|
buf.delete_text((int) row, (int) col, (std::size_t) dlen);
|
||||||
@@ -1229,8 +1302,10 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
}
|
}
|
||||||
case SwapRecType::SPLIT: {
|
case SwapRecType::SPLIT: {
|
||||||
std::size_t off = 0;
|
std::size_t off = 0;
|
||||||
if (payload.empty()) {
|
// SPLIT payload: encver(1) + row(4) + col(4)
|
||||||
err = "Swap record missing SPLIT payload";
|
// Minimum: 1 + 4 + 4 = 9 bytes
|
||||||
|
if (payload.size() < 9) {
|
||||||
|
err = "SPLIT payload too short (need at least 9 bytes)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const std::uint8_t encver = payload[off++];
|
const std::uint8_t encver = payload[off++];
|
||||||
@@ -1240,7 +1315,7 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
}
|
}
|
||||||
std::uint32_t row = 0, col = 0;
|
std::uint32_t row = 0, col = 0;
|
||||||
if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col)) {
|
if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col)) {
|
||||||
err = "Malformed SPLIT payload";
|
err = "Malformed SPLIT payload (failed to parse row/col)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
buf.split_line((int) row, (int) col);
|
buf.split_line((int) row, (int) col);
|
||||||
@@ -1248,8 +1323,10 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
}
|
}
|
||||||
case SwapRecType::JOIN: {
|
case SwapRecType::JOIN: {
|
||||||
std::size_t off = 0;
|
std::size_t off = 0;
|
||||||
if (payload.empty()) {
|
// JOIN payload: encver(1) + row(4)
|
||||||
err = "Swap record missing JOIN payload";
|
// Minimum: 1 + 4 = 5 bytes
|
||||||
|
if (payload.size() < 5) {
|
||||||
|
err = "JOIN payload too short (need at least 5 bytes)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const std::uint8_t encver = payload[off++];
|
const std::uint8_t encver = payload[off++];
|
||||||
@@ -1259,7 +1336,7 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
}
|
}
|
||||||
std::uint32_t row = 0;
|
std::uint32_t row = 0;
|
||||||
if (!parse_u32_le(payload, off, row)) {
|
if (!parse_u32_le(payload, off, row)) {
|
||||||
err = "Malformed JOIN payload";
|
err = "Malformed JOIN payload (failed to parse row)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
buf.join_lines((int) row);
|
buf.join_lines((int) row);
|
||||||
@@ -1267,8 +1344,10 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
}
|
}
|
||||||
case SwapRecType::CHKPT: {
|
case SwapRecType::CHKPT: {
|
||||||
std::size_t off = 0;
|
std::size_t off = 0;
|
||||||
|
// CHKPT payload: encver(1) + nbytes(4) + data(nbytes)
|
||||||
|
// Minimum: 1 + 4 = 5 bytes
|
||||||
if (payload.size() < 5) {
|
if (payload.size() < 5) {
|
||||||
err = "Malformed CHKPT payload";
|
err = "CHKPT payload too short (need at least 5 bytes)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const std::uint8_t encver = payload[off++];
|
const std::uint8_t encver = payload[off++];
|
||||||
@@ -1278,7 +1357,7 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
}
|
}
|
||||||
std::uint32_t nbytes = 0;
|
std::uint32_t nbytes = 0;
|
||||||
if (!parse_u32_le(payload, off, nbytes)) {
|
if (!parse_u32_le(payload, off, nbytes)) {
|
||||||
err = "Malformed CHKPT payload";
|
err = "Malformed CHKPT payload (failed to parse nbytes)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (off + nbytes > payload.size()) {
|
if (off + nbytes > payload.size()) {
|
||||||
@@ -1295,4 +1374,54 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
SwapManager::report_error(const std::string &message, Buffer *buf)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
SwapError err;
|
||||||
|
err.timestamp_ns = now_ns();
|
||||||
|
err.message = message;
|
||||||
|
if (buf && !buf->Filename().empty()) {
|
||||||
|
err.buffer_name = buf->Filename();
|
||||||
|
} else if (buf) {
|
||||||
|
err.buffer_name = "<unnamed>";
|
||||||
|
} else {
|
||||||
|
err.buffer_name = "<unknown>";
|
||||||
|
}
|
||||||
|
errors_.push_back(err);
|
||||||
|
// Bound the error queue to 100 entries
|
||||||
|
while (errors_.size() > 100) {
|
||||||
|
errors_.pop_front();
|
||||||
|
}
|
||||||
|
++total_error_count_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
SwapManager::HasErrors() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
return !errors_.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
SwapManager::GetLastError() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
if (errors_.empty())
|
||||||
|
return "";
|
||||||
|
const SwapError &e = errors_.back();
|
||||||
|
return "[" + e.buffer_name + "] " + e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
SwapManager::GetErrorCount() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
return total_error_count_;
|
||||||
|
}
|
||||||
} // namespace kte
|
} // namespace kte
|
||||||
29
Swap.h
29
Swap.h
@@ -10,6 +10,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
|
#include <deque>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
@@ -131,6 +132,20 @@ public:
|
|||||||
// Per-buffer toggle
|
// Per-buffer toggle
|
||||||
void SetSuspended(Buffer &buf, bool on);
|
void SetSuspended(Buffer &buf, bool on);
|
||||||
|
|
||||||
|
// Error reporting for background thread
|
||||||
|
struct SwapError {
|
||||||
|
std::uint64_t timestamp_ns{0};
|
||||||
|
std::string message;
|
||||||
|
std::string buffer_name; // filename or "<unnamed>"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Query error state (thread-safe)
|
||||||
|
bool HasErrors() const;
|
||||||
|
|
||||||
|
std::string GetLastError() const;
|
||||||
|
|
||||||
|
std::size_t GetErrorCount() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
class BufferRecorder final : public SwapRecorder {
|
class BufferRecorder final : public SwapRecorder {
|
||||||
public:
|
public:
|
||||||
@@ -190,11 +205,12 @@ private:
|
|||||||
|
|
||||||
static bool write_header(int fd);
|
static bool write_header(int fd);
|
||||||
|
|
||||||
static bool open_ctx(JournalCtx &ctx, const std::string &path);
|
static bool open_ctx(JournalCtx &ctx, const std::string &path, std::string &err);
|
||||||
|
|
||||||
static void close_ctx(JournalCtx &ctx);
|
static void close_ctx(JournalCtx &ctx);
|
||||||
|
|
||||||
static bool compact_to_checkpoint(JournalCtx &ctx, const std::vector<std::uint8_t> &chkpt_record);
|
static bool compact_to_checkpoint(JournalCtx &ctx, const std::vector<std::uint8_t> &chkpt_record,
|
||||||
|
std::string &err);
|
||||||
|
|
||||||
static std::uint32_t crc32(const std::uint8_t *data, std::size_t len, std::uint32_t seed = 0);
|
static std::uint32_t crc32(const std::uint8_t *data, std::size_t len, std::uint32_t seed = 0);
|
||||||
|
|
||||||
@@ -210,11 +226,14 @@ private:
|
|||||||
|
|
||||||
void process_one(const Pending &p);
|
void process_one(const Pending &p);
|
||||||
|
|
||||||
|
// Error reporting helper (called from writer thread)
|
||||||
|
void report_error(const std::string &message, Buffer *buf = nullptr);
|
||||||
|
|
||||||
// State
|
// State
|
||||||
SwapConfig cfg_{};
|
SwapConfig cfg_{};
|
||||||
std::unordered_map<Buffer *, JournalCtx> journals_;
|
std::unordered_map<Buffer *, JournalCtx> journals_;
|
||||||
std::unordered_map<Buffer *, std::unique_ptr<BufferRecorder> > recorders_;
|
std::unordered_map<Buffer *, std::unique_ptr<BufferRecorder> > recorders_;
|
||||||
std::mutex mtx_;
|
mutable std::mutex mtx_;
|
||||||
std::condition_variable cv_;
|
std::condition_variable cv_;
|
||||||
std::vector<Pending> queue_;
|
std::vector<Pending> queue_;
|
||||||
std::uint64_t next_seq_{0};
|
std::uint64_t next_seq_{0};
|
||||||
@@ -222,5 +241,9 @@ private:
|
|||||||
std::uint64_t inflight_{0};
|
std::uint64_t inflight_{0};
|
||||||
std::atomic<bool> running_{false};
|
std::atomic<bool> running_{false};
|
||||||
std::thread worker_;
|
std::thread worker_;
|
||||||
|
|
||||||
|
// Error tracking (protected by mtx_)
|
||||||
|
std::deque<SwapError> errors_; // bounded to max 100 entries
|
||||||
|
std::size_t total_error_count_{0};
|
||||||
};
|
};
|
||||||
} // namespace kte
|
} // namespace kte
|
||||||
221
main.cc
221
main.cc
@@ -181,124 +181,139 @@ main(int argc, char *argv[])
|
|||||||
return RunStressHighlighter(stress_seconds);
|
return RunStressHighlighter(stress_seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine frontend
|
// Top-level exception handler to prevent data loss and ensure cleanup
|
||||||
|
try {
|
||||||
|
// Determine frontend
|
||||||
#if !defined(KTE_BUILD_GUI)
|
#if !defined(KTE_BUILD_GUI)
|
||||||
if (req_gui) {
|
if (req_gui) {
|
||||||
std::cerr << "kte: GUI not built. Reconfigure with -DBUILD_GUI=ON and required deps installed." <<
|
std::cerr << "kte: GUI not built. Reconfigure with -DBUILD_GUI=ON and required deps installed."
|
||||||
std::endl;
|
<<
|
||||||
return 2;
|
std::endl;
|
||||||
}
|
return 2;
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
bool use_gui = false;
|
bool use_gui = false;
|
||||||
if (req_gui) {
|
if (req_gui) {
|
||||||
use_gui = true;
|
use_gui = true;
|
||||||
} else if (req_term) {
|
} else if (req_term) {
|
||||||
use_gui = false;
|
use_gui = false;
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Default depends on build target: kge defaults to GUI, kte to terminal
|
// Default depends on build target: kge defaults to GUI, kte to terminal
|
||||||
#if defined(KTE_DEFAULT_GUI)
|
#if defined(KTE_DEFAULT_GUI)
|
||||||
use_gui = true;
|
use_gui = true;
|
||||||
#else
|
#else
|
||||||
use_gui = false;
|
use_gui = false;
|
||||||
#endif
|
#endif
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Open files passed on the CLI; support +N to jump to line N in the next file.
|
|
||||||
// If no files are provided, create an empty buffer.
|
|
||||||
if (optind < argc) {
|
|
||||||
// Seed a scratch buffer so the UI has something to show while deferred opens
|
|
||||||
// (and potential swap recovery prompts) are processed.
|
|
||||||
editor.AddBuffer(Buffer());
|
|
||||||
std::size_t pending_line = 0; // 0 = no pending line
|
|
||||||
for (int i = optind; i < argc; ++i) {
|
|
||||||
const char *arg = argv[i];
|
|
||||||
if (arg && arg[0] == '+') {
|
|
||||||
// Parse +<digits>
|
|
||||||
const char *p = arg + 1;
|
|
||||||
if (*p != '\0') {
|
|
||||||
bool all_digits = true;
|
|
||||||
for (const char *q = p; *q; ++q) {
|
|
||||||
if (!std::isdigit(static_cast<unsigned char>(*q))) {
|
|
||||||
all_digits = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (all_digits) {
|
|
||||||
// Clamp to >=1 later; 0 disables.
|
|
||||||
try {
|
|
||||||
unsigned long v = std::stoul(p);
|
|
||||||
if (v > std::numeric_limits<std::size_t>::max()) {
|
|
||||||
std::cerr <<
|
|
||||||
"kte: Warning: Line number too large, ignoring\n";
|
|
||||||
pending_line = 0;
|
|
||||||
} else {
|
|
||||||
pending_line = static_cast<std::size_t>(v);
|
|
||||||
}
|
|
||||||
} catch (...) {
|
|
||||||
// Ignore malformed huge numbers
|
|
||||||
pending_line = 0;
|
|
||||||
}
|
|
||||||
continue; // look for the next file arg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fall through: not a +number, treat as filename starting with '+'
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string path = arg;
|
|
||||||
editor.RequestOpenFile(path, pending_line);
|
|
||||||
pending_line = 0; // consumed (if set)
|
|
||||||
}
|
}
|
||||||
// If we ended with a pending +N but no subsequent file, ignore it.
|
|
||||||
} else {
|
|
||||||
// Create a single empty buffer
|
|
||||||
editor.AddBuffer(Buffer());
|
|
||||||
editor.SetStatus("new: empty buffer");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install built-in commands
|
|
||||||
InstallDefaultCommands();
|
|
||||||
|
|
||||||
// Select frontend
|
|
||||||
std::unique_ptr<Frontend> fe;
|
|
||||||
#if defined(KTE_BUILD_GUI)
|
|
||||||
if (use_gui) {
|
|
||||||
fe = std::make_unique<GUIFrontend>();
|
|
||||||
} else
|
|
||||||
#endif
|
#endif
|
||||||
{
|
|
||||||
fe = std::make_unique<TerminalFrontend>();
|
// Open files passed on the CLI; support +N to jump to line N in the next file.
|
||||||
}
|
// If no files are provided, create an empty buffer.
|
||||||
|
if (optind < argc) {
|
||||||
|
// Seed a scratch buffer so the UI has something to show while deferred opens
|
||||||
|
// (and potential swap recovery prompts) are processed.
|
||||||
|
editor.AddBuffer(Buffer());
|
||||||
|
std::size_t pending_line = 0; // 0 = no pending line
|
||||||
|
for (int i = optind; i < argc; ++i) {
|
||||||
|
const char *arg = argv[i];
|
||||||
|
if (arg && arg[0] == '+') {
|
||||||
|
// Parse +<digits>
|
||||||
|
const char *p = arg + 1;
|
||||||
|
if (*p != '\0') {
|
||||||
|
bool all_digits = true;
|
||||||
|
for (const char *q = p; *q; ++q) {
|
||||||
|
if (!std::isdigit(static_cast<unsigned char>(*q))) {
|
||||||
|
all_digits = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (all_digits) {
|
||||||
|
// Clamp to >=1 later; 0 disables.
|
||||||
|
try {
|
||||||
|
unsigned long v = std::stoul(p);
|
||||||
|
if (v > std::numeric_limits<std::size_t>::max()) {
|
||||||
|
std::cerr <<
|
||||||
|
"kte: Warning: Line number too large, ignoring\n";
|
||||||
|
pending_line = 0;
|
||||||
|
} else {
|
||||||
|
pending_line = static_cast<std::size_t>(v);
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
// Ignore malformed huge numbers
|
||||||
|
pending_line = 0;
|
||||||
|
}
|
||||||
|
continue; // look for the next file arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fall through: not a +number, treat as filename starting with '+'
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string path = arg;
|
||||||
|
editor.RequestOpenFile(path, pending_line);
|
||||||
|
pending_line = 0; // consumed (if set)
|
||||||
|
}
|
||||||
|
// If we ended with a pending +N but no subsequent file, ignore it.
|
||||||
|
} else {
|
||||||
|
// Create a single empty buffer
|
||||||
|
editor.AddBuffer(Buffer());
|
||||||
|
editor.SetStatus("new: empty buffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install built-in commands
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
// Select frontend
|
||||||
|
std::unique_ptr<Frontend> fe;
|
||||||
|
#if defined(KTE_BUILD_GUI)
|
||||||
|
if (use_gui) {
|
||||||
|
fe = std::make_unique<GUIFrontend>();
|
||||||
|
} else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
fe = std::make_unique<TerminalFrontend>();
|
||||||
|
}
|
||||||
|
|
||||||
#if defined(KTE_BUILD_GUI) && defined(__APPLE__)
|
#if defined(KTE_BUILD_GUI) && defined(__APPLE__)
|
||||||
if (use_gui) {
|
if (use_gui) {
|
||||||
/* likely using the .app, so need to cd */
|
/* likely using the .app, so need to cd */
|
||||||
const char *home = getenv("HOME");
|
const char *home = getenv("HOME");
|
||||||
if (!home) {
|
if (!home) {
|
||||||
std::cerr << "kge.app: HOME environment variable not set" << std::endl;
|
std::cerr << "kge.app: HOME environment variable not set" << std::endl;
|
||||||
return 1;
|
return 1;
|
||||||
|
}
|
||||||
|
if (chdir(home) != 0) {
|
||||||
|
std::cerr << "kge.app: failed to chdir to " << home << ": "
|
||||||
|
<< std::strerror(errno) << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (chdir(home) != 0) {
|
|
||||||
std::cerr << "kge.app: failed to chdir to " << home << ": "
|
|
||||||
<< std::strerror(errno) << std::endl;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (!fe->Init(argc, argv, editor)) {
|
if (!fe->Init(argc, argv, editor)) {
|
||||||
std::cerr << "kte: failed to initialize frontend" << std::endl;
|
std::cerr << "kte: failed to initialize frontend" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Execute(editor, CommandId::CenterOnCursor);
|
||||||
|
|
||||||
|
bool running = true;
|
||||||
|
while (running) {
|
||||||
|
fe->Step(editor, running);
|
||||||
|
}
|
||||||
|
|
||||||
|
fe->Shutdown();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
std::cerr << "\n*** FATAL ERROR ***\n"
|
||||||
|
<< "kte encountered an unhandled exception: " << e.what() << "\n"
|
||||||
|
<< "The editor will now exit. Any unsaved changes may be recovered from swap files.\n";
|
||||||
|
return 1;
|
||||||
|
} catch (...) {
|
||||||
|
std::cerr << "\n*** FATAL ERROR ***\n"
|
||||||
|
<< "kte encountered an unknown exception.\n"
|
||||||
|
<< "The editor will now exit. Any unsaved changes may be recovered from swap files.\n";
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Execute(editor, CommandId::CenterOnCursor);
|
|
||||||
|
|
||||||
bool running = true;
|
|
||||||
while (running) {
|
|
||||||
fe->Step(editor, running);
|
|
||||||
}
|
|
||||||
|
|
||||||
fe->Shutdown();
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
813
tests/test_swap_edge_cases.cc
Normal file
813
tests/test_swap_edge_cases.cc
Normal file
@@ -0,0 +1,813 @@
|
|||||||
|
#include "Test.h"
|
||||||
|
|
||||||
|
#include "Buffer.h"
|
||||||
|
#include "Swap.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
|
||||||
|
// CRC32 helper (same algorithm as SwapManager::crc32)
|
||||||
|
static std::uint32_t
|
||||||
|
crc32(const std::uint8_t *data, std::size_t len, std::uint32_t seed = 0)
|
||||||
|
{
|
||||||
|
static std::uint32_t table[256];
|
||||||
|
static bool inited = false;
|
||||||
|
if (!inited) {
|
||||||
|
for (std::uint32_t i = 0; i < 256; ++i) {
|
||||||
|
std::uint32_t c = i;
|
||||||
|
for (int j = 0; j < 8; ++j)
|
||||||
|
c = (c & 1) ? (0xEDB88320u ^ (c >> 1)) : (c >> 1);
|
||||||
|
table[i] = c;
|
||||||
|
}
|
||||||
|
inited = true;
|
||||||
|
}
|
||||||
|
std::uint32_t c = ~seed;
|
||||||
|
for (std::size_t i = 0; i < len; ++i)
|
||||||
|
c = table[(c ^ data[i]) & 0xFFu] ^ (c >> 8);
|
||||||
|
return ~c;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Build a valid 64-byte swap file header
|
||||||
|
static std::string
|
||||||
|
build_swap_header()
|
||||||
|
{
|
||||||
|
std::uint8_t hdr[64];
|
||||||
|
std::memset(hdr, 0, sizeof(hdr));
|
||||||
|
// Magic
|
||||||
|
const std::uint8_t magic[8] = {'K', 'T', 'E', '_', 'S', 'W', 'P', '\0'};
|
||||||
|
std::memcpy(hdr, magic, 8);
|
||||||
|
// Version = 1 (little-endian)
|
||||||
|
hdr[8] = 1;
|
||||||
|
hdr[9] = 0;
|
||||||
|
hdr[10] = 0;
|
||||||
|
hdr[11] = 0;
|
||||||
|
// Flags = 0
|
||||||
|
// Created time (just use 0 for tests)
|
||||||
|
return std::string(reinterpret_cast<char *>(hdr), sizeof(hdr));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Build a swap record: [type u8][len u24][payload][crc32 u32]
|
||||||
|
static std::string
|
||||||
|
build_swap_record(std::uint8_t type, const std::vector<std::uint8_t> &payload)
|
||||||
|
{
|
||||||
|
std::vector<std::uint8_t> record;
|
||||||
|
|
||||||
|
// Record header: type(1) + length(3)
|
||||||
|
record.push_back(type);
|
||||||
|
std::uint32_t len = static_cast<std::uint32_t>(payload.size());
|
||||||
|
record.push_back(static_cast<std::uint8_t>(len & 0xFFu));
|
||||||
|
record.push_back(static_cast<std::uint8_t>((len >> 8) & 0xFFu));
|
||||||
|
record.push_back(static_cast<std::uint8_t>((len >> 16) & 0xFFu));
|
||||||
|
|
||||||
|
// Payload
|
||||||
|
record.insert(record.end(), payload.begin(), payload.end());
|
||||||
|
|
||||||
|
// CRC32 (compute over header + payload)
|
||||||
|
std::uint32_t crc = crc32(record.data(), record.size());
|
||||||
|
record.push_back(static_cast<std::uint8_t>(crc & 0xFFu));
|
||||||
|
record.push_back(static_cast<std::uint8_t>((crc >> 8) & 0xFFu));
|
||||||
|
record.push_back(static_cast<std::uint8_t>((crc >> 16) & 0xFFu));
|
||||||
|
record.push_back(static_cast<std::uint8_t>((crc >> 24) & 0xFFu));
|
||||||
|
|
||||||
|
return std::string(reinterpret_cast<char *>(record.data()), record.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Build complete swap file with header and records
|
||||||
|
static std::string
|
||||||
|
build_swap_file(const std::vector<std::string> &records)
|
||||||
|
{
|
||||||
|
std::string file = build_swap_header();
|
||||||
|
for (const auto &rec: records) {
|
||||||
|
file += rec;
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Write bytes to file
|
||||||
|
static void
|
||||||
|
write_file_bytes(const std::string &path, const std::string &bytes)
|
||||||
|
{
|
||||||
|
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||||
|
out.write(bytes.data(), static_cast<std::streamsize>(bytes.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Helper to encode u32 little-endian
|
||||||
|
static void
|
||||||
|
put_u32_le(std::vector<std::uint8_t> &out, std::uint32_t v)
|
||||||
|
{
|
||||||
|
out.push_back(static_cast<std::uint8_t>(v & 0xFFu));
|
||||||
|
out.push_back(static_cast<std::uint8_t>((v >> 8) & 0xFFu));
|
||||||
|
out.push_back(static_cast<std::uint8_t>((v >> 16) & 0xFFu));
|
||||||
|
out.push_back(static_cast<std::uint8_t>((v >> 24) & 0xFFu));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// 1. MINIMUM VALID PAYLOAD SIZE TESTS
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
TEST (SwapEdge_INS_MinimumValidPayload)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_ins_min.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_ins_min.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// INS record: encver(1) + row(4) + col(4) + nbytes(4) = 13 bytes minimum
|
||||||
|
// nbytes=0 means zero-length insertion
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
put_u32_le(payload, 0); // col
|
||||||
|
put_u32_le(payload, 0); // nbytes=0
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err));
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_DEL_MinimumValidPayload)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_del_min.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_del_min.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// DEL record: encver(1) + row(4) + col(4) + dlen(4) = 13 bytes minimum
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
put_u32_le(payload, 0); // col
|
||||||
|
put_u32_le(payload, 0); // dlen=0
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::DEL), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err));
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_SPLIT_MinimumValidPayload)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_split_min.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_split_min.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// SPLIT record: encver(1) + row(4) + col(4) = 9 bytes minimum
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
put_u32_le(payload, 0); // col
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::SPLIT), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err));
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_JOIN_MinimumValidPayload)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_join_min.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_join_min.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\nworld\n");
|
||||||
|
|
||||||
|
// JOIN record: encver(1) + row(4) = 5 bytes minimum
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::JOIN), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err));
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_CHKPT_MinimumValidPayload)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_chkpt_min.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_chkpt_min.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// CHKPT record: encver(1) + nbytes(4) = 5 bytes minimum
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // nbytes=0
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::CHKPT), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err));
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// 2. TRUNCATED PAYLOAD TESTS (BELOW MINIMUM)
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
TEST (SwapEdge_INS_TruncatedPayload_1Byte)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_ins_trunc1.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_ins_trunc1.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// INS record with only 1 byte (just encver)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver only
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("INS payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_INS_TruncatedPayload_5Bytes)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_ins_trunc5.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_ins_trunc5.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// INS record with 5 bytes (encver + row only)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("INS payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_DEL_TruncatedPayload_9Bytes)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_del_trunc9.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_del_trunc9.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// DEL record with 9 bytes (encver + row + col, missing dlen)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
put_u32_le(payload, 0); // col
|
||||||
|
// missing dlen
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::DEL), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("DEL payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_SPLIT_TruncatedPayload_5Bytes)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_split_trunc5.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_split_trunc5.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// SPLIT record with 5 bytes (encver + row, missing col)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
// missing col
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::SPLIT), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("SPLIT payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_JOIN_TruncatedPayload_1Byte)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_join_trunc1.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_join_trunc1.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\nworld\n");
|
||||||
|
|
||||||
|
// JOIN record with 1 byte (just encver)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver only
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::JOIN), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("JOIN payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_CHKPT_TruncatedPayload_3Bytes)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_chkpt_trunc3.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_chkpt_trunc3.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// CHKPT record with 3 bytes (encver + partial nbytes)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
payload.push_back(0); // partial nbytes (only 2 bytes instead of 4)
|
||||||
|
payload.push_back(0);
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::CHKPT), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("CHKPT payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// 3. DATA OVERFLOW TESTS
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
TEST (SwapEdge_INS_TruncatedData_NbytesExceedsPayload)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_ins_overflow.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_ins_overflow.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// INS record where nbytes=100 but payload only contains 13 bytes total
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
put_u32_le(payload, 0); // col
|
||||||
|
put_u32_le(payload, 100); // nbytes=100 (but no data follows)
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("Truncated INS payload bytes") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_CHKPT_TruncatedData_NbytesExceedsPayload)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_chkpt_overflow.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_chkpt_overflow.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// CHKPT record where nbytes=1000 but payload only contains 5 bytes total
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 1000); // nbytes=1000 (but no data follows)
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::CHKPT), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("Truncated CHKPT payload bytes") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// 4. UNSUPPORTED ENCODING VERSION TESTS
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
TEST (SwapEdge_INS_UnsupportedEncodingVersion)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_ins_badenc.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_ins_badenc.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// INS record with encver=2 (unsupported)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(2); // encver=2 (unsupported)
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
put_u32_le(payload, 0); // col
|
||||||
|
put_u32_le(payload, 0); // nbytes
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("Unsupported swap payload encoding") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_CHKPT_UnsupportedEncodingVersion)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_chkpt_badenc.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_chkpt_badenc.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// CHKPT record with encver=99 (unsupported)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(99); // encver=99 (unsupported)
|
||||||
|
put_u32_le(payload, 0); // nbytes
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::CHKPT), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("Unsupported swap checkpoint encoding") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// 5. BOUNDARY CONDITION TESTS
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
TEST (SwapEdge_INS_ExactlyEnoughBytes)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_ins_exact.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_ins_exact.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// INS record with nbytes=10 and exactly 23 bytes total (13 header + 10 data)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
put_u32_le(payload, 0); // col
|
||||||
|
put_u32_le(payload, 10); // nbytes=10
|
||||||
|
// Add exactly 10 bytes of data
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
payload.push_back('X');
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err));
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_INS_OneByteTooFew)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_ins_toofew.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_ins_toofew.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// INS record with nbytes=10 but only 22 bytes total (13 header + 9 data)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
put_u32_le(payload, 0); // col
|
||||||
|
put_u32_le(payload, 10); // nbytes=10
|
||||||
|
// Add only 9 bytes of data (one too few)
|
||||||
|
for (int i = 0; i < 9; i++) {
|
||||||
|
payload.push_back('X');
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("Truncated INS payload bytes") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// 6. MIXED VALID AND INVALID RECORDS
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
TEST (SwapEdge_MixedRecords_ValidThenInvalid)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_mixed1.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_mixed1.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// First record: valid INS
|
||||||
|
std::vector<std::uint8_t> payload1;
|
||||||
|
payload1.push_back(1); // encver
|
||||||
|
put_u32_le(payload1, 0); // row
|
||||||
|
put_u32_le(payload1, 0); // col
|
||||||
|
put_u32_le(payload1, 1); // nbytes=1
|
||||||
|
payload1.push_back('X'); // data
|
||||||
|
|
||||||
|
std::string rec1 = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload1);
|
||||||
|
|
||||||
|
// Second record: truncated DEL
|
||||||
|
std::vector<std::uint8_t> payload2;
|
||||||
|
payload2.push_back(1); // encver only
|
||||||
|
|
||||||
|
std::string rec2 = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::DEL), payload2);
|
||||||
|
|
||||||
|
std::string file = build_swap_file({rec1, rec2});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("DEL payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
// Verify first INS was applied before failure
|
||||||
|
auto view = b.GetLineView(0);
|
||||||
|
std::string line(view.data(), view.size());
|
||||||
|
ASSERT_TRUE(line.find('X') != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_MixedRecords_MultipleValidOneInvalid)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_mixed2.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_mixed2.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "ab\n");
|
||||||
|
|
||||||
|
// First record: valid INS at (0,0)
|
||||||
|
std::vector<std::uint8_t> payload1;
|
||||||
|
payload1.push_back(1);
|
||||||
|
put_u32_le(payload1, 0);
|
||||||
|
put_u32_le(payload1, 0);
|
||||||
|
put_u32_le(payload1, 1);
|
||||||
|
payload1.push_back('X');
|
||||||
|
std::string rec1 = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload1);
|
||||||
|
|
||||||
|
// Second record: valid INS at (0,1)
|
||||||
|
std::vector<std::uint8_t> payload2;
|
||||||
|
payload2.push_back(1);
|
||||||
|
put_u32_le(payload2, 0);
|
||||||
|
put_u32_le(payload2, 1);
|
||||||
|
put_u32_le(payload2, 1);
|
||||||
|
payload2.push_back('Y');
|
||||||
|
std::string rec2 = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload2);
|
||||||
|
|
||||||
|
// Third record: truncated SPLIT
|
||||||
|
std::vector<std::uint8_t> payload3;
|
||||||
|
payload3.push_back(1); // encver only
|
||||||
|
std::string rec3 = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::SPLIT), payload3);
|
||||||
|
|
||||||
|
std::string file = build_swap_file({rec1, rec2, rec3});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("SPLIT payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
// Verify first two INS were applied
|
||||||
|
auto view = b.GetLineView(0);
|
||||||
|
std::string line(view.data(), view.size());
|
||||||
|
ASSERT_TRUE(line.find('X') != std::string::npos);
|
||||||
|
ASSERT_TRUE(line.find('Y') != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// 7. EMPTY PAYLOAD TEST
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
TEST (SwapEdge_EmptyPayload_INS)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_empty.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_empty.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// INS record with zero-length payload
|
||||||
|
std::vector<std::uint8_t> payload; // empty
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("INS payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// 8. CRC MISMATCH TEST
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
TEST (SwapEdge_ValidStructure_BadCRC)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_badcrc.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_badcrc.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// Build a valid INS record
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1);
|
||||||
|
put_u32_le(payload, 0);
|
||||||
|
put_u32_le(payload, 0);
|
||||||
|
put_u32_le(payload, 1);
|
||||||
|
payload.push_back('X');
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
|
||||||
|
// Corrupt the CRC (last 4 bytes)
|
||||||
|
rec[rec.size() - 1] ^= 0xFF;
|
||||||
|
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("CRC mismatch") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user