snapshot_test.v

6.9 KB · raw

1module snapshot23import os4import src.storage56fn temp_root(name string) !string {7	dir := os.join_path(os.temp_dir(), 'koh3_snap_${os.getpid()}_${name}')8	os.rmdir_all(dir) or {}9	os.mkdir_all(dir, os.MkdirParams{})!10	return dir11}1213fn put(root string, rel string, text string) ! {14	full := os.join_path(root, rel)15	os.mkdir_all(os.dir(full), os.MkdirParams{})!16	os.write_file(full, text)!17}1819fn test_ignore_matches_the_documented_shapes() {20	ig := parse('21# a comment22*.log23build/24/anchored.txt25out/**26docs/*.md27')28	assert ig.matches('error.log', false)29	assert ig.matches('deep/nested/error.log', false)30	assert !ig.matches('error.txt', false)31	// A trailing slash names a directory: the directory itself and everything32	// below it, but not a file that shares the name.33	assert ig.matches('build', true)34	assert ig.matches('build/thing.o', false)35	assert ig.matches('src/build/nested.o', false)36	assert !ig.matches('build', false)37	assert ig.matches('anchored.txt', false)38	assert !ig.matches('sub/anchored.txt', false)39	assert ig.matches('out/anything/at/all', false)40	assert ig.matches('docs/readme.md', false)41	assert !ig.matches('docs/readme.txt', false)42	assert !ig.matches('src/main.v', false)43}4445fn test_hard_ignores_are_not_overridable() {46	for path in ['.koh', '.koh-wal', '.koh-shm', '.koh-v2', '.koh-v2/snapshots/x.json', '.git',47		'.git/config', 'LOG.md'] {48		assert hard_ignored(path), '${path} should be hard ignored'49	}50	assert !hard_ignored('.kohignore')51	assert !hard_ignored('src/log.md')52	// A pattern list cannot un-ignore them, and `.kohignore` itself is content.53	ig := parse('')54	assert ig.matches('.koh', false)55	assert !ig.matches('.kohignore', false)56}5758fn test_walk_skips_ignored_files_directories_and_symlinks() {59	mut store := storage.create(temp_root('walk')!)!60	root := store.root61	defer {62		store.close()63		os.rmdir_all(root) or {}64	}65	put(root, 'keep.txt', 'keep\n')!66	put(root, 'nested/deep/file.txt', 'deep\n')!67	put(root, 'skip.log', 'noise\n')!68	put(root, 'build/output.o', 'binary\n')!69	put(root, '.kohignore', '*.log\nbuild/\n')!70	os.symlink('keep.txt', os.join_path(root, 'link.txt')) or {}7172	entries := walk(store, root, load(root))!73	mut paths := []string{}74	for entry in entries {75		paths << entry.path76	}77	assert paths == ['.kohignore', 'keep.txt', 'nested/deep/file.txt']78}7980fn test_walk_records_the_executable_bit() {81	mut store := storage.create(temp_root('exec')!)!82	root := store.root83	defer {84		store.close()85		os.rmdir_all(root) or {}86	}87	put(root, 'run.sh', '#!/bin/sh\necho hi\n')!88	put(root, 'plain.txt', 'hello\n')!89	os.chmod(os.join_path(root, 'run.sh'), 0o755)!9091	mut executable := false92	mut plain_executable := true93	for entry in walk(store, root, Ignore{})! {94		if entry.path == 'run.sh' {95			executable = entry.executable96		}97		if entry.path == 'plain.txt' {98			plain_executable = entry.executable99		}100	}101	assert executable102	assert !plain_executable103}104105// The point of nanosecond timestamps: a file rewritten inside the same second106// with the same size must not reuse the cached hash, or a save would record107// content that is no longer on disk.108fn test_cache_does_not_reuse_a_same_size_same_second_edit() {109	mut store := storage.create(temp_root('cache')!)!110	root := store.root111	defer {112		store.close()113		os.rmdir_all(root) or {}114	}115	put(root, 'file.txt', 'aaaa\n')!116	first := walk(store, root, Ignore{})!117	assert first[0].hash == storage.hash_hex('aaaa\n'.bytes())118119	put(root, 'file.txt', 'bbbb\n')!120	second := walk(store, root, Ignore{})!121	assert second[0].hash == storage.hash_hex('bbbb\n'.bytes())122	assert second[0].hash != first[0].hash123}124125fn test_restore_writes_removes_and_leaves_ignored_files_alone() {126	mut store := storage.create(temp_root('restore')!)!127	root := store.root128	defer {129		store.close()130		os.rmdir_all(root) or {}131	}132	put(root, 'keep.txt', 'recorded\n')!133	put(root, 'gone/inside.txt', 'also recorded\n')!134	put(root, 'noise.log', 'ignored, must survive\n')!135	put(root, '.kohignore', '*.log\n')!136	target := walk(store, root, load(root))!137	store_objects(store, root, target)!138139	// Drift the working tree: clobber one file, delete another, and add a file140	// the recorded tree has never seen.141	os.write_file(os.join_path(root, 'keep.txt'), 'clobbered\n')!142	os.rm(full_path(root, 'gone/inside.txt'))!143	put(root, 'extra.txt', 'never saved\n')!144145	// The removal list names an ignored file (which survives), a file the146	// target still has (rewritten, not removed), and one genuinely extra file.147	report := restore_files(store, root, load(root), target,148		['extra.txt', '.kohignore', 'noise.log'])!149150	// The clobbered and the deleted file are written; the unchanged151	// .kohignore is left as it is.152	assert report.written == 2153	assert report.removed == 1154	assert os.read_file(full_path(root, 'keep.txt'))! == 'recorded\n'155	assert os.read_file(full_path(root, 'gone/inside.txt'))! == 'also recorded\n'156	assert !os.exists(full_path(root, 'extra.txt'))157	assert os.read_file(full_path(root, 'noise.log'))! == 'ignored, must survive\n'158	assert os.read_file(full_path(root, '.kohignore'))! == '*.log\n'159}160161fn test_restore_keeps_the_executable_bit() {162	mut store := storage.create(temp_root('restore_exec')!)!163	root := store.root164	defer {165		store.close()166		os.rmdir_all(root) or {}167	}168	put(root, 'run.sh', '#!/bin/sh\necho hi\n')!169	os.chmod(os.join_path(root, 'run.sh'), 0o755)!170	target := walk(store, root, Ignore{})!171	store_objects(store, root, target)!172	os.chmod(os.join_path(root, 'run.sh'), 0o644)!173174	restore_files(store, root, Ignore{}, target, [])!175	mode := (lstat(os.join_path(root, 'run.sh')) or { panic('missing') }).mode176	assert is_executable(mode)177}178179fn test_diff_reports_kinds_counts_and_binary_files() {180	mut store := storage.create(temp_root('diff')!)!181	root := store.root182	defer {183		store.close()184		os.rmdir_all(root) or {}185	}186	put(root, 'text.txt', 'one\ntwo\n')!187	put(root, 'same.txt', 'unchanged\n')!188	put(root, 'gone.txt', 'bye\n')!189	before := walk(store, root, Ignore{})!190	store_objects(store, root, before)!191192	os.write_file(os.join_path(root, 'text.txt'), 'one\nthree\nfour\n')!193	os.rm(full_path(root, 'gone.txt'))!194	os.write_file(os.join_path(root, 'added.txt'), 'new\n')!195	os.write_bytes(os.join_path(root, 'blob.bin'), [u8(0), 0x01, 0x02])!196	after := walk(store, root, Ignore{})!197198	// A save's side of a diff is bytes from the object store; only the side199	// that is the working tree reads from disk.200	deltas := diff_trees(store, Tree{201		files: before202	}, Tree{203		root:  root204		files: after205	})!206	mut by_path := map[string]Delta{}207	for delta in deltas {208		by_path[delta.path] = delta209	}210	assert deltas.len == 4211	assert by_path['text.txt'].kind == 'modified'212	assert by_path['text.txt'].added == 2213	assert by_path['text.txt'].removed == 1214	assert by_path['added.txt'].kind == 'added'215	assert by_path['gone.txt'].kind == 'removed'216	assert by_path['blob.bin'].binary217	assert by_path['blob.bin'].patch == binary_notice218	assert 'same.txt' !in by_path219}220221fn full_path(root string, rel string) string {222	return os.join_path(root, rel)223}