migrate_test.v

6.0 KB · raw

1//! Migration tests, run against a fixture made by the Koh 2 binary itself.2//!3//! The fixture is why these are worth having: it is not a guess at Koh 2's4//! format, it is the format, including the inlined small files and the object5//! store. When `koh` (Koh 2) is not on the machine these tests skip rather6//! than pretend.78module migrate910import json11import os12import src.events13import src.history14import src.snapshot15import src.storage1617struct Fixture {18	dir     string19	first   string20	second  string21	message string22}2324fn koh2_binary() ?string {25	path := os.find_abs_path_of_executable('koh') or { return none }26	return path27}2829fn sh(dir string, command string) !string {30	result := os.execute('cd ${dir} && ${command}')31	if result.exit_code != 0 {32		return error('${command} failed: ${result.output}')33	}34	return result.output35}3637fn make_fixture(koh2 string) !Fixture {38	dir := os.join_path(os.temp_dir(), 'koh3_migrate_${os.getpid()}')39	os.rmdir_all(dir) or {}40	os.mkdir_all(dir, os.MkdirParams{})!41	sh(dir, '${koh2} init')!42	// One file big enough for the object store, one small enough to be inlined.43	os.write_file(os.join_path(dir, 'big.txt'), 'x'.repeat(700))!44	os.write_file(os.join_path(dir, 'small.txt'), 'tiny\n')!45	sh(dir, '${koh2} save "first save"')!46	sh(dir, '${koh2} checkpoint "before something"')!47	os.write_file(os.join_path(dir, 'big.txt'), 'y'.repeat(700))!48	sh(dir, '${koh2} save "second save"')!4950	// The head comes from Koh 2's own state, because save IDs are hashes (their51	// directory order means nothing) and koh2 timestamps are whole seconds (so52	// a save and the checkpoint after it can share one).53	mut first := ''54	mut first_time := i64(9223372036854775807)55	for name in os.ls(os.join_path(dir, '.koh', 'snapshots'))! {56		if !name.ends_with('.json') {57			continue58		}59		record :=60			json.decode(Manifest, os.read_file(os.join_path(dir, '.koh', 'snapshots', name))!)!61		if record.timestamp < first_time {62			first_time = record.timestamp63			first = record.id64		}65	}66	return Fixture{67		dir:     dir68		first:   first69		second:  koh2_head(dir)70		message: 'first save'71	}72}7374// koh2_head reads the lane head Koh 2 recorded.75fn koh2_head(dir string) string {76	db := storage.connect(os.join_path(dir, '.koh', 'koh.db'), 1000) or { return '' }77	defer {78		db.close()79	}80	mut stmt := db.prepare('SELECT value FROM state WHERE key = ?') or { return '' }81	defer {82		stmt.finalize()83	}84	stmt.bind_text(1, 'head_main') or { return '' }85	if stmt.step() or { return '' } {86		return stmt.text(0)87	}88	return ''89}9091fn test_migrating_a_koh2_repo_keeps_every_save_exactly() {92	koh2 := koh2_binary() or { return }93	fixture := make_fixture(koh2)!94	defer {95		os.rmdir_all(fixture.dir) or {}96	}97	// Flag the first save, so a save-level flag has to become an event.98	sh(fixture.dir, '${koh2} flag ${fixture.first} --reason "sketchy"')!99100	// What the manifests say, read independently of the migration.101	mut manifests := map[string][]string{}102	for name in os.ls(os.join_path(fixture.dir, '.koh', 'snapshots'))! {103		if !name.ends_with('.json') {104			continue105		}106		record := json.decode(Manifest,107			os.read_file(os.join_path(fixture.dir, '.koh', 'snapshots', name))!)!108		mut files := []string{}109		for file in record.files {110			files << '${file.path} ${bare_hash(file.hash)}'111		}112		manifests[record.id] = files113	}114115	report := run(fixture.dir, default_lane)!116	// The fixture lives on main, because that is the lane koh2 starts on.117	assert report.lane == 'main'118	assert report.saves == 3119	assert report.checkpoints == 1120	assert report.flags == 1121	assert report.objects >= 3122123	// The swap happened: `.koh` is a file now and the old data is archived.124	assert os.is_file(os.join_path(fixture.dir, '.koh'))125	assert os.is_dir(os.join_path(fixture.dir, '.koh-v2'))126	assert os.is_file(os.join_path(fixture.dir, '.kohignore'))127128	store := storage.open(fixture.dir)!129	defer {130		store.close()131	}132	// Every save kept its ID, its files and their hashes.133	assert history.save_count(store)! == 3134	for id, expected in manifests {135		save := history.by_id(store, id) or {136			assert false, 'save ${id} did not survive the migration'137			return138		}139		assert save.machine == 'koh2'140		mut actual := []string{}141		for file in history.files_of(store, id)! {142			actual << '${file.path} ${file.hash}'143			// Every object is present and hashes back to its key.144			if _ := store.get_object(file.hash) {145			} else {146				assert false, 'object ${file.hash} is missing or corrupt'147			}148		}149		assert actual == expected150	}151	// The checkpoint mark and the parent chain came across.152	middle := history.by_id(store, fixture.first) or { panic('the first save did not survive') }153	assert (history.head(store) or { panic('no head after migrating') }).id == fixture.second154	assert middle.message == 'first save'155156	// The flag became an event, and the working tree is exactly what Koh 3157	// expects once the carried ignore file is saved.158	assert events.flag_state(store, fixture.first)!.flagged159	assert events.flag_state(store, fixture.first)!.reason == 'sketchy'160	entries := snapshot.walk(store, fixture.dir, snapshot.load(fixture.dir))!161	mut changed := []string{}162	head_files := snapshot.index_files(history.files_of(store, fixture.second)!)163	for entry in entries {164		if entry.path !in head_files {165			changed << entry.path166		} else if head_files[entry.path].hash != entry.hash {167			changed << entry.path168		}169	}170	// The only difference is the ignore file Koh 3 just carried to the root.171	assert changed == ['.kohignore']172}173174fn test_migrate_refuses_a_tree_that_does_not_match_the_last_save() {175	koh2 := koh2_binary() or { return }176	fixture := make_fixture(koh2)!177	defer {178		os.rmdir_all(fixture.dir) or {}179	}180	os.write_file(os.join_path(fixture.dir, 'big.txt'), 'unsaved change\n')!181	if _ := run(fixture.dir, default_lane) {182		assert false, 'a dirty tree must be refused'183	} else {184		assert err.msg().contains('save with koh2 first')185	}186	// Nothing was touched: still a Koh 2 repo, no archive, no staging file.187	assert os.is_dir(os.join_path(fixture.dir, '.koh'))188	assert !os.exists(os.join_path(fixture.dir, '.koh-v2'))189	assert !os.exists(os.join_path(fixture.dir, staging_file))190}