// 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 #include 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 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 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 second(path); } catch (const DB::LockedError&) { threw = true; } Check(threw, "second Table on the same file in this process gets LockedError"); } { DB::Table 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; }