update docs
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Physical acceptance records
|
||||
|
||||
Physical tests are deferred until the Pi and its attached hardware are available.
|
||||
Follow [the complete procedure](../../docs/stress-testing.md), including bay
|
||||
Follow [the complete procedure](../../docs/developer/stress-testing.md), including bay
|
||||
calibration, repeatable device populations, explicit SAFE handling and separate
|
||||
external timing measurements.
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise destructive cleanup only in small, disposable Git repositories."""
|
||||
import fcntl
|
||||
import contextlib
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
PROJECT = Path(__file__).resolve().parents[2]
|
||||
SCRIPT = PROJECT / 'tools/clean-builds'
|
||||
loader = importlib.machinery.SourceFileLoader('clean_builds', str(SCRIPT))
|
||||
spec = importlib.util.spec_from_loader(loader.name, loader)
|
||||
clean = importlib.util.module_from_spec(spec)
|
||||
loader.exec_module(clean)
|
||||
|
||||
|
||||
class CleanupTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temporary = tempfile.TemporaryDirectory(prefix='fds-clean-test.')
|
||||
self.addCleanup(self.temporary.cleanup)
|
||||
self.root = Path(self.temporary.name) / 'project'
|
||||
(self.root / 'tools').mkdir(parents=True)
|
||||
(self.root / 'out/manifests').mkdir(parents=True)
|
||||
shutil.copyfile(SCRIPT, self.root / 'tools/clean-builds')
|
||||
subprocess.run(['git', 'init', '-q', self.root], check=True)
|
||||
|
||||
def file(self, name, content='fixture'):
|
||||
path = self.root / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content)
|
||||
return path
|
||||
|
||||
def run_cli(self, *args):
|
||||
return subprocess.run([sys.executable, self.root / 'tools/clean-builds', *args],
|
||||
cwd=self.root, text=True, capture_output=True, timeout=30)
|
||||
|
||||
def selected(self):
|
||||
return {p.relative_to(self.root).as_posix() for p in clean.candidates(self.root)[0]}
|
||||
|
||||
def test_cleanup_and_repeat_preserve_published_and_user_files(self):
|
||||
old = self.file('out/rootfs-build.OLD123/root/data')
|
||||
self.file('target/debug/program')
|
||||
keep = [
|
||||
self.file('out/rootfs-build.NEW123/rootfs.tar'),
|
||||
self.file('out/fds-os-0.1.0/release.img'),
|
||||
self.file('out/inputs-m12-v5/lock.json'),
|
||||
self.file('out/rebuild-m12-v5-a/out/image.img'),
|
||||
self.file('out/my-emulator/data.qcow2'),
|
||||
self.file('out/emulator/session.json'),
|
||||
self.file('out/my-cartridge.img'),
|
||||
self.file('out/cache/rootfs/package.xbps'),
|
||||
self.file('out/logs/build.log'),
|
||||
self.file('out/manifests/acceptance.json'),
|
||||
self.file('out/.gitkeep'),
|
||||
self.file('.host/xbps/tool'),
|
||||
self.file('vendor/void-packages/hostdir/download'),
|
||||
]
|
||||
(self.root / 'out/rootfs-cli.tar').symlink_to('rootfs-build.NEW123/rootfs.tar')
|
||||
preview = self.run_cli('--dry-run')
|
||||
self.assertEqual(preview.returncode, 0, preview.stderr)
|
||||
self.assertTrue(old.exists())
|
||||
self.assertFalse((self.root / 'out/.clean.lock').exists())
|
||||
result = self.run_cli()
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertFalse(old.exists())
|
||||
self.assertFalse((self.root / 'target').exists())
|
||||
self.assertTrue(all(p.read_text() == 'fixture' for p in keep))
|
||||
self.assertEqual((self.root / 'out/rootfs-cli.tar').read_text(), 'fixture')
|
||||
repeat = self.run_cli()
|
||||
self.assertEqual(repeat.returncode, 0, repeat.stderr)
|
||||
self.assertIn('removed 0 disposable directories', repeat.stdout)
|
||||
|
||||
def test_text_pointers_latest_fixture_markers_and_transitive_links(self):
|
||||
self.file('out/emu-test.old000/normal/data.qcow2')
|
||||
self.file('out/emu-test.new000/normal/data.qcow2')
|
||||
self.file('out/workstation-emulator-current.txt', str(self.root / 'out/emu-test.new000') + '\n')
|
||||
self.file('out/dasung-s6.old000/compiled/a')
|
||||
self.file('out/dasung-s6.new000/compiled/a')
|
||||
self.file('out/manifests/dasung-s6-database.txt', str(self.root / 'out/dasung-s6.new000/compiled'))
|
||||
self.file('out/workstation-images.new000/acceptance.json')
|
||||
self.file('out/workstation-images-current.txt', 'out/workstation-images.new000\n')
|
||||
self.file('out/m9-images.old000/root/program.img')
|
||||
self.file('out/m9-images.new000/root/program.img')
|
||||
os.utime(self.root / 'out/m9-images.old000', ns=(1, 1))
|
||||
self.file('out/m8-vm.kept00/.fds-keep', '')
|
||||
self.file('out/m8-vm.other0/disk.img')
|
||||
(self.root / 'out/m8-vm.kept00/dependency').symlink_to('../m8-vm.other0')
|
||||
self.assertEqual(self.selected(), {
|
||||
'out/emu-test.old000', 'out/dasung-s6.old000', 'out/m9-images.old000'})
|
||||
|
||||
def test_symlinks_do_not_delete_external_content(self):
|
||||
outside = Path(self.temporary.name) / 'outside'
|
||||
outside.mkdir()
|
||||
(outside / 'data').write_text('precious')
|
||||
(self.root / 'target').symlink_to(outside)
|
||||
(self.root / 'out/m8-vm.abcdef').symlink_to(outside)
|
||||
work = self.file('out/m8-vm.ghijkl/data').parent
|
||||
(work / 'external').symlink_to(outside)
|
||||
result = self.run_cli()
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertFalse(work.exists())
|
||||
self.assertTrue((self.root / 'target').is_symlink())
|
||||
self.assertEqual((outside / 'data').read_text(), 'precious')
|
||||
shutil.rmtree(self.root / 'out')
|
||||
(self.root / 'out').symlink_to(outside)
|
||||
self.assertNotEqual(self.run_cli().returncode, 0)
|
||||
self.assertEqual((outside / 'data').read_text(), 'precious')
|
||||
|
||||
def test_tracked_files_are_kept(self):
|
||||
self.file('out/m2-vm.abcdef/notes')
|
||||
subprocess.run(['git', '-C', self.root, 'add', 'out/m2-vm.abcdef/notes'], check=True)
|
||||
self.assertEqual(self.selected(), set())
|
||||
|
||||
def test_directory_permissions_and_hardlinks(self):
|
||||
kept = self.file('out/kept-file')
|
||||
kept.chmod(0o444)
|
||||
root = self.root / 'out/rootfs-build.abcdef/root'
|
||||
root.mkdir(parents=True)
|
||||
os.link(kept, root / 'file')
|
||||
root.chmod(0o555)
|
||||
result = self.run_cli()
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertFalse(root.exists())
|
||||
self.assertEqual(kept.stat().st_mode & 0o777, 0o444)
|
||||
self.assertEqual(kept.read_text(), 'fixture')
|
||||
|
||||
def test_held_build_lock_rejects_without_deletion(self):
|
||||
old = self.file('out/m2-vm.abcdef/image')
|
||||
with (self.root / 'out/.rootfs.lock').open('w') as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX)
|
||||
result = self.run_cli()
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn('locked', result.stderr)
|
||||
self.assertTrue(old.exists())
|
||||
|
||||
def test_live_process_rejects_without_deletion(self):
|
||||
old = self.file('out/m2-vm.abcdef/image')
|
||||
child = subprocess.Popen(
|
||||
[sys.executable, '-c', 'import sys; print("ready", flush=True); sys.stdin.read()'],
|
||||
cwd=old.parent, stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
|
||||
try:
|
||||
self.assertEqual(child.stdout.readline().strip(), 'ready')
|
||||
result = self.run_cli()
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn('is using', result.stderr)
|
||||
self.assertTrue(old.exists())
|
||||
finally:
|
||||
child.communicate(timeout=10)
|
||||
|
||||
def test_mount_rejection_including_escaped_spaces(self):
|
||||
mount = self.root / 'out/m2-vm.abcdef/a directory'
|
||||
line = '1 0 0:1 / ' + str(mount).replace(' ', r'\040') + ' rw - tmpfs tmpfs rw\n'
|
||||
with self.assertRaisesRegex(ValueError, 'mounted path'):
|
||||
clean.check_mounts([mount.parent], line)
|
||||
clean.check_mounts([self.root / 'target'], line)
|
||||
|
||||
def test_newly_published_output_aborts_before_any_deletion(self):
|
||||
old = self.file('out/system-build.abcdef/system.img')
|
||||
|
||||
def publish(_paths):
|
||||
(self.root / 'out/fds-system-cli.img').symlink_to('system-build.abcdef/system.img')
|
||||
return 0
|
||||
|
||||
with patch.object(clean, 'footprint', side_effect=publish), contextlib.redirect_stdout(io.StringIO()):
|
||||
with self.assertRaisesRegex(ValueError, 'selection changed'):
|
||||
clean.clean(self.root, False)
|
||||
self.assertTrue(old.exists())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2)
|
||||
@@ -250,5 +250,9 @@ with VM(work,'writeback-error',system,extra=['-device',controller]) as vm:
|
||||
vm.qmp('device_del',{'id':'bay2'});bay(vm,2,'empty')
|
||||
vm.send('s6-setuidgid fds fds poweroff');finished(vm)
|
||||
print('PASS: attached-device writeback EIO is retained across restarts and refuses SAFE and shutdown',flush=True)
|
||||
link=project/'out/m10-vm-latest'
|
||||
temporary=link.with_suffix('.next')
|
||||
temporary.symlink_to(work.name)
|
||||
temporary.replace(link)
|
||||
print(f'PASS: M10 ordered shutdown evidence: {work}')
|
||||
print('SKIP: physical Pi poweroff/reboot, flash-controller durability, battery behavior and sub-second hardware timing targets')
|
||||
|
||||
@@ -53,7 +53,7 @@ def desktop(vm, enabled):
|
||||
return p['desktop']==('windowmaker' if enabled else 'cli') and (p['ready_ns'] is not None if enabled else True)
|
||||
return json.loads(wait(vm,'fds --json profiles',condition))['profiles']
|
||||
def image(name, replacements):
|
||||
replacements={**replacements, **{f'usr/bin/{binary}':(project/'out'/binary).read_bytes() for binary in ['fds','fds-cartridged','fds-profile']}}
|
||||
replacements={**replacements, **{f'usr/bin/{binary}':(project/'out'/binary).read_bytes() for binary in ['fds','fds-program','fds-control','fds-cartridged','fds-profile']}}
|
||||
with tarfile.open(project/'out/rootfs-development.tar') as source, tarfile.open(work/(name+'.tar'),'w',format=tarfile.PAX_FORMAT) as output:
|
||||
assert source.extractfile('usr/share/fds/image-profile').read().strip()==b'development', 'Build PROFILE=development before this test'
|
||||
seen=set()
|
||||
@@ -86,10 +86,13 @@ with VM(work,'probe',probe,extra=['-device',controller,'-device','usb-kbd,bus=xh
|
||||
print(capture(vm,'cat /run/log/xserver/current /run/log/desktop/current /run/log/cartridged/current'),flush=True)
|
||||
raise
|
||||
shell(vm,'test ! -d /home/fds/.cache/fontconfig','M8_PREBUILT_FONT_CACHE')
|
||||
shell(vm,'cmp /etc/WindowMaker/WindowMaker /usr/share/fds/eink/WindowMaker && cmp /etc/WindowMaker/WMRootMenu /usr/share/fds/eink/WMRootMenu && grep -q "FDS Control" /etc/WindowMaker/WMRootMenu','FDS_GLOBAL_RETRO_DEFAULTS')
|
||||
(work/'activation.json').write_text(json.dumps(p,indent=2)+'\n')
|
||||
assert p['ready_ns']>=p['activation_ns']>0
|
||||
shell(vm,'export DISPLAY=:0 XAUTHORITY=/run/fds/x11/authority; s6-setuidgid fds xdpyinfo >/tmp/display-info; ! grep -q "COMPOSITE" /tmp/display-info','M8_X11_AUTHENTICATED')
|
||||
shell(vm,'if env XAUTHORITY=/dev/null xdpyinfo >/dev/null 2>&1; then false; else true; fi','M8_X11_REJECTS_NO_COOKIE')
|
||||
panel=wait(vm,'s6-setuidgid fds xdotool search --onlyvisible --name "^FDS Control$"',lambda s:s.isdigit())
|
||||
shell(vm,'test "$(ps -o uid= -C fds-control | xargs)" = 1000','FDS_CONTROL_UNPRIVILEGED')
|
||||
window=wait(vm,'s6-setuidgid fds xdotool search --onlyvisible --name "^FDS Terminal$"',lambda s:s.isdigit())
|
||||
shell(vm,f's6-setuidgid fds xdotool windowactivate --sync {window} type --clearmodifiers "printf M8_INPUT_WORKED > /home/fds/desktop-input"; s6-setuidgid fds xdotool key --clearmodifiers Return','M8_X11_INPUT_SENT')
|
||||
wait(vm,'cat /home/fds/desktop-input 2>/dev/null',lambda s:s=='M8_INPUT_WORKED')
|
||||
@@ -145,12 +148,58 @@ with VM(work,'cartridges',fixture,extra=['-device',controller,'-netdev','user,id
|
||||
add(env,2);bay(vm,2,'mounted_read_only');desktop(vm,True);remove(2);desktop(vm,False)
|
||||
add(program,4);entry=bay(vm,4,'mounted_read_only');assert entry['mount']=='/run/fds/apps/fds.program.test'
|
||||
shell(vm,'test ! -e /home/fds/program-identity && ! /run/fds/apps/fds.program.test/app/bin/check','M8_NO_AUTORUN')
|
||||
started=query(vm,'s6-setuidgid fds fds --json run 4 -- check');assert started['started_pid']>1
|
||||
shell(vm,'fds profile activate windowmaker','FDS_CONTROL_START_DESKTOP')
|
||||
desktop(vm,True)
|
||||
shell(vm,'export DISPLAY=:0 XAUTHORITY=/run/fds/x11/authority','FDS_CONTROL_DISPLAY')
|
||||
panel=wait(vm,'s6-setuidgid fds xdotool search --onlyvisible --name "^FDS Control$"',lambda s:s.isdigit())
|
||||
wait(vm,'s6-setuidgid fds xdotool search --onlyvisible --name "^FDS Terminal$"',lambda s:s.isdigit())
|
||||
shell(vm,f's6-setuidgid fds xdotool windowactivate --sync {panel} windowraise {panel} mousemove --window {panel} 70 218 click 1','FDS_CONTROL_SELECT_BAY')
|
||||
def panel_state():
|
||||
output=capture(vm,f's6-setuidgid fds xprop -id {panel} _FDS_CONTROL_STATE')
|
||||
return json.loads(json.loads(output.split(' = ',1)[1]))
|
||||
deadline=time.monotonic()+30
|
||||
while True:
|
||||
snapshot=panel_state()
|
||||
if snapshot['bay']==4 and snapshot['commands']>0 and not snapshot['busy']: break
|
||||
assert time.monotonic()<deadline,snapshot
|
||||
shell(vm,f's6-setuidgid fds xdotool windowactivate --sync {panel} windowraise {panel} mousemove --window {panel} 420 518 click 1','FDS_CONTROL_RUN_CLICK')
|
||||
wait(vm,'cat /home/fds/program-identity 2>/dev/null',lambda s:'uid=1000(fds)' in s)
|
||||
assert query(vm,'fds --json bay 4')['bays'][0]['consumers']>0
|
||||
shell(vm,f's6-setuidgid fds xdotool windowactivate --sync {panel}','FDS_CONTROL_SHOW_PANEL')
|
||||
shell(vm,'s6-setuidgid fds xwd -root -silent -out /home/fds/control.xwd','FDS_CONTROL_SCREENSHOT')
|
||||
(work/'legacy-control.xwd').write_bytes(gzip.decompress(base64.b64decode(capture(vm,'gzip -c /home/fds/control.xwd | base64 -w0'))))
|
||||
wait(vm,'cat /home/fds/program-identity 2>/dev/null',lambda s:'uid=1000(fds)' in s)
|
||||
assert '/run/fds/apps/fds.program.test/app/lib' in capture(vm,'cat /home/fds/program-paths')
|
||||
shell(vm,'if fds run 4 -- escape; then false; else true; fi','M8_PROGRAM_ESCAPE_REJECTED')
|
||||
shell(vm,'if fds run 4 -- ../check; then false; else true; fi; fds eject 4','M8_PROGRAM_SAFE')
|
||||
shell(vm,'if fds run 4 -- ../check; then false; else true; fi','M8_PROGRAM_PATH_REJECTED')
|
||||
deadline=time.monotonic()+30
|
||||
while panel_state()['busy']:
|
||||
assert time.monotonic()<deadline
|
||||
shell(vm,f's6-setuidgid fds xdotool windowactivate --sync {panel} windowraise {panel} mousemove --window {panel} 612 518 click 1','FDS_CONTROL_EJECT_CLICK')
|
||||
assert bay(vm,4,'safe')['consumers']==0;remove(4)
|
||||
# Launch a format-2, Void-built command through the real panel as well.
|
||||
built=Path((project/'out/workstation-images-current.txt').read_text().strip())
|
||||
assert json.loads((built/'acceptance.json').read_text())['status']=='passed'
|
||||
add(built/'software.img',4);bay(vm,4,'mounted_read_only')
|
||||
deadline=time.monotonic()+30
|
||||
while True:
|
||||
snapshot=panel_state()
|
||||
if snapshot['commands']>=2 and not snapshot['busy']: break
|
||||
assert time.monotonic()<deadline,snapshot
|
||||
shell(vm,f's6-setuidgid fds xdotool windowactivate --sync {panel} windowraise {panel} mousemove --window {panel} 420 518 click 1','FDS_CONTROL_RUN_VOID')
|
||||
hello_window=wait(vm,'s6-setuidgid fds xdotool search --onlyvisible --name "^demo.hello:hello$"',lambda s:s.isdigit())
|
||||
shell(vm,f's6-setuidgid fds xdotool windowsize {hello_window} 650 360 windowmove {hello_window} 930 70 windowactivate --sync {panel}','FDS_CONTROL_VOID_RESULT')
|
||||
shell(vm,'s6-setuidgid fds xwd -root -silent -out /home/fds/control.xwd','FDS_CONTROL_VOID_SCREENSHOT')
|
||||
(work/'control.xwd').write_bytes(gzip.decompress(base64.b64decode(capture(vm,'gzip -c /home/fds/control.xwd | base64 -w0'))))
|
||||
shell(vm,f's6-setuidgid fds xdotool windowactivate --sync {panel} windowraise {panel} key r','FDS_CONTROL_KEYBOARD_RESCAN')
|
||||
deadline=time.monotonic()+30
|
||||
while True:
|
||||
snapshot=panel_state()
|
||||
if not snapshot['busy'] and snapshot['status']=='Cartridge inventory refreshed.': break
|
||||
assert time.monotonic()<deadline,snapshot
|
||||
shell(vm,f's6-setuidgid fds xdotool windowactivate --sync {panel} windowraise {panel} mousemove --window {panel} 612 518 click 1','FDS_CONTROL_EJECT_VOID')
|
||||
assert bay(vm,4,'safe')['consumers']==0;remove(4)
|
||||
shell(vm,'fds profile deactivate && ! pgrep -x fds-control','FDS_CONTROL_SESSION_STOPPED')
|
||||
shell(vm,'modprobe cdc_ether','M8_NETWORK_DRIVER')
|
||||
vm.qmp('device_add',{'driver':'usb-net','id':'ethernet','netdev':'net','bus':'xhci.0','port':'3'})
|
||||
wait(vm,'fds --json profiles',lambda s:len(json.loads(s)['profiles']['network'])==1)
|
||||
@@ -173,7 +222,7 @@ with VM(work,'no-dhcp-server',fixture,extra=['-device',controller,'-netdev','hub
|
||||
shell(vm,'fds network off','M8_NO_DHCP_STOPPED')
|
||||
print('PASS: console and service controls work with Ethernet present and no DHCP server',flush=True)
|
||||
# Convert this test's raw X11 screenshot to a portable PNG without external tools.
|
||||
data=(work/'desktop.xwd').read_bytes();header=struct.unpack('>25I',data[:100]);size,version,fmt,depth,width,height,xoff,order,unit,bitorder,pad,bpp,stride,visual,rm,gm,bm,*_=header
|
||||
data=(work/'control.xwd').read_bytes();header=struct.unpack('>25I',data[:100]);size,version,fmt,depth,width,height,xoff,order,unit,bitorder,pad,bpp,stride,visual,rm,gm,bm,*_=header
|
||||
assert version==7 and fmt==2 and bpp==32 and (rm,gm,bm)==(0xff0000,0xff00,0xff)
|
||||
offset=size+header[19]*12
|
||||
raw=bytearray()
|
||||
@@ -183,7 +232,8 @@ for y in range(height):
|
||||
pixel=int.from_bytes(data[offset+y*stride+x*4:offset+y*stride+x*4+4], 'little' if order==0 else 'big')
|
||||
raw.extend(((pixel>>16)&255,(pixel>>8)&255,pixel&255))
|
||||
def chunk(kind,body):return struct.pack('>I',len(body))+kind+body+struct.pack('>I',zlib.crc32(kind+body)&0xffffffff)
|
||||
(work/'desktop.png').write_bytes(b'\x89PNG\r\n\x1a\n'+chunk(b'IHDR',struct.pack('>IIBBBBB',width,height,8,2,0,0,0))+chunk(b'IDAT',zlib.compress(raw))+chunk(b'IEND',b''))
|
||||
(work/'control.png').write_bytes(b'\x89PNG\r\n\x1a\n'+chunk(b'IHDR',struct.pack('>IIBBBBB',width,height,8,2,0,0,0))+chunk(b'IDAT',zlib.compress(raw))+chunk(b'IEND',b''))
|
||||
(work/'acceptance.json').write_text(json.dumps(dict(status='passed', native_arm_x11=True, panel_uid=1000, global_grayscale_defaults=True, legacy_and_void_programs_launched_by_mouse=True, keyboard_rescan=True, panel_safe_eject=True, desktop_stop_removes_panel=True, physical_display='not tested'),indent=2)+'\n')
|
||||
link=project/'out/m8-vm-latest';temporary=link.with_suffix('.next');temporary.symlink_to(work.name);temporary.replace(link)
|
||||
print(f'PASS: M8 ARM desktop and network verification: {work}')
|
||||
print('SKIP: Pi DRM/VC4 output, physical E-Ink quality and monitor input-to-refresh latency')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -14,12 +14,19 @@ from image_formats import gpt, LINUX_FILESYSTEM
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--cli', type=Path, required=True)
|
||||
parser.add_argument('--image-tool-runner', type=Path)
|
||||
parser.add_argument('--cc', nargs='+', default=['aarch64-linux-gnu-gcc'])
|
||||
parser.add_argument('--void-packages', type=Path, default=project / 'vendor/void-packages')
|
||||
parser.add_argument('--xbps-tool-runner', type=Path)
|
||||
parser.add_argument('--xbps-bin', type=Path)
|
||||
args = parser.parse_args()
|
||||
work = Path(tempfile.mkdtemp(prefix='workstation-images.', dir=project / 'out'))
|
||||
cli = [str(args.cli.resolve())]
|
||||
if args.image_tool_runner:
|
||||
cli += ['--image-tool-runner', str(args.image_tool_runner)]
|
||||
cli += ['--void-packages', str(args.void_packages.resolve())]
|
||||
if args.xbps_tool_runner:
|
||||
cli += ['--xbps-tool-runner', str(args.xbps_tool_runner.resolve())]
|
||||
if args.xbps_bin:
|
||||
cli += ['--xbps-bin', str(args.xbps_bin.resolve())]
|
||||
log = (work / 'commands.log').open('w')
|
||||
|
||||
|
||||
@@ -36,39 +43,32 @@ def digest(path):
|
||||
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
||||
|
||||
|
||||
(work / 'hello-root/bin').mkdir(parents=True)
|
||||
(work / 'report-root/bin').mkdir(parents=True)
|
||||
(work / 'hello.c').write_text('#include <stdio.h>\nint main(void) { puts("FDS cartridge AArch64 hello"); return 0; }\n')
|
||||
script = work / 'report-root/bin/report'
|
||||
script.write_text('#!/bin/sh\nprintf "FDS cartridge script report\\n"\nid\n[ "${1-}" != hold ] || exec tail -f /dev/null\n')
|
||||
script.chmod(0o755)
|
||||
recipe = f'''format=1
|
||||
id="demo.hello"
|
||||
name="AArch64 hello"
|
||||
version="1.0"
|
||||
architecture="aarch64"
|
||||
root="hello-root"
|
||||
[commands]
|
||||
hello="bin/hello"
|
||||
[build]
|
||||
directory={json.dumps(str(project))}
|
||||
command={json.dumps([*args.cc, '-O2', str(work / 'hello.c'), '-o', str(work / 'hello-root/bin/hello')])}
|
||||
'''
|
||||
(work / 'hello.toml').write_text(recipe)
|
||||
(work / 'report.toml').write_text('format=1\nid="demo.report"\nname="Script report"\nversion="1.0"\narchitecture="any"\nroot="report-root"\n[commands]\nreport="bin/report"\n')
|
||||
invoke(['software', 'build', work / 'hello.toml', work / 'hello-bundle'])
|
||||
invoke(['software', 'pack', work / 'report.toml', work / 'report-bundle'])
|
||||
# Architecture-independent bundles cannot hide actual ELF executables.
|
||||
(work / 'wrong-architecture.toml').write_text(recipe.replace('architecture="aarch64"', 'architecture="any"'))
|
||||
invoke(['software', 'pack', work / 'wrong-architecture.toml', work / 'rejected-architecture'], False)
|
||||
assert not (work / 'rejected-architecture').exists()
|
||||
# Source links outside the software root must never become bundle contents.
|
||||
(work / 'report-root/bin/escape').symlink_to('/etc/passwd')
|
||||
invoke(['software', 'pack', work / 'report.toml', work / 'rejected-symlink'], False)
|
||||
assert not (work / 'rejected-symlink').exists()
|
||||
(work / 'report-root/bin/escape').unlink()
|
||||
invoke(['software', 'pack', work / 'report.toml', work / 'report-bundle'], False)
|
||||
(work / 'cartridge.toml').write_text('format=1\nid="demo.workstation"\nname="Workstation software"\nversion="1.0"\n[[payload]]\nbundles=["hello-bundle"]\n[[payload]]\nbundles=["report-bundle"]\n')
|
||||
for name in ['hello', 'report']:
|
||||
recipe = project / f'examples/software/{name}/software.toml'
|
||||
if name == 'report':
|
||||
text = recipe.read_text().replace('[commands]', '[commands]\nshell="usr/bin/bash"\nwhere="usr/bin/printenv"')
|
||||
text = text.replace('template = "void"', 'template = ' + json.dumps(str(recipe.parent / 'void')))
|
||||
recipe = work / 'report-source.toml'; recipe.write_text(text)
|
||||
invoke(['software', 'build', recipe, work / f'{name}-installed'])
|
||||
inspected = json.loads(invoke(['software', 'inspect', work / f'{name}-installed']))
|
||||
assert inspected['installed'] and inspected['packages']
|
||||
assert f'fds-demo-{name}-1.0_1' in inspected['packages']
|
||||
assert any(p.startswith('glibc-') for p in inspected['packages'])
|
||||
assert (work / f'{name}-installed/root/usr/bin/{name}').is_file()
|
||||
assert not list((work / f'{name}-installed').glob('*.tar.xz'))
|
||||
# The actual installed trees, including command links, are integrity checked.
|
||||
program = work / 'hello-installed/root/usr/bin/hello'
|
||||
original_program = program.read_bytes()
|
||||
bad_elf = bytearray(original_program); bad_elf[18:20] = bytes([62, 0])
|
||||
program.write_bytes(bad_elf)
|
||||
invoke(['software', 'inspect', work / 'hello-installed'], False)
|
||||
program.write_bytes(original_program)
|
||||
escape = work / 'report-installed/root/usr/bin/escape'
|
||||
escape.symlink_to('/etc/passwd')
|
||||
invoke(['software', 'inspect', work / 'report-installed'], False)
|
||||
escape.unlink()
|
||||
invoke(['software', 'build', project / 'examples/software/hello/software.toml', work / 'hello-installed'], False)
|
||||
(work / 'cartridge.toml').write_text('format=2\nid="demo.workstation"\nname="Workstation software"\nversion="1.0"\n[[payload]]\nsources=["hello-installed"]\n[[payload]]\nsources=["report-installed"]\n')
|
||||
image = work / 'software.img'
|
||||
original = json.loads(invoke(['create', work / 'cartridge.toml', image]))
|
||||
assert len(original['image']['partitions']) == 3
|
||||
@@ -78,18 +78,15 @@ assert [p['name'] for p in observed['partitions']] == ['FDS_METADATA', 'FDS_PAYL
|
||||
invoke(['create', work / 'cartridge.toml', work / 'repeat.img'])
|
||||
assert digest(image) == digest(work / 'repeat.img')
|
||||
invoke(['create', work / 'cartridge.toml', image], False)
|
||||
shared = (work / 'cartridge.toml').read_text().replace('bundles=["hello-bundle"]\n[[payload]]\nbundles=["report-bundle"]', 'bundles=["hello-bundle","report-bundle"]')
|
||||
(work / 'collision.toml').write_text((work / 'cartridge.toml').read_text().replace('demo.workstation', 'demo.second'))
|
||||
invoke(['create', work / 'collision.toml', work / 'collision.img'])
|
||||
shared = (work / 'cartridge.toml').read_text().replace('sources=["hello-installed"]\n[[payload]]\nsources=["report-installed"]', 'sources=["hello-installed","report-installed"]')
|
||||
(work / 'shared.toml').write_text(shared)
|
||||
grouped = json.loads(invoke(['create', work / 'shared.toml', work / 'shared.img']))
|
||||
assert len(grouped['image']['partitions']) == 2
|
||||
assert [s['partition'] for s in grouped['catalogue']['software']] == [2, 2]
|
||||
for name in ['software.img', 'shared.img']:
|
||||
subprocess.run(['sfdisk', '--verify', str(work / name)], check=True, stdout=log, stderr=log)
|
||||
for directory in ['hello-bundle', 'report-bundle']:
|
||||
archive = next((work / directory).glob('*.tar.xz'))
|
||||
subprocess.run(['xz', '--test', str(archive)], check=True)
|
||||
subprocess.run(['tar', '-tJf', str(archive)], check=True, stdout=log)
|
||||
|
||||
size = image.stat().st_size
|
||||
for label, extra in [('exact', 0), ('larger', 8 * 1024 * 1024)]:
|
||||
target = work / (label + '.target')
|
||||
@@ -135,14 +132,14 @@ for part in parts:
|
||||
target.write(source.read(part['bytes']))
|
||||
filesystems.append(filesystem)
|
||||
tools = [str(args.image_tool_runner.resolve())] if args.image_tool_runner else []
|
||||
for case in ['archive-corrupt', 'catalogue-mapping']:
|
||||
number = 2 if case == 'archive-corrupt' else 1
|
||||
for case in ['program-corrupt', 'catalogue-mapping']:
|
||||
number = 2 if case == 'program-corrupt' else 1
|
||||
tree = work / (case + '-tree')
|
||||
subprocess.run([*tools, 'fsck.erofs', '--extract=' + str(tree), str(filesystems[number-1])], check=True, stdout=log, stderr=log)
|
||||
if number == 2:
|
||||
archive = next((tree / 'bundles').glob('*.tar.xz'))
|
||||
content = bytearray(archive.read_bytes()); content[len(content)//2] ^= 1
|
||||
archive.write_bytes(content)
|
||||
program = tree / 'programs/demo.hello/usr/bin/hello'
|
||||
content = bytearray(program.read_bytes()); content[len(content)//2] ^= 1
|
||||
program.write_bytes(content)
|
||||
else:
|
||||
metadata = tree / 'FDS/SOFTWARE.TOML'
|
||||
metadata.write_text(metadata.read_text().replace('partition = 2', 'partition = 4'))
|
||||
@@ -153,11 +150,11 @@ for case in ['archive-corrupt', 'catalogue-mapping']:
|
||||
gpt(malformed, [(part['name'], LINUX_FILESYSTEM, filesystem) for part, filesystem in zip(parts, selected)])
|
||||
invoke(['inspect', malformed], False)
|
||||
record = dict(status='passed', work=str(work), cli_sha256=digest(args.cli.resolve()), compiled_aarch64_software=True,
|
||||
script_bundle=True, elf_in_any_bundle_and_escaping_source_symlink_rejected=True, shared_and_separate_payload_partitions=True,
|
||||
repeat_image_identical=True, independent_gpt_xz_tar_checks=True,
|
||||
void_source_packages=True, installed_runtime_dependencies=True, wrong_elf_and_escaping_symlink_rejected=True, shared_and_separate_payload_partitions=True,
|
||||
repeat_image_identical=True, direct_installed_erofs_programs=True,
|
||||
exact_and_larger_target_readback=True, wrong_confirmation_unchanged=True,
|
||||
changed_source_unchanged_target=True, stale_target_preview_rejected=True,
|
||||
corrupted_gpt_rejected=True, corrupt_archive_and_catalogue_rejected=True, physical_usb_write='not performed')
|
||||
corrupted_gpt_rejected=True, corrupt_program_and_catalogue_rejected=True, physical_usb_write='not performed')
|
||||
(work / 'acceptance.json').write_text(json.dumps(record, indent=2) + '\n')
|
||||
(project / 'out/workstation-images-current.txt').write_text(str(work) + '\n')
|
||||
print('PASS: workstation software/image/write acceptance:', work)
|
||||
|
||||
Reference in New Issue
Block a user