1234module release56import json27import os8import time9import src.build10import src.history11import src.machine12import src.remote13import src.snapshot14import src.storage151617fn testsuite_begin() {18 os.setenv('HOME', os.join_path(os.temp_dir(), 'koh3-test-home-${os.getpid()}'), true)19}2021fn scratch(name string) !string {22 dir := os.join_path(os.temp_dir(), 'koh3_release_${os.getpid()}_${name}')23 os.rmdir_all(dir) or {}24 os.mkdir_all(dir, os.MkdirParams{})!25 return dir26}2728fn write(root string, rel string, text string) ! {29 full := os.join_path(root, rel)30 os.mkdir_all(os.dir(full), os.MkdirParams{})!31 os.write_file(full, text)!32}3334fn open_repo(name string) !storage.Store {35 dir := os.join_path(os.temp_dir(), 'koh3_release_proj_${os.getpid()}_${name}')36 os.rmdir_all(dir) or {}37 os.mkdir_all(dir, os.MkdirParams{})!38 mut store := storage.create(dir)!39 write(dir, '.kohignore', '*.log\n')!40 return store41}4243fn make_save(store &storage.Store, message string) !history.Save {44 return history.save_worktree(store, store.root, snapshot.load(store.root), history.SaveInput{45 message: message46 })!47}48495051fn demo_config(store &storage.Store, platforms string) !build.Config {52 write(store.root, 'VERSION', '1.2.3\n')!53 mut command := 'mkdir -p out'54 for platform in platforms.split(',') {55 trimmed := platform.trim_space()56 command += ' && printf koh > out/demo-1.2.3-${trimmed}'57 }58 write(store.root, 'koh.build', 'command = ${command}59output = out/demo-{version}-{platform}60platforms = ${platforms}61')!62 return build.read_config(store.root)!63}6465fn test_publishing_writes_the_files_the_record_and_latest() {66 base := scratch('publish')!67 defer {68 os.rmdir_all(base) or {}69 }70 mut store := open_repo('publish')!71 defer {72 store.close()73 }74 config := demo_config(store, 'macos-aarch64')!75 head := make_save(store, 'first')!76 mut backend := remote.new_folder(os.join_path(base, 'remote'))!7778 published := publish(store, mut backend, 'proj', config, '1.2.3', '')!79 assert published.version == '1.2.3'80 assert published.files.len == 18182 name := 'demo-1.2.3-macos-aarch64'83 artifact := backend.get(file_key('proj', '1.2.3', 'macos-aarch64', name))!84 assert artifact.found85 assert artifact.body.bytestr() == 'koh'8687 record := json2.decode[Record](backend.get(release_key('proj', '1.2.3'))!.body.bytestr())!88 assert record.format == 189 assert record.repo == 'proj'90 assert record.version == '1.2.3'91 assert record.save == head.id92 assert record.machine == machine.name()93 assert record.notes == ''94 assert record.files.len == 195 assert record.files[0].platform == 'macos-aarch64'96 assert record.files[0].name == name97 assert record.files[0].size == 398 assert record.files[0].blake3 == storage.hash_hex('koh'.bytes())99 assert record.files[0].sha256.len == 64100 101 assert backend.get(latest_key('proj'))!.body.bytestr() == '1.2.3'102}103104105106fn test_a_publish_retries_after_a_partial_upload() {107 base := scratch('retry')!108 defer {109 os.rmdir_all(base) or {}110 }111 mut store := open_repo('retry')!112 defer {113 store.close()114 }115 config := demo_config(store, 'macos-aarch64, linux-aarch64')!116 make_save(store, 'first')!117 mut backend := remote.new_folder(os.join_path(base, 'remote'))!118119 publish(store, mut backend, 'proj', config, '1.2.3', '')!120 121 122 backend.delete(release_key('proj', '1.2.3'))!123 backend.delete(latest_key('proj'))!124 again := publish(store, mut backend, 'proj', config, '1.2.3', '')!125 assert again.files.len == 2126 assert backend.get(release_key('proj', '1.2.3'))!.found127 assert backend.get(latest_key('proj'))!.body.bytestr() == '1.2.3'128 assert backend.get(file_key('proj', '1.2.3', 'macos-aarch64', 'demo-1.2.3-macos-aarch64'))!.found129 assert backend.get(file_key('proj', '1.2.3', 'linux-aarch64', 'demo-1.2.3-linux-aarch64'))!.found130}131132133134fn test_two_platforms_building_to_one_file_are_refused() {135 base := scratch('collision')!136 defer {137 os.rmdir_all(base) or {}138 }139 mut store := open_repo('collision')!140 defer {141 store.close()142 }143 write(store.root, 'VERSION', '1.2.3\n')!144 145 write(store.root, 'koh.build', 'command = mkdir -p out && printf koh > out/demo146output = out/demo147platforms = macos-aarch64, linux-aarch64148')!149 config := build.read_config(store.root)!150 make_save(store, 'first')!151 mut backend := remote.new_folder(os.join_path(base, 'remote'))!152153 if _ := publish(store, mut backend, 'proj', config, '1.2.3', '') {154 assert false, 'two platforms building to one file must be refused'155 } else {156 assert err.msg().contains('two platforms build to the same file')157 }158 assert !os.exists(os.join_path(store.root, 'out'))159 assert backend.list('proj/releases')!.len == 0160}161162163164fn test_a_different_file_for_a_published_version_is_refused() {165 base := scratch('changed')!166 defer {167 os.rmdir_all(base) or {}168 }169 mut store := open_repo('changed')!170 defer {171 store.close()172 }173 config := demo_config(store, 'macos-aarch64')!174 make_save(store, 'first')!175 mut backend := remote.new_folder(os.join_path(base, 'remote'))!176 name := 'demo-1.2.3-macos-aarch64'177 backend.put_if_absent(file_key('proj', '1.2.3', 'macos-aarch64', name), 'other'.bytes())!178179 if _ := publish(store, mut backend, 'proj', config, '1.2.3', '') {180 assert false, 'a different file for a published version must be refused'181 } else {182 assert err.msg().contains('already has a different ${name}')183 }184 185 assert !backend.get(release_key('proj', '1.2.3'))!.found186 assert !backend.get(latest_key('proj'))!.found187}188189190fn test_a_failed_build_uploads_nothing() {191 base := scratch('failed')!192 defer {193 os.rmdir_all(base) or {}194 }195 mut store := open_repo('failed')!196 defer {197 store.close()198 }199 200 201 write(store.root, 'VERSION', '1.2.3\n')!202 write(store.root, 'koh.build', 'command = mkdir -p out && printf koh > out/demo-1.2.3-macos-aarch64203output = out/demo-{version}-{platform}204platforms = macos-aarch64, linux-aarch64205')!206 config := build.read_config(store.root)!207 make_save(store, 'first')!208 mut backend := remote.new_folder(os.join_path(base, 'remote'))!209210 if _ := publish(store, mut backend, 'proj', config, '1.2.3', '') {211 assert false, 'a build that produced nothing must stop the release'212 } else {213 assert err.msg().contains('did not create')214 }215 assert backend.list('proj/releases')!.len == 0216}217218219220fn test_a_release_file_name_kepr_would_refuse_is_refused() {221 assert good_name('demo-1.2.3-android-arm64.apk')222 assert good_name('Haven.app.zip')223 assert !good_name('')224 assert !good_name('.')225 assert !good_name('..')226 assert !good_name('a/b')227 assert !good_name('a\\b')228 assert !good_name('a"b')229 assert !good_name('a\x07b')230 assert !good_name('x'.repeat(256))231}232233fn test_the_notes_rule_is_koh_2s() {234 check_notes('')!235 check_notes(' ')!236 check_notes('fixed the crash on startup when the config file was missing entirely')!237 if _ := check_notes('too short to say anything at all') {238 assert false, 'notes under 40 characters must be refused'239 }240 if _ := check_notes('this note is long enough in characters but few words') {241 assert false, 'notes under 12 words must be refused'242 }243}