1234module release56import json27import os8import time9import src.build10import src.history11import src.machine12import src.remote13import src.snapshot14import src.storage1516fn scratch(name string) !string {17 dir := os.join_path(os.temp_dir(), 'koh3_release_${os.getpid()}_${name}')18 os.rmdir_all(dir) or {}19 os.mkdir_all(dir, os.MkdirParams{})!20 return dir21}2223fn write(root string, rel string, text string) ! {24 full := os.join_path(root, rel)25 os.mkdir_all(os.dir(full), os.MkdirParams{})!26 os.write_file(full, text)!27}2829fn open_repo(name string) !storage.Store {30 dir := os.join_path(os.temp_dir(), 'koh3_release_proj_${os.getpid()}_${name}')31 os.rmdir_all(dir) or {}32 os.mkdir_all(dir, os.MkdirParams{})!33 mut store := storage.create(dir)!34 write(dir, '.kohignore', '*.log\n')!35 return store36}3738fn make_save(store &storage.Store, message string) !history.Save {39 return history.save_worktree(store, store.root, snapshot.load(store.root), history.SaveInput{40 message: message41 })!42}43444546fn demo_config(store &storage.Store, platforms string) !build.Config {47 write(store.root, 'VERSION', '1.2.3\n')!48 mut command := 'mkdir -p out'49 for platform in platforms.split(',') {50 trimmed := platform.trim_space()51 command += ' && printf koh > out/demo-1.2.3-${trimmed}'52 }53 write(store.root, 'koh.build', 'command = ${command}54output = out/demo-{version}-{platform}55platforms = ${platforms}56')!57 return build.read_config(store.root)!58}5960fn test_publishing_writes_the_files_the_record_and_latest() {61 base := scratch('publish')!62 defer {63 os.rmdir_all(base) or {}64 }65 mut store := open_repo('publish')!66 defer {67 store.close()68 }69 config := demo_config(store, 'macos-aarch64')!70 head := make_save(store, 'first')!71 mut backend := remote.new_folder(os.join_path(base, 'remote'))!7273 published := publish(store, mut backend, 'proj', config, '1.2.3', '')!74 assert published.version == '1.2.3'75 assert published.files.len == 17677 name := 'demo-1.2.3-macos-aarch64'78 artifact := backend.get(file_key('proj', '1.2.3', 'macos-aarch64', name))!79 assert artifact.found80 assert artifact.body.bytestr() == 'koh'8182 record := json2.decode[Record](backend.get(release_key('proj', '1.2.3'))!.body.bytestr())!83 assert record.format == 184 assert record.repo == 'proj'85 assert record.version == '1.2.3'86 assert record.save == head.id87 assert record.machine == machine.name()88 assert record.notes == ''89 assert record.files.len == 190 assert record.files[0].platform == 'macos-aarch64'91 assert record.files[0].name == name92 assert record.files[0].size == 393 assert record.files[0].blake3 == storage.hash_hex('koh'.bytes())94 assert record.files[0].sha256.len == 6495 96 assert backend.get(latest_key('proj'))!.body.bytestr() == '1.2.3'97}9899100101fn test_a_publish_retries_after_a_partial_upload() {102 base := scratch('retry')!103 defer {104 os.rmdir_all(base) or {}105 }106 mut store := open_repo('retry')!107 defer {108 store.close()109 }110 config := demo_config(store, 'macos-aarch64, linux-aarch64')!111 make_save(store, 'first')!112 mut backend := remote.new_folder(os.join_path(base, 'remote'))!113114 publish(store, mut backend, 'proj', config, '1.2.3', '')!115 116 117 backend.delete(release_key('proj', '1.2.3'))!118 backend.delete(latest_key('proj'))!119 again := publish(store, mut backend, 'proj', config, '1.2.3', '')!120 assert again.files.len == 2121 assert backend.get(release_key('proj', '1.2.3'))!.found122 assert backend.get(latest_key('proj'))!.body.bytestr() == '1.2.3'123 assert backend.get(file_key('proj', '1.2.3', 'macos-aarch64', 'demo-1.2.3-macos-aarch64'))!.found124 assert backend.get(file_key('proj', '1.2.3', 'linux-aarch64', 'demo-1.2.3-linux-aarch64'))!.found125}126127128129fn test_two_platforms_building_to_one_file_are_refused() {130 base := scratch('collision')!131 defer {132 os.rmdir_all(base) or {}133 }134 mut store := open_repo('collision')!135 defer {136 store.close()137 }138 write(store.root, 'VERSION', '1.2.3\n')!139 140 write(store.root, 'koh.build', 'command = mkdir -p out && printf koh > out/demo141output = out/demo142platforms = macos-aarch64, linux-aarch64143')!144 config := build.read_config(store.root)!145 make_save(store, 'first')!146 mut backend := remote.new_folder(os.join_path(base, 'remote'))!147148 if _ := publish(store, mut backend, 'proj', config, '1.2.3', '') {149 assert false, 'two platforms building to one file must be refused'150 } else {151 assert err.msg().contains('two platforms build to the same file')152 }153 assert !os.exists(os.join_path(store.root, 'out'))154 assert backend.list('proj/releases')!.len == 0155}156157158159fn test_a_different_file_for_a_published_version_is_refused() {160 base := scratch('changed')!161 defer {162 os.rmdir_all(base) or {}163 }164 mut store := open_repo('changed')!165 defer {166 store.close()167 }168 config := demo_config(store, 'macos-aarch64')!169 make_save(store, 'first')!170 mut backend := remote.new_folder(os.join_path(base, 'remote'))!171 name := 'demo-1.2.3-macos-aarch64'172 backend.put_if_absent(file_key('proj', '1.2.3', 'macos-aarch64', name), 'other'.bytes())!173174 if _ := publish(store, mut backend, 'proj', config, '1.2.3', '') {175 assert false, 'a different file for a published version must be refused'176 } else {177 assert err.msg().contains('already has a different ${name}')178 }179 180 assert !backend.get(release_key('proj', '1.2.3'))!.found181 assert !backend.get(latest_key('proj'))!.found182}183184185fn test_a_failed_build_uploads_nothing() {186 base := scratch('failed')!187 defer {188 os.rmdir_all(base) or {}189 }190 mut store := open_repo('failed')!191 defer {192 store.close()193 }194 195 196 write(store.root, 'VERSION', '1.2.3\n')!197 write(store.root, 'koh.build', 'command = mkdir -p out && printf koh > out/demo-1.2.3-macos-aarch64198output = out/demo-{version}-{platform}199platforms = macos-aarch64, linux-aarch64200')!201 config := build.read_config(store.root)!202 make_save(store, 'first')!203 mut backend := remote.new_folder(os.join_path(base, 'remote'))!204205 if _ := publish(store, mut backend, 'proj', config, '1.2.3', '') {206 assert false, 'a build that produced nothing must stop the release'207 } else {208 assert err.msg().contains('did not create')209 }210 assert backend.list('proj/releases')!.len == 0211}212213214215fn test_a_release_file_name_kepr_would_refuse_is_refused() {216 assert good_name('demo-1.2.3-android-arm64.apk')217 assert good_name('Haven.app.zip')218 assert !good_name('')219 assert !good_name('.')220 assert !good_name('..')221 assert !good_name('a/b')222 assert !good_name('a\\b')223 assert !good_name('a"b')224 assert !good_name('a\x07b')225 assert !good_name('x'.repeat(256))226}227228fn test_the_notes_rule_is_koh_2s() {229 check_notes('')!230 check_notes(' ')!231 check_notes('fixed the crash on startup when the config file was missing entirely')!232 if _ := check_notes('too short to say anything at all') {233 assert false, 'notes under 40 characters must be refused'234 }235 if _ := check_notes('this note is long enough in characters but few words') {236 assert false, 'notes under 12 words must be refused'237 }238}