/** * MigrationTest - Unit tests for Migration system */ using Implexus.Core; using Implexus.Engine; using Implexus.Storage; using Implexus.Migrations; /** * Abstract base class for async test operations (replaces async delegates). */ public abstract class AsyncTestOperation : Object { public abstract async void execute_async() throws Error; } /** * Helper for running async tests synchronously. */ void run_async_test(AsyncTestOperation op) { var loop = new MainLoop(); Error? error = null; op.execute_async.begin((obj, res) => { try { op.execute_async.end(res); } catch (Error e) { error = e; } loop.quit(); }); loop.run(); if (error != null) { warning("Async test error: %s", ((!)error).message); } } public static int main(string[] args) { int passed = 0; int failed = 0; // === MigrationStorage Tests === // Test 1: Record migration if (test_record_migration()) { passed++; stdout.puts("PASS: test_record_migration\n"); } else { failed++; stdout.puts("FAIL: test_record_migration\n"); } // Test 2: Get applied versions if (test_get_applied_versions()) { passed++; stdout.puts("PASS: test_get_applied_versions\n"); } else { failed++; stdout.puts("FAIL: test_get_applied_versions\n"); } // Test 3: Is applied if (test_is_applied()) { passed++; stdout.puts("PASS: test_is_applied\n"); } else { failed++; stdout.puts("FAIL: test_is_applied\n"); } // Test 4: Remove migration if (test_remove_migration()) { passed++; stdout.puts("PASS: test_remove_migration\n"); } else { failed++; stdout.puts("FAIL: test_remove_migration\n"); } // Test 5: Get migration record if (test_get_migration_record()) { passed++; stdout.puts("PASS: test_get_migration_record\n"); } else { failed++; stdout.puts("FAIL: test_get_migration_record\n"); } // === MigrationRunner Tests === // Test 6: Register migration if (test_register_migration()) { passed++; stdout.puts("PASS: test_register_migration\n"); } else { failed++; stdout.puts("FAIL: test_register_migration\n"); } // Test 7: Get pending versions if (test_get_pending_versions()) { passed++; stdout.puts("PASS: test_get_pending_versions\n"); } else { failed++; stdout.puts("FAIL: test_get_pending_versions\n"); } // Test 8: Run pending migrations run_async_test(new TestRunPendingOperation(ref passed, ref failed)); // Test 9: Run to version run_async_test(new TestRunToVersionOperation(ref passed, ref failed)); // Test 10: Rollback to version run_async_test(new TestRollbackToVersionOperation(ref passed, ref failed)); // Test 11: Migration order run_async_test(new TestMigrationOrderOperation(ref passed, ref failed)); // Test 12: Error already applied run_async_test(new TestErrorAlreadyAppliedOperation(ref passed, ref failed)); // Test 13: Error missing migration run_async_test(new TestErrorMissingMigrationOperation(ref passed, ref failed)); // Test 14: Version conflict if (test_version_conflict()) { passed++; stdout.puts("PASS: test_version_conflict\n"); } else { failed++; stdout.puts("FAIL: test_version_conflict\n"); } // === BootstrapMigration Tests === // Test 15: Bootstrap version if (test_bootstrap_version()) { passed++; stdout.puts("PASS: test_bootstrap_version\n"); } else { failed++; stdout.puts("FAIL: test_bootstrap_version\n"); } // Test 16: Bootstrap irreversible run_async_test(new TestBootstrapIrreversibleOperation(ref passed, ref failed)); // Test 17: Bootstrap execution run_async_test(new TestBootstrapExecutionOperation(ref passed, ref failed)); stdout.printf("\nResults: %d passed, %d failed\n", passed, failed); return failed > 0 ? 1 : 0; } // === Helper Classes === /** * Simple test migration that creates a container. */ public class TestMigration : Object, Migration { public string _version; public string _description; public string _container_name; public bool up_called { get; private set; default = false; } public bool down_called { get; private set; default = false; } public TestMigration(string version, string description, string container_name) { _version = version; _description = description; _container_name = container_name; } public string version { owned get { return _version; } } public string description { owned get { return _description; } } public async void up_async(Engine engine) throws MigrationError { up_called = true; try { var root = yield engine.get_root_async(); yield root.create_container_async(_container_name); } catch (EngineError e) { throw new MigrationError.EXECUTION_FAILED( "Failed to create container: %s".printf(e.message) ); } } public async void down_async(Engine engine) throws MigrationError { down_called = true; try { var root = yield engine.get_root_async(); var child = yield root.get_child_async(_container_name); if (child != null) { yield ((!) child).delete_async(); } } catch (EngineError e) { throw new MigrationError.EXECUTION_FAILED( "Failed to delete container: %s".printf(e.message) ); } } } /** * Migration that throws an error on up_async(). */ public class FailingMigration : Object, Migration { public string _version; public string _description; public FailingMigration(string version) { _version = version; _description = "Failing migration"; } public string version { owned get { return _version; } } public string description { owned get { return _description; } } public async void up_async(Engine engine) throws MigrationError { throw new MigrationError.EXECUTION_FAILED("Intentional failure"); } public async void down_async(Engine engine) throws MigrationError { // Does nothing } } // === Async Test Operation Classes === public class TestRunPendingOperation : AsyncTestOperation { private unowned int _passed; private unowned int _failed; public TestRunPendingOperation(ref int passed, ref int failed) { _passed = passed; _failed = failed; } public override async void execute_async() throws Error { bool result = yield test_run_pending(); if (result) { _passed++; stdout.puts("PASS: test_run_pending\n"); } else { _failed++; stdout.puts("FAIL: test_run_pending\n"); } } } public class TestRunToVersionOperation : AsyncTestOperation { private unowned int _passed; private unowned int _failed; public TestRunToVersionOperation(ref int passed, ref int failed) { _passed = passed; _failed = failed; } public override async void execute_async() throws Error { bool result = yield test_run_to_version(); if (result) { _passed++; stdout.puts("PASS: test_run_to_version\n"); } else { _failed++; stdout.puts("FAIL: test_run_to_version\n"); } } } public class TestRollbackToVersionOperation : AsyncTestOperation { private unowned int _passed; private unowned int _failed; public TestRollbackToVersionOperation(ref int passed, ref int failed) { _passed = passed; _failed = failed; } public override async void execute_async() throws Error { bool result = yield test_rollback_to_version(); if (result) { _passed++; stdout.puts("PASS: test_rollback_to_version\n"); } else { _failed++; stdout.puts("FAIL: test_rollback_to_version\n"); } } } public class TestMigrationOrderOperation : AsyncTestOperation { private unowned int _passed; private unowned int _failed; public TestMigrationOrderOperation(ref int passed, ref int failed) { _passed = passed; _failed = failed; } public override async void execute_async() throws Error { bool result = yield test_migration_order(); if (result) { _passed++; stdout.puts("PASS: test_migration_order\n"); } else { _failed++; stdout.puts("FAIL: test_migration_order\n"); } } } public class TestErrorAlreadyAppliedOperation : AsyncTestOperation { private unowned int _passed; private unowned int _failed; public TestErrorAlreadyAppliedOperation(ref int passed, ref int failed) { _passed = passed; _failed = failed; } public override async void execute_async() throws Error { bool result = yield test_error_already_applied(); if (result) { _passed++; stdout.puts("PASS: test_error_already_applied\n"); } else { _failed++; stdout.puts("FAIL: test_error_already_applied\n"); } } } public class TestErrorMissingMigrationOperation : AsyncTestOperation { private unowned int _passed; private unowned int _failed; public TestErrorMissingMigrationOperation(ref int passed, ref int failed) { _passed = passed; _failed = failed; } public override async void execute_async() throws Error { bool result = yield test_error_missing_migration(); if (result) { _passed++; stdout.puts("PASS: test_error_missing_migration\n"); } else { _failed++; stdout.puts("FAIL: test_error_missing_migration\n"); } } } public class TestBootstrapIrreversibleOperation : AsyncTestOperation { private unowned int _passed; private unowned int _failed; public TestBootstrapIrreversibleOperation(ref int passed, ref int failed) { _passed = passed; _failed = failed; } public override async void execute_async() throws Error { bool result = yield test_bootstrap_irreversible(); if (result) { _passed++; stdout.puts("PASS: test_bootstrap_irreversible\n"); } else { _failed++; stdout.puts("FAIL: test_bootstrap_irreversible\n"); } } } public class TestBootstrapExecutionOperation : AsyncTestOperation { private unowned int _passed; private unowned int _failed; public TestBootstrapExecutionOperation(ref int passed, ref int failed) { _passed = passed; _failed = failed; } public override async void execute_async() throws Error { bool result = yield test_bootstrap_execution(); if (result) { _passed++; stdout.puts("PASS: test_bootstrap_execution\n"); } else { _failed++; stdout.puts("FAIL: test_bootstrap_execution\n"); } } } // === Helper Functions === /** * Creates a temporary directory for testing. */ string create_temp_dir() { string temp_dir = DirUtils.mkdtemp("implexus_migration_test_XXXXXX"); return temp_dir; } /** * Cleans up a temporary directory. */ void cleanup_dir(string path) { try { Dir dir = Dir.open(path, 0); string? name; while ((name = dir.read_name()) != null) { FileUtils.unlink(Path.build_filename(path, name)); } } catch (FileError e) { // Ignore errors } DirUtils.remove(path); } // === MigrationStorage Tests === // Test 1: Record migration stores migration records correctly bool test_record_migration() { string temp_dir = create_temp_dir(); try { var engine = new EmbeddedEngine.with_path(temp_dir); var storage = new MigrationStorage(engine); // Record a migration storage.record_migration("2026031301", "Create users table"); // Verify it was recorded if (!storage.is_applied("2026031301")) { cleanup_dir(temp_dir); return false; } cleanup_dir(temp_dir); return true; } catch (Error e) { cleanup_dir(temp_dir); return false; } } // Test 2: Get applied versions returns all recorded versions bool test_get_applied_versions() { string temp_dir = create_temp_dir(); try { var engine = new EmbeddedEngine.with_path(temp_dir); var storage = new MigrationStorage(engine); // Record migrations in non-sorted order storage.record_migration("2026031303", "Third migration"); storage.record_migration("2026031301", "First migration"); storage.record_migration("2026031302", "Second migration"); // Get applied versions and convert to vector for checking var versions = storage.get_applied_versions(); var version_set = new Invercargill.DataStructures.HashSet(); foreach (var v in versions) { version_set.add(v); } // Should have 3 versions if (version_set.length != 3) { cleanup_dir(temp_dir); return false; } // Verify all three versions are present if (!version_set.has("2026031301")) { cleanup_dir(temp_dir); return false; } if (!version_set.has("2026031302")) { cleanup_dir(temp_dir); return false; } if (!version_set.has("2026031303")) { cleanup_dir(temp_dir); return false; } cleanup_dir(temp_dir); return true; } catch (MigrationError e) { cleanup_dir(temp_dir); return false; } } // Test 3: Is applied correctly identifies applied migrations bool test_is_applied() { string temp_dir = create_temp_dir(); try { var engine = new EmbeddedEngine.with_path(temp_dir); var storage = new MigrationStorage(engine); // Check before recording if (storage.is_applied("2026031301")) { cleanup_dir(temp_dir); return false; } // Record migration storage.record_migration("2026031301", "Test migration"); // Check after recording if (!storage.is_applied("2026031301")) { cleanup_dir(temp_dir); return false; } // Check non-existent migration if (storage.is_applied("9999999999")) { cleanup_dir(temp_dir); return false; } cleanup_dir(temp_dir); return true; } catch (Error e) { cleanup_dir(temp_dir); return false; } } // Test 4: Remove migration removes records correctly bool test_remove_migration() { string temp_dir = create_temp_dir(); try { var engine = new EmbeddedEngine.with_path(temp_dir); var storage = new MigrationStorage(engine); // Record migration storage.record_migration("2026031301", "Test migration"); // Verify it was recorded if (!storage.is_applied("2026031301")) { cleanup_dir(temp_dir); return false; } // Remove migration storage.remove_migration("2026031301"); // Verify it was removed if (storage.is_applied("2026031301")) { cleanup_dir(temp_dir); return false; } cleanup_dir(temp_dir); return true; } catch (Error e) { cleanup_dir(temp_dir); return false; } } // Test 5: Get migration record returns correct metadata bool test_get_migration_record() { string temp_dir = create_temp_dir(); try { var engine = new EmbeddedEngine.with_path(temp_dir); var storage = new MigrationStorage(engine); // Record migration storage.record_migration("2026031301", "Create users table"); // Get the record var record = storage.get_migration_record("2026031301"); if (record == null) { cleanup_dir(temp_dir); return false; } if (((!) record).version != "2026031301") { cleanup_dir(temp_dir); return false; } if (((!) record).description != "Create users table") { cleanup_dir(temp_dir); return false; } if (((!) record).applied_at == null) { cleanup_dir(temp_dir); return false; } // Check non-existent migration var missing = storage.get_migration_record("9999999999"); if (missing != null) { cleanup_dir(temp_dir); return false; } cleanup_dir(temp_dir); return true; } catch (Error e) { cleanup_dir(temp_dir); return false; } } // === MigrationRunner Tests === // Test 6: Register migration stores migrations bool test_register_migration() { string temp_dir = create_temp_dir(); try { var engine = new EmbeddedEngine.with_path(temp_dir); var runner = new MigrationRunner(engine); var migration = new TestMigration("2026031301", "Test migration", "test_container"); runner.register_migration(migration); // Verify pending count is 1 if (runner.get_pending_count() != 1) { cleanup_dir(temp_dir); return false; } cleanup_dir(temp_dir); return true; } catch (Error e) { cleanup_dir(temp_dir); return false; } } // Test 7: Get pending versions identifies unapplied migrations bool test_get_pending_versions() { string temp_dir = create_temp_dir(); try { var engine = new EmbeddedEngine.with_path(temp_dir); var runner = new MigrationRunner(engine); // Register migrations runner.register_migration(new TestMigration("2026031301", "First", "container1")); runner.register_migration(new TestMigration("2026031302", "Second", "container2")); runner.register_migration(new TestMigration("2026031303", "Third", "container3")); // Apply one migration manually var storage = new MigrationStorage(engine); storage.record_migration("2026031302", "Second"); // Get pending versions var pending = runner.get_pending_versions(); // Convert to vector var pending_list = new Invercargill.DataStructures.Vector(); foreach (var v in pending) { pending_list.add(v); } // Should have 2 pending (2026031301 and 2026031303) if (pending_list.length != 2) { cleanup_dir(temp_dir); return false; } // Should be sorted if (pending_list[0] != "2026031301") { cleanup_dir(temp_dir); return false; } if (pending_list[1] != "2026031303") { cleanup_dir(temp_dir); return false; } cleanup_dir(temp_dir); return true; } catch (Error e) { cleanup_dir(temp_dir); return false; } } // Test 8: Run pending executes migrations in order async bool test_run_pending() throws Error { string temp_dir = create_temp_dir(); var engine = new EmbeddedEngine.with_path(temp_dir); var runner = new MigrationRunner(engine); // Register migrations var m1 = new TestMigration("2026031301", "First", "container1"); var m2 = new TestMigration("2026031302", "Second", "container2"); var m3 = new TestMigration("2026031303", "Third", "container3"); runner.register_migration(m1); runner.register_migration(m2); runner.register_migration(m3); // Run pending int count = yield runner.run_pending_async(); // Should have run 3 migrations if (count != 3) { cleanup_dir(temp_dir); return false; } // Verify all were called if (!m1.up_called || !m2.up_called || !m3.up_called) { cleanup_dir(temp_dir); return false; } // Verify all are applied if (!runner.is_applied("2026031301") || !runner.is_applied("2026031302") || !runner.is_applied("2026031303")) { cleanup_dir(temp_dir); return false; } // Verify containers were created bool exists1 = yield engine.entity_exists_async(new EntityPath("/container1")); bool exists2 = yield engine.entity_exists_async(new EntityPath("/container2")); bool exists3 = yield engine.entity_exists_async(new EntityPath("/container3")); if (!exists1 || !exists2 || !exists3) { cleanup_dir(temp_dir); return false; } cleanup_dir(temp_dir); return true; } // Test 9: Run to version runs migrations up to specific version async bool test_run_to_version() throws Error { string temp_dir = create_temp_dir(); var engine = new EmbeddedEngine.with_path(temp_dir); var runner = new MigrationRunner(engine); // Register migrations var m1 = new TestMigration("2026031301", "First", "container1"); var m2 = new TestMigration("2026031302", "Second", "container2"); var m3 = new TestMigration("2026031303", "Third", "container3"); runner.register_migration(m1); runner.register_migration(m2); runner.register_migration(m3); // Run to version (inclusive) int count = yield runner.run_to_version_async("2026031302"); // Should have run 2 migrations if (count != 2) { cleanup_dir(temp_dir); return false; } // Verify first two were called if (!m1.up_called || !m2.up_called) { cleanup_dir(temp_dir); return false; } // Third should NOT be called if (m3.up_called) { cleanup_dir(temp_dir); return false; } // Verify only first two are applied if (!runner.is_applied("2026031301") || !runner.is_applied("2026031302")) { cleanup_dir(temp_dir); return false; } if (runner.is_applied("2026031303")) { cleanup_dir(temp_dir); return false; } cleanup_dir(temp_dir); return true; } // Test 10: Rollback to version rolls back migrations correctly async bool test_rollback_to_version() throws Error { string temp_dir = create_temp_dir(); var engine = new EmbeddedEngine.with_path(temp_dir); var runner = new MigrationRunner(engine); // Register migrations var m1 = new TestMigration("2026031301", "First", "container1"); var m2 = new TestMigration("2026031302", "Second", "container2"); var m3 = new TestMigration("2026031303", "Third", "container3"); runner.register_migration(m1); runner.register_migration(m2); runner.register_migration(m3); // Run all yield runner.run_pending_async(); // Rollback to version 2026031301 (keeps 2026031301, removes 2026031302 and 2026031303) int count = yield runner.rollback_to_version_async("2026031301"); // Should have rolled back 2 migrations if (count != 2) { cleanup_dir(temp_dir); return false; } // Verify down was called on m2 and m3 if (!m2.down_called || !m3.down_called) { cleanup_dir(temp_dir); return false; } // m1 down should NOT be called if (m1.down_called) { cleanup_dir(temp_dir); return false; } // Verify only first is still applied if (!runner.is_applied("2026031301")) { cleanup_dir(temp_dir); return false; } if (runner.is_applied("2026031302") || runner.is_applied("2026031303")) { cleanup_dir(temp_dir); return false; } // Verify containers 2 and 3 were deleted bool exists1 = yield engine.entity_exists_async(new EntityPath("/container1")); bool exists2 = yield engine.entity_exists_async(new EntityPath("/container2")); bool exists3 = yield engine.entity_exists_async(new EntityPath("/container3")); if (!exists1) { cleanup_dir(temp_dir); return false; } if (exists2 || exists3) { cleanup_dir(temp_dir); return false; } cleanup_dir(temp_dir); return true; } // Test 11: Migrations run in correct order by version string async bool test_migration_order() throws Error { string temp_dir = create_temp_dir(); var engine = new EmbeddedEngine.with_path(temp_dir); var runner = new MigrationRunner(engine); // Register migrations in non-sorted order var m3 = new TestMigration("2026031303", "Third", "container3"); var m1 = new TestMigration("2026031301", "First", "container1"); var m2 = new TestMigration("2026031302", "Second", "container2"); runner.register_migration(m3); runner.register_migration(m1); runner.register_migration(m2); // Run pending - should execute in version order int count = yield runner.run_pending_async(); if (count != 3) { cleanup_dir(temp_dir); return false; } // Verify all were called if (!m1.up_called || !m2.up_called || !m3.up_called) { cleanup_dir(temp_dir); return false; } // Verify all are applied in order var versions = runner.get_applied_versions(); var version_list = new Invercargill.DataStructures.Vector(); foreach (var v in versions) { version_list.add(v); } if (version_list.length != 3) { cleanup_dir(temp_dir); return false; } if (version_list[0] != "2026031301" || version_list[1] != "2026031302" || version_list[2] != "2026031303") { cleanup_dir(temp_dir); return false; } cleanup_dir(temp_dir); return true; } // Test 12: Error handling for already applied migrations async bool test_error_already_applied() throws Error { string temp_dir = create_temp_dir(); var engine = new EmbeddedEngine.with_path(temp_dir); var runner = new MigrationRunner(engine); // Register and apply migration var m1 = new TestMigration("2026031301", "First", "container1"); runner.register_migration(m1); yield runner.run_pending_async(); // Try to run the same migration again via run_one_async try { yield runner.run_one_async("2026031301"); // Should have thrown cleanup_dir(temp_dir); return false; } catch (MigrationError.ALREADY_APPLIED e) { // Expected } cleanup_dir(temp_dir); return true; } // Test 13: Error handling for missing migrations async bool test_error_missing_migration() throws Error { string temp_dir = create_temp_dir(); var engine = new EmbeddedEngine.with_path(temp_dir); var runner = new MigrationRunner(engine); // Verify the migration doesn't exist in pending list var pending = runner.get_pending_versions(); bool found = false; foreach (var v in pending) { if (v == "9999999999") { found = true; break; } } // The migration should NOT be in the pending list // because it was never registered if (found) { cleanup_dir(temp_dir); return false; } // Verify is_applied returns false for non-existent migration if (runner.is_applied("9999999999")) { cleanup_dir(temp_dir); return false; } cleanup_dir(temp_dir); return true; } // Test 14: Version conflict when registering duplicate bool test_version_conflict() { string temp_dir = create_temp_dir(); try { var engine = new EmbeddedEngine.with_path(temp_dir); var runner = new MigrationRunner(engine); // Register first migration var m1 = new TestMigration("2026031301", "First", "container1"); runner.register_migration(m1); // Try to register duplicate version var m2 = new TestMigration("2026031301", "Duplicate", "container2"); try { runner.register_migration(m2); // Should have thrown cleanup_dir(temp_dir); return false; } catch (MigrationError.VERSION_CONFLICT e) { // Expected } cleanup_dir(temp_dir); return true; } catch (Error e) { cleanup_dir(temp_dir); return false; } } // === BootstrapMigration Tests === // Test 15: Bootstrap version is "0000000000" bool test_bootstrap_version() { var bootstrap = new BootstrapMigration(); if (bootstrap.version != "0000000000") { return false; } return true; } // Test 16: Bootstrap down throws IRREVERSIBLE error async bool test_bootstrap_irreversible() throws Error { string temp_dir = create_temp_dir(); var engine = new EmbeddedEngine.with_path(temp_dir); var bootstrap = new BootstrapMigration(); try { yield bootstrap.down_async(engine); // Should have thrown cleanup_dir(temp_dir); return false; } catch (MigrationError.IRREVERSIBLE e) { // Expected } cleanup_dir(temp_dir); return true; } // Test 17: Bootstrap execution works correctly async bool test_bootstrap_execution() throws Error { string temp_dir = create_temp_dir(); var engine = new EmbeddedEngine.with_path(temp_dir); var runner = new MigrationRunner(engine); // Register bootstrap var bootstrap = new BootstrapMigration(); runner.register_migration(bootstrap); // Run pending int count = yield runner.run_pending_async(); // Should have run 1 migration if (count != 1) { cleanup_dir(temp_dir); return false; } // Verify bootstrap is applied if (!runner.is_applied("0000000000")) { cleanup_dir(temp_dir); return false; } // Verify root exists (bootstrap ensures this) var root = yield engine.get_root_async(); if (root == null) { cleanup_dir(temp_dir); return false; } cleanup_dir(temp_dir); return true; }