release_test.v

7.9 KB · raw

1//! Folder-backend tests for publishing: what lands in the bucket, what a retry2//! does, what a second build of the same version does, and the notes rule.34module 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}4344// The build writes the file `output` names, by hand: only `output` takes the45// {version} and {platform} tokens.46fn 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	// The record is what makes the version exist, and latest moves last.96	assert backend.get(latest_key('proj'))!.body.bytestr() == '1.2.3'97}9899// An interrupted publish leaves files behind; running it again finishes it,100// because the same bytes are the same release.101fn 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	// Stand in for a run that stopped before the record and the pointer: both116	// are removed, the uploaded files stay.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}126127// Two platforms that build to one file are refused before anything runs: no128// build, no upload, and the bucket is untouched.129fn 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	// No {platform}, so both platforms would write the same file.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}156157// A published version never changes: a second build with different bytes for158// the same platform is refused before anything is uploaded.159fn 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	// Nothing else was written: no record, and latest never moved.180	assert !backend.get(release_key('proj', '1.2.3'))!.found181	assert !backend.get(latest_key('proj'))!.found182}183184// One platform failing stops the release before the first upload.185fn 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	// The command writes only the macos file, so the linux build produces195	// nothing and the whole release stops there.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}212213// A folder where a file belongs, or a name Kepr would refuse to read, is214// refused here: Kepr refuses the whole repo's sync over one bad record.215fn 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}