release_test.v

8.1 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.storage1516// Global rules must not make source fixtures depend on the developer’s home.17fn 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}4849// The build writes the file `output` names, by hand: only `output` takes the50// {version} and {platform} tokens.51fn 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	// The record is what makes the version exist, and latest moves last.101	assert backend.get(latest_key('proj'))!.body.bytestr() == '1.2.3'102}103104// An interrupted publish leaves files behind; running it again finishes it,105// because the same bytes are the same release.106fn 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	// Stand in for a run that stopped before the record and the pointer: both121	// are removed, the uploaded files stay.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}131132// Two platforms that build to one file are refused before anything runs: no133// build, no upload, and the bucket is untouched.134fn 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	// No {platform}, so both platforms would write the same file.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}161162// A published version never changes: a second build with different bytes for163// the same platform is refused before anything is uploaded.164fn 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	// Nothing else was written: no record, and latest never moved.185	assert !backend.get(release_key('proj', '1.2.3'))!.found186	assert !backend.get(latest_key('proj'))!.found187}188189// One platform failing stops the release before the first upload.190fn 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	// The command writes only the macos file, so the linux build produces200	// nothing and the whole release stops there.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}217218// A folder where a file belongs, or a name Kepr would refuse to read, is219// refused here: Kepr refuses the whole repo's sync over one bad record.220fn 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}