tools: group scripts by what runs them
tools/windows/ .ps1 (pwsh, so usable from Linux and macOS too)
tools/posix/ .sh
tools/python/ .py
tools/ .c and .pl
The .c and .pl files stay at the top level because they are not
host-specific: the build compiles or invokes them on every platform.
Call sites updated in the Makefile, ports/mk/port.mk, docs and the
project memory. The one thing that does not survive a move on its own is
a script deriving the repo root from its own location -- all nine
PowerShell tools used Split-Path $PSScriptRoot -Parent, and memory-sync.sh
used dirname/.., both of which quietly become tools/ one level deeper.
Two levels now.
Two pre-existing Windows failures surfaced while verifying, both real and
neither caused by the move:
- schfs_fsck.c did not compile under mingw at all: --extract uses
POSIX mkdir(path,mode), symlink() and link(), none of which mingw
has. That took the whole tool with it, and `make schfs-check` with
it. mkdir maps to _mkdir; symlink and link fail loudly rather than
writing a regular file in their place, because an extracted tree
that differs from the image while reporting success defeats the
point of --extract.
- the round-trip test then failed at `ln -s`, since Git Bash cannot
create symlinks without developer mode. It skips with a message now.
Not a fudge: mkschfs's mingw build cannot see a symlink either, so
the test would have compared a tree with none against an image with
none and called it a pass -- coverage reporting itself as present
while being absent.
Verified on Windows: build clean, all seven bootcheck suites green,
make schfs-check passes.
This commit is contained in:
parent
531ef120ef
commit
754d96ebc5
26 changed files with 127 additions and 39 deletions
136
tools/posix/mutate.sh
Normal file
136
tools/posix/mutate.sh
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
#!/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
|
||||
cd /mnt/d/DEV/SchOS
|
||||
|
||||
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
|
||||
|
||||
# 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 build/schfs_objtest > /tmp/mk.log 2>&1; then
|
||||
echo " rejected at compile time"
|
||||
return
|
||||
fi
|
||||
if ./build/schfs_objtest > /tmp/rt.log 2>&1; then
|
||||
echo " NOT CAUGHT -- the suite passed with this bug present"
|
||||
FAILED=1
|
||||
else
|
||||
echo " caught: $(grep -aE 'FAIL' /tmp/rt.log | head -1 | sed 's/^ *//')"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
if ! make build/mkschfs build/schfs_fsck > /tmp/mk.log 2>&1; then
|
||||
echo " rejected at compile time"
|
||||
return
|
||||
fi
|
||||
if sh tools/schfs_roundtrip.sh ./build/mkschfs ./build/schfs_fsck \
|
||||
> /tmp/rt.log 2>&1; then
|
||||
echo " NOT CAUGHT -- the suite passed with this bug present"
|
||||
FAILED=1
|
||||
else
|
||||
echo " caught: $(grep -aE 'FAIL|Only in|differ|loop' /tmp/rt.log | head -1 | sed 's/^ *//')"
|
||||
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
|
||||
Loading…
Reference in a new issue