Crafter.DB/tests/ShouldVerify/main.cpp

111 lines
5.4 KiB
C++
Raw Permalink Normal View History

// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Verify() reports a heap handle pointing outside the committed heap and an
// ordered column out of sequence, and is silent on a healthy file; the
// type-erased DynamicRow reads any column by name without the struct.
import Crafter.DB;
import std;
using namespace Crafter;
namespace fs = std::filesystem;
namespace {
int failures = 0;
void Check(bool condition, std::string_view what) {
if (!condition) {
std::println("FAIL: {}", what);
++failures;
}
}
struct Reading {
std::string sensor;
std::optional<std::string> comment;
double value;
std::int64_t at;
static constexpr std::uint32_t version = 1;
static constexpr auto orderedBy = &Reading::at;
};
void Patch(const fs::path& path, std::uint64_t offset, std::span<const std::byte> bytes) {
std::fstream file(path, std::ios::in | std::ios::out | std::ios::binary);
file.seekp(static_cast<std::streamoff>(offset));
file.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
}
}
int main() {
const fs::path dir = fs::temp_directory_path() / "crafter-db-verify";
fs::remove_all(dir);
const fs::path path = dir / "r.cdb";
std::uint64_t rowsOffset = 0;
{
DB::Table<Reading> table(path);
table.Append({ "thermometer-kitchen", std::nullopt, 21.5, 100 });
table.Append({ "hygrometer", std::string("calibrated last week"), 40.0, 200 });
table.Append({ "barometer-outside-wall", std::nullopt, 1013.25, 300 });
table.Erase(1);
const DB::VerifyResult healthy = table.Verify();
Check(healthy.Ok(), healthy.Ok() ? "healthy table verifies" : std::format("healthy table verifies: {}", healthy.problems[0]));
const DB::DynamicRow dynamic = table.File().Dynamic(2);
const std::optional<std::size_t> sensor = dynamic.Find("sensor");
Check(sensor && dynamic.Text(*sensor) == "barometer-outside-wall", "DynamicRow text by name");
const std::optional<std::size_t> value = dynamic.Find("value");
Check(value && dynamic.Scalar(*value) && std::get<double>(*dynamic.Scalar(*value)) == 1013.25, "DynamicRow double by name");
const std::optional<std::size_t> comment = dynamic.Find("comment");
Check(comment && !dynamic.Present(*comment), "DynamicRow sees a disengaged optional");
Check(table.File().Dynamic(1).Deleted() && table.File().Dynamic(1).Present(*comment) && table.File().Dynamic(1).Text(*comment) == "calibrated last week", "DynamicRow on a tombstoned row with an engaged optional");
const std::string described = dynamic.Describe();
Check(described.find("sensor=\"barometer-outside-wall\"") != std::string::npos && described.find("comment=null") != std::string::npos && described.find("at=300") != std::string::npos, std::format("Describe: {}", described));
Check(!dynamic.Find("nope").has_value(), "unknown column name");
table.Flush();
const DB::Inspection info = DB::TableFile::Inspect(path);
rowsOffset = info.header.extents[0][0].fileOffset;
}
// Damage the heap offset of row 2's long sensor name (heap value).
{
const fs::path damaged = dir / "handle.cdb";
fs::copy_file(path, damaged);
const DB::Inspection info = DB::TableFile::Inspect(damaged);
std::uint16_t sensorOffset = 0;
for (std::size_t i = 0; i < info.schema.columns.size(); i++) {
if (info.schema.Name(i) == "sensor") sensorOffset = info.schema.columns[i].offset;
}
const std::uint64_t bogus = 1 << 30;
Patch(damaged, rowsOffset + 2 * info.header.rowSize + sensorOffset + 8, std::as_bytes(std::span(&bogus, 1)));
const DB::Table<Reading> table(damaged, { .readOnly = true });
const DB::VerifyResult result = table.Verify();
Check(!result.Ok() && result.problems[0].find("row 2") != std::string::npos && result.problems[0].find("sensor") != std::string::npos, result.Ok() ? "damaged handle reported" : std::format("damaged handle reported: {}", result.problems[0]));
bool threw = false;
try {
[[maybe_unused]] const std::string_view text = table[2][&Reading::sensor];
} catch (const DB::CorruptError&) {
threw = true;
}
Check(threw, "reading through the damaged handle throws CorruptError instead of faulting");
Check(table[0][&Reading::sensor] == "thermometer-kitchen", "other rows still read");
}
// Break the order of the ordered column.
{
const fs::path damaged = dir / "order.cdb";
fs::copy_file(path, damaged);
const DB::Inspection info = DB::TableFile::Inspect(damaged);
const std::int64_t early = 50;
Patch(damaged, rowsOffset + 2 * info.header.rowSize + info.schema.columns[info.header.orderedColumn].offset, std::as_bytes(std::span(&early, 1)));
const DB::Table<Reading> table(damaged, { .readOnly = true });
const DB::VerifyResult result = table.Verify();
Check(!result.Ok() && result.problems[0].find("out of order") != std::string::npos, result.Ok() ? "order violation reported" : std::format("order violation reported: {}", result.problems[0]));
}
fs::remove_all(dir);
if (failures == 0) std::println("ok");
return failures == 0 ? 0 : 1;
}