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 mut first_time := i64(9223372036854775807)55 for name in os.ls(os.join_path(dir, '.koh', 'snapshots'))! {56 if !name.ends_with('.json') {57 continue58 }59 record :=60 json.decode(Manifest, os.read_file(os.join_path(dir, '.koh', 'snapshots', name))!)!61 if record.timestamp < first_time {62 first_time = record.timestamp63 first = record.id64 }65 }66 return Fixture{67 dir: dir68 first: first69 second: koh2_head(dir)70 message: 'first save'71 }72}737475fn koh2_head(dir string) string {76 db := storage.connect(os.join_path(dir, '.koh', 'koh.db'), 1000) or { return '' }77 defer {78 db.close()79 }80 mut stmt := db.prepare('SELECT value FROM state WHERE key = ?') or { return '' }81 defer {82 stmt.finalize()83 }84 stmt.bind_text(1, 'head_main') or { return '' }85 if stmt.step() or { return '' } {86 return stmt.text(0)87 }88 return ''89}9091fn test_migrating_a_koh2_repo_keeps_every_save_exactly() {92 koh2 := koh2_binary() or { return }93 fixture := make_fixture(koh2)!94 defer {95 os.rmdir_all(fixture.dir) or {}96 }97 98 sh(fixture.dir, '${koh2} flag ${fixture.first} --reason "sketchy"')!99100 101 mut manifests := map[string][]string{}102 for name in os.ls(os.join_path(fixture.dir, '.koh', 'snapshots'))! {103 if !name.ends_with('.json') {104 continue105 }106 record := json.decode(Manifest,107 os.read_file(os.join_path(fixture.dir, '.koh', 'snapshots', name))!)!108 mut files := []string{}109 for file in record.files {110 files << '${file.path} ${bare_hash(file.hash)}'111 }112 manifests[record.id] = files113 }114115 report := run(fixture.dir, default_lane)!116 117 assert report.lane == 'main'118 assert report.saves == 3119 assert report.checkpoints == 1120 assert report.flags == 1121 assert report.objects >= 3122123 124 assert os.is_file(os.join_path(fixture.dir, '.koh'))125 assert os.is_dir(os.join_path(fixture.dir, '.koh-v2'))126 assert os.is_file(os.join_path(fixture.dir, '.kohignore'))127128 store := storage.open(fixture.dir)!129 defer {130 store.close()131 }132 133 assert history.save_count(store)! == 3134 for id, expected in manifests {135 save := history.by_id(store, id) or {136 assert false, 'save ${id} did not survive the migration'137 return138 }139 assert save.machine == 'koh2'140 mut actual := []string{}141 for file in history.files_of(store, id)! {142 actual << '${file.path} ${file.hash}'143 144 if _ := store.get_object(file.hash) {145 } else {146 assert false, 'object ${file.hash} is missing or corrupt'147 }148 }149 assert actual == expected150 }151 152 middle := history.by_id(store, fixture.first) or { panic('the first save did not survive') }153 assert (history.head(store) or { panic('no head after migrating') }).id == fixture.second154 assert middle.message == 'first save'155156 157 158 assert events.flag_state(store, fixture.first)!.flagged159 assert events.flag_state(store, fixture.first)!.reason == 'sketchy'160 entries := snapshot.walk(store, fixture.dir, snapshot.load(fixture.dir))!161 mut changed := []string{}162 head_files := snapshot.index_files(history.files_of(store, fixture.second)!)163 for entry in entries {164 if entry.path !in head_files {165 changed << entry.path166 } else if head_files[entry.path].hash != entry.hash {167 changed << entry.path168 }169 }170 171 assert changed == ['.kohignore']172}173174fn test_migrate_refuses_a_tree_that_does_not_match_the_last_save() {175 koh2 := koh2_binary() or { return }176 fixture := make_fixture(koh2)!177 defer {178 os.rmdir_all(fixture.dir) or {}179 }180 os.write_file(os.join_path(fixture.dir, 'big.txt'), 'unsaved change\n')!181 if _ := run(fixture.dir, default_lane) {182 assert false, 'a dirty tree must be refused'183 } else {184 assert err.msg().contains('save with koh2 first')185 }186 187 assert os.is_dir(os.join_path(fixture.dir, '.koh'))188 assert !os.exists(os.join_path(fixture.dir, '.koh-v2'))189 assert !os.exists(os.join_path(fixture.dir, staging_file))190}