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_a_different_file_for_a_published_version_is_refused() {130 base := scratch('changed')!131 defer {132 os.rmdir_all(base) or {}133 }134 mut store := open_repo('changed')!135 defer {136 store.close()137 }138 config := demo_config(store, 'macos-aarch64')!139 make_save(store, 'first')!140 mut backend := remote.new_folder(os.join_path(base, 'remote'))!141 name := 'demo-1.2.3-macos-aarch64'142 backend.put_if_absent(file_key('proj', '1.2.3', 'macos-aarch64', name), 'other'.bytes())!143144 if _ := publish(store, mut backend, 'proj', config, '1.2.3', '') {145 assert false, 'a different file for a published version must be refused'146 } else {147 assert err.msg().contains('already has a different ${name}')148 }149 150 assert !backend.get(release_key('proj', '1.2.3'))!.found151 assert !backend.get(latest_key('proj'))!.found152}153154155fn test_a_failed_build_uploads_nothing() {156 base := scratch('failed')!157 defer {158 os.rmdir_all(base) or {}159 }160 mut store := open_repo('failed')!161 defer {162 store.close()163 }164 165 166 write(store.root, 'VERSION', '1.2.3\n')!167 write(store.root, 'koh.build', 'command = mkdir -p out && printf koh > out/demo-1.2.3-macos-aarch64168output = out/demo-{version}-{platform}169platforms = macos-aarch64, linux-aarch64170')!171 config := build.read_config(store.root)!172 make_save(store, 'first')!173 mut backend := remote.new_folder(os.join_path(base, 'remote'))!174175 if _ := publish(store, mut backend, 'proj', config, '1.2.3', '') {176 assert false, 'a build that produced nothing must stop the release'177 } else {178 assert err.msg().contains('did not create')179 }180 assert backend.list('proj/releases')!.len == 0181}182183184185fn test_a_release_file_name_kepr_would_refuse_is_refused() {186 assert good_name('demo-1.2.3-android-arm64.apk')187 assert good_name('Haven.app.zip')188 assert !good_name('')189 assert !good_name('.')190 assert !good_name('..')191 assert !good_name('a/b')192 assert !good_name('a\\b')193 assert !good_name('a"b')194 assert !good_name('a\x07b')195 assert !good_name('x'.repeat(256))196}197198fn test_the_notes_rule_is_koh_2s() {199 check_notes('')!200 check_notes(' ')!201 check_notes('fixed the crash on startup when the config file was missing entirely')!202 if _ := check_notes('too short to say anything at all') {203 assert false, 'notes under 40 characters must be refused'204 }205 if _ := check_notes('this note is long enough in characters but few words') {206 assert false, 'notes under 12 words must be refused'207 }208}