cmd_doctor_test.v

9.1 KB · raw

1module cli23import os4import compress.zlib5import crypto.sha2566import json27import src.exchange8import src.release9import src.remote10import src.events11import src.history12import src.snapshot13import src.storage1415fn test_doctor_checks_cycles_and_missing_heads_without_walking_forever() {16	saves := [history.Save{ id: 'aaaaaaaaaaaa', parent: 'bbbbbbbbbbbb' },17		history.Save{ id: 'bbbbbbbbbbbb', parent: 'aaaaaaaaaaaa' }]18	assert doctor_history(saves, 'cccccccccccc').any(it.contains('head'))19	assert doctor_history(saves, 'aaaaaaaaaaaa').any(it.contains('cycle'))20	assert doctor_history([history.Save{ id: 'aaaaaaaaaaaa', parent: 'cccccccccccc' }],21		'aaaaaaaaaaaa').any(it.contains('missing parent'))22	assert doctor_history([]history.Save{}, '').len == 023	assert doctor_history([history.Save{ id: 'aaaaaaaaaaaa' }], '').any(it.contains('no head'))24}2526fn test_doctor_checks_off_line_paths_and_file_directory_collisions() {27	mut report := DoctorJson{}28	doctor_files('aaaaaaaaaaaa', [snapshot.FileEntry{ path: '../unsafe' },29		snapshot.FileEntry{ path: 'a' }, snapshot.FileEntry{ path: 'a/b' }], []events.Event{},30		mut report)31	assert report.problems.any(it.contains('unsafe path'))32	assert report.problems.any(it.contains('file and a directory'))33}3435fn test_doctor_reports_unsafe_save_paths_with_a_quoted_strip_command() {36	dir := os.join_path(os.temp_dir(), 'koh3_doctor_paths_${os.getpid()}')37	os.rmdir_all(dir) or {}38	os.mkdir_all(dir)!39	defer {40		os.rmdir_all(dir) or {}41	}42	store := storage.create(dir)!43	defer {44		store.close()45	}46	hash := store.put_object('unsafe'.bytes())!47	store.begin()!48	history.import_save(store, history.Save{49		id: 'badbadbadbad'50	}, [snapshot.FileEntry{51		path: "../June's file"52		hash: hash53	}])!54	history.set_head(store, 'badbadbadbad')!55	store.end()!56	mut report := DoctorJson{}57	doctor_files('badbadbadbad', history.files_of(store, 'badbadbadbad')!, []events.Event{}, mut report)58	assert report.problems == [59		"save [badbadbadbad] has unsafe path ../June's file — koh strip '../June'\\''s file'",60	]61}6263struct DoctorReadOnly {64mut:65	items map[string][]u866}6768fn (mut backend DoctorReadOnly) get(key string) !remote.Object {69	if body := backend.items[key] {70		return remote.Object{ found: true, body: body }71	}72	return remote.Object{}73}7475fn (mut backend DoctorReadOnly) list(prefix string) ![]string {76	return backend.items.keys().filter(it.starts_with('${prefix}/'))77}7879fn (mut backend DoctorReadOnly) put_if_absent(key string, body []u8) !bool {80	panic('Doctor attempted a remote write')81}8283fn (mut backend DoctorReadOnly) put_if_match(key string, body []u8, tag string) !bool {84	panic('Doctor attempted a remote write')85}8687fn (mut backend DoctorReadOnly) delete(key string) ! {88	panic('Doctor attempted a remote delete')89}9091fn test_doctor_remote_audits_unreferenced_objects_and_release_checksums_without_writes() {92	data := 'file'.bytes()93	hash := storage.hash_hex(data)94	mut backend := DoctorReadOnly{95		items: {96			'repo/head':                             'aaaaaaaaaaaa'.bytes()97			'repo/saves/aaaaaaaaaaaa':               json2.encode(exchange.SaveRecord{98				id:    'aaaaaaaaaaaa'99				files: [exchange.FileRecord{ path: 'file', hash: hash }]100			}).bytes()101			'repo/objects/${hash}':                  zlib.compress(data)!102			'repo/releases/latest':                  '1.0.0'.bytes()103			'repo/releases/1.0.0/release.json':      json2.encode(release.Record{104				repo:    'repo'105				version: '1.0.0'106				save:    'aaaaaaaaaaaa'107				files:   [release.File{108					platform: 'linux-aarch64'109					name:     'app'110					size:     data.len111					blake3:   hash112					sha256:   sha256.sum256(data).hex()113				}]114			}).bytes()115			'repo/releases/1.0.0/linux-aarch64/app': data116		}117	}118	mut report := DoctorJson{ head_id: 'aaaaaaaaaaaa' }119	doctor_remote_content(mut backend, 'repo', mut report)!120	assert report.problems.len == 0121	assert report.remote_saves == 1122	assert report.remote_releases == 1123	backend.items['repo/objects/${storage.hash_hex('unreferenced'.bytes())}'] = 'bad zlib'.bytes()124	backend.items['repo/releases/1.0.0/linux-aarch64/app'] = 'corrupt'.bytes()125	mut damaged := DoctorJson{}126	doctor_remote_content(mut backend, 'repo', mut damaged)!127	assert damaged.problems.any(it.contains('damaged remote object'))128	assert damaged.problems.any(it.contains('checksum verification'))129}130131fn test_doctor_reports_event_errors_strip_residue_and_flags() {132	mut report := DoctorJson{}133	saves := [history.Save{ id: 'aaaaaaaaaaaa' }]134	event_list := [events.Event{ id: 'strip', kind: 'strip', path: 'private' },135		events.Event{ id: 'flag', kind: 'flag', save_id: 'aaaaaaaaaaaa' },136		events.Event{ id: 'bad', kind: 'unknown' }]137	doctor_events(event_list, saves, mut report)138	doctor_files(saves[0].id, [snapshot.FileEntry{ path: 'private/file' }], event_list, mut report)139	assert report.flagged_saves == 1140	assert report.problems.any(it.contains('unknown kind'))141	assert report.problems.any(it.contains('stripped path'))142}143144struct DoctorMemory {145mut:146	items map[string][]u8147}148149fn (mut backend DoctorMemory) get(key string) !remote.Object {150	if body := backend.items[key] {151		return remote.Object{ found: true, body: body }152	}153	return remote.Object{}154}155156fn (mut backend DoctorMemory) list(prefix string) ![]string {157	return backend.items.keys().filter(it.starts_with('${prefix}/'))158}159160fn (mut backend DoctorMemory) put_if_absent(key string, body []u8) !bool {161	if key in backend.items {162		return false163	}164	backend.items[key] = body165	return true166}167168fn (mut backend DoctorMemory) put_if_match(key string, body []u8, tag string) !bool {169	return false170}171172fn (mut backend DoctorMemory) delete(key string) ! {173	backend.items.delete(key)174}175176fn doctor_store(name string) !(storage.Store, string) {177	dir := os.join_path(os.temp_dir(), 'koh3_doctor_${name}_${os.getpid()}')178	os.rmdir_all(dir) or {}179	os.mkdir_all(dir)!180	return storage.create(dir)!, dir181}182183fn test_doctor_fetches_missing_parents_from_the_remote_without_writing_to_it() {184	store, dir := doctor_store('fetch')!185	defer {186		store.close()187		os.rmdir_all(dir) or {}188	}189	data := 'parent'.bytes()190	hash := storage.hash_hex(data)191	mut backend := DoctorReadOnly{192		items: {193			'repo/saves/aaaaaaaaaaaa': json2.encode(exchange.SaveRecord{194				id:    'aaaaaaaaaaaa'195				files: [exchange.FileRecord{ path: 'file', hash: hash }]196			}).bytes()197			'repo/objects/${hash}':    zlib.compress(data)!198		}199	}200	store.db.exec('PRAGMA foreign_keys=OFF')!201	store.begin()!202	history.import_save(store, history.Save{ id: 'bbbbbbbbbbbb', parent: 'aaaaaaaaaaaa' },203		[]snapshot.FileEntry{})!204	history.set_head(store, 'bbbbbbbbbbbb')!205	store.end()!206	store.db.exec('PRAGMA foreign_keys=ON')!207	missing := missing_saves(history.all_saves(store)!, 'bbbbbbbbbbbb')208	assert missing == ['aaaaaaaaaaaa']209	assert exchange.fetch_missing(store, mut backend, 'repo', missing)! == 1210	assert missing_saves(history.all_saves(store)!, 'bbbbbbbbbbbb').len == 0211	assert store.get_object(hash)! == data212	assert exchange.fetch_missing(store, mut backend, 'repo', ['cccccccccccc']) or {213		assert err.msg().contains('does not have save')214		0215	} == 0216}217218fn test_doctor_reapplies_recorded_strips_and_finds_the_only_tip() {219	store, dir := doctor_store('strip')!220	defer {221		store.close()222		os.rmdir_all(dir) or {}223	}224	secret := store.put_object('secret'.bytes())!225	kept := store.put_object('kept'.bytes())!226	store.begin()!227	history.import_save(store, history.Save{ id: 'aaaaaaaaaaaa' }, [228		snapshot.FileEntry{ path: 'keep', hash: kept },229		snapshot.FileEntry{ path: 'private/key', hash: secret },230	])!231	history.import_save(store, history.Save{ id: 'bbbbbbbbbbbb', parent: 'aaaaaaaaaaaa' },232		[snapshot.FileEntry{ path: 'keep', hash: kept }])!233	events.strip(store, '', 'private')!234	store.end()!235	assert only_tip(store) or { '' } == 'bbbbbbbbbbbb'236	mut report := DoctorJson{}237	doctor_files('aaaaaaaaaaaa', history.files_of(store, 'aaaaaaaaaaaa')!, events.all(store)!, mut238		report)239	assert report.strip_residue240	assert exchange.reapply_strips(store)! == 1241	assert history.files_of(store, 'aaaaaaaaaaaa')!.map(it.path) == ['keep']242	assert !store.has_object(secret)!243	assert events.strip_events(store)!.len == 1244}245246fn test_doctor_restores_objects_from_working_files_and_resends_them_to_the_remote() {247	store, dir := doctor_store('objects')!248	defer {249		store.close()250		os.rmdir_all(dir) or {}251	}252	data := 'content'.bytes()253	hash := store.put_object(data)!254	store.begin()!255	history.import_save(store, history.Save{ id: 'aaaaaaaaaaaa' }, [256		snapshot.FileEntry{ path: 'app', hash: hash },257	])!258	history.set_head(store, 'aaaaaaaaaaaa')!259	store.end()!260	store.db.exec("UPDATE objects SET data = x'00'")!261	problems := store.check_objects()!262	assert problems.map(it.hash) == [hash]263	assert local_copy(store, hash) == none264	os.write_file(os.join_path(dir, 'app'), 'content')!265	store.repair_object(hash, local_copy(store, hash) or { panic('no working copy') })!266	assert store.check_objects()!.len == 0267	store.repair_object(hash, 'other'.bytes()) or {268		assert err.msg().contains('does not match')269	}270	mut backend := DoctorMemory{271		items: {272			'repo/objects/${hash}': 'damaged'.bytes()273		}274	}275	assert exchange.resend(store, mut backend, 'repo', [hash], ['aaaaaaaaaaaa'])! == 2276	assert zlib.decompress(backend.items['repo/objects/${hash}'])! == data277	assert exchange.decode_save(backend.items['repo/saves/aaaaaaaaaaaa'])!.files[0].hash == hash278}