update docs
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import hashlib
|
||||
import lzma
|
||||
import os
|
||||
import pty
|
||||
import selectors
|
||||
@@ -63,6 +65,73 @@ def clean(number):
|
||||
assert f'/run/fds/software/{number:02}/' not in text
|
||||
assert '/run/fds/apps/demo.workstation' not in text
|
||||
|
||||
def interactive_program_checks():
|
||||
master, slave = pty.openpty()
|
||||
original = termios.tcgetattr(slave)
|
||||
console = subprocess.Popen([cli, '--session', str(session), 'console'], stdin=slave, stdout=slave, stderr=slave)
|
||||
selector = selectors.DefaultSelector(); selector.register(master, selectors.EVENT_READ)
|
||||
pending = bytearray()
|
||||
def until(marker):
|
||||
output = pending; deadline = time.monotonic() + 30
|
||||
while marker not in output:
|
||||
assert time.monotonic() < deadline, output
|
||||
for _, _ in selector.select(max(0, deadline-time.monotonic())):
|
||||
chunk = os.read(master, 8192); assert chunk
|
||||
output.extend(chunk)
|
||||
end = output.index(marker) + len(marker)
|
||||
result = bytes(output[:end]); del output[:end]
|
||||
log.write(repr(result) + '\n'); log.flush()
|
||||
return result
|
||||
try:
|
||||
until(b'FDS> ')
|
||||
os.write(master, b"stty -g >/tmp/program-tty-before; b01:demo.report:shell -c 'printf \"READY:%s\\n\" tty; read -r line; printf \"REPLY:%s\\n\" \"$line\"'\n")
|
||||
until(b'READY:tty\r\n')
|
||||
os.write(master, b'literal interactive input\n')
|
||||
until(b'REPLY:literal interactive input\r\n')
|
||||
until(b'FDS> ')
|
||||
os.write(master, b"b01:demo.report:shell -c 'printf \"SIGNAL:%s\\n\" ready; exec tail -f /dev/null'; printf 'RETURN:%s\\n' \"$?\"\n")
|
||||
until(b'SIGNAL:ready\r\n')
|
||||
os.write(master, b'\x03')
|
||||
until(b'RETURN:130\r\n')
|
||||
until(b'FDS> ')
|
||||
os.write(master, b"b01:demo.report:shell -c 'printf \"STOP:%s\\n\" ready; exec tail -f /dev/null'\n")
|
||||
until(b'STOP:ready\r\n')
|
||||
os.write(master, b'\x1a')
|
||||
until(b'Stopped')
|
||||
until(b'FDS> ')
|
||||
os.write(master, b'fg\n')
|
||||
until(b"exec tail -f /dev/null'\r\n")
|
||||
os.write(master, b'\x03')
|
||||
until(b'FDS> ')
|
||||
os.write(master, b"test \"$(stty -g)\" = \"$(cat /tmp/program-tty-before)\" && printf 'RESTORE:%s\\n' passed\n")
|
||||
until(b'RESTORE:passed\r\n')
|
||||
os.write(master, b'\x1d')
|
||||
assert console.wait(timeout=10) == 0
|
||||
assert termios.tcgetattr(slave) == original
|
||||
finally:
|
||||
if console.poll() is None: console.kill(); console.wait()
|
||||
selector.close(); os.close(master); os.close(slave)
|
||||
|
||||
# Compatibility fixture only: the public creator never emits archive payloads.
|
||||
legacy_metadata=work/'legacy-metadata'; (legacy_metadata/'FDS').mkdir(parents=True)
|
||||
legacy_payload=work/'legacy-payload'; (legacy_payload/'bundles').mkdir(parents=True)
|
||||
legacy_program=b'#!/bin/sh\nprintf "Legacy reader: %s\\n" "$1"\n'
|
||||
legacy_tar=io.BytesIO()
|
||||
with tarfile.open(fileobj=legacy_tar,mode='w',format=tarfile.USTAR_FORMAT) as archive:
|
||||
directory=tarfile.TarInfo('bin');directory.type=tarfile.DIRTYPE;directory.mode=0o755;archive.addfile(directory)
|
||||
program=tarfile.TarInfo('bin/legacy');program.mode=0o755;program.size=len(legacy_program);archive.addfile(program,io.BytesIO(legacy_program))
|
||||
legacy_bytes=lzma.compress(legacy_tar.getvalue(),format=lzma.FORMAT_XZ)
|
||||
(legacy_payload/'bundles/legacy.reader.tar.xz').write_bytes(legacy_bytes)
|
||||
(legacy_metadata/'FDS/CARTRIDGE.TOML').write_text('format=1\n[cartridge]\nid="legacy.fixture"\nname="Legacy reader fixture"\nclass="program"\nversion="1"\n[media]\nwritable=false\n')
|
||||
(legacy_metadata/'FDS/SOFTWARE.TOML').write_text(f'format=1\n[[software]]\nid="legacy.reader"\nname="Legacy reader"\nversion="1"\narchitecture="any"\npartition=2\narchive_bytes={len(legacy_bytes)}\nunpacked_bytes={len(legacy_program)}\nentries=2\nsha256="{hashlib.sha256(legacy_bytes).hexdigest()}"\n[software.commands]\nlegacy="bin/legacy"\n')
|
||||
legacy_parts=[]
|
||||
for name,tree in [('FDS_METADATA',legacy_metadata),('FDS_PAYLOAD02',legacy_payload)]:
|
||||
filesystem=work/(name+'.erofs')
|
||||
subprocess.run([str(project/'tools/in-image-tools'),'mkfs.erofs','--quiet','-T','0',str(filesystem),str(tree)],check=True,stdout=log,stderr=log)
|
||||
legacy_parts.append((name,LINUX_FILESYSTEM,filesystem))
|
||||
legacy=work/'legacy.img';gpt(legacy,legacy_parts)
|
||||
subprocess.run([str(project/'out/workstation/fds-cartridge'),'--image-tool-runner',str(project/'tools/in-image-tools'),'inspect',str(legacy)],check=True,stdout=log,stderr=log)
|
||||
|
||||
# A workstation-created disposable DATA fixture, never a host block device.
|
||||
data_root = work / 'data-root'
|
||||
(data_root / 'FDS').mkdir(parents=True)
|
||||
@@ -118,32 +187,59 @@ try:
|
||||
if number in [1, 6, 12]:
|
||||
guest('fds', 'run', number, '--', 'demo.hello:hello')
|
||||
guest('fds', 'run', number, '--', 'demo.report:report')
|
||||
guest('touch', f'/run/fds/software/{number:02}/cache-demo.hello/unexpected', ok=False)
|
||||
guest('touch', f'/run/fds/software/{number:02}/payload02/programs/demo.hello/unexpected', ok=False)
|
||||
guest('fds', 'run', number, '--', '../escape', ok=False)
|
||||
guest('fds', 'run', number, '--', 'demo.hello:missing', ok=False)
|
||||
assert guest('hello', 'literal $(id); and spaces').strip() == 'Hello from an FDS AArch64 software cartridge: literal $(id); and spaces'
|
||||
assert guest(f'b{number:02}:demo.report:shell', '-c', 'id -u').strip() == '1000'
|
||||
assert guest('sh', '-c', f'printf "pipe input" | b{number:02}:demo.report:shell -c "cat"') == 'pipe input'
|
||||
assert guest('sh', '-c', f'b{number:02}:demo.report:shell -c "exit 37"; printf "%s" "$?"') == '37'
|
||||
assert guest('sh', '-c', f'cd /tmp; b{number:02}:demo.report:shell -c "pwd"').strip() == '/tmp'
|
||||
assert '/cache-' not in mounts()
|
||||
assert state['commands']
|
||||
if number == 1:
|
||||
interactive_program_checks()
|
||||
|
||||
if number == 6:
|
||||
guest('fds', 'run', number, '--', 'demo.report:report', 'hold')
|
||||
assert query('bay', number)['bays'][0]['consumers'] > 0
|
||||
guest('sh', '-c', 'report hold >/tmp/direct-hold.log 2>&1 & echo $! >/tmp/direct-launcher')
|
||||
deadline = time.monotonic() + 20
|
||||
while query('bay', number)['bays'][0]['consumers'] < 2:
|
||||
assert time.monotonic() < deadline
|
||||
assert query('bay', number)['bays'][0]['consumers'] >= 2
|
||||
invoke('unplug', number)
|
||||
else:
|
||||
invoke('eject', number)
|
||||
state = wait_bay(number, 'empty')
|
||||
assert state['consumers'] == 0
|
||||
clean(number)
|
||||
if number == 6:
|
||||
deadline = time.monotonic() + 20
|
||||
while guest('sh', '-c', 'test ! -e /proc/$(cat /tmp/direct-launcher); printf "%s" "$?"').strip() != '0':
|
||||
assert time.monotonic() < deadline
|
||||
assert not guest('sh', '-c', 'command -v hello || true').strip()
|
||||
# Two different cartridges export the same names. Lowest bay wins, and
|
||||
# fully qualified commands remain available before and after an eject.
|
||||
invoke('insert', 8, software); wait_bay(8, 'mounted_read_only')
|
||||
invoke('insert', 2, builder / 'collision.img'); wait_bay(2, 'mounted_read_only')
|
||||
assert '/02/' in guest('where', 'FDS_APP')
|
||||
assert '/08/' in guest('b08:demo.report:where', 'FDS_APP')
|
||||
invoke('eject', 2); wait_bay(2, 'empty')
|
||||
assert '/08/' in guest('where', 'FDS_APP')
|
||||
invoke('eject', 8); wait_bay(8, 'empty')
|
||||
remaining = json.loads(invoke('status'))
|
||||
assert remaining['state']['cartridges'] == {}
|
||||
assert len(remaining['block_nodes']) == 2, remaining['block_nodes']
|
||||
output = guest('cat', '/run/log/cartridged/current')
|
||||
assert 'FDS cartridge AArch64 hello' in output
|
||||
assert 'FDS cartridge script report' in output
|
||||
assert 'Hello from an FDS AArch64 software cartridge' in output
|
||||
assert 'FDS cartridge system report' in output
|
||||
assert 'uid=1000(fds)' in output
|
||||
(work / 'program-output.log').write_text(output)
|
||||
invoke('insert', 3, builder / 'archive-corrupt.img')
|
||||
wait_bay(3, 'mounted_read_only')
|
||||
failure = guest('fds', 'run', 3, '--', 'demo.hello:hello', ok=False)
|
||||
assert 'digest' in failure.lower() or 'sha' in failure.lower(), failure
|
||||
assert '/run/fds/software/03/cache-' not in mounts()
|
||||
invoke('eject', 3)
|
||||
invoke('insert', 3, builder / 'program-corrupt.img')
|
||||
failed = wait_bay(3, 'error')
|
||||
assert 'digest' in failed['detail'].lower() or 'hash' in failed['detail'].lower(), failed
|
||||
assert '/run/fds/software/03/' not in mounts()
|
||||
invoke('unplug', 3)
|
||||
wait_bay(3, 'empty')
|
||||
invoke('insert', 4, builder / 'catalogue-mapping.img')
|
||||
wait_bay(4, 'error')
|
||||
@@ -162,7 +258,13 @@ try:
|
||||
assert digest(data) == original_data
|
||||
assert overlay.is_file()
|
||||
assert len(json.loads(invoke('status'))['block_nodes']) == 2
|
||||
# Keep a verified software cache mounted across native shutdown.
|
||||
invoke('insert', 7, legacy); wait_bay(7, 'mounted_read_only')
|
||||
assert guest('legacy', 'compatibility').strip() == 'Legacy reader: compatibility'
|
||||
assert '/run/fds/software/07/cache-legacy.reader' in mounts()
|
||||
guest('fds', 'run', 7, '--', 'legacy.reader:legacy', 'background')
|
||||
invoke('eject', 7); wait_bay(7, 'empty'); clean(7)
|
||||
assert not guest('sh', '-c', 'command -v legacy || true').strip()
|
||||
# Keep a directly mounted software tree active across native shutdown.
|
||||
invoke('insert', 12, software)
|
||||
wait_bay(12, 'mounted_read_only')
|
||||
guest('fds', 'run', 12, '--', 'demo.report:report', 'hold')
|
||||
@@ -193,15 +295,22 @@ try:
|
||||
assert guest('id', '-u').strip() == '0'
|
||||
invoke('insert', 1, software)
|
||||
wait_bay(1, 'mounted_read_only')
|
||||
guest('fds', 'run', 1, '--', 'demo.report:report', 'hold')
|
||||
assert '/run/fds/software/01/cache-demo.report' in mounts()
|
||||
guest('sh', '-c', 'cd /home/fds; /run/fds/bin/report hold >/tmp/restart-direct.log 2>&1 &')
|
||||
deadline = time.monotonic() + 20
|
||||
while query('bay', 1)['bays'][0]['consumers'] == 0:
|
||||
assert time.monotonic() < deadline
|
||||
assert '/run/fds/software/01/payload03' in mounts()
|
||||
assert '/cache-' not in mounts()
|
||||
guest('s6-rc', '-l', '/run/s6-rc', '-d', 'change', 'cartridged')
|
||||
guest('s6-rc', '-l', '/run/s6-rc', '-u', 'change', 'cartridged')
|
||||
wait_bay(1, 'mounted_read_only')
|
||||
assert query('bay', 1)['bays'][0]['consumers'] == 0
|
||||
assert '/run/fds/software/01/cache-' not in mounts()
|
||||
guest('fds', 'run', 1, '--', 'demo.hello:hello')
|
||||
# Extra payload aliases must prevent SAFE; cache is genuinely read-only.
|
||||
assert 'Hello from' in guest('sh', '-c', 'cd /home/fds; /run/fds/bin/hello')
|
||||
guest('mkdir', '-m', '700', '/tmp/private-cwd')
|
||||
assert 'Permission denied' in guest('sh', '-c', 'cd /tmp/private-cwd; /run/fds/bin/hello', ok=False)
|
||||
# Extra payload aliases must prevent SAFE; program trees are read-only.
|
||||
guest('mkdir', '/run/extra-payload')
|
||||
guest('mount', '--bind', '/run/fds/software/01/payload02', '/run/extra-payload')
|
||||
invoke('eject', 1, ok=False)
|
||||
@@ -216,10 +325,11 @@ finally:
|
||||
assert digest(software) == original_software and digest(data) == original_data
|
||||
record = dict(status='passed', ordinary_guest_uid=1000, public_emulator_cli=True,
|
||||
all_twelve_usb_bays=True, interactive_console_detach_and_terminal_restore=True, both_payload_partitions_executed=True,
|
||||
shared_partition_executed=True, readonly_cache=True,
|
||||
shared_partition_executed=True, readonly_program_trees_without_extraction=True, legacy_archive_host_and_guest_reader=True,
|
||||
direct_path_arguments_pipes_exit_status_cwd=True, interactive_program_io_signals_and_restore=True, command_collision_fallback=True,
|
||||
safe_eject=True, forced_removal_stops_consumer=True,
|
||||
corrupt_archive_and_catalogue_rejected=True, data_overlay_preserves_source=True,
|
||||
restart_cleans_cache_and_consumers=True, extra_mount_blocks_safe=True,
|
||||
corrupt_program_and_catalogue_rejected=True, data_overlay_preserves_source=True,
|
||||
restart_cleans_mounts_commands_and_consumers=True, extra_mount_blocks_safe=True,
|
||||
native_shutdown_with_active_software=True, physical_pi='not tested')
|
||||
(work / 'acceptance.json').write_text(json.dumps(record, indent=2) + '\n')
|
||||
(project / 'out/workstation-emulator-current.txt').write_text(str(work) + '\n')
|
||||
|
||||
Reference in New Issue
Block a user