#!/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