12345678module migrate910import json211import 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 json2.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 := json2.decode[Manifest](os.read_file(os.join_path(fixture.dir, '.koh', 'snapshots', name))!)!106 mut files := []string{}107 for file in record.files {108 files << '${file.path} ${bare_hash(file.hash)}'109 }110 manifests[record.id] = files111 }112113 report := run(fixture.dir, default_lane)!114 115 assert report.lane == 'main'116 assert report.saves == 3117 assert report.checkpoints == 1118 assert report.flags == 1119 assert report.objects >= 3120121 122 assert os.is_file(os.join_path(fixture.dir, '.koh'))123 assert os.is_dir(os.join_path(fixture.dir, '.koh-v2'))124 assert os.is_file(os.join_path(fixture.dir, '.kohignore'))125126 store := storage.open(fixture.dir)!127 defer {128 store.close()129 }130 131 assert history.save_count(store)! == 3132 for id, expected in manifests {133 save := history.by_id(store, id) or {134 assert false, 'save ${id} did not survive the migration'135 return136 }137 assert save.machine == 'koh2'138 mut actual := []string{}139 for file in history.files_of(store, id)! {140 actual << '${file.path} ${file.hash}'141 142 if _ := store.get_object(file.hash) {143 } else {144 assert false, 'object ${file.hash} is missing or corrupt'145 }146 }147 assert actual == expected148 }149 150 middle := history.by_id(store, fixture.first) or { panic('the first save did not survive') }151 assert (history.head(store) or { panic('no head after migrating') }).id == fixture.second152 assert middle.message == 'first save'153154 155 156 assert events.flag_state(store, fixture.first)!.flagged157 assert events.flag_state(store, fixture.first)!.reason == 'sketchy'158 entries := snapshot.walk(store, fixture.dir, snapshot.load(fixture.dir))!159 mut changed := []string{}160 head_files := snapshot.index_files(history.files_of(store, fixture.second)!)161 for entry in entries {162 if entry.path !in head_files {163 changed << entry.path164 } else if head_files[entry.path].hash != entry.hash {165 changed << entry.path166 }167 }168 169 assert changed == ['.kohignore']170}171172173174fn test_migration_renames_the_koh_2_build_file() {175 koh2 := koh2_binary() or { return }176 fixture := make_fixture(koh2)!177 defer {178 os.rmdir_all(fixture.dir) or {}179 }180 build_file := 'command = true\noutput = out/demo\nplatforms = macos-aarch64\n'181 os.write_file(os.join_path(fixture.dir, 'kepr.build'), build_file)!182 sh(fixture.dir, '${koh2} save "add the build file"')!183184 report := run(fixture.dir, default_lane)!185 assert report.build_renamed186 assert !os.exists(os.join_path(fixture.dir, 'kepr.build'))187 assert os.read_file(os.join_path(fixture.dir, 'koh.build'))! == build_file188189 store := storage.open(fixture.dir)!190 defer {191 store.close()192 }193 head := history.head(store) or { panic('no head after migrating') }194 assert head.message == 'migrate: rename kepr.build to koh.build'195 files := snapshot.walk(store, fixture.dir, snapshot.load(fixture.dir))!196 assert snapshot.same_tree(history.files_of(store, head.id)!, files)197}198199200201fn test_migration_refuses_two_build_files_without_moving_anything() {202 koh2 := koh2_binary() or { return }203 fixture := make_fixture(koh2)!204 defer {205 os.rmdir_all(fixture.dir) or {}206 }207 os.write_file(os.join_path(fixture.dir, 'kepr.build'), 'command = true\n')!208 os.write_file(os.join_path(fixture.dir, 'koh.build'), 'command = true\n')!209 sh(fixture.dir, '${koh2} save "both build files"')!210 if _ := run(fixture.dir, default_lane) {211 assert false, 'both build files must be refused'212 } else {213 assert err.msg().contains('both kepr.build and koh.build')214 }215 assert os.is_dir(os.join_path(fixture.dir, '.koh'))216 assert !os.exists(os.join_path(fixture.dir, '.koh-v2'))217}218219220fn test_migration_without_a_build_file_is_unchanged() {221 koh2 := koh2_binary() or { return }222 fixture := make_fixture(koh2)!223 defer {224 os.rmdir_all(fixture.dir) or {}225 }226 report := run(fixture.dir, default_lane)!227 assert !report.build_renamed228 assert !os.exists(os.join_path(fixture.dir, 'koh.build'))229 assert os.is_dir(os.join_path(fixture.dir, '.koh-v2'))230 store := storage.open(fixture.dir)!231 defer {232 store.close()233 }234 head := history.head(store) or { panic('no head after migrating') }235 236 237 238 saved := history.files_of(store, head.id)!239 mut unsaved := []string{}240 for file in snapshot.walk(store, fixture.dir, snapshot.load(fixture.dir))! {241 if !saved.any(it.path == file.path) {242 unsaved << file.path243 }244 }245 assert unsaved == [snapshot.ignore_file]246}247248fn test_migrate_refuses_a_tree_that_does_not_match_the_last_save() {249 koh2 := koh2_binary() or { return }250 fixture := make_fixture(koh2)!251 defer {252 os.rmdir_all(fixture.dir) or {}253 }254 os.write_file(os.join_path(fixture.dir, 'big.txt'), 'unsaved change\n')!255 if _ := run(fixture.dir, default_lane) {256 assert false, 'a dirty tree must be refused'257 } else {258 assert err.msg().contains('save with koh2 first')259 }260 261 assert os.is_dir(os.join_path(fixture.dir, '.koh'))262 assert !os.exists(os.join_path(fixture.dir, '.koh-v2'))263 assert !os.exists(os.join_path(fixture.dir, draft_file))264}265266267fn test_a_saved_path_is_only_read_inside_the_repo() {268 root := os.join_path(os.temp_dir(), 'koh3_migrate_paths_${os.getpid()}', 'repo')269 os.mkdir_all(root)!270 defer {271 os.rmdir_all(os.dir(root)) or {}272 }273 os.write_file(os.join_path(os.dir(root), 'outside.txt'), 'secret\n')!274 os.write_file(os.join_path(root, 'inside.txt'), 'fine\n')!275 assert disk_hash(root, 'inside.txt') != none276 for path in ['../outside.txt', '/etc/hosts', './inside.txt', 'a//inside.txt'] {277 assert disk_hash(root, path) == none, path278 }279}