Initial commit: Crafter.DB
Header-only C++26 module library for typed, file-backed tables with schema reflection, migrations, queries, and crash-safe commits. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
bfd0cc261b
34 changed files with 7506 additions and 0 deletions
98
tests/ShouldLockAcrossProcesses/main.cpp
Normal file
98
tests/ShouldLockAcrossProcesses/main.cpp
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// One writer per file: a second writer in another process or in this one
|
||||
// gets LockedError, readers are refused while a writer holds the file,
|
||||
// readers coexist with each other, and a read-only table refuses writes.
|
||||
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
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 Item {
|
||||
std::string name;
|
||||
static constexpr std::uint32_t version = 1;
|
||||
};
|
||||
|
||||
// Returns the child's exit code: 0 = opened fine, 3 = LockedError, 4 = other.
|
||||
int ChildOpens(const fs::path& path, bool readOnly) {
|
||||
const pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
try {
|
||||
DB::Table<Item> table(path, { .mode = DB::OpenMode::OpenExisting, .readOnly = readOnly });
|
||||
_exit(0);
|
||||
} catch (const DB::LockedError&) {
|
||||
_exit(3);
|
||||
} catch (...) {
|
||||
_exit(4);
|
||||
}
|
||||
}
|
||||
int status = 0;
|
||||
waitpid(pid, &status, 0);
|
||||
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
const fs::path dir = fs::temp_directory_path() / "crafter-db-lock";
|
||||
fs::remove_all(dir);
|
||||
const fs::path path = dir / "items.cdb";
|
||||
|
||||
{
|
||||
DB::Table<Item> writer(path);
|
||||
writer.Append({ "one" });
|
||||
Check(ChildOpens(path, false) == 3, "second writer in another process gets LockedError");
|
||||
Check(ChildOpens(path, true) == 3, "reader in another process is refused while a writer holds the file");
|
||||
|
||||
bool threw = false;
|
||||
try {
|
||||
DB::Table<Item> second(path);
|
||||
} catch (const DB::LockedError&) {
|
||||
threw = true;
|
||||
}
|
||||
Check(threw, "second Table on the same file in this process gets LockedError");
|
||||
}
|
||||
|
||||
{
|
||||
DB::Table<Item> reader(path, { .readOnly = true });
|
||||
Check(ChildOpens(path, true) == 0, "readers coexist across processes");
|
||||
Check(ChildOpens(path, false) == 3, "writer is refused while a reader holds the file");
|
||||
|
||||
bool threw = false;
|
||||
try {
|
||||
reader.Append({ "two" });
|
||||
} catch (const DB::ReadOnlyError&) {
|
||||
threw = true;
|
||||
}
|
||||
Check(threw, "Append on a read-only table throws ReadOnlyError");
|
||||
threw = false;
|
||||
try {
|
||||
reader.Erase(0);
|
||||
} catch (const DB::ReadOnlyError&) {
|
||||
threw = true;
|
||||
}
|
||||
Check(threw, "Erase on a read-only table throws ReadOnlyError");
|
||||
Check(reader.Size() == 1 && reader[0][&Item::name] == "one", "read-only table reads");
|
||||
}
|
||||
|
||||
Check(ChildOpens(path, false) == 0, "writer opens once everyone is gone");
|
||||
|
||||
fs::remove_all(dir);
|
||||
if (failures == 0) std::println("ok");
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
Loading…
Reference in a new issue