SchOS/tools/posix/schcom_test.sh
Nexoniarz 141f2785ab tools: scripts that run from anywhere, and a mutation test that ran nothing
./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.
2026-08-11 10:39:58 +02:00

154 lines
5.7 KiB
Shell

#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
#
# Run Schcom and a client inside SchOS, then check the actual pixels from the
# host with QEMU's screendump. The host has no connection to the compositor, so
# it is a real oracle: if the block is the right colour in the right place, the
# buffer really was shared, composited and presented.
#
# One display device on purpose (-vga std, no virtio-gpu): with two, screendump
# captures whichever QEMU considers active, which need not be the one /dev/fb0
# maps.
set -e
# Run from anywhere: resolve the repo root from this script's own location
# rather than trusting the caller's working directory. Everything below is
# relative to it, and running from tools/posix/ used to fail with "missing
# build/kernel/kernel -- run make first" on a tree that was perfectly built.
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
repo=$(CDPATH= cd -- "$script_dir/../.." && pwd)
cd "$repo"
BUILD=${BUILD:-build}
mkdir -p "$BUILD"
LOG=$BUILD/schcom-test.log
PPM=${1:-$BUILD/schcom.ppm}
rm -f "$LOG" "$PPM"
# The firmware varstore is written by OVMF, so this run gets its own copy
# rather than sharing the pristine one every other run starts from.
cp -f build/OVMF_VARS.fd build/schcom-OVMF_VARS.fd
PORT=$(python3 - <<'EOF'
import socket
s=socket.socket(); s.bind(('127.0.0.1',0)); print(s.getsockname()[1]); s.close()
EOF
)
(
sleep 26
printf 'schcom --frames 24 &\n'; sleep 6
printf 'schcom-demo --colour 0x00C04020 --at 100 80 --size 240 160 --wait 40 &\n'; sleep 10
printf 'schcom-demo --colour 0x002060C0 --at 260 180 --size 240 160 --wait 30 &\n'; sleep 12
printf '\n'
sleep 40
) | timeout -k 5 120 qemu-system-x86_64 \
-machine pc-i440fx-9.2 -cpu qemu64,+rdtscp \
-drive if=pflash,format=raw,unit=0,readonly=on,file=build/OVMF_CODE.fd \
-drive if=pflash,format=raw,unit=1,file=build/schcom-OVMF_VARS.fd \
-drive file=fat:rw:build/esp,index=0,media=disk \
-drive file=build/schfs.img,format=raw,index=1,media=disk \
-m 1G -smp 1 \
-display none -vga std \
-monitor tcp:127.0.0.1:$PORT,server,nowait \
-chardev stdio,id=c0,logfile="$LOG" -serial chardev:c0 \
>/dev/null 2>&1 &
QPID=$!
# Wait until *both* clients have committed, then photograph. Breaking on the
# first one screenshots a desktop the second client has not reached yet, which
# looks exactly like a compositor that cannot handle two clients.
for i in $(seq 1 90); do
[ "$(grep -ac 'committed' "$LOG" 2>/dev/null)" -ge 2 ] && break
sleep 1
done
sleep 2
python3 - "$PORT" "$PPM" <<'EOF'
import socket, sys, time
port, ppm = int(sys.argv[1]), sys.argv[2]
s = socket.create_connection(('127.0.0.1', port), timeout=10)
time.sleep(0.5)
s.sendall(b'screendump ' + ppm.encode() + b'\n')
time.sleep(2.0)
s.close()
EOF
sleep 1
kill -9 $QPID 2>/dev/null || true
wait 2>/dev/null || true
echo "=== guest output ==="
sed 's/\x1b\[[0-9;=]*[A-Za-z]//g' "$LOG" | tr -d '\r' | grep -aE 'schcom|surface|committed|listening' | head -12
echo "=== pixel check (host oracle) ==="
python3 - "$PPM" <<'EOF'
import sys
path = sys.argv[1]
try:
data = open(path, 'rb').read()
except FileNotFoundError:
print("FAIL: no screendump produced"); sys.exit(1)
# Binary PPM: P6, whitespace-separated width height maxval, then RGB triples.
def token(buf, i):
while buf[i:i+1].isspace(): i += 1
j = i
while not buf[j:j+1].isspace(): j += 1
return buf[i:j], j
if data[:2] != b'P6':
print("FAIL: not a P6 PPM"); sys.exit(1)
i = 2
w, i = token(data, i); h, i = token(data, i); mx, i = token(data, i)
w, h = int(w), int(h)
px = data[i+1:]
print(f" screendump {w}x{h}")
def at(x, y):
o = (y*w + x)*3
return px[o], px[o+1], px[o+2]
def near(got, want, tol=8):
return all(abs(a-b) <= tol for a, b in zip(got, want))
fails = 0
inside = at(150, 120) # middle of the 240x160 block at 100,80
if near(inside, (0xC0, 0x40, 0x20)):
print(f" [ok] surface pixel at 150,120 is {inside} -- the client's colour")
else:
print(f" FAIL: 150,120 is {inside}, expected (192, 64, 32)"); fails += 1
desktop = at(20, 20) # compositor background 0x203040
if near(desktop, (0x20, 0x30, 0x40)):
print(f" [ok] desktop pixel at 20,20 is {desktop} -- compositor background")
else:
print(f" FAIL: 20,20 is {desktop}, expected (32, 48, 64)"); fails += 1
border = at(100, 80) # top-left of the first surface
if near(border, (0xFF, 0xFF, 0xFF)):
print(f" [ok] border pixel at 100,80 is {border} -- surface landed exactly")
else:
print(f" FAIL: 100,80 is {border}, expected white border"); fails += 1
# Second client, created later, so it is above the first where they overlap.
second = at(460, 320) # inside surface 2, clear of the pointer
if near(second, (0x20, 0x60, 0xC0)):
print(f" [ok] second surface at 460,320 is {second} -- two clients composited")
else:
print(f" FAIL: 460,320 is {second}, expected (32, 96, 192)"); fails += 1
# 260,180..340,240 is where the two overlap. The later surface must win: if
# stacking were ignored the first client's colour would show through here, and
# a compositor that cannot order two windows cannot host a window manager.
overlap = at(300, 220)
if near(overlap, (0x20, 0x60, 0xC0)):
print(f" [ok] overlap at 300,220 is {overlap} -- newer surface on top")
elif near(overlap, (0xC0, 0x40, 0x20)):
print(f" FAIL: overlap shows the older surface -- stacking order ignored"); fails += 1
else:
print(f" FAIL: overlap at 300,220 is {overlap}, expected the newer surface"); fails += 1
print("schcom end-to-end: PASSED" if not fails else f"schcom end-to-end: {fails} FAILED")
sys.exit(1 if fails else 0)
EOF