Phase 6: Native Executables implementieren und Change archivieren
This commit is contained in:
218
tests/support/native-abnahme.py
Normal file
218
tests/support/native-abnahme.py
Normal file
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Native CLI-/Standalone-Parität. Vorher cargo build --release für tbc/tbrt/tb-template."""
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def run(args, cwd, *, data=b'', env=None):
|
||||
return subprocess.run([str(a) for a in args], cwd=cwd, input=data,
|
||||
capture_output=True, timeout=30, env=env)
|
||||
|
||||
|
||||
def must(result):
|
||||
assert result.returncode == 0, (result.args, result.returncode, result.stdout, result.stderr)
|
||||
return result
|
||||
|
||||
|
||||
def terminal(binary, arguments, cwd, env, abort=False):
|
||||
"""Echte Unix-PTY, Eingabe/Abbruch und unveränderte Terminalattribute."""
|
||||
import fcntl
|
||||
import select
|
||||
import signal
|
||||
import termios
|
||||
import time
|
||||
master, slave = os.openpty()
|
||||
original = termios.tcgetattr(slave)
|
||||
fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', 30, 100, 0, 0))
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
os.setsid()
|
||||
fcntl.ioctl(slave, termios.TIOCSCTTY, 0)
|
||||
for fd in (0, 1, 2): os.dup2(slave, fd)
|
||||
os.close(master)
|
||||
os.close(slave)
|
||||
result = subprocess.run([str(binary), *map(str, arguments)], cwd=cwd, env=env, check=False)
|
||||
restored = termios.tcgetattr(0) == original
|
||||
os._exit(result.returncode if restored else 99)
|
||||
transcript = bytearray()
|
||||
sent = False
|
||||
reaped = False
|
||||
deadline = time.monotonic() + 15
|
||||
try:
|
||||
while True:
|
||||
if select.select([master], [], [], .05)[0]:
|
||||
transcript.extend(os.read(master, 65536))
|
||||
if not sent and b'READY' in transcript:
|
||||
os.write(master, b'\x03' if abort else b'Ada\r')
|
||||
sent = True
|
||||
child, status = os.waitpid(pid, os.WNOHANG)
|
||||
if child:
|
||||
reaped = True
|
||||
assert os.waitstatus_to_exitcode(status) == (3 if abort else 0), bytes(transcript)
|
||||
break
|
||||
assert time.monotonic() < deadline, bytes(transcript[-2000:])
|
||||
assert sent and b'?1049h' in transcript and b'?1049l' in transcript, transcript
|
||||
assert (b'Abgebrochen' if abort else b'PTYAda') in transcript, transcript
|
||||
finally:
|
||||
if not reaped:
|
||||
os.killpg(pid, signal.SIGKILL)
|
||||
os.waitpid(pid, 0)
|
||||
os.close(master)
|
||||
os.close(slave)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--bin-dir', type=Path, default=Path('target/release'))
|
||||
parser.add_argument('--target', required=True)
|
||||
args = parser.parse_args()
|
||||
binaries = args.bin_dir.resolve()
|
||||
suffix = '.exe' if args.target == 'x86_64-pc-windows-msvc' else ''
|
||||
tbc = binaries / ('tbc' + suffix)
|
||||
rt = binaries / ('tbrt' + suffix)
|
||||
prepare = binaries / ('tb-template' + suffix)
|
||||
env = dict(os.environ, PATH='') # weder Cargo noch tbc/tbrt über PATH auflösbar
|
||||
with tempfile.TemporaryDirectory(prefix='tb-native-') as work:
|
||||
root = Path(work)
|
||||
template = root / ('runtime' + suffix)
|
||||
shutil.copy2(rt, template)
|
||||
must(run([prepare, template, args.target], root))
|
||||
empty = run([template], root, env=env)
|
||||
assert empty.returncode == 1 and b'Nutzlast' in empty.stderr, empty
|
||||
cases = {
|
||||
'console': ('PRINT "Hallo, Welt!"\nPRINT COMMAND$\nINPUT "Name"; n$\nPRINT n$\nLPRINT "Papier"\nEND\n', b'Ada\n'),
|
||||
'restart': ('OPEN "count.txt" FOR BINARY AS #1\nn%=LOF(1)\nCLOSE #1\nIF n%=0 THEN\nOPEN "count.txt" FOR OUTPUT AS #1\nPRINT #1, "einmal"\nCLOSE #1\nx%=99\nRUN\nEND IF\nPRINT x%; COMMAND$\nEND\n', b''),
|
||||
'stop': ('PRINT "vor STOP"\nSTOP\nPRINT "falsch"\n', b''),
|
||||
'error': ('PRINT "vor Fehler"\nERROR 6\n', b''),
|
||||
'project': ('CALL Ergebnis\nEND\n', b''),
|
||||
'form': ('VERSION 1.00\nBegin Form Form1\n Caption = "Native Form"\n Width = 40\n Height = 10\n Begin CommandButton Ok\n Caption = "OK"\n Default = -1\n Left = 2\n Top = 2\n Width = 10\n Height = 1\n End\nEnd\nForm1.Show\nSUB Ok_Click\nForm1.Hide\nCLS\nPRINT "FORMDONE"\nEND\nEND SUB\n', b'\n'),
|
||||
'external': ('RUN "NEXT.TBC"\n', b''),
|
||||
}
|
||||
for name, (code, data) in cases.items():
|
||||
case = root / name
|
||||
sources, reference, clean = (case / p for p in ('sources', 'reference', 'clean'))
|
||||
for d in (sources, reference, clean): d.mkdir(parents=True)
|
||||
source = sources / ('main.frm' if name == 'form' else 'main.bas')
|
||||
source.write_text(code, encoding='utf-8')
|
||||
if name == 'project':
|
||||
(sources / 'lib.bas').write_text("'$INCLUDE: 'OUTER.BI'\n")
|
||||
(sources / 'outer.bi').write_text("'$INCLUDE: 'INNER.BI'\n")
|
||||
(sources / 'inner.bi').write_text('SUB Ergebnis\nOPEN "seed.txt" FOR INPUT AS #1\nLINE INPUT #1, s$\nCLOSE #1\nOPEN "result.txt" FOR OUTPUT AS #2\nPRINT #2, s$\nCLOSE #2\nPRINT s$\nERROR 6\nEND SUB\n')
|
||||
source = sources / 'app.mak'
|
||||
source.write_text('MAIN.BAS\nLIB.BAS\n')
|
||||
for d in (reference, clean): (d / 'seed.txt').write_text('Grüße\n', encoding='utf-8')
|
||||
if name == 'form':
|
||||
source.write_text(code.replace('PRINT "FORMDONE"', 'CALL Ergebnis'), encoding='utf-8')
|
||||
(sources / 'lib.bas').write_text("'$INCLUDE: 'OUTER.BI'\n")
|
||||
(sources / 'outer.bi').write_text("'$INCLUDE: 'INNER.BI'\n")
|
||||
(sources / 'inner.bi').write_text('SUB Ergebnis\nPRINT "FORMDONE"\nEND SUB\n')
|
||||
source = sources / 'app.mak'
|
||||
source.write_text('MAIN.FRM\nLIB.BAS\n')
|
||||
if name == 'external':
|
||||
child = sources / 'next.bas'
|
||||
child.write_text('PRINT "EXTERNAL"; COMMAND$\nEND\n')
|
||||
must(run([tbc, 'build', child], root))
|
||||
shutil.copy2(child.with_suffix('.tbc'), clean / 'next.tbc')
|
||||
expected = run([tbc, 'run', source, 'alpha', 'ä beta'], reference, data=data)
|
||||
exe = clean / ('program' + suffix)
|
||||
build_source = source
|
||||
if name == 'error':
|
||||
must(run([tbc, 'build', source], root))
|
||||
build_source = source.with_suffix('.tbc')
|
||||
command = [tbc, 'build', build_source, '--exe', '--target', args.target, '--template', template, '-o', exe]
|
||||
must(run(command, root, env=env))
|
||||
original = exe.read_bytes()
|
||||
conflict = run(command, root, env=env)
|
||||
assert conflict.returncode == 1 and b'--force' in conflict.stderr
|
||||
assert exe.read_bytes() == original
|
||||
must(run(command + ['--force'], root, env=env))
|
||||
if sys.platform == 'darwin':
|
||||
must(run(['/usr/bin/codesign', '--verify', '--strict', exe], root))
|
||||
shutil.rmtree(sources)
|
||||
actual = run([exe, 'alpha', 'ä beta'], clean, data=data, env=env)
|
||||
assert (actual.returncode, actual.stdout, actual.stderr) == (expected.returncode, expected.stdout, expected.stderr), (name, expected, actual)
|
||||
if name == 'form': assert b'FORMDONE' in actual.stdout, actual
|
||||
if name == 'restart': assert b'99' not in actual.stdout and b'alpha' in actual.stdout, actual
|
||||
if name == 'project': assert b'inner.bi:9:1:' in actual.stderr.lower(), actual
|
||||
for file in reference.iterdir():
|
||||
assert (clean / file.name).read_bytes() == file.read_bytes(), file
|
||||
assert sorted(p.name for p in clean.iterdir()) == sorted([exe.name] + [p.name for p in reference.iterdir()] + (['next.tbc'] if name == 'external' else []))
|
||||
print('PASS native', name, flush=True)
|
||||
if os.name == 'posix':
|
||||
source = root / 'terminal.bas'
|
||||
source.write_text('PRINT "READY"\nINPUT s$\nPRINT "PTY"; s$\nEND\n')
|
||||
exe = root / 'terminal'
|
||||
must(run([tbc, 'build', source, '--exe', '--template', template, '--target', args.target, '-o', exe], root, env=env))
|
||||
for abort in (False, True):
|
||||
terminal(tbc, ['run', source], root, env, abort)
|
||||
source.unlink()
|
||||
for abort in (False, True):
|
||||
terminal(exe, [], root, env, abort)
|
||||
print('PASS PTY Eingabe, Abbruch und Wiederherstellung (CLI/native)', flush=True)
|
||||
if os.name == 'posix':
|
||||
import signal
|
||||
import time
|
||||
source = root / 'cancel.bas'
|
||||
source.write_text('PRINT "nicht veröffentlicht"\nEND\n')
|
||||
output = root / 'cancelled'
|
||||
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||
output.write_bytes(b'original')
|
||||
process = subprocess.Popen([str(tbc), 'build', str(source), '--exe', '--template', str(template), '--target', args.target, '-o', str(output), '--force'], cwd=root, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
try:
|
||||
deadline = time.monotonic() + 10
|
||||
while not list(root.glob('.tb-export-*.tmp')):
|
||||
assert process.poll() is None and time.monotonic() < deadline, 'Export endete vor Abbruchprobe'
|
||||
time.sleep(.001)
|
||||
process.send_signal(sig)
|
||||
stdout, stderr = process.communicate(timeout=15)
|
||||
assert process.returncode == 1 and b'abgebrochen' in stderr, (process.returncode, stdout, stderr)
|
||||
assert output.read_bytes() == b'original'
|
||||
assert not list(root.glob('.tb-export-*.tmp'))
|
||||
finally:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
process.wait()
|
||||
print('PASS Exportabbruch SIGINT/SIGTERM, Original und Bereinigung', flush=True)
|
||||
# Beschädigte Nutzlast wird auf macOS neu signiert, damit der Lader statt des Kernels prüft.
|
||||
valid = (root / 'console/clean' / ('program' + suffix)).read_bytes()
|
||||
footer = valid.rfind(b'TBPCODE!')
|
||||
assert footer >= 0
|
||||
for name, mutate in (
|
||||
('length', lambda b: struct.pack_into('<Q', b, footer + 32, 2**64-1)),
|
||||
('version', lambda b: struct.pack_into('<I', b, footer + 8, 99)),
|
||||
('truncated', lambda b: b.__delitem__(slice(footer-10, footer))),
|
||||
):
|
||||
bad = root / ('bad-' + name + suffix)
|
||||
data = bytearray(valid)
|
||||
if sys.platform == 'darwin':
|
||||
# Zunächst Signatur entfernen; Finalisierung wird anschließend erneut angewendet.
|
||||
bad.write_bytes(data)
|
||||
must(run(['/usr/bin/codesign', '--remove-signature', bad], root))
|
||||
data = bytearray(bad.read_bytes())
|
||||
mutate(data)
|
||||
if sys.platform == 'darwin':
|
||||
at = 32
|
||||
for _ in range(struct.unpack_from('<I', data, 16)[0]):
|
||||
cmd, size = struct.unpack_from('<II', data, at)
|
||||
if cmd == 0x19 and data[at+8:at+24].rstrip(b'\0') == b'__LINKEDIT':
|
||||
start = struct.unpack_from('<Q', data, at+40)[0]
|
||||
struct.pack_into('<Q', data, at+48, len(data)-start)
|
||||
at += size
|
||||
bad.write_bytes(data)
|
||||
bad.chmod(0o755)
|
||||
if sys.platform == 'darwin':
|
||||
must(run(['/usr/bin/codesign', '--force', '--sign', '-', '--timestamp=none', bad], root))
|
||||
result = run([bad], root, env=env)
|
||||
assert result.returncode == 1 and b'Ladefehler' in result.stderr and not result.stdout, result
|
||||
print('PASS corrupted', name, flush=True)
|
||||
print('Native Abnahme bestanden:', args.target)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user