Three different things used to do this job, and which one you got depended on where the machine was running. On a terminal the host held the line and did the echoing and the backspacing; behind a window the console's own gatherer did it; from a file nothing did it at all. One job, three implementations, none of them in the system - which is why there was no way to move about in a line and nowhere for a history to live. So editLine does it. Key mode while a line is being read and line mode straight after, so nothing else in the system and no program calling osReadLine notices anything changed. Left and Right, Home and End, Backspace for the character before the cursor and Delete for the one under it, and anything typed goes in where the cursor is with the rest of the line moving along. Ctrl-D means the end of input again, on an empty line, because that was a thing the terminal did while it was holding the line and it is not holding it now. Same trade as the echoing. MOST KEYSTROKES DRAW NOTHING BUT THEMSELVES. A character typed at the end of a line needs no cursor moved: printing it is the whole change, and a backspace there is three ordinary bytes. That matters beyond speed - moving the cursor by hand is what a terminal is TOLD about, in an escape sequence, so redrawing on every keypress would fill every recorded transcript in this suite with them. The line is only reprinted when something happened in the middle of it. Where the line STARTS is worked out backwards from where printing ended, rather than trusted from what was remembered. That is what makes it survive the screen scrolling: a line printed on the bottom row moves everything up by one, and a remembered row would be one too low from then on. The command line holds 127 characters, up from 63. The limit started to be felt the moment a line could be moved about in. 58 recordings changed, and every one of them by the echo. THE PROOF IS NOT A HEURISTIC: a CosmOS built with the echo silenced reproduces 187 of the 188 recordings byte for byte. The one exception is cosmosTyped, the backspace test, where the rub-out marks now come from the shell instead of from the console's gatherer - same marks, different author. cosmosEditKeys is the new test, and every line in it is typed wrong and then corrected with a different key. Its last line is eighty six characters at a prompt in column two on an eighty column screen, so the line runs onto the row below and the shell has to find the start of something it can no longer see; breaking either half of that arithmetic fails it. Also: agree.sh looked for "> the same", anchored to a prompt that no longer precedes what a command prints. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
342 lines
15 KiB
Bash
Executable File
342 lines
15 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
|
|
# Not anchored to a prompt any more. The shell echoes the line it was given and then ends
|
|
# it, so what a command prints now starts at the beginning of a line instead of following
|
|
# the "> " that asked for it.
|
|
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
|
|
|
|
# ---- A temporary is what the entry says it is, not what it is called ----
|
|
#
|
|
# Saving something writes a temporary first, and the temporary was told apart from a real
|
|
# file by being called "sbfs.part" or "sbfs.out". Those are legal names. A file of your own
|
|
# under either of them was deleted by the next save of anything at all in the same
|
|
# directory - so this puts one there with the host, saves with the machine, and takes it
|
|
# off again to see whether it survived.
|
|
#
|
|
# The entry now says outright that it is a temporary, and a file that does not say so
|
|
# belongs to somebody, so the save is refused instead of helping itself to the name.
|
|
#
|
|
# ONE DISK EACH, and that is not tidiness. Both on one disk, the first save ate sbfs.part
|
|
# and the streaming test's source file WAS sbfs.part - so with the guard removed the copy
|
|
# failed for want of a source, never opened a stream, and the check passed while reporting
|
|
# on nothing at all.
|
|
"$ASM" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/Files.asm" -o Files.sbx >/dev/null 2>&1
|
|
printf 'this file is mine and is not scratch\n' > mine.txt
|
|
|
|
"$TOOL" format part.img 512 4 >/dev/null
|
|
"$TOOL" mkdir part.img /Apps >/dev/null
|
|
"$TOOL" put part.img Files.sbx /Apps/Files.sbx >/dev/null
|
|
"$TOOL" put part.img mine.txt /sbfs.part >/dev/null
|
|
printf 'Files\nexit\n' | "$EMU" cosmos.bin --fast --disk part.img > part.txt 2>&1
|
|
"$TOOL" get part.img /sbfs.part keptPart.txt >/dev/null 2>&1
|
|
if cmp -s mine.txt keptPart.txt; then
|
|
report ok "a file called sbfs.part" "a whole-file save left it alone"
|
|
else
|
|
report FAIL "a file called sbfs.part" "saving a different file destroyed it"
|
|
fi
|
|
if grep -q "would not save" part.txt; then
|
|
report ok "and the save was refused" "it did not take a name it did not own"
|
|
else
|
|
report FAIL "and the save was refused" "the save reported success"
|
|
fi
|
|
|
|
"$TOOL" format out.img 512 4 >/dev/null
|
|
"$TOOL" mkdir out.img /Apps >/dev/null
|
|
"$TOOL" put out.img Copy.sbx /Apps/Copy.sbx >/dev/null
|
|
"$TOOL" put out.img mine.txt /source.txt >/dev/null
|
|
"$TOOL" put out.img mine.txt /sbfs.out >/dev/null
|
|
printf 'Copy /source.txt /copied.txt\nexit\n' \
|
|
| "$EMU" cosmos.bin --fast --cycles 100000000 --disk out.img > out.txt 2>&1
|
|
"$TOOL" get out.img /sbfs.out keptOut.txt >/dev/null 2>&1
|
|
if cmp -s mine.txt keptOut.txt; then
|
|
report ok "a file called sbfs.out" "a streamed write left it alone"
|
|
else
|
|
report FAIL "a file called sbfs.out" "streaming a different file destroyed it"
|
|
fi
|
|
|
|
# The source is still there too, which says the refusal happened before anything was
|
|
# deleted rather than half way through.
|
|
"$TOOL" get out.img /source.txt keptSource.txt >/dev/null 2>&1
|
|
if cmp -s mine.txt keptSource.txt; then
|
|
report ok "and it stopped early" "the source was never touched"
|
|
else
|
|
report FAIL "and it stopped early" "the copy got far enough to disturb the source"
|
|
fi
|
|
|
|
# ---- And a write that stopped is visible from both sides ----
|
|
#
|
|
# Nothing here can crash the machine half way through a commit, so the wreckage is forged:
|
|
# the flag is set by hand on a finished file, which is byte for byte what a save that was
|
|
# interrupted between writing its temporary and naming it would have left. Both listings
|
|
# have to say so, because the bytes are recoverable and only a listing can point at them.
|
|
"$TOOL" format wreck.img 512 4 >/dev/null
|
|
"$TOOL" put wreck.img mine.txt /stranded.txt >/dev/null
|
|
python3 - wreck.img <<'PATCH'
|
|
import sys
|
|
image = open(sys.argv[1], "r+b")
|
|
image.seek(8)
|
|
start = int.from_bytes(image.read(2), "big") # First directory block.
|
|
image.seek(start * 256)
|
|
directory = bytearray(image.read(256))
|
|
for at in range(0, 256, 32):
|
|
if directory[at] & 0x01 and directory[at + 6:at + 14] == b"stranded":
|
|
directory[at] |= 0x04 # Never committed.
|
|
image.seek(start * 256)
|
|
image.write(directory)
|
|
break
|
|
else:
|
|
sys.exit("could not find the entry to strand")
|
|
image.close()
|
|
PATCH
|
|
|
|
if "$TOOL" list wreck.img | grep -q "unfinished write"; then
|
|
report ok "the host sees the wreckage" "listed and explained"
|
|
else
|
|
report FAIL "the host sees the wreckage" "SplitDisk listed it as an ordinary file"
|
|
fi
|
|
if printf 'dir\nexit\n' | "$EMU" cosmos.bin --fast --disk wreck.img 2>&1 \
|
|
| grep -q "<unfinished>"; then
|
|
report ok "the machine sees it too" "dir marks it rather than sizing it"
|
|
else
|
|
report FAIL "the machine sees it too" "dir showed it as an ordinary file"
|
|
fi
|
|
|
|
# ---- A boot area is blocks neither of them will touch ----
|
|
#
|
|
# Blocks between the superblock and the directory, reserved by moving the directory up
|
|
# rather than by anything new: both implementations work out the first usable block as
|
|
# directoryStart + directoryBlocks, and directoryStart has always been a field. So this
|
|
# checks a claim that no code was written to make true - which is exactly the kind most
|
|
# worth checking.
|
|
#
|
|
# The host formats it and the MACHINE fills it, because the machine's allocator is the one
|
|
# that had no idea any of this was happening.
|
|
"$TOOL" format bootarea.img 512 4 32 >/dev/null
|
|
"$TOOL" mkdir bootarea.img /Apps >/dev/null
|
|
"$TOOL" put bootarea.img Files.sbx /Apps/Files.sbx >/dev/null
|
|
printf 'Files\nFiles\nexit\n' \
|
|
| "$EMU" cosmos.bin --fast --disk bootarea.img > bootarea.txt 2>&1
|
|
|
|
if python3 - bootarea.img <<'CHECK'
|
|
import sys
|
|
image = open(sys.argv[1], "rb").read()
|
|
first = int.from_bytes(image[8:10], "big")
|
|
boot = image[256:first * 256]
|
|
sys.exit(0 if boot == bytes(len(boot)) else 1)
|
|
CHECK
|
|
then
|
|
report ok "the boot area is left alone" "the machine allocated around it"
|
|
else
|
|
report FAIL "the boot area is left alone" "something wrote into the reserved blocks"
|
|
fi
|
|
|
|
if "$TOOL" list bootarea.img | grep -q "kept.txt\|1 file\|0 files"; then
|
|
report ok "and the disk still works" "the host reads what the machine wrote"
|
|
else
|
|
report FAIL "and the disk still works" "the host could not read it back"
|
|
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
|