./mkiso.sh from tools/posix/ failed with "missing build/kernel/kernel -- run make first" on a tree that was built perfectly well: the paths were relative to the caller's directory rather than the repo. mkiso.sh and schcom_test.sh now resolve the root from their own location, the way memory-sync.sh already did. An output path given to mkiso.sh is made absolute first, so `mkiso.sh out.iso` still writes where the caller is standing and not where the repo is. mutate.sh had it worse: it began with `cd /mnt/d/DEV/SchOS`, an absolute WSL path from the Windows machine it was written on, so it could not run against any other checkout at all. Fixing that exposed why it mattered. Every one of its nine mutations reported "rejected at compile time" -- and none of them had reached a compiler. The host tools moved to build/tools/ during the bt/ -> tools/ migration and this script still asked for build/mkschfs, so make answered "No rule to make target" and the harness read that as the mutation being caught by the build. A mutation test whose every case is silently skipped is precisely the thing its own header warns about: a test that cannot fail, believed anyway. With the paths corrected all nine are caught by the suites, which is what this was always supposed to demonstrate: file_read ignores the end-of-file clamp -> extracted file differs dirent_walk advances past the next entry -> entries lost dir_lookup returns the wrong objectid -> fsck: no such file extents point one block past their data -> mkschfs write fails an overwrite drops the tail of the split -> FAIL: CoW keeps the run truncate leaves stale bytes in the tail block -> FAIL: reads as zeros committed frees never return to the free map -> FAIL: create hog -> -3 hard links do not bump nlink -> FAIL: nlink=1, expected 2 data checksum covers only the first 16 bytes -> FAIL: tree walk The build failure is now printed rather than swallowed, so the next time a target is renamed it says so instead of reporting nine passes. Two mutations also reported a bare "caught:" with no reason -- the suite had failed but nothing matched the quote patterns -- so the fallback is now the last thing the suite actually said. schfs-check and schfs-test still pass, and the tree restores clean.
163 lines
6.3 KiB
Shell
163 lines
6.3 KiB
Shell
#!/bin/sh
|
|
# Mutation-test the SchFS suites: inject a bug, confirm the tests report it,
|
|
# restore. A test that cannot fail is worse than no test, because it is
|
|
# believed. Run by hand; not part of the normal build.
|
|
#
|
|
# Every mutation is verified to have actually changed the file before the
|
|
# suite runs. A sed pattern that silently stops matching -- after a refactor,
|
|
# say -- would otherwise look exactly like a passing mutation test, which is
|
|
# the same failure this script exists to catch.
|
|
set -e
|
|
|
|
# Run from anywhere. This used to `cd /mnt/d/DEV/SchOS` -- an absolute WSL path
|
|
# from the Windows machine this was written on, which meant the script could not
|
|
# run on any other checkout at all.
|
|
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
repo=$(CDPATH= cd -- "$script_dir/../.." && pwd)
|
|
cd "$repo"
|
|
|
|
|
|
# Where the host tools land. These moved to build/tools/ in the bt/ -> tools/
|
|
# migration and this script kept the old names; make then answered "No rule to
|
|
# make target", which the harness reported as "rejected at compile time". Every
|
|
# mutation looked handled and not one of them ever reached a test.
|
|
MKSCHFS=build/tools/mkschfs
|
|
FSCK=build/tools/schfs_fsck
|
|
OBJT=build/tools/schfs_objtest
|
|
|
|
OBJ=kernel/fs/schfs/obj.c
|
|
OBJW=kernel/fs/schfs/objw.c
|
|
SPACE=kernel/fs/schfs/space.c
|
|
MK=tools/mkschfs.c
|
|
cp $OBJ /tmp/obj.orig
|
|
cp $OBJW /tmp/objw.orig
|
|
cp $SPACE /tmp/space.orig
|
|
cp $MK /tmp/mk.orig
|
|
|
|
restore() {
|
|
cp /tmp/obj.orig $OBJ
|
|
cp /tmp/objw.orig $OBJW
|
|
cp /tmp/space.orig $SPACE
|
|
cp /tmp/mk.orig $MK
|
|
}
|
|
trap restore EXIT
|
|
|
|
FAILED=0
|
|
|
|
# Quote the line that says why, falling back to the last thing the suite said.
|
|
# Two mutations used to report a bare "caught:" with nothing after it -- the
|
|
# suite had failed, but nothing in it matched the patterns below, so the
|
|
# evidence was an exit status and an empty string.
|
|
why() {
|
|
line=$(grep -aE 'FAIL|Only in|differ|loop|error|assert' "$1" | head -1)
|
|
[ -n "$line" ] || line=$(grep -av '^[[:space:]]*$' "$1" | tail -1)
|
|
printf '%s' "$line" | sed 's/^ *//'
|
|
}
|
|
|
|
# try <description> <file> <sed-expression> [suite]
|
|
#
|
|
# suite is "image" (the default: build an image and diff it against the source
|
|
# tree) or "obj" (the in-memory object-layer test). They catch different things:
|
|
# the image round-trip cannot see a bug that only shows up after a delete,
|
|
# because it never deletes anything.
|
|
try() {
|
|
desc=$1; file=$2; expr=$3; suite=${4:-image}
|
|
restore
|
|
before=$(cksum < "$file")
|
|
sed -i "$expr" "$file"
|
|
after=$(cksum < "$file")
|
|
|
|
echo "mutation: $desc"
|
|
if [ "$before" = "$after" ]; then
|
|
echo " BROKEN MUTATION -- the pattern no longer matches anything"
|
|
FAILED=1
|
|
return
|
|
fi
|
|
|
|
if [ "$suite" = obj ]; then
|
|
if ! make "$OBJT" > /tmp/mk.log 2>&1; then
|
|
echo " rejected at compile time"
|
|
sed -n '$p' /tmp/mk.log | sed 's/^/ /'
|
|
return
|
|
fi
|
|
if "./$OBJT" > /tmp/rt.log 2>&1; then
|
|
echo " NOT CAUGHT -- the suite passed with this bug present"
|
|
FAILED=1
|
|
else
|
|
echo " caught: $(why /tmp/rt.log)"
|
|
fi
|
|
return
|
|
fi
|
|
|
|
if ! make "$MKSCHFS" "$FSCK" > /tmp/mk.log 2>&1; then
|
|
echo " rejected at compile time"
|
|
sed -n '$p' /tmp/mk.log | sed 's/^/ /'
|
|
return
|
|
fi
|
|
if sh tools/posix/schfs_roundtrip.sh "./$MKSCHFS" "./$FSCK" \
|
|
> /tmp/rt.log 2>&1; then
|
|
echo " NOT CAUGHT -- the suite passed with this bug present"
|
|
FAILED=1
|
|
else
|
|
echo " caught: $(why /tmp/rt.log)"
|
|
fi
|
|
}
|
|
|
|
# Reads run past end-of-file, so every file comes back padded.
|
|
try "file_read ignores the end-of-file clamp" $OBJ \
|
|
's|if (off + n > in.size)|if (0)|'
|
|
|
|
# The dirent walk skips forward too far, losing entries in any packed list.
|
|
try "dirent_walk advances past the next entry" $OBJ \
|
|
's| off += step;| off += step + 8;|'
|
|
|
|
# Directory lookup returns the neighbouring object.
|
|
try "dir_lookup returns the wrong objectid" $OBJ \
|
|
's| f->ino = d->objectid;| f->ino = d->objectid + 1;|'
|
|
|
|
# Every extent points one block past its data.
|
|
#
|
|
# Note this replaced an earlier mutation that shortened the extent by a block.
|
|
# That one was NOT caught, and correctly so: an extent covering too little just
|
|
# leaves the tail unmapped, and schfs_file_append then allocates it separately,
|
|
# so the file still reads back byte for byte. The design absorbs it. Pointing
|
|
# at the wrong block cannot be absorbed.
|
|
try "extents point one block past their data" $OBJW \
|
|
's| e.disk_block = first;| e.disk_block = first + 1;|'
|
|
|
|
# Copy-on-write splits an extent into head, replacement and tail. Losing the
|
|
# tail leaves everything after the rewritten block unmapped, so it silently
|
|
# reads back as zeros -- data loss that looks exactly like a sparse file.
|
|
try "an overwrite drops the tail of the extent it split" $OBJW \
|
|
's| if (fblk + 1 < start + e.nblocks) {| if (0) {|' obj
|
|
|
|
# Truncating down and growing again must read zeros. Skipping the zeroing of
|
|
# the partial last block hands back whatever the file used to hold there --
|
|
# which, across a delete and a reallocation, is somebody else's data.
|
|
try "truncate leaves stale bytes in the last partial block" $OBJW \
|
|
's| memset((char \*)dblk + tail, 0, SCHFS_BLKSIZE - tail);| /* mutated */;|' obj
|
|
|
|
# Deferred frees never rejoin the free map, so every delete leaks. Nothing that
|
|
# only reads or only writes can see this; it takes a test that deletes
|
|
# everything and then checks the free count.
|
|
try "committed frees are never returned to the free map" $SPACE \
|
|
's| inuse\[w\] &= ~pend\[w\];| /* mutated */;|' obj
|
|
|
|
# Hard links are not counted, so dedup is indistinguishable from duplication
|
|
# and only the explicit nlink assertion can tell the difference.
|
|
try "hard links do not bump nlink" $MK \
|
|
's|^ in.nlink++;| /* mutated */;|'
|
|
|
|
# Data checksums cover only the head of a block, so tail corruption is missed.
|
|
try "data checksum covers only the first 16 bytes" $OBJW \
|
|
's|uint32_t c = schfs_crc32c(0, data, SCHFS_BLKSIZE);|uint32_t c = schfs_crc32c(0, data, 16);|'
|
|
|
|
restore
|
|
make build/mkschfs build/schfs_fsck > /dev/null 2>&1
|
|
echo
|
|
if [ $FAILED -eq 0 ]; then
|
|
echo "all mutations were caught"
|
|
else
|
|
echo "SOME MUTATIONS WERE NOT CAUGHT -- the suite has a blind spot"
|
|
exit 1
|
|
fi
|