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:
Nexoniarz 2026-08-09 14:38:02 +02:00
commit 754d96ebc5
26 changed files with 127 additions and 39 deletions

View file

@ -1,136 +0,0 @@
#!/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