Files
SplitBit-Emulator/Tests/agree.sh
T
AnachronautandClaude Opus 5 54f5cfe8a4 Add Copy and Compare, which stream in both directions at once
Copy joins the read and write streaming services: source and destination are
both larger than Data Memory while the program owns one block. Compare reads
two files through separate blocks and ignores the bytes past a short final
block, which belong to neither file. Between them they exercise empty,
exact-block, part-block and 84,000 byte files, and the host extracts the copy
afterwards so that two native programs agreeing with each other is not the only
oracle.

Written by ChatGPT, as their headers record, along with the agree.sh section
and the manifest entry that drive them.

THIS SHOULD HAVE COME FIRST. The commit before it staged whole files rather
than the hunks it meant, so its manifest already names these two programs while
their source was still untracked - that commit will not build on its own. Left
in place rather than rewritten, since the pair is right and only their order is
wrong.

NOTES.md is their review of the streaming work. The first item in it is fixed
by the commit before this one; the rest are still open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-25 20:21:43 -04:00

207 lines
9.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Checks the two implementations of SBFS against each other, on the same disk.
#
# SplitDisk and sbfs.asm are two programs written from one specification and sharing no
# code at all - one is C on the host, the other is SplitBit assembly running on the
# machine. disk.sh checks the host half against the format and run.sh checks the machine
# half against recorded output, but neither of those can catch the two of them agreeing
# with themselves and disagreeing with each other.
#
# SO THIS BUILDS THE SAME DISK TWICE, once with each, and compares the images byte for
# byte. Every field either one writes and the other only reads is checked here and nowhere
# else: which entry a thing lands in, which block, what a directory's unused fields hold,
# the version in the superblock, the free count. A disagreement in any of those is a disk
# one of them can read and the other cannot, and the way that is usually discovered is
# somebody's file coming back wrong months later.
#
# Written by Anachronaut
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
WORK="$ROOT/Tests/build/agree"
ASM="$ROOT/Assembler"
TOOL="$ROOT/SplitDisk"
EMU="$ROOT/SplitBit"
PASS=0
FAIL=0
FAILED_NAMES=()
GREEN=$'\033[32m'; RED=$'\033[31m'; RESET=$'\033[0m'
[ -t 1 ] || { GREEN=""; RESET=""; RED=""; }
report() {
local mark="$1" name="$2" note="${3:-}"
if [ "$mark" = "ok" ]; then
PASS=$((PASS + 1)); printf " [%sok %s] %-24s %s\n" "$GREEN" "$RESET" "$name" "$note"
else
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
printf " [%sFAIL%s] %-24s %s\n" "$RED" "$RESET" "$name" "$note"
fi
}
for tool in "$ASM" "$TOOL" "$EMU"; do
[ -x "$tool" ] || { echo "$(basename "$tool") is not built."; exit 1; }
done
rm -rf "$WORK"; mkdir -p "$WORK"
cd "$WORK" || exit 1
"$ASM" -I "$ROOT/Programs/CosmOS/Source" "$ROOT/Programs/CosmOS/Source/cosmos.asm" \
-o cosmos.bin >/dev/null 2>&1 || { echo "CosmOS would not assemble."; exit 1; }
echo "Checking the two SBFS implementations against each other."
# ---- The same tree, made both ways ----
#
# The order matters and is the same on both sides, because both allocate first fit and both
# take the first free entry: given the same operations in the same order they should reach
# the same bytes, and any difference is a real one rather than an artefact of the script.
"$TOOL" format host.img 128 2 >/dev/null
"$TOOL" mkdir host.img /Apps >/dev/null
"$TOOL" mkdir host.img /Apps/Deep >/dev/null
"$TOOL" mkdir host.img /Notes >/dev/null
"$TOOL" format machine.img 128 2 >/dev/null
printf 'mkdir /Apps\nmkdir /Apps/Deep\nmkdir /Notes\nexit\n' \
| "$EMU" cosmos.bin --fast --disk machine.img >/dev/null 2>&1
if cmp -s host.img machine.img; then
report ok "three directories" "byte for byte"
else
report FAIL "three directories" "$(cmp host.img machine.img 2>&1 | head -1)"
fi
# ---- Removing one puts the disk back exactly ----
#
# A wiped entry has to be indistinguishable from one that was never used, or a disk that
# has had something deleted stops matching a fresh one that never did. Both sides zero all
# thirty two bytes, and this is what says so.
"$TOOL" rmdir host.img /Apps/Deep >/dev/null
printf 'rmdir /Apps/Deep\nexit\n' | "$EMU" cosmos.bin --fast --disk machine.img >/dev/null 2>&1
if cmp -s host.img machine.img; then
report ok "and removing one" "byte for byte"
else
report FAIL "and removing one" "$(cmp host.img machine.img 2>&1 | head -1)"
fi
# ---- A file put down a path ----
#
# The machine writes files through the careful order a save has to use - make a temporary,
# write it, delete the original, rename the temporary - and the host writes the entry once.
# Two quite different routes to what has to be the same disk.
# The editor goes on both disks FIRST and by the same route, so that the only thing left
# differing is how the payload got written. Putting it on one disk before the payload and
# the other after was enough to move every entry and block after it, and the comparison
# duly failed on a difference the script had introduced.
"$ASM" -I "$ROOT/Programs/Libraries" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Edit.asm" -o Edit.sbx >/dev/null 2>&1
"$TOOL" put host.img Edit.sbx >/dev/null
"$TOOL" put machine.img Edit.sbx >/dev/null
printf 'a file that lives in a directory\n' > payload.txt
"$TOOL" put host.img payload.txt /Notes/payload.txt >/dev/null
printf 'load Edit.sbx\nrun /Notes/payload.txt\na\na file that lives in a directory\n.\nw\nq\nexit\n' \
| "$EMU" cosmos.bin --fast --disk machine.img >/dev/null 2>&1
if cmp -s host.img machine.img; then
report ok "a file down a path" "byte for byte"
else
report FAIL "a file down a path" "$(cmp host.img machine.img 2>&1 | head -1)"
fi
# ---- A file written a block at a time ----
#
# The machine never holds more than 256 bytes of this file, and the host reads it back
# whole. Every block is filled with a byte that says which block it is, so a block written
# into the wrong place of the file is visible rather than merely being the right length -
# which is the failure this actually had while it was being built, and the one a check on
# the size alone would have passed.
#
# The last block is a part block on purpose. A tail is where every off-by-one in a
# filesystem hides.
"$ASM" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Pour.asm" -o Pour.sbx >/dev/null 2>&1
"$TOOL" format poured.img 256 2 >/dev/null
"$TOOL" put poured.img Pour.sbx >/dev/null
printf 'Pour 12\nexit\n' | "$EMU" cosmos.bin --fast --disk poured.img >/dev/null 2>&1
"$TOOL" get poured.img poured.dat gotPoured.dat >/dev/null 2>&1
if [ -f gotPoured.dat ] && [ "$(wc -c < gotPoured.dat | tr -d ' ')" = "3112" ]; then
report ok "streamed out block by block" "3112 bytes, twelve blocks and a tail"
else
report FAIL "streamed out block by block" "wanted 3112 bytes, got $(wc -c < gotPoured.dat 2>/dev/null || echo nothing)"
fi
# Every byte has to name the block it came from, or a block went somewhere else.
if od -An -v -tu1 gotPoured.dat 2>/dev/null | awk '
{ for (i = 1; i <= NF; i++) { if ($i != 65 + int(n / 256)) bad = 1; n++ } }
END { exit (bad ? 1 : 0) }'; then
report ok "and every block landed" "each byte names its own block"
else
report FAIL "and every block landed" "a block is not where it was written"
fi
# ---- A large file copied and compared on the machine ----
#
# Copy joins the read and write streaming services: the source and destination are both
# larger than Data Memory, while the application owns one block. Compare then reads both
# through separate blocks and must ignore bytes past the tail. The host extracts the copy
# afterwards, so two native programs agreeing with each other is not the only oracle.
"$ASM" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Copy.asm" -o Copy.sbx >/dev/null 2>&1
"$ASM" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Compare.asm" -o Compare.sbx >/dev/null 2>&1
"$TOOL" format copied.img 1024 4 >/dev/null
"$TOOL" mkdir copied.img /Apps >/dev/null
"$TOOL" mkdir copied.img /Input >/dev/null
"$TOOL" mkdir copied.img /Output >/dev/null
"$TOOL" put copied.img Copy.sbx /Apps/Copy.sbx >/dev/null
"$TOOL" put copied.img Compare.sbx /Apps/Compare.sbx >/dev/null
awk 'BEGIN { for (i = 0; i < 84000; i++) printf "%c", 65 + (i % 26) }' > toCopy.dat
"$TOOL" put copied.img toCopy.dat /Input/source.dat >/dev/null
printf 'Copy /Input/source.dat /Output/copy.dat\nCompare /Input/source.dat /Output/copy.dat\nexit\n' \
| "$EMU" cosmos.bin --fast --cycles 1000000000 --disk copied.img > copied.txt 2>&1
"$TOOL" get copied.img /Output/copy.dat copiedBack.dat >/dev/null 2>&1
if cmp -s toCopy.dat copiedBack.dat; then
report ok "large native copy" "84000 bytes, host-identical"
else
report FAIL "large native copy" "the host read back different bytes"
fi
if grep -q '^> the same$' copied.txt; then
report ok "native compare" "the streamed files agree"
else
report FAIL "native compare" "Compare did not call the copied files equal"
fi
# ---- And each can read what the other wrote ----
#
# Matching bytes and being readable are not the same claim. A field both of them write
# wrongly in the same way would pass every comparison above.
"$TOOL" get machine.img /Notes/payload.txt fromMachine.txt >/dev/null 2>&1
if cmp -s payload.txt fromMachine.txt; then
report ok "the host reads it back" "$(wc -c < fromMachine.txt | tr -d ' ') bytes"
else
report FAIL "the host reads it back" "the file came back different"
fi
"$TOOL" mkdir host.img /Notes/Inner >/dev/null
printf 'x' > deep.txt
"$TOOL" put host.img deep.txt /Notes/Inner/deep.txt >/dev/null
seen=$(printf 'cd /Notes/Inner\ndir\nexit\n' \
| "$EMU" cosmos.bin --fast --disk host.img 2>&1 | grep -c "deep.txt")
if [ "$seen" -ge 1 ]; then
report ok "the machine reads it back" "found it three deep"
else
report FAIL "the machine reads it back" "the machine could not see it"
fi
echo
if [ "$FAIL" -eq 0 ]; then
echo "All $PASS agreement checks passed."
exit 0
fi
echo "$PASS passed, $FAIL failed: ${FAILED_NAMES[*]}"
exit 1