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}198199fn test_migrate_refuses_a_tree_that_does_not_match_the_last_save() {200 koh2 := koh2_binary() or { return }201 fixture := make_fixture(koh2)!202 defer {203 os.rmdir_all(fixture.dir) or {}204 }205 os.write_file(os.join_path(fixture.dir, 'big.txt'), 'unsaved change\n')!206 if _ := run(fixture.dir, default_lane) {207 assert false, 'a dirty tree must be refused'208 } else {209 assert err.msg().contains('save with koh2 first')210 }211 212 assert os.is_dir(os.join_path(fixture.dir, '.koh'))213 assert !os.exists(os.join_path(fixture.dir, '.koh-v2'))214 assert !os.exists(os.join_path(fixture.dir, staging_file))215}216217218fn test_a_saved_path_is_only_read_inside_the_repo() {219 root := os.join_path(os.temp_dir(), 'koh3_migrate_paths_${os.getpid()}', 'repo')220 os.mkdir_all(root)!221 defer {222 os.rmdir_all(os.dir(root)) or {}223 }224 os.write_file(os.join_path(os.dir(root), 'outside.txt'), 'secret\n')!225 os.write_file(os.join_path(root, 'inside.txt'), 'fine\n')!226 assert disk_hash(root, 'inside.txt') != none227 for path in ['../outside.txt', '/etc/hosts', './inside.txt', 'a//inside.txt'] {228 assert disk_hash(root, path) == none, path229 }230}