MigrationTest.vala 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081
  1. /**
  2. * MigrationTest - Unit tests for Migration system
  3. */
  4. using Implexus.Core;
  5. using Implexus.Engine;
  6. using Implexus.Storage;
  7. using Implexus.Migrations;
  8. /**
  9. * Abstract base class for async test operations (replaces async delegates).
  10. */
  11. public abstract class AsyncTestOperation : Object {
  12. public abstract async void execute_async() throws Error;
  13. }
  14. /**
  15. * Helper for running async tests synchronously.
  16. */
  17. void run_async_test(AsyncTestOperation op) {
  18. var loop = new MainLoop();
  19. Error? error = null;
  20. op.execute_async.begin((obj, res) => {
  21. try {
  22. op.execute_async.end(res);
  23. } catch (Error e) {
  24. error = e;
  25. }
  26. loop.quit();
  27. });
  28. loop.run();
  29. if (error != null) {
  30. warning("Async test error: %s", ((!)error).message);
  31. }
  32. }
  33. public static int main(string[] args) {
  34. int passed = 0;
  35. int failed = 0;
  36. // === MigrationStorage Tests ===
  37. // Test 1: Record migration
  38. if (test_record_migration()) {
  39. passed++;
  40. stdout.puts("PASS: test_record_migration\n");
  41. } else {
  42. failed++;
  43. stdout.puts("FAIL: test_record_migration\n");
  44. }
  45. // Test 2: Get applied versions
  46. if (test_get_applied_versions()) {
  47. passed++;
  48. stdout.puts("PASS: test_get_applied_versions\n");
  49. } else {
  50. failed++;
  51. stdout.puts("FAIL: test_get_applied_versions\n");
  52. }
  53. // Test 3: Is applied
  54. if (test_is_applied()) {
  55. passed++;
  56. stdout.puts("PASS: test_is_applied\n");
  57. } else {
  58. failed++;
  59. stdout.puts("FAIL: test_is_applied\n");
  60. }
  61. // Test 4: Remove migration
  62. if (test_remove_migration()) {
  63. passed++;
  64. stdout.puts("PASS: test_remove_migration\n");
  65. } else {
  66. failed++;
  67. stdout.puts("FAIL: test_remove_migration\n");
  68. }
  69. // Test 5: Get migration record
  70. if (test_get_migration_record()) {
  71. passed++;
  72. stdout.puts("PASS: test_get_migration_record\n");
  73. } else {
  74. failed++;
  75. stdout.puts("FAIL: test_get_migration_record\n");
  76. }
  77. // === MigrationRunner Tests ===
  78. // Test 6: Register migration
  79. if (test_register_migration()) {
  80. passed++;
  81. stdout.puts("PASS: test_register_migration\n");
  82. } else {
  83. failed++;
  84. stdout.puts("FAIL: test_register_migration\n");
  85. }
  86. // Test 7: Get pending versions
  87. if (test_get_pending_versions()) {
  88. passed++;
  89. stdout.puts("PASS: test_get_pending_versions\n");
  90. } else {
  91. failed++;
  92. stdout.puts("FAIL: test_get_pending_versions\n");
  93. }
  94. // Test 8: Run pending migrations
  95. run_async_test(new TestRunPendingOperation(ref passed, ref failed));
  96. // Test 9: Run to version
  97. run_async_test(new TestRunToVersionOperation(ref passed, ref failed));
  98. // Test 10: Rollback to version
  99. run_async_test(new TestRollbackToVersionOperation(ref passed, ref failed));
  100. // Test 11: Migration order
  101. run_async_test(new TestMigrationOrderOperation(ref passed, ref failed));
  102. // Test 12: Error already applied
  103. run_async_test(new TestErrorAlreadyAppliedOperation(ref passed, ref failed));
  104. // Test 13: Error missing migration
  105. run_async_test(new TestErrorMissingMigrationOperation(ref passed, ref failed));
  106. // Test 14: Version conflict
  107. if (test_version_conflict()) {
  108. passed++;
  109. stdout.puts("PASS: test_version_conflict\n");
  110. } else {
  111. failed++;
  112. stdout.puts("FAIL: test_version_conflict\n");
  113. }
  114. // === BootstrapMigration Tests ===
  115. // Test 15: Bootstrap version
  116. if (test_bootstrap_version()) {
  117. passed++;
  118. stdout.puts("PASS: test_bootstrap_version\n");
  119. } else {
  120. failed++;
  121. stdout.puts("FAIL: test_bootstrap_version\n");
  122. }
  123. // Test 16: Bootstrap irreversible
  124. run_async_test(new TestBootstrapIrreversibleOperation(ref passed, ref failed));
  125. // Test 17: Bootstrap execution
  126. run_async_test(new TestBootstrapExecutionOperation(ref passed, ref failed));
  127. stdout.printf("\nResults: %d passed, %d failed\n", passed, failed);
  128. return failed > 0 ? 1 : 0;
  129. }
  130. // === Helper Classes ===
  131. /**
  132. * Simple test migration that creates a container.
  133. */
  134. public class TestMigration : Object, Migration {
  135. public string _version;
  136. public string _description;
  137. public string _container_name;
  138. public bool up_called { get; private set; default = false; }
  139. public bool down_called { get; private set; default = false; }
  140. public TestMigration(string version, string description, string container_name) {
  141. _version = version;
  142. _description = description;
  143. _container_name = container_name;
  144. }
  145. public string version { owned get { return _version; } }
  146. public string description { owned get { return _description; } }
  147. public async void up_async(Engine engine) throws MigrationError {
  148. up_called = true;
  149. try {
  150. var root = yield engine.get_root_async();
  151. yield root.create_container_async(_container_name);
  152. } catch (EngineError e) {
  153. throw new MigrationError.EXECUTION_FAILED(
  154. "Failed to create container: %s".printf(e.message)
  155. );
  156. }
  157. }
  158. public async void down_async(Engine engine) throws MigrationError {
  159. down_called = true;
  160. try {
  161. var root = yield engine.get_root_async();
  162. var child = yield root.get_child_async(_container_name);
  163. if (child != null) {
  164. yield ((!) child).delete_async();
  165. }
  166. } catch (EngineError e) {
  167. throw new MigrationError.EXECUTION_FAILED(
  168. "Failed to delete container: %s".printf(e.message)
  169. );
  170. }
  171. }
  172. }
  173. /**
  174. * Migration that throws an error on up_async().
  175. */
  176. public class FailingMigration : Object, Migration {
  177. public string _version;
  178. public string _description;
  179. public FailingMigration(string version) {
  180. _version = version;
  181. _description = "Failing migration";
  182. }
  183. public string version { owned get { return _version; } }
  184. public string description { owned get { return _description; } }
  185. public async void up_async(Engine engine) throws MigrationError {
  186. throw new MigrationError.EXECUTION_FAILED("Intentional failure");
  187. }
  188. public async void down_async(Engine engine) throws MigrationError {
  189. // Does nothing
  190. }
  191. }
  192. // === Async Test Operation Classes ===
  193. public class TestRunPendingOperation : AsyncTestOperation {
  194. private unowned int _passed;
  195. private unowned int _failed;
  196. public TestRunPendingOperation(ref int passed, ref int failed) {
  197. _passed = passed;
  198. _failed = failed;
  199. }
  200. public override async void execute_async() throws Error {
  201. bool result = yield test_run_pending();
  202. if (result) {
  203. _passed++;
  204. stdout.puts("PASS: test_run_pending\n");
  205. } else {
  206. _failed++;
  207. stdout.puts("FAIL: test_run_pending\n");
  208. }
  209. }
  210. }
  211. public class TestRunToVersionOperation : AsyncTestOperation {
  212. private unowned int _passed;
  213. private unowned int _failed;
  214. public TestRunToVersionOperation(ref int passed, ref int failed) {
  215. _passed = passed;
  216. _failed = failed;
  217. }
  218. public override async void execute_async() throws Error {
  219. bool result = yield test_run_to_version();
  220. if (result) {
  221. _passed++;
  222. stdout.puts("PASS: test_run_to_version\n");
  223. } else {
  224. _failed++;
  225. stdout.puts("FAIL: test_run_to_version\n");
  226. }
  227. }
  228. }
  229. public class TestRollbackToVersionOperation : AsyncTestOperation {
  230. private unowned int _passed;
  231. private unowned int _failed;
  232. public TestRollbackToVersionOperation(ref int passed, ref int failed) {
  233. _passed = passed;
  234. _failed = failed;
  235. }
  236. public override async void execute_async() throws Error {
  237. bool result = yield test_rollback_to_version();
  238. if (result) {
  239. _passed++;
  240. stdout.puts("PASS: test_rollback_to_version\n");
  241. } else {
  242. _failed++;
  243. stdout.puts("FAIL: test_rollback_to_version\n");
  244. }
  245. }
  246. }
  247. public class TestMigrationOrderOperation : AsyncTestOperation {
  248. private unowned int _passed;
  249. private unowned int _failed;
  250. public TestMigrationOrderOperation(ref int passed, ref int failed) {
  251. _passed = passed;
  252. _failed = failed;
  253. }
  254. public override async void execute_async() throws Error {
  255. bool result = yield test_migration_order();
  256. if (result) {
  257. _passed++;
  258. stdout.puts("PASS: test_migration_order\n");
  259. } else {
  260. _failed++;
  261. stdout.puts("FAIL: test_migration_order\n");
  262. }
  263. }
  264. }
  265. public class TestErrorAlreadyAppliedOperation : AsyncTestOperation {
  266. private unowned int _passed;
  267. private unowned int _failed;
  268. public TestErrorAlreadyAppliedOperation(ref int passed, ref int failed) {
  269. _passed = passed;
  270. _failed = failed;
  271. }
  272. public override async void execute_async() throws Error {
  273. bool result = yield test_error_already_applied();
  274. if (result) {
  275. _passed++;
  276. stdout.puts("PASS: test_error_already_applied\n");
  277. } else {
  278. _failed++;
  279. stdout.puts("FAIL: test_error_already_applied\n");
  280. }
  281. }
  282. }
  283. public class TestErrorMissingMigrationOperation : AsyncTestOperation {
  284. private unowned int _passed;
  285. private unowned int _failed;
  286. public TestErrorMissingMigrationOperation(ref int passed, ref int failed) {
  287. _passed = passed;
  288. _failed = failed;
  289. }
  290. public override async void execute_async() throws Error {
  291. bool result = yield test_error_missing_migration();
  292. if (result) {
  293. _passed++;
  294. stdout.puts("PASS: test_error_missing_migration\n");
  295. } else {
  296. _failed++;
  297. stdout.puts("FAIL: test_error_missing_migration\n");
  298. }
  299. }
  300. }
  301. public class TestBootstrapIrreversibleOperation : AsyncTestOperation {
  302. private unowned int _passed;
  303. private unowned int _failed;
  304. public TestBootstrapIrreversibleOperation(ref int passed, ref int failed) {
  305. _passed = passed;
  306. _failed = failed;
  307. }
  308. public override async void execute_async() throws Error {
  309. bool result = yield test_bootstrap_irreversible();
  310. if (result) {
  311. _passed++;
  312. stdout.puts("PASS: test_bootstrap_irreversible\n");
  313. } else {
  314. _failed++;
  315. stdout.puts("FAIL: test_bootstrap_irreversible\n");
  316. }
  317. }
  318. }
  319. public class TestBootstrapExecutionOperation : AsyncTestOperation {
  320. private unowned int _passed;
  321. private unowned int _failed;
  322. public TestBootstrapExecutionOperation(ref int passed, ref int failed) {
  323. _passed = passed;
  324. _failed = failed;
  325. }
  326. public override async void execute_async() throws Error {
  327. bool result = yield test_bootstrap_execution();
  328. if (result) {
  329. _passed++;
  330. stdout.puts("PASS: test_bootstrap_execution\n");
  331. } else {
  332. _failed++;
  333. stdout.puts("FAIL: test_bootstrap_execution\n");
  334. }
  335. }
  336. }
  337. // === Helper Functions ===
  338. /**
  339. * Creates a temporary directory for testing.
  340. */
  341. string create_temp_dir() {
  342. string temp_dir = DirUtils.mkdtemp("implexus_migration_test_XXXXXX");
  343. return temp_dir;
  344. }
  345. /**
  346. * Cleans up a temporary directory.
  347. */
  348. void cleanup_dir(string path) {
  349. try {
  350. Dir dir = Dir.open(path, 0);
  351. string? name;
  352. while ((name = dir.read_name()) != null) {
  353. FileUtils.unlink(Path.build_filename(path, name));
  354. }
  355. } catch (FileError e) {
  356. // Ignore errors
  357. }
  358. DirUtils.remove(path);
  359. }
  360. // === MigrationStorage Tests ===
  361. // Test 1: Record migration stores migration records correctly
  362. bool test_record_migration() {
  363. string temp_dir = create_temp_dir();
  364. try {
  365. var engine = new EmbeddedEngine.with_path(temp_dir);
  366. var storage = new MigrationStorage(engine);
  367. // Record a migration
  368. storage.record_migration("2026031301", "Create users table");
  369. // Verify it was recorded
  370. if (!storage.is_applied("2026031301")) {
  371. cleanup_dir(temp_dir);
  372. return false;
  373. }
  374. cleanup_dir(temp_dir);
  375. return true;
  376. } catch (Error e) {
  377. cleanup_dir(temp_dir);
  378. return false;
  379. }
  380. }
  381. // Test 2: Get applied versions returns all recorded versions
  382. bool test_get_applied_versions() {
  383. string temp_dir = create_temp_dir();
  384. try {
  385. var engine = new EmbeddedEngine.with_path(temp_dir);
  386. var storage = new MigrationStorage(engine);
  387. // Record migrations in non-sorted order
  388. storage.record_migration("2026031303", "Third migration");
  389. storage.record_migration("2026031301", "First migration");
  390. storage.record_migration("2026031302", "Second migration");
  391. // Get applied versions and convert to vector for checking
  392. var versions = storage.get_applied_versions();
  393. var version_set = new Invercargill.DataStructures.HashSet<string>();
  394. foreach (var v in versions) {
  395. version_set.add(v);
  396. }
  397. // Should have 3 versions
  398. if (version_set.length != 3) {
  399. cleanup_dir(temp_dir);
  400. return false;
  401. }
  402. // Verify all three versions are present
  403. if (!version_set.has("2026031301")) {
  404. cleanup_dir(temp_dir);
  405. return false;
  406. }
  407. if (!version_set.has("2026031302")) {
  408. cleanup_dir(temp_dir);
  409. return false;
  410. }
  411. if (!version_set.has("2026031303")) {
  412. cleanup_dir(temp_dir);
  413. return false;
  414. }
  415. cleanup_dir(temp_dir);
  416. return true;
  417. } catch (MigrationError e) {
  418. cleanup_dir(temp_dir);
  419. return false;
  420. }
  421. }
  422. // Test 3: Is applied correctly identifies applied migrations
  423. bool test_is_applied() {
  424. string temp_dir = create_temp_dir();
  425. try {
  426. var engine = new EmbeddedEngine.with_path(temp_dir);
  427. var storage = new MigrationStorage(engine);
  428. // Check before recording
  429. if (storage.is_applied("2026031301")) {
  430. cleanup_dir(temp_dir);
  431. return false;
  432. }
  433. // Record migration
  434. storage.record_migration("2026031301", "Test migration");
  435. // Check after recording
  436. if (!storage.is_applied("2026031301")) {
  437. cleanup_dir(temp_dir);
  438. return false;
  439. }
  440. // Check non-existent migration
  441. if (storage.is_applied("9999999999")) {
  442. cleanup_dir(temp_dir);
  443. return false;
  444. }
  445. cleanup_dir(temp_dir);
  446. return true;
  447. } catch (Error e) {
  448. cleanup_dir(temp_dir);
  449. return false;
  450. }
  451. }
  452. // Test 4: Remove migration removes records correctly
  453. bool test_remove_migration() {
  454. string temp_dir = create_temp_dir();
  455. try {
  456. var engine = new EmbeddedEngine.with_path(temp_dir);
  457. var storage = new MigrationStorage(engine);
  458. // Record migration
  459. storage.record_migration("2026031301", "Test migration");
  460. // Verify it was recorded
  461. if (!storage.is_applied("2026031301")) {
  462. cleanup_dir(temp_dir);
  463. return false;
  464. }
  465. // Remove migration
  466. storage.remove_migration("2026031301");
  467. // Verify it was removed
  468. if (storage.is_applied("2026031301")) {
  469. cleanup_dir(temp_dir);
  470. return false;
  471. }
  472. cleanup_dir(temp_dir);
  473. return true;
  474. } catch (Error e) {
  475. cleanup_dir(temp_dir);
  476. return false;
  477. }
  478. }
  479. // Test 5: Get migration record returns correct metadata
  480. bool test_get_migration_record() {
  481. string temp_dir = create_temp_dir();
  482. try {
  483. var engine = new EmbeddedEngine.with_path(temp_dir);
  484. var storage = new MigrationStorage(engine);
  485. // Record migration
  486. storage.record_migration("2026031301", "Create users table");
  487. // Get the record
  488. var record = storage.get_migration_record("2026031301");
  489. if (record == null) {
  490. cleanup_dir(temp_dir);
  491. return false;
  492. }
  493. if (((!) record).version != "2026031301") {
  494. cleanup_dir(temp_dir);
  495. return false;
  496. }
  497. if (((!) record).description != "Create users table") {
  498. cleanup_dir(temp_dir);
  499. return false;
  500. }
  501. if (((!) record).applied_at == null) {
  502. cleanup_dir(temp_dir);
  503. return false;
  504. }
  505. // Check non-existent migration
  506. var missing = storage.get_migration_record("9999999999");
  507. if (missing != null) {
  508. cleanup_dir(temp_dir);
  509. return false;
  510. }
  511. cleanup_dir(temp_dir);
  512. return true;
  513. } catch (Error e) {
  514. cleanup_dir(temp_dir);
  515. return false;
  516. }
  517. }
  518. // === MigrationRunner Tests ===
  519. // Test 6: Register migration stores migrations
  520. bool test_register_migration() {
  521. string temp_dir = create_temp_dir();
  522. try {
  523. var engine = new EmbeddedEngine.with_path(temp_dir);
  524. var runner = new MigrationRunner(engine);
  525. var migration = new TestMigration("2026031301", "Test migration", "test_container");
  526. runner.register_migration(migration);
  527. // Verify pending count is 1
  528. if (runner.get_pending_count() != 1) {
  529. cleanup_dir(temp_dir);
  530. return false;
  531. }
  532. cleanup_dir(temp_dir);
  533. return true;
  534. } catch (Error e) {
  535. cleanup_dir(temp_dir);
  536. return false;
  537. }
  538. }
  539. // Test 7: Get pending versions identifies unapplied migrations
  540. bool test_get_pending_versions() {
  541. string temp_dir = create_temp_dir();
  542. try {
  543. var engine = new EmbeddedEngine.with_path(temp_dir);
  544. var runner = new MigrationRunner(engine);
  545. // Register migrations
  546. runner.register_migration(new TestMigration("2026031301", "First", "container1"));
  547. runner.register_migration(new TestMigration("2026031302", "Second", "container2"));
  548. runner.register_migration(new TestMigration("2026031303", "Third", "container3"));
  549. // Apply one migration manually
  550. var storage = new MigrationStorage(engine);
  551. storage.record_migration("2026031302", "Second");
  552. // Get pending versions
  553. var pending = runner.get_pending_versions();
  554. // Convert to vector
  555. var pending_list = new Invercargill.DataStructures.Vector<string>();
  556. foreach (var v in pending) {
  557. pending_list.add(v);
  558. }
  559. // Should have 2 pending (2026031301 and 2026031303)
  560. if (pending_list.length != 2) {
  561. cleanup_dir(temp_dir);
  562. return false;
  563. }
  564. // Should be sorted
  565. if (pending_list[0] != "2026031301") {
  566. cleanup_dir(temp_dir);
  567. return false;
  568. }
  569. if (pending_list[1] != "2026031303") {
  570. cleanup_dir(temp_dir);
  571. return false;
  572. }
  573. cleanup_dir(temp_dir);
  574. return true;
  575. } catch (Error e) {
  576. cleanup_dir(temp_dir);
  577. return false;
  578. }
  579. }
  580. // Test 8: Run pending executes migrations in order
  581. async bool test_run_pending() throws Error {
  582. string temp_dir = create_temp_dir();
  583. var engine = new EmbeddedEngine.with_path(temp_dir);
  584. var runner = new MigrationRunner(engine);
  585. // Register migrations
  586. var m1 = new TestMigration("2026031301", "First", "container1");
  587. var m2 = new TestMigration("2026031302", "Second", "container2");
  588. var m3 = new TestMigration("2026031303", "Third", "container3");
  589. runner.register_migration(m1);
  590. runner.register_migration(m2);
  591. runner.register_migration(m3);
  592. // Run pending
  593. int count = yield runner.run_pending_async();
  594. // Should have run 3 migrations
  595. if (count != 3) {
  596. cleanup_dir(temp_dir);
  597. return false;
  598. }
  599. // Verify all were called
  600. if (!m1.up_called || !m2.up_called || !m3.up_called) {
  601. cleanup_dir(temp_dir);
  602. return false;
  603. }
  604. // Verify all are applied
  605. if (!runner.is_applied("2026031301") ||
  606. !runner.is_applied("2026031302") ||
  607. !runner.is_applied("2026031303")) {
  608. cleanup_dir(temp_dir);
  609. return false;
  610. }
  611. // Verify containers were created
  612. bool exists1 = yield engine.entity_exists_async(new EntityPath("/container1"));
  613. bool exists2 = yield engine.entity_exists_async(new EntityPath("/container2"));
  614. bool exists3 = yield engine.entity_exists_async(new EntityPath("/container3"));
  615. if (!exists1 || !exists2 || !exists3) {
  616. cleanup_dir(temp_dir);
  617. return false;
  618. }
  619. cleanup_dir(temp_dir);
  620. return true;
  621. }
  622. // Test 9: Run to version runs migrations up to specific version
  623. async bool test_run_to_version() throws Error {
  624. string temp_dir = create_temp_dir();
  625. var engine = new EmbeddedEngine.with_path(temp_dir);
  626. var runner = new MigrationRunner(engine);
  627. // Register migrations
  628. var m1 = new TestMigration("2026031301", "First", "container1");
  629. var m2 = new TestMigration("2026031302", "Second", "container2");
  630. var m3 = new TestMigration("2026031303", "Third", "container3");
  631. runner.register_migration(m1);
  632. runner.register_migration(m2);
  633. runner.register_migration(m3);
  634. // Run to version (inclusive)
  635. int count = yield runner.run_to_version_async("2026031302");
  636. // Should have run 2 migrations
  637. if (count != 2) {
  638. cleanup_dir(temp_dir);
  639. return false;
  640. }
  641. // Verify first two were called
  642. if (!m1.up_called || !m2.up_called) {
  643. cleanup_dir(temp_dir);
  644. return false;
  645. }
  646. // Third should NOT be called
  647. if (m3.up_called) {
  648. cleanup_dir(temp_dir);
  649. return false;
  650. }
  651. // Verify only first two are applied
  652. if (!runner.is_applied("2026031301") || !runner.is_applied("2026031302")) {
  653. cleanup_dir(temp_dir);
  654. return false;
  655. }
  656. if (runner.is_applied("2026031303")) {
  657. cleanup_dir(temp_dir);
  658. return false;
  659. }
  660. cleanup_dir(temp_dir);
  661. return true;
  662. }
  663. // Test 10: Rollback to version rolls back migrations correctly
  664. async bool test_rollback_to_version() throws Error {
  665. string temp_dir = create_temp_dir();
  666. var engine = new EmbeddedEngine.with_path(temp_dir);
  667. var runner = new MigrationRunner(engine);
  668. // Register migrations
  669. var m1 = new TestMigration("2026031301", "First", "container1");
  670. var m2 = new TestMigration("2026031302", "Second", "container2");
  671. var m3 = new TestMigration("2026031303", "Third", "container3");
  672. runner.register_migration(m1);
  673. runner.register_migration(m2);
  674. runner.register_migration(m3);
  675. // Run all
  676. yield runner.run_pending_async();
  677. // Rollback to version 2026031301 (keeps 2026031301, removes 2026031302 and 2026031303)
  678. int count = yield runner.rollback_to_version_async("2026031301");
  679. // Should have rolled back 2 migrations
  680. if (count != 2) {
  681. cleanup_dir(temp_dir);
  682. return false;
  683. }
  684. // Verify down was called on m2 and m3
  685. if (!m2.down_called || !m3.down_called) {
  686. cleanup_dir(temp_dir);
  687. return false;
  688. }
  689. // m1 down should NOT be called
  690. if (m1.down_called) {
  691. cleanup_dir(temp_dir);
  692. return false;
  693. }
  694. // Verify only first is still applied
  695. if (!runner.is_applied("2026031301")) {
  696. cleanup_dir(temp_dir);
  697. return false;
  698. }
  699. if (runner.is_applied("2026031302") || runner.is_applied("2026031303")) {
  700. cleanup_dir(temp_dir);
  701. return false;
  702. }
  703. // Verify containers 2 and 3 were deleted
  704. bool exists1 = yield engine.entity_exists_async(new EntityPath("/container1"));
  705. bool exists2 = yield engine.entity_exists_async(new EntityPath("/container2"));
  706. bool exists3 = yield engine.entity_exists_async(new EntityPath("/container3"));
  707. if (!exists1) {
  708. cleanup_dir(temp_dir);
  709. return false;
  710. }
  711. if (exists2 || exists3) {
  712. cleanup_dir(temp_dir);
  713. return false;
  714. }
  715. cleanup_dir(temp_dir);
  716. return true;
  717. }
  718. // Test 11: Migrations run in correct order by version string
  719. async bool test_migration_order() throws Error {
  720. string temp_dir = create_temp_dir();
  721. var engine = new EmbeddedEngine.with_path(temp_dir);
  722. var runner = new MigrationRunner(engine);
  723. // Register migrations in non-sorted order
  724. var m3 = new TestMigration("2026031303", "Third", "container3");
  725. var m1 = new TestMigration("2026031301", "First", "container1");
  726. var m2 = new TestMigration("2026031302", "Second", "container2");
  727. runner.register_migration(m3);
  728. runner.register_migration(m1);
  729. runner.register_migration(m2);
  730. // Run pending - should execute in version order
  731. int count = yield runner.run_pending_async();
  732. if (count != 3) {
  733. cleanup_dir(temp_dir);
  734. return false;
  735. }
  736. // Verify all were called
  737. if (!m1.up_called || !m2.up_called || !m3.up_called) {
  738. cleanup_dir(temp_dir);
  739. return false;
  740. }
  741. // Verify all are applied in order
  742. var versions = runner.get_applied_versions();
  743. var version_list = new Invercargill.DataStructures.Vector<string>();
  744. foreach (var v in versions) {
  745. version_list.add(v);
  746. }
  747. if (version_list.length != 3) {
  748. cleanup_dir(temp_dir);
  749. return false;
  750. }
  751. if (version_list[0] != "2026031301" ||
  752. version_list[1] != "2026031302" ||
  753. version_list[2] != "2026031303") {
  754. cleanup_dir(temp_dir);
  755. return false;
  756. }
  757. cleanup_dir(temp_dir);
  758. return true;
  759. }
  760. // Test 12: Error handling for already applied migrations
  761. async bool test_error_already_applied() throws Error {
  762. string temp_dir = create_temp_dir();
  763. var engine = new EmbeddedEngine.with_path(temp_dir);
  764. var runner = new MigrationRunner(engine);
  765. // Register and apply migration
  766. var m1 = new TestMigration("2026031301", "First", "container1");
  767. runner.register_migration(m1);
  768. yield runner.run_pending_async();
  769. // Try to run the same migration again via run_one_async
  770. try {
  771. yield runner.run_one_async("2026031301");
  772. // Should have thrown
  773. cleanup_dir(temp_dir);
  774. return false;
  775. } catch (MigrationError.ALREADY_APPLIED e) {
  776. // Expected
  777. }
  778. cleanup_dir(temp_dir);
  779. return true;
  780. }
  781. // Test 13: Error handling for missing migrations
  782. async bool test_error_missing_migration() throws Error {
  783. string temp_dir = create_temp_dir();
  784. var engine = new EmbeddedEngine.with_path(temp_dir);
  785. var runner = new MigrationRunner(engine);
  786. // Verify the migration doesn't exist in pending list
  787. var pending = runner.get_pending_versions();
  788. bool found = false;
  789. foreach (var v in pending) {
  790. if (v == "9999999999") {
  791. found = true;
  792. break;
  793. }
  794. }
  795. // The migration should NOT be in the pending list
  796. // because it was never registered
  797. if (found) {
  798. cleanup_dir(temp_dir);
  799. return false;
  800. }
  801. // Verify is_applied returns false for non-existent migration
  802. if (runner.is_applied("9999999999")) {
  803. cleanup_dir(temp_dir);
  804. return false;
  805. }
  806. cleanup_dir(temp_dir);
  807. return true;
  808. }
  809. // Test 14: Version conflict when registering duplicate
  810. bool test_version_conflict() {
  811. string temp_dir = create_temp_dir();
  812. try {
  813. var engine = new EmbeddedEngine.with_path(temp_dir);
  814. var runner = new MigrationRunner(engine);
  815. // Register first migration
  816. var m1 = new TestMigration("2026031301", "First", "container1");
  817. runner.register_migration(m1);
  818. // Try to register duplicate version
  819. var m2 = new TestMigration("2026031301", "Duplicate", "container2");
  820. try {
  821. runner.register_migration(m2);
  822. // Should have thrown
  823. cleanup_dir(temp_dir);
  824. return false;
  825. } catch (MigrationError.VERSION_CONFLICT e) {
  826. // Expected
  827. }
  828. cleanup_dir(temp_dir);
  829. return true;
  830. } catch (Error e) {
  831. cleanup_dir(temp_dir);
  832. return false;
  833. }
  834. }
  835. // === BootstrapMigration Tests ===
  836. // Test 15: Bootstrap version is "0000000000"
  837. bool test_bootstrap_version() {
  838. var bootstrap = new BootstrapMigration();
  839. if (bootstrap.version != "0000000000") {
  840. return false;
  841. }
  842. return true;
  843. }
  844. // Test 16: Bootstrap down throws IRREVERSIBLE error
  845. async bool test_bootstrap_irreversible() throws Error {
  846. string temp_dir = create_temp_dir();
  847. var engine = new EmbeddedEngine.with_path(temp_dir);
  848. var bootstrap = new BootstrapMigration();
  849. try {
  850. yield bootstrap.down_async(engine);
  851. // Should have thrown
  852. cleanup_dir(temp_dir);
  853. return false;
  854. } catch (MigrationError.IRREVERSIBLE e) {
  855. // Expected
  856. }
  857. cleanup_dir(temp_dir);
  858. return true;
  859. }
  860. // Test 17: Bootstrap execution works correctly
  861. async bool test_bootstrap_execution() throws Error {
  862. string temp_dir = create_temp_dir();
  863. var engine = new EmbeddedEngine.with_path(temp_dir);
  864. var runner = new MigrationRunner(engine);
  865. // Register bootstrap
  866. var bootstrap = new BootstrapMigration();
  867. runner.register_migration(bootstrap);
  868. // Run pending
  869. int count = yield runner.run_pending_async();
  870. // Should have run 1 migration
  871. if (count != 1) {
  872. cleanup_dir(temp_dir);
  873. return false;
  874. }
  875. // Verify bootstrap is applied
  876. if (!runner.is_applied("0000000000")) {
  877. cleanup_dir(temp_dir);
  878. return false;
  879. }
  880. // Verify root exists (bootstrap ensures this)
  881. var root = yield engine.get_root_async();
  882. if (root == null) {
  883. cleanup_dir(temp_dir);
  884. return false;
  885. }
  886. cleanup_dir(temp_dir);
  887. return true;
  888. }