12345678module migrate910import json11import os12import src.events13import src.history14import src.snapshot15import src.storage1617struct Fixture {18 dir string19 first string20 second string21 message string22}2324fn koh2_binary() ?string {25 path := os.find_abs_path_of_executable('koh') or { return none }26 return path27}2829fn sh(dir string, command string) !string {30 result := os.execute('cd ${dir} && ${command}')31 if result.exit_code != 0 {32 return error('${command} failed: ${result.output}')33 }34 return result.output35}3637fn make_fixture(koh2 string) !Fixture {38 dir := os.join_path(os.temp_dir(), 'koh3_migrate_${os.getpid()}')39 os.rmdir_all(dir) or {}40 os.mkdir_all(dir, os.MkdirParams{})!41 sh(dir, '${koh2} init')!42 43 os.write_file(os.join_path(dir, 'big.txt'), 'x'.repeat(700))!44 os.write_file(os.join_path(dir, 'small.txt'), 'tiny\n')!45 sh(dir, '${koh2} save "first save"')!46 sh(dir, '${koh2} checkpoint "before something"')!47 os.write_file(os.join_path(dir, 'big.txt'), 'y'.repeat(700))!48 sh(dir, '${koh2} save "second save"')!4950 51 52 53 mut first := ''54 for name in os.ls(os.join_path(dir, '.koh', 'snapshots'))! {55 if !name.ends_with('.json') {56 continue57 }58 record :=59 json.decode(Manifest, os.read_file(os.join_path(dir, '.koh', 'snapshots', name))!)!60 if record.message == 'first save' {61 first = record.id62 }63 }64 assert first != ''65 return Fixture{66 dir: dir67 first: first68 second: koh2_head(dir)69 message: 'first save'70 }71}727374fn koh2_head(dir string) string {75 db := storage.connect(os.join_path(dir, '.koh', 'koh.db'), 1000) or { return '' }76 defer {77 db.close()78 }79 mut stmt := db.prepare('SELECT value FROM state WHERE key = ?') or { return '' }80 defer {81 stmt.finalize()82 }83 stmt.bind_text(1, 'head_main') or { return '' }84 if stmt.step() or { return '' } {85 return stmt.text(0)86 }87 return ''88}8990fn test_migrating_a_koh2_repo_keeps_every_save_exactly() {91 koh2 := koh2_binary() or { return }92 fixture := make_fixture(koh2)!93 defer {94 os.rmdir_all(fixture.dir) or {}95 }96 97 sh(fixture.dir, '${koh2} flag ${fixture.first} --reason "sketchy"')!9899 100 mut manifests := map[string][]string{}101 for name in os.ls(os.join_path(fixture.dir, '.koh', 'snapshots'))! {102 if !name.ends_with('.json') {103 continue104 }105 record := json.decode(Manifest,106 os.read_file(os.join_path(fixture.dir, '.koh', 'snapshots', name))!)!107 mut files := []string{}108 for file in record.files {109 files << '${file.path} ${bare_hash(file.hash)}'110 }111 manifests[record.id] = files112 }113114 report := run(fixture.dir, default_lane)!115 116 assert report.lane == 'main'117 assert report.saves == 3118 assert report.checkpoints == 1119 assert report.flags == 1120 assert report.objects >= 3121122 123 assert os.is_file(os.join_path(fixture.dir, '.koh'))124 assert os.is_dir(os.join_path(fixture.dir, '.koh-v2'))125 assert os.is_file(os.join_path(fixture.dir, '.kohignore'))126127 store := storage.open(fixture.dir)!128 defer {129 store.close()130 }131 132 assert history.save_count(store)! == 3133 for id, expected in manifests {134 save := history.by_id(store, id) or {135 assert false, 'save ${id} did not survive the migration'136 return137 }138 assert save.machine == 'koh2'139 mut actual := []string{}140 for file in history.files_of(store, id)! {141 actual << '${file.path} ${file.hash}'142 143 if _ := store.get_object(file.hash) {144 } else {145 assert false, 'object ${file.hash} is missing or corrupt'146 }147 }148 assert actual == expected149 }150 151 middle := history.by_id(store, fixture.first) or { panic('the first save did not survive') }152 assert (history.head(store) or { panic('no head after migrating') }).id == fixture.second153 assert middle.message == 'first save'154155 156 157 assert events.flag_state(store, fixture.first)!.flagged158 assert events.flag_state(store, fixture.first)!.reason == 'sketchy'159 entries := snapshot.walk(store, fixture.dir, snapshot.load(fixture.dir))!160 mut changed := []string{}161 head_files := snapshot.index_files(history.files_of(store, fixture.second)!)162 for entry in entries {163 if entry.path !in head_files {164 changed << entry.path165 } else if head_files[entry.path].hash != entry.hash {166 changed << entry.path167 }168 }169 170 assert changed == ['.kohignore']171}172173fn test_migrate_refuses_a_tree_that_does_not_match_the_last_save() {174 koh2 := koh2_binary() or { return }175 fixture := make_fixture(koh2)!176 defer {177 os.rmdir_all(fixture.dir) or {}178 }179 os.write_file(os.join_path(fixture.dir, 'big.txt'), 'unsaved change\n')!180 if _ := run(fixture.dir, default_lane) {181 assert false, 'a dirty tree must be refused'182 } else {183 assert err.msg().contains('save with koh2 first')184 }185 186 assert os.is_dir(os.join_path(fixture.dir, '.koh'))187 assert !os.exists(os.join_path(fixture.dir, '.koh-v2'))188 assert !os.exists(os.join_path(fixture.dir, staging_file))189}