|
@@ -0,0 +1,427 @@
|
|
|
|
|
+// v2.0 storage engine — unit tests for the v1.x → v2.0 migration tool.
|
|
|
|
|
+//
|
|
|
|
|
+// Each test materialises a fresh v1.x data dir (snapshots/ + wal/) by driving
|
|
|
|
|
+// the v1.x SnapshotManager + MemoryStore APIs directly, then runs
|
|
|
|
|
+// migrate_v1_to_v2() against a fresh LMDB env and asserts on the v2.0 state.
|
|
|
|
|
+//
|
|
|
|
|
+// Task 3.1 covers: empty-dir, single-collection, multi-collection, marker
|
|
|
|
|
+// presence, idempotence, special-character ids, progress callback, failure-
|
|
|
|
|
+// preserves-source.
|
|
|
|
|
+
|
|
|
|
|
+#include <algorithm>
|
|
|
|
|
+#include <atomic>
|
|
|
|
|
+#include <cassert>
|
|
|
|
|
+#include <cstdint>
|
|
|
|
|
+#include <cstdlib>
|
|
|
|
|
+#include <cstring>
|
|
|
|
|
+#include <filesystem>
|
|
|
|
|
+#include <fstream>
|
|
|
|
|
+#include <functional>
|
|
|
|
|
+#include <iostream>
|
|
|
|
|
+#include <stdexcept>
|
|
|
|
|
+#include <string>
|
|
|
|
|
+#include <unistd.h>
|
|
|
|
|
+#include <vector>
|
|
|
|
|
+
|
|
|
|
|
+#include <lmdb.h>
|
|
|
|
|
+#include <nlohmann/json.hpp>
|
|
|
|
|
+
|
|
|
|
|
+#include "document.hpp"
|
|
|
|
|
+#include "memory_store.hpp"
|
|
|
|
|
+#include "persistence/snapshot.hpp"
|
|
|
|
|
+#include "persistence/wal.hpp"
|
|
|
|
|
+#include "storage/document_store.hpp"
|
|
|
|
|
+#include "storage/document_store_lmdb.hpp"
|
|
|
|
|
+#include "storage/lmdb_env.hpp"
|
|
|
|
|
+#include "storage/lmdb_dbi.hpp"
|
|
|
|
|
+#include "storage/lmdb_txn.hpp"
|
|
|
|
|
+#include "storage/migrate_v1_to_v2.hpp"
|
|
|
|
|
+
|
|
|
|
|
+namespace fs = std::filesystem;
|
|
|
|
|
+
|
|
|
|
|
+using smartbotic::database::CollectionOptions;
|
|
|
|
|
+using smartbotic::database::Document;
|
|
|
|
|
+using smartbotic::database::MemoryStore;
|
|
|
|
|
+using smartbotic::database::SnapshotManager;
|
|
|
|
|
+using smartbotic::db::storage::DocumentStore;
|
|
|
|
|
+using smartbotic::db::storage::LmdbDocumentStore;
|
|
|
|
|
+using smartbotic::db::storage::LmdbEnv;
|
|
|
|
|
+using smartbotic::db::storage::LmdbEnvOpts;
|
|
|
|
|
+using smartbotic::db::storage::migrate_v1_to_v2;
|
|
|
|
|
+using smartbotic::db::storage::migration_complete;
|
|
|
|
|
+using smartbotic::db::storage::MigrationProgress;
|
|
|
|
|
+using smartbotic::db::storage::MigrationResult;
|
|
|
|
|
+
|
|
|
|
|
+namespace {
|
|
|
|
|
+
|
|
|
|
|
+// -------------------------------------------------------------------------
|
|
|
|
|
+// Test helpers
|
|
|
|
|
+// -------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+void check(bool cond, const char* msg) {
|
|
|
|
|
+ if (!cond) {
|
|
|
|
|
+ std::cerr << "FAIL: " << msg << "\n";
|
|
|
|
|
+ std::abort();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+std::string make_tmpdir(const char* tag) {
|
|
|
|
|
+ static std::atomic<int> counter{0};
|
|
|
|
|
+ int n = counter.fetch_add(1);
|
|
|
|
|
+ std::string path = "/tmp/migrate-test-" + std::to_string(::getpid()) +
|
|
|
|
|
+ "-" + std::to_string(n) + "-" + tag;
|
|
|
|
|
+ std::error_code ec;
|
|
|
|
|
+ fs::remove_all(path, ec);
|
|
|
|
|
+ fs::create_directories(path);
|
|
|
|
|
+ return path;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+MemoryStore::Config defaultStoreConfig() {
|
|
|
|
|
+ MemoryStore::Config cfg;
|
|
|
|
|
+ cfg.nodeId = "test";
|
|
|
|
|
+ cfg.maxMemoryBytes = 256ULL * 1024 * 1024;
|
|
|
|
|
+ return cfg;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+Document makeDoc(const std::string& id, const nlohmann::json& data) {
|
|
|
|
|
+ Document d;
|
|
|
|
|
+ d.id = id;
|
|
|
|
|
+ d.set_data(data);
|
|
|
|
|
+ d.version = 1;
|
|
|
|
|
+ d.createdAt = 1731628800123ULL;
|
|
|
|
|
+ d.updatedAt = 1731628800123ULL;
|
|
|
|
|
+ return d;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Build a v1.x data dir under <root>:
|
|
|
|
|
+// <root>/snapshots/ — created by writeSnapshot()
|
|
|
|
|
+// <root>/wal/ — empty
|
|
|
|
|
+struct V1Builder {
|
|
|
|
|
+ fs::path data_dir;
|
|
|
|
|
+ std::unique_ptr<MemoryStore> store;
|
|
|
|
|
+
|
|
|
|
|
+ explicit V1Builder(const std::string& path) : data_dir(path) {
|
|
|
|
|
+ fs::create_directories(data_dir / "snapshots");
|
|
|
|
|
+ fs::create_directories(data_dir / "wal");
|
|
|
|
|
+ store = std::make_unique<MemoryStore>(defaultStoreConfig());
|
|
|
|
|
+ store->start();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ~V1Builder() {
|
|
|
|
|
+ if (store) store->stop();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void createCollection(const std::string& name, const CollectionOptions& opts = {}) {
|
|
|
|
|
+ store->createCollection(name, opts);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void insert(const std::string& collection, const Document& doc) {
|
|
|
|
|
+ store->insert(collection, doc);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Materialise the snapshot file. Required before migrate_v1_to_v2()
|
|
|
|
|
+ // can pick anything up.
|
|
|
|
|
+ fs::path writeSnapshot(uint64_t walSequence = 1) {
|
|
|
|
|
+ SnapshotManager::Config sc;
|
|
|
|
|
+ sc.snapshotDir = data_dir / "snapshots";
|
|
|
|
|
+ sc.compressionEnabled = true;
|
|
|
|
|
+ sc.validateAfterWrite = true;
|
|
|
|
|
+ SnapshotManager mgr(sc);
|
|
|
|
|
+ return mgr.createSnapshot(*store, walSequence);
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+struct TmpEnv {
|
|
|
|
|
+ std::string path;
|
|
|
|
|
+ LmdbEnv env;
|
|
|
|
|
+ explicit TmpEnv(const char* tag)
|
|
|
|
|
+ : path(make_tmpdir(tag)),
|
|
|
|
|
+ env(LmdbEnvOpts{path + "/env", 64ULL << 20, 256, 126, false}) {}
|
|
|
|
|
+ ~TmpEnv() {
|
|
|
|
|
+ std::error_code ec;
|
|
|
|
|
+ fs::remove_all(path, ec);
|
|
|
|
|
+ }
|
|
|
|
|
+ TmpEnv(const TmpEnv&) = delete;
|
|
|
|
|
+ TmpEnv& operator=(const TmpEnv&) = delete;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// Quick checksum over a directory tree (size + content of every regular
|
|
|
|
|
+// file). Used to verify migration doesn't touch the v1.x source.
|
|
|
|
|
+struct DirChecksum {
|
|
|
|
|
+ std::vector<std::pair<std::string, std::string>> entries;
|
|
|
|
|
+
|
|
|
|
|
+ void capture(const fs::path& root) {
|
|
|
|
|
+ entries.clear();
|
|
|
|
|
+ for (auto it = fs::recursive_directory_iterator(root);
|
|
|
|
|
+ it != fs::recursive_directory_iterator(); ++it) {
|
|
|
|
|
+ if (!it->is_regular_file()) continue;
|
|
|
|
|
+ std::string rel = fs::relative(it->path(), root).string();
|
|
|
|
|
+ std::ifstream f(it->path(), std::ios::binary);
|
|
|
|
|
+ std::string content((std::istreambuf_iterator<char>(f)),
|
|
|
|
|
+ std::istreambuf_iterator<char>());
|
|
|
|
|
+ entries.emplace_back(rel, std::move(content));
|
|
|
|
|
+ }
|
|
|
|
|
+ std::sort(entries.begin(), entries.end());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ bool operator==(const DirChecksum& other) const {
|
|
|
|
|
+ return entries == other.entries;
|
|
|
|
|
+ }
|
|
|
|
|
+ bool operator!=(const DirChecksum& other) const { return !(*this == other); }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+} // anonymous namespace
|
|
|
|
|
+
|
|
|
|
|
+// -------------------------------------------------------------------------
|
|
|
|
|
+// Task 3.1: Migration core
|
|
|
|
|
+// -------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+void test_empty_v1_dir_yields_empty_v2_env() {
|
|
|
|
|
+ auto dir = make_tmpdir("empty");
|
|
|
|
|
+ // No snapshots/ subdir, just an empty dir.
|
|
|
|
|
+ TmpEnv env_dir("empty-env");
|
|
|
|
|
+ LmdbDocumentStore target(env_dir.env);
|
|
|
|
|
+
|
|
|
|
|
+ MigrationResult r = migrate_v1_to_v2(dir, target, env_dir.env);
|
|
|
|
|
+ check(r.success, "empty v1 dir should succeed");
|
|
|
|
|
+ check(r.docs_migrated == 0, "empty v1 dir migrates 0 docs");
|
|
|
|
|
+ check(r.collections_migrated == 0, "empty v1 dir migrates 0 collections");
|
|
|
|
|
+
|
|
|
|
|
+ // Marker IS written for empty case so subsequent boots don't re-check.
|
|
|
|
|
+ check(migration_complete(env_dir.env), "marker should be set after empty migration");
|
|
|
|
|
+
|
|
|
|
|
+ std::error_code ec;
|
|
|
|
|
+ fs::remove_all(dir, ec);
|
|
|
|
|
+ std::cout << "PASS: empty v1 dir yields empty v2 env\n";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_single_collection_with_docs_migrates() {
|
|
|
|
|
+ auto dir = make_tmpdir("single");
|
|
|
|
|
+ {
|
|
|
|
|
+ V1Builder b(dir);
|
|
|
|
|
+ b.createCollection("widgets");
|
|
|
|
|
+ for (int i = 0; i < 100; i++) {
|
|
|
|
|
+ b.insert("widgets", makeDoc("w" + std::to_string(i),
|
|
|
|
|
+ {{"index", i}, {"name", "widget-" + std::to_string(i)}}));
|
|
|
|
|
+ }
|
|
|
|
|
+ b.writeSnapshot();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ TmpEnv env_dir("single-env");
|
|
|
|
|
+ LmdbDocumentStore target(env_dir.env);
|
|
|
|
|
+ MigrationResult r = migrate_v1_to_v2(dir, target, env_dir.env);
|
|
|
|
|
+ check(r.success, "single collection migration succeeds");
|
|
|
|
|
+ check(r.docs_migrated == 100, "100 docs migrated");
|
|
|
|
|
+ check(r.collections_migrated == 1, "1 collection migrated");
|
|
|
|
|
+
|
|
|
|
|
+ check(target.count("widgets") == 100, "v2 widgets has 100 docs");
|
|
|
|
|
+ auto d = target.get("widgets", "w42");
|
|
|
|
|
+ check(d.has_value(), "w42 exists in v2");
|
|
|
|
|
+ check(d->data()["name"] == "widget-42", "data round-trips");
|
|
|
|
|
+ check(d->data()["index"] == 42, "data round-trips (int)");
|
|
|
|
|
+
|
|
|
|
|
+ std::error_code ec;
|
|
|
|
|
+ fs::remove_all(dir, ec);
|
|
|
|
|
+ std::cout << "PASS: single collection with docs migrates\n";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_multiple_collections_migrate() {
|
|
|
|
|
+ auto dir = make_tmpdir("multi");
|
|
|
|
|
+ {
|
|
|
|
|
+ V1Builder b(dir);
|
|
|
|
|
+ b.createCollection("colA");
|
|
|
|
|
+ b.createCollection("colB");
|
|
|
|
|
+ b.createCollection("colC");
|
|
|
|
|
+ for (int i = 0; i < 50; i++) {
|
|
|
|
|
+ b.insert("colA", makeDoc("a" + std::to_string(i), {{"k", "A"}, {"i", i}}));
|
|
|
|
|
+ b.insert("colB", makeDoc("b" + std::to_string(i), {{"k", "B"}, {"i", i}}));
|
|
|
|
|
+ b.insert("colC", makeDoc("c" + std::to_string(i), {{"k", "C"}, {"i", i}}));
|
|
|
|
|
+ }
|
|
|
|
|
+ b.writeSnapshot();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ TmpEnv env_dir("multi-env");
|
|
|
|
|
+ LmdbDocumentStore target(env_dir.env);
|
|
|
|
|
+ MigrationResult r = migrate_v1_to_v2(dir, target, env_dir.env);
|
|
|
|
|
+ check(r.success, "multi-collection migration succeeds");
|
|
|
|
|
+ check(r.docs_migrated == 150, "150 docs migrated total");
|
|
|
|
|
+ check(r.collections_migrated == 3, "3 collections migrated");
|
|
|
|
|
+
|
|
|
|
|
+ check(target.count("colA") == 50, "colA has 50");
|
|
|
|
|
+ check(target.count("colB") == 50, "colB has 50");
|
|
|
|
|
+ check(target.count("colC") == 50, "colC has 50");
|
|
|
|
|
+
|
|
|
|
|
+ // Isolation: a25 should NOT exist in colB.
|
|
|
|
|
+ check(!target.get("colB", "a25").has_value(), "isolation preserved");
|
|
|
|
|
+ check(target.get("colA", "a25").has_value(), "colA a25 present");
|
|
|
|
|
+ check(target.get("colC", "c25").has_value(), "colC c25 present");
|
|
|
|
|
+
|
|
|
|
|
+ std::error_code ec;
|
|
|
|
|
+ fs::remove_all(dir, ec);
|
|
|
|
|
+ std::cout << "PASS: multiple collections migrate\n";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_marker_is_set_after_successful_migration() {
|
|
|
|
|
+ auto dir = make_tmpdir("marker");
|
|
|
|
|
+ {
|
|
|
|
|
+ V1Builder b(dir);
|
|
|
|
|
+ b.createCollection("c");
|
|
|
|
|
+ b.insert("c", makeDoc("d1", {{"v", 1}}));
|
|
|
|
|
+ b.writeSnapshot();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ TmpEnv env_dir("marker-env");
|
|
|
|
|
+ LmdbDocumentStore target(env_dir.env);
|
|
|
|
|
+ check(!migration_complete(env_dir.env), "marker absent pre-migration");
|
|
|
|
|
+ auto r = migrate_v1_to_v2(dir, target, env_dir.env);
|
|
|
|
|
+ check(r.success, "migration succeeds");
|
|
|
|
|
+ check(migration_complete(env_dir.env), "marker present post-migration");
|
|
|
|
|
+
|
|
|
|
|
+ std::error_code ec;
|
|
|
|
|
+ fs::remove_all(dir, ec);
|
|
|
|
|
+ std::cout << "PASS: marker set after successful migration\n";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_idempotent_re_run() {
|
|
|
|
|
+ auto dir = make_tmpdir("idempotent");
|
|
|
|
|
+ {
|
|
|
|
|
+ V1Builder b(dir);
|
|
|
|
|
+ b.createCollection("c");
|
|
|
|
|
+ for (int i = 0; i < 10; i++) {
|
|
|
|
|
+ b.insert("c", makeDoc("d" + std::to_string(i), {{"v", i}}));
|
|
|
|
|
+ }
|
|
|
|
|
+ b.writeSnapshot();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ TmpEnv env_dir("idempotent-env");
|
|
|
|
|
+ LmdbDocumentStore target(env_dir.env);
|
|
|
|
|
+ auto r1 = migrate_v1_to_v2(dir, target, env_dir.env);
|
|
|
|
|
+ check(r1.success, "first run succeeds");
|
|
|
|
|
+ check(r1.docs_migrated == 10, "first run migrates 10 docs");
|
|
|
|
|
+
|
|
|
|
|
+ auto r2 = migrate_v1_to_v2(dir, target, env_dir.env);
|
|
|
|
|
+ check(r2.success, "second run succeeds (idempotent)");
|
|
|
|
|
+ check(r2.docs_migrated == 0, "second run migrates 0 docs (already done)");
|
|
|
|
|
+ check(r2.collections_migrated == 0, "second run migrates 0 collections");
|
|
|
|
|
+
|
|
|
|
|
+ std::error_code ec;
|
|
|
|
|
+ fs::remove_all(dir, ec);
|
|
|
|
|
+ std::cout << "PASS: idempotent re-run\n";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_collections_with_special_chars_in_id() {
|
|
|
|
|
+ auto dir = make_tmpdir("special");
|
|
|
|
|
+ {
|
|
|
|
|
+ V1Builder b(dir);
|
|
|
|
|
+ b.createCollection("c");
|
|
|
|
|
+ b.insert("c", makeDoc("id with spaces", {{"v", "a"}}));
|
|
|
|
|
+ b.insert("c", makeDoc("id/with/slashes", {{"v", "b"}}));
|
|
|
|
|
+ // Unicode (UTF-8 bytes are well-formed in LMDB keys).
|
|
|
|
|
+ b.insert("c", makeDoc(std::string("id_unicode_") + "\xc3\xa1\xc3\xa9\xc3\xad",
|
|
|
|
|
+ {{"v", "c"}}));
|
|
|
|
|
+ b.writeSnapshot();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ TmpEnv env_dir("special-env");
|
|
|
|
|
+ LmdbDocumentStore target(env_dir.env);
|
|
|
|
|
+ auto r = migrate_v1_to_v2(dir, target, env_dir.env);
|
|
|
|
|
+ check(r.success, "special-id migration succeeds");
|
|
|
|
|
+ check(r.docs_migrated == 3, "3 docs migrated");
|
|
|
|
|
+ check(target.get("c", "id with spaces").has_value(), "spaces id present");
|
|
|
|
|
+ check(target.get("c", "id/with/slashes").has_value(), "slashes id present");
|
|
|
|
|
+ check(target.get("c", std::string("id_unicode_") + "\xc3\xa1\xc3\xa9\xc3\xad")
|
|
|
|
|
+ .has_value(),
|
|
|
|
|
+ "unicode id present");
|
|
|
|
|
+
|
|
|
|
|
+ std::error_code ec;
|
|
|
|
|
+ fs::remove_all(dir, ec);
|
|
|
|
|
+ std::cout << "PASS: special-char ids round-trip\n";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_progress_callback_invoked() {
|
|
|
|
|
+ auto dir = make_tmpdir("progress");
|
|
|
|
|
+ {
|
|
|
|
|
+ V1Builder b(dir);
|
|
|
|
|
+ b.createCollection("c");
|
|
|
|
|
+ // > 1000 docs to guarantee at least one progress beat.
|
|
|
|
|
+ for (int i = 0; i < 1500; i++) {
|
|
|
|
|
+ b.insert("c", makeDoc("d" + std::to_string(i), {{"i", i}}));
|
|
|
|
|
+ }
|
|
|
|
|
+ b.writeSnapshot();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ TmpEnv env_dir("progress-env");
|
|
|
|
|
+ LmdbDocumentStore target(env_dir.env);
|
|
|
|
|
+ int beats = 0;
|
|
|
|
|
+ uint64_t last_total = 0;
|
|
|
|
|
+ auto cb = [&](const MigrationProgress& p) {
|
|
|
|
|
+ ++beats;
|
|
|
|
|
+ if (p.docs_total > 0) last_total = p.docs_total;
|
|
|
|
|
+ };
|
|
|
|
|
+ auto r = migrate_v1_to_v2(dir, target, env_dir.env, cb);
|
|
|
|
|
+ check(r.success, "migration with cb succeeds");
|
|
|
|
|
+ check(beats >= 1, "at least one progress beat fired");
|
|
|
|
|
+ check(last_total == 1500, "docs_total = 1500 at some beat");
|
|
|
|
|
+
|
|
|
|
|
+ std::error_code ec;
|
|
|
|
|
+ fs::remove_all(dir, ec);
|
|
|
|
|
+ std::cout << "PASS: progress callback invoked\n";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void test_failure_preserves_v1_dir() {
|
|
|
|
|
+ auto dir = make_tmpdir("preserve");
|
|
|
|
|
+ {
|
|
|
|
|
+ V1Builder b(dir);
|
|
|
|
|
+ b.createCollection("c");
|
|
|
|
|
+ b.insert("c", makeDoc("d1", {{"v", 1}}));
|
|
|
|
|
+ b.writeSnapshot();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Corrupt the latest snapshot file so the migration fails on load.
|
|
|
|
|
+ fs::path snap_dir = fs::path(dir) / "snapshots";
|
|
|
|
|
+ for (auto& e : fs::directory_iterator(snap_dir)) {
|
|
|
|
|
+ if (e.is_regular_file()) {
|
|
|
|
|
+ uintmax_t sz = fs::file_size(e.path());
|
|
|
|
|
+ fs::resize_file(e.path(), sz / 2);
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Capture the v1 dir state AFTER corruption — this is our baseline.
|
|
|
|
|
+ DirChecksum baseline;
|
|
|
|
|
+ baseline.capture(dir);
|
|
|
|
|
+
|
|
|
|
|
+ TmpEnv env_dir("preserve-env");
|
|
|
|
|
+ LmdbDocumentStore target(env_dir.env);
|
|
|
|
|
+ auto r = migrate_v1_to_v2(dir, target, env_dir.env);
|
|
|
|
|
+ check(!r.success, "corrupt-snapshot migration should fail");
|
|
|
|
|
+ check(!r.error.empty(), "error message is populated");
|
|
|
|
|
+
|
|
|
|
|
+ DirChecksum after;
|
|
|
|
|
+ after.capture(dir);
|
|
|
|
|
+ check(baseline == after, "v1 dir unchanged after failed migration");
|
|
|
|
|
+ check(!migration_complete(env_dir.env), "marker NOT set after failure");
|
|
|
|
|
+
|
|
|
|
|
+ std::error_code ec;
|
|
|
|
|
+ fs::remove_all(dir, ec);
|
|
|
|
|
+ std::cout << "PASS: failure preserves v1 dir\n";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// -------------------------------------------------------------------------
|
|
|
|
|
+// main()
|
|
|
|
|
+// -------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+int main() {
|
|
|
|
|
+ // Task 3.1: Migration core
|
|
|
|
|
+ test_empty_v1_dir_yields_empty_v2_env();
|
|
|
|
|
+ test_single_collection_with_docs_migrates();
|
|
|
|
|
+ test_multiple_collections_migrate();
|
|
|
|
|
+ test_marker_is_set_after_successful_migration();
|
|
|
|
|
+ test_idempotent_re_run();
|
|
|
|
|
+ test_collections_with_special_chars_in_id();
|
|
|
|
|
+ test_progress_callback_invoked();
|
|
|
|
|
+ test_failure_preserves_v1_dir();
|
|
|
|
|
+
|
|
|
|
|
+ std::cout << "\nAll migrate_v1_to_v2 tests passed.\n";
|
|
|
|
|
+ return 0;
|
|
|
|
|
+}
|