release_test.v

6.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// A published version never changes: a second build with different bytes for128// the same platform is refused before anything is uploaded.129fn 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	// Nothing else was written: no record, and latest never moved.150	assert !backend.get(release_key('proj', '1.2.3'))!.found151	assert !backend.get(latest_key('proj'))!.found152}153154// One platform failing stops the release before the first upload.155fn 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	// The command writes only the macos file, so the linux build produces165	// nothing and the whole release stops there.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}182183// A folder where a file belongs, or a name Kepr would refuse to read, is184// refused here: Kepr refuses the whole repo's sync over one bad record.185fn 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}