mkdir and rmdir are the machine's own now, and a file goes where its path says rather than always in the root. A disk can be organised without the host tool touching it. Everything below the surface works in terms of a directory and a name rather than a path. sbfsWalkParent splits the last name off, walks the rest, and hands back the two - and the separator stays on the end of the head, which is what makes one rule cover every kind of path: "/x" leaves "/", which is the root; "x" leaves nothing, which is where the machine already is; and "A/x" leaves "A/", which is neither and needs no special case to say so. Saving works in those two as well, and had to. The careful order a save uses - make a temporary, write it, delete the original, rename the temporary - only works if the temporary is made in the SAME directory as the file, because the rename at the end changes a name and does not move anything. Renaming to a path naming a different directory is refused for that reason, rather than quietly being a lie the disk goes along with. Three things this cost, all found by running it: mkdir Apps/Deep made /Apps/Apps. The leaf was worked out into SbfsWanted and then the head was walked - and walking goes through sbfsPathNext, which puts every name it meets into SbfsWanted on the way past. The head's last name landed exactly where the leaf was. It has somewhere of its own now. rmdir took a directory with something still in it, which is the one failure the whole design is arranged to prevent. Looking for children clobbered DP2 and rebuilt it from the buffer and the entry count with the subtraction the wrong way round, so the pointer walked off the end of the block and found nothing. The comparison goes through a CALL now, like the two beside it, and DP2 comes back on the entry because a RET puts it there. SplitDisk's "in use but not reachable from the root" line is what caught it. Refusing a name longer than twenty two used to read the twenty third character of a shorter one, which is somebody else's string. It is measured now. Tests/agree.sh is new and is the gate this rung was for: the same disk built twice, once with SplitDisk and once with CosmOS, compared byte for byte. The two share no code and only a written specification, and every field one writes and the other only reads is checked there and nowhere else - which entry a thing lands in, which block, what a directory's unused fields hold, the version, the free count. It caught a wrong parent immediately when that was broken on purpose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
295 lines
17 KiB
Bash
Executable File
295 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Builds the disk images the tests read from.
|
|
#
|
|
# These are made with SplitDisk, which is the other implementation of the same format.
|
|
# That is the point of them: a SplitBit program reading one of these is being checked
|
|
# against something written by different code from a written specification, rather than
|
|
# against itself.
|
|
#
|
|
# Written by Anachronaut
|
|
|
|
set -eu
|
|
|
|
# Made absolute before anything else. This script cds into its own working directory part
|
|
# of the way down, so a relative build path would be measured from the wrong place from
|
|
# there on - and the way that failed was not an error but a disk quietly missing some of
|
|
# the files it was supposed to have, which is a much worse thing to debug.
|
|
mkdir -p "$1"
|
|
BUILD="$(cd "$1" && pwd)"
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
TOOL="$ROOT/SplitDisk"
|
|
DISKS="$BUILD/disks"
|
|
WORK="$BUILD/.diskwork"
|
|
|
|
[ -x "$TOOL" ] || { echo "SplitDisk is not built."; exit 1; }
|
|
mkdir -p "$DISKS" "$WORK"
|
|
|
|
# Two directory blocks, so that a file can be put beyond the first one and the walk from
|
|
# block to block gets exercised rather than assumed.
|
|
"$TOOL" format "$DISKS/sbfs.img" 64 2 >/dev/null
|
|
|
|
cd "$WORK"
|
|
printf 'hello from a file' > greeting.txt
|
|
|
|
# Eight files fill the first directory block exactly, so everything after this lands in
|
|
# the second one.
|
|
for i in 1 2 3 4 5 6 7 8; do printf 'filler %d' "$i" > "filler$i.txt"; done
|
|
|
|
# Longer than a block, so reading it has to cross from one to the next. The pattern
|
|
# repeats every twenty six bytes, which makes a misplaced block obvious to read.
|
|
awk 'BEGIN { for (i = 0; i < 700; i++) printf "%c", 65 + (i % 26) }' > across.txt
|
|
|
|
: > empty.txt
|
|
printf 'exactly twenty two!!!!' > longname.txt
|
|
|
|
"$TOOL" put "$DISKS/sbfs.img" greeting.txt >/dev/null
|
|
for i in 1 2 3 4 5 6 7 8; do "$TOOL" put "$DISKS/sbfs.img" "filler$i.txt" >/dev/null; done
|
|
"$TOOL" put "$DISKS/sbfs.img" across.txt >/dev/null
|
|
"$TOOL" put "$DISKS/sbfs.img" empty.txt >/dev/null
|
|
"$TOOL" put "$DISKS/sbfs.img" longname.txt aName22CharactersLong! >/dev/null
|
|
|
|
# A disk with a loadable program on it. The program is assembled here rather than kept as
|
|
# bytes, so that what gets loaded is always built from the source beside it.
|
|
"$TOOL" format "$DISKS/load.img" 64 2 >/dev/null
|
|
# The assembler writes a loadable program itself, because the source says where it goes.
|
|
"$ROOT/Assembler" "$ROOT/Programs/Loader/loadable.asm" -o "$WORK/hello.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/load.img" "$WORK/hello.sbx" >/dev/null
|
|
|
|
# A disk for CosmOS. greet.sbx asks the system for everything it does rather than talking
|
|
# to the hardware itself, so loading and running it exercises the whole path: the loader,
|
|
# the vector table, the service handlers, and giving the machine back at the end.
|
|
#
|
|
# hello.sbx is the opposite case, and that is why it is here: it is the original
|
|
# hello.asm, written before any of this existed, and it still writes straight to port
|
|
# 0x00 rather than calling osPrintString. A program is allowed to reach past the system
|
|
# to the hardware, so something has to check that one still gives the machine back.
|
|
#
|
|
# notes.txt is there so that loading something that is not a program can be tried too.
|
|
# 512K, and four directory blocks for 32 files. It was 64K and 16, which was ample for a
|
|
# disk that held programs and nothing else - but the native assembler reads SOURCE from
|
|
# here, and the CosmOS sources alone are 104,142 bytes against the 65,536 a 256 block disk
|
|
# holds. A machine that is going to assemble itself has to be able to hold its own source.
|
|
"$TOOL" format "$DISKS/cosmos.img" 2048 4 >/dev/null
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/greet.asm" -o "$WORK/greet.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/cosmos.img" "$WORK/greet.sbx" >/dev/null
|
|
# Assembled to a different working name so it cannot tread on load.img's hello.sbx above,
|
|
# then put under the name the shell asks for.
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/hello.asm" -o "$WORK/appHello.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/cosmos.img" "$WORK/appHello.sbx" hello.sbx >/dev/null
|
|
# Life.sbx is the one that had to be taught to stop. It runs to a still life and returns
|
|
# on its own, so the test needs no cycle limit: whether it ends is the thing being checked
|
|
# and a limit would hide the answer by supplying one.
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/Libraries" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/Life.asm" -o "$WORK/Life.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Life.sbx" >/dev/null
|
|
# Snake.sbx is the one that is played rather than watched. It reads the console a key at a
|
|
# time without ever waiting for one, so a script of moves drives it a move to a frame, and
|
|
# what is recorded is a whole game: turning, eating, growing, and running into a wall.
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/Libraries" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/Snake.asm" -o "$WORK/Snake.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Snake.sbx" >/dev/null
|
|
# Keys.sbx brings a vector of its own, which is what the version two format exists for. It
|
|
# is the only program here the system has to install anything for, so it is what says the
|
|
# whole path works: written into the file, installed at run, taken back out at exit.
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/Libraries" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/Keys.asm" -o "$WORK/Keys.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Keys.sbx" >/dev/null
|
|
# Say.sbx is the first program that can be told anything. Everything before it did the same
|
|
# thing however it was started.
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/Say.asm" -o "$WORK/Say.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Say.sbx" >/dev/null
|
|
# Break.sbx stops itself twice and shows what the registers were each time. It needs no disk
|
|
# of its own: it only prints.
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/Break.asm" -o "$WORK/Break.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Break.sbx" >/dev/null
|
|
printf 'this is not a program' > notes.txt
|
|
"$TOOL" put "$DISKS/cosmos.img" notes.txt >/dev/null
|
|
|
|
# A disk of its own for the writing test, with one file already on it so that what it
|
|
# writes has to be placed somewhere that does not tread on what is there.
|
|
"$TOOL" format "$DISKS/write.img" 32 1 >/dev/null
|
|
printf 'already here' > here.txt
|
|
"$TOOL" put "$DISKS/write.img" here.txt >/dev/null
|
|
|
|
# A disk of its own for the editing test, which deletes and renames things and would
|
|
# otherwise leave the writing test's disk looking nothing like the writing test expects.
|
|
# It starts with one file on it so that a document written here has to be placed around
|
|
# something, and so that the disk can be compared afterwards against one that never had
|
|
# any of it: the metadata should come back exactly as it was.
|
|
"$TOOL" format "$DISKS/edit.img" 32 1 >/dev/null
|
|
"$TOOL" put "$DISKS/edit.img" here.txt >/dev/null
|
|
|
|
# A disk for the shell's delete and rename, which is its own because those change what is
|
|
# on it. A fixture whose name has a directory in it is used as it stands rather than being
|
|
# made fresh per test, so a test that writes to a shared one would quietly change what
|
|
# every test after it sees.
|
|
"$TOOL" format "$DISKS/files.img" 32 1 >/dev/null
|
|
printf 'the first one' > one.txt
|
|
printf 'the second one' > two.txt
|
|
"$TOOL" put "$DISKS/files.img" one.txt >/dev/null
|
|
"$TOOL" put "$DISKS/files.img" two.txt >/dev/null
|
|
|
|
# A disk for the editor, holding nothing but the editor. Its own, because the whole point
|
|
# of it is that it writes: a document made on a shared fixture would turn up in the file
|
|
# listing of every test that came after it.
|
|
"$TOOL" format "$DISKS/editor.img" 256 2 >/dev/null
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/Libraries" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/Edit.asm" -o "$WORK/Edit.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/editor.img" "$WORK/Edit.sbx" >/dev/null
|
|
|
|
# A disk that has directories on it AND gets written to, which nothing else covers: every
|
|
# other version two fixture is read only, and every fixture that is written to is flat.
|
|
# Making a file runs the allocator over a directory entry, and a directory has no blocks,
|
|
# so it must be in nobody's way while a run of free ones is looked for. Its own disk, for
|
|
# the same reason the editor has one: what it writes would otherwise turn up in the
|
|
# listing of every test that came after it.
|
|
"$TOOL" format "$DISKS/treewrite.img" 256 2 >/dev/null
|
|
"$TOOL" mkdir "$DISKS/treewrite.img" /Folder >/dev/null
|
|
"$TOOL" mkdir "$DISKS/treewrite.img" /Folder/Inner >/dev/null
|
|
"$TOOL" put "$DISKS/treewrite.img" "$WORK/Edit.sbx" >/dev/null
|
|
|
|
# A disk for the file services, holding nothing but the program that exercises them. Its
|
|
# own, because that program writes: it tidies up after itself, but a run that stopped part
|
|
# way would leave a document behind on a fixture every later test reads.
|
|
"$TOOL" format "$DISKS/services.img" 256 2 >/dev/null
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/Files.asm" -o "$WORK/Files.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/services.img" "$WORK/Files.sbx" >/dev/null
|
|
|
|
# A disk for streaming, and the only one here that has to be big: the file on it is bigger
|
|
# than the machine's Data Memory, which is the entire point of the services it checks.
|
|
#
|
|
# THE FILE IS GENERATED RATHER THAN TAKEN FROM THE REPOSITORY. Concatenating the CosmOS
|
|
# sources would be a truer picture of what streaming is for, and would change the recorded
|
|
# checksum every time a line of CosmOS was edited - so a real difference would arrive in a
|
|
# crowd of meaningless ones, which is the same trap the cycle counts used to set. This is
|
|
# 84000 bytes, which is 328 whole blocks and 32 bytes over, so the short block at the end
|
|
# is exercised rather than assumed.
|
|
"$TOOL" format "$DISKS/stream.img" 1024 2 >/dev/null
|
|
awk 'BEGIN { for (i = 0; i < 4000; i++) printf "streaming line %05d\n", i }' > big.txt
|
|
awk 'BEGIN { for (i = 0; i < 50; i++) printf "small %05d\n", i }' > small.txt
|
|
"$TOOL" put "$DISKS/stream.img" big.txt >/dev/null
|
|
"$TOOL" put "$DISKS/stream.img" small.txt >/dev/null
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/Stream.asm" -o "$WORK/Stream.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/stream.img" "$WORK/Stream.sbx" >/dev/null
|
|
|
|
# A disk for Type. The first file crosses several blocks and deliberately has no zero byte
|
|
# for the application to mistake for an end marker. The second proves that zero blocks is
|
|
# a valid empty file rather than an error.
|
|
"$TOOL" format "$DISKS/type.img" 64 2 >/dev/null
|
|
printf 'first line\nsecond line\n' > readable.txt
|
|
awk 'BEGIN { for (i = 0; i < 30; i++) printf "line %02d: ABCDEFGHIJKLMNOPQRSTUVWXYZ\n", i }' >> readable.txt
|
|
: > empty.txt
|
|
"$TOOL" put "$DISKS/type.img" readable.txt >/dev/null
|
|
"$TOOL" put "$DISKS/type.img" empty.txt >/dev/null
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/Type.asm" -o "$WORK/Type.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/type.img" "$WORK/Type.sbx" >/dev/null
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/More.asm" -o "$WORK/More.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/type.img" "$WORK/More.sbx" >/dev/null
|
|
|
|
# A disk with directories on it, which is the whole of what version two adds. Built by the
|
|
# host tool, because at this rung the machine can read a tree and not yet make one - and
|
|
# that split is the point: the two implementations are checked against each other rather
|
|
# than each against itself.
|
|
#
|
|
# ONE NAME IS PUT DOWN TWICE, in two directories, because a name meaning something
|
|
# different in each place is what directories are for and what a flat filesystem cannot do.
|
|
#
|
|
# The two are DIFFERENT PROGRAMS on purpose. Say echoes whatever it is given and hello
|
|
# prints one fixed line, so which of them ran is written in the output. Two copies of one
|
|
# program would have been the easier fixture and a useless one: resolving to the wrong
|
|
# Say.sbx would have printed exactly what resolving to the right one prints, and the test
|
|
# would have passed while the path walk was ignoring directories entirely.
|
|
"$TOOL" format "$DISKS/tree.img" 256 4 >/dev/null
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/Say.asm" -o "$WORK/Say.sbx" >/dev/null
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/hello.asm" -o "$WORK/treeHello.sbx" >/dev/null
|
|
"$TOOL" mkdir "$DISKS/tree.img" /Apps >/dev/null
|
|
"$TOOL" mkdir "$DISKS/tree.img" /Apps/Deep >/dev/null
|
|
"$TOOL" put "$DISKS/tree.img" "$WORK/Say.sbx" /Apps/Say.sbx >/dev/null
|
|
"$TOOL" put "$DISKS/tree.img" "$WORK/treeHello.sbx" /Apps/Deep/Say.sbx >/dev/null
|
|
printf 'this is not a program' > rooted.txt
|
|
"$TOOL" put "$DISKS/tree.img" rooted.txt >/dev/null
|
|
|
|
# A blank disk for the machine to build a tree on itself. It starts as a version ONE disk
|
|
# with nothing at all on it, because half of what this checks is that making the first
|
|
# directory raises the version - the number says what is on a disk rather than what made
|
|
# it, so a disk with no directories is flat whoever formatted it.
|
|
"$TOOL" format "$DISKS/build.img" 128 2 >/dev/null
|
|
|
|
# A disk for moving about on. Two directories hold a file of THE SAME NAME with different
|
|
# text in it, which is the fixture the working directory needs: "notes.txt" has to mean a
|
|
# different file from each of them, and the only way to see that it does is for the two to
|
|
# say different things.
|
|
#
|
|
# It is also what checks the remembered file is dropped when the machine moves. That cache
|
|
# is keyed on the path as somebody typed it, so "notes.txt" is the same key in both places
|
|
# and nothing about the entry it remembers looks wrong - it is the kind of stale that is
|
|
# believed rather than noticed.
|
|
#
|
|
# Type and Say go in /Apps, so that a program can be started from anywhere; hello goes in
|
|
# /A under the name Say.sbx, so that where you are is visibly tried before /Apps.
|
|
"$TOOL" format "$DISKS/cwd.img" 256 4 >/dev/null
|
|
"$TOOL" mkdir "$DISKS/cwd.img" /Apps >/dev/null
|
|
"$TOOL" mkdir "$DISKS/cwd.img" /A >/dev/null
|
|
"$TOOL" mkdir "$DISKS/cwd.img" /B >/dev/null
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/Type.asm" -o "$WORK/Type.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/cwd.img" "$WORK/Type.sbx" /Apps/Type.sbx >/dev/null
|
|
"$TOOL" put "$DISKS/cwd.img" "$WORK/Say.sbx" /Apps/Say.sbx >/dev/null
|
|
"$TOOL" put "$DISKS/cwd.img" "$WORK/treeHello.sbx" /A/Say.sbx >/dev/null
|
|
printf 'these are the notes in A\n' > notesA.txt
|
|
printf 'and these are the very different notes in B\n' > notesB.txt
|
|
"$TOOL" put "$DISKS/cwd.img" notesA.txt /A/notes.txt >/dev/null
|
|
"$TOOL" put "$DISKS/cwd.img" notesB.txt /B/notes.txt >/dev/null
|
|
# Wander is the only thing that can move the machine from inside a program, which makes it
|
|
# the only thing that can check the shell puts the working directory back afterwards.
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/Wander.asm" -o "$WORK/Wander.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/cwd.img" "$WORK/Wander.sbx" /Apps/Wander.sbx >/dev/null
|
|
|
|
# A disk for invoking a program by typing its name. Four files, each there to say one
|
|
# thing about how a typed word turns into a file name.
|
|
#
|
|
# dir.sbx is a working program under the name of a built-in command, which is the only way
|
|
# to check that the built-ins really are tried first. If the search ever moved ahead of the
|
|
# dispatch chain, this disk would start answering "dir" with a program.
|
|
#
|
|
# notes.sbx is text under a program's name, so that a file that IS found and IS NOT a
|
|
# program can be told apart from a word that names nothing. notes.txt is the same text
|
|
# under its own name, which no typed word can reach: "notes.txt" looks for notes.txt.sbx.
|
|
"$TOOL" format "$DISKS/invoke.img" 64 2 >/dev/null
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
|
|
"$ROOT/Programs/CosmOS/Apps/Say.asm" -o "$WORK/Say.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/invoke.img" "$WORK/Say.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/invoke.img" "$WORK/Say.sbx" dir.sbx >/dev/null
|
|
printf 'this is not a program' > notes.txt
|
|
"$TOOL" put "$DISKS/invoke.img" notes.txt >/dev/null
|
|
"$TOOL" put "$DISKS/invoke.img" notes.txt notes.sbx >/dev/null
|
|
|
|
# A disk for the assembler that runs on the machine. It holds a source file and the two
|
|
# programs that check the front end - the reader on its own and the tokenizer on its own -
|
|
# because a fault in either of those would otherwise turn up much later as a mysterious
|
|
# wrong byte in an output file.
|
|
#
|
|
# hello.asm is here rather than something written for the occasion because it is the
|
|
# oldest program in the repository: the first thing this machine ever ran is the first
|
|
# thing it assembles for itself.
|
|
"$TOOL" format "$DISKS/asm.img" 2048 4 >/dev/null
|
|
cp "$ROOT/Programs/Examples/hello.asm" hello.asm
|
|
"$TOOL" put "$DISKS/asm.img" hello.asm >/dev/null
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" -I "$ROOT/Programs/CosmOS/Assembler" \
|
|
"$ROOT/Programs/CosmOS/Assembler/readTest.asm" -o "$WORK/readTest.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/asm.img" "$WORK/readTest.sbx" >/dev/null
|
|
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" -I "$ROOT/Programs/CosmOS/Assembler" \
|
|
"$ROOT/Programs/CosmOS/Assembler/tokenTest.asm" -o "$WORK/tokenTest.sbx" >/dev/null
|
|
"$TOOL" put "$DISKS/asm.img" "$WORK/tokenTest.sbx" >/dev/null
|