Phase 6: Plattformprüfungen und absolute RGB-Farben, Change 05 archivieren

This commit is contained in:
2026-09-07 21:12:25 +02:00
parent 5aa920b768
commit 796564795a
55 changed files with 3772 additions and 200 deletions

103
tests/platform/input.frm Normal file
View File

@@ -0,0 +1,103 @@
VERSION 1.00
Begin Form Probe
Caption = "Plattformprobe: Ereignisse genau einmal"
Width = 70
Height = 18
Begin Menu mnuDatei
Caption = "&Datei"
Begin Menu mnuEnde
Caption = "&Ende"
End
End
Begin Label Hint
Caption = "Textfeld: F1-F12 / Modifier. Datei > Ende beendet."
Left = 2
Top = 2
Width = 65
Height = 1
End
Begin TextBox Text1
Left = 2
Top = 4
Width = 50
Height = 1
TabIndex = 0
End
Begin Label Counts
Caption = "Down/Up/Press/Click/Move: 0/0/0/0/0"
Left = 2
Top = 6
Width = 65
Height = 1
End
Begin Label Unicode
Caption = "Unicode: ÄÖÜ äöü ß 中界 ┌─┐"
Left = 2
Top = 8
Width = 60
Height = 1
End
Begin CommandButton Count
Caption = "&Zählen"
Left = 2
Top = 10
Width = 14
Height = 1
TabIndex = 1
End
Begin CommandButton Done
Caption = "&Ende"
Default = -1
Left = 20
Top = 10
Width = 14
Height = 1
TabIndex = 2
End
End
DIM SHARED downs&, ups&, presses&, clicks&, moves&
OPEN "platform-events.log" FOR OUTPUT AS #1
CLOSE #1
Probe.Show
Text1.SetFocus
SUB RecordEvent(kind$, code%, modifiers%)
OPEN "platform-events.log" FOR APPEND AS #1
PRINT #1, kind$; ":"; code%; ":"; modifiers%
CLOSE #1
Counts.Caption = "Down/Up/Press/Click/Move:" + STR$(downs&) + "/" + STR$(ups&) + "/" + STR$(presses&) + "/" + STR$(clicks&) + "/" + STR$(moves&)
END SUB
SUB Text1_KeyDown(KeyCode AS INTEGER, Shift AS INTEGER)
downs& = downs& + 1
CALL RecordEvent("Down", KeyCode, Shift)
END SUB
SUB Text1_KeyUp(KeyCode AS INTEGER, Shift AS INTEGER)
ups& = ups& + 1
CALL RecordEvent("Up", KeyCode, Shift)
END SUB
SUB Text1_KeyPress(KeyAscii AS INTEGER)
presses& = presses& + 1
CALL RecordEvent("Press", KeyAscii, 0)
END SUB
SUB Probe_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)
CALL RecordEvent("MouseDown", Button, Shift)
END SUB
SUB Probe_MouseUp(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)
CALL RecordEvent("MouseUp", Button, Shift)
END SUB
SUB Probe_MouseMove(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)
moves& = moves& + 1
CALL RecordEvent("MouseMove", Button, Shift)
END SUB
SUB Count_Click()
clicks& = clicks& + 1
CALL RecordEvent("Click", 0, 0)
END SUB
SUB Done_Click()
CALL RecordEvent("End", 0, 0)
END
END SUB
SUB mnuEnde_Click()
CALL RecordEvent("End", 0, 0)
END
END SUB

View File

@@ -2,6 +2,7 @@
"""Headless Unix PTY smoke: real terminal handoff, child Ctrl+C and IDE resume.
Run after cargo build -p tb-ide: python3 tests/support/ide-execution-pty.py
"""
import argparse
import fcntl
import os
from pathlib import Path
@@ -13,10 +14,10 @@ import tempfile
import termios
import time
binary = Path(__file__).resolve().parents[2] / 'target/debug/tb'
def exercise(abort=False, file_shell=False):
def exercise(binary, transcript_dir=None, abort=False, file_shell=False):
with tempfile.TemporaryDirectory(prefix='tb-pty-') as directory:
source = Path(directory) / 'main.bas'
command = "printf '\\nWAITING_CHILD\\n'; exec sleep 30" if abort else "printf '\\nSHELL_PROOF\\n'; exit 7"
@@ -32,7 +33,7 @@ def exercise(abort=False, file_shell=False):
os.dup2(slave, fd)
os.close(master)
os.close(slave)
os.environ['TERM'] = 'xterm-256color'
os.environ.setdefault('TERM', 'xterm-256color')
os.environ['XDG_CONFIG_HOME'] = directory
# Keep the controlling terminal alive while checking restoration.
signal.signal(signal.SIGINT, lambda *_: None)
@@ -83,7 +84,17 @@ def exercise(abort=False, file_shell=False):
transcript.extend(os.read(master, 65536))
assert transcript.count(b'?1049h') >= 2
assert transcript.count(b'?1049l') >= 2
# Tatsächliche Ausgabe muss auch bei NO_COLOR/fehlender Erkennung
# dieselben absoluten IDE-Farben enthalten, ohne Palettenänderung.
assert b'38;2;170;170;170' in transcript
assert b'48;2;0;0;170' in transcript
assert b'48;2;170;0;170' in transcript
assert b'38;5;' not in transcript and b'48;5;' not in transcript
assert b'\x1b]' not in transcript
finally:
if transcript_dir:
name = 'file-shell' if file_shell else 'shell-abort' if abort else 'shell-exit'
(transcript_dir / (name + '.ansi')).write_bytes(transcript)
if not reaped:
os.killpg(pid, signal.SIGKILL)
os.waitpid(pid, 0)
@@ -91,6 +102,18 @@ def exercise(abort=False, file_shell=False):
os.close(slave)
for kwargs in ({}, {'abort': True}, {'file_shell': True}):
exercise(**kwargs)
print('PASS', kwargs or {'shell_exit': 7})
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--binary', type=Path, default=Path(__file__).resolve().parents[2] / 'target/debug/tb')
parser.add_argument('--transcript-dir', type=Path)
args = parser.parse_args()
binary = args.binary.resolve(strict=True)
if args.transcript_dir:
args.transcript_dir.mkdir(parents=True, exist_ok=True)
for kwargs in ({}, {'abort': True}, {'file_shell': True}):
exercise(binary, args.transcript_dir, **kwargs)
print('PASS', kwargs or {'shell_exit': 7})
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Automatisierte Zielabnahme mit Artefaktbindung; reale Emulatornachweise bleiben separat."""
import argparse
import datetime
import hashlib
import json
import os
from pathlib import Path
import platform
import subprocess
import sys
ROOT = Path(__file__).resolve().parents[2]
TARGETS = {
('Windows', 'AMD64'): 'x86_64-pc-windows-msvc',
('Darwin', 'arm64'): 'aarch64-apple-darwin',
('Linux', 'x86_64'): 'x86_64-unknown-linux-gnu',
('Linux', 'aarch64'): 'aarch64-unknown-linux-gnu',
}
TBL_SHA256 = '5d25c2f8c95ae535e55a6c84c1ddd0d964358863f4f3c780c50ac4a2eb3594df'
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--bin-dir', type=Path, default=ROOT / 'target/release')
parser.add_argument('--output', type=Path, required=True, help='neues Verzeichnis für Bericht und Logs')
parser.add_argument('--windows-console-test', type=Path, help='tb-ui-Testexecutable derselben Revision')
args = parser.parse_args()
target = TARGETS.get((platform.system(), platform.machine()))
if target is None:
parser.error(f'Kein vereinbartes Releaseziel: {platform.system()} {platform.machine()}')
binaries = args.bin_dir.resolve()
suffix = '.exe' if os.name == 'nt' else ''
programs = {name: binaries / (name + suffix) for name in ('tb', 'tbc', 'tbrt', 'tb-template')}
for path in programs.values():
if not path.is_file():
parser.error(f'Prüfwerkzeug fehlt: {path}')
library = ROOT / 'tests/libraries/portable.tbl'
if digest(library) != TBL_SHA256:
parser.error('Gemeinsame portable.tbl hat eine abweichende Prüfsumme')
revision = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip()
changes = subprocess.check_output(['git', 'diff', '--binary', 'HEAD'], cwd=ROOT)
status = subprocess.check_output(['git', 'status', '--porcelain'], cwd=ROOT, text=True)
args.output.mkdir(parents=True) # vorhandene Nachweise nie überschreiben
report = {
'scope': 'automated-target-checks',
'started_utc': datetime.datetime.now(datetime.timezone.utc).isoformat(),
'host_target': target, 'os_version': platform.platform(),
'python_version': platform.python_version(), 'checkout_revision': revision,
'working_tree_dirty': bool(status),
'working_tree_diff_sha256': hashlib.sha256(changes).hexdigest(),
'program_sha256': {name: digest(path) for name, path in programs.items()},
'tbl_sha256': TBL_SHA256,
'manual_terminal_status': 'not_run', 'matrix_complete': False, 'checks': {},
}
(args.output / 'working-tree.patch').write_bytes(changes)
(args.output / 'working-tree-status.txt').write_text(status, encoding='utf-8')
# Neue Prüfscripte können noch untracked sein; ihre genauen Bytes festhalten.
report['harness_sha256'] = {p.name: digest(p) for p in Path(__file__).parent.glob('*abnahme.py')}
report['harness_sha256']['ide-execution-pty.py'] = digest(Path(__file__).with_name('ide-execution-pty.py'))
commands = {
'library': [sys.executable, str(Path(__file__).with_name('library-abnahme.py')), '--bin-dir', str(binaries)],
'native': [sys.executable, str(Path(__file__).with_name('native-abnahme.py')), '--bin-dir', str(binaries), '--target', target],
}
if os.name == 'posix':
commands['ide-pty'] = [sys.executable, str(Path(__file__).with_name('ide-execution-pty.py')),
'--binary', str(programs['tb']), '--transcript-dir', str(args.output.resolve() / 'pty')]
elif args.windows_console_test:
executable = args.windows_console_test.resolve(strict=True)
report['windows_console_test_sha256'] = digest(executable)
commands['windows-console'] = [str(executable), '--exact',
'terminal::tests::windows_console_restores_modes', '--ignored', '--nocapture']
else:
report['checks']['windows-console'] = {'status': 'missing', 'reason': '--windows-console-test fehlt'}
report['checks'].update({name: {'status': 'not_run'} for name in commands})
try:
for name, command in commands.items():
print('Prüfung:', name, flush=True)
with (args.output / (name + '.log')).open('wb') as log:
try:
result = subprocess.run(command, cwd=ROOT, stdout=log, stderr=subprocess.STDOUT, timeout=300)
report['checks'][name] = {'status': 'passed' if result.returncode == 0 else 'failed',
'exit_code': result.returncode}
except OSError as error:
report['checks'][name] = {'status': 'failed', 'reason': str(error)}
except subprocess.TimeoutExpired:
report['checks'][name] = {'status': 'failed', 'reason': 'Zeitlimit 300s'}
finally:
stable = all(digest(path) == report['program_sha256'][name] for name, path in programs.items()) and digest(library) == TBL_SHA256
report['checks']['artifact-stability'] = {'status': 'passed' if stable else 'failed'}
report['all_automated_passed'] = bool(report['checks']) and all(
check['status'] == 'passed' for check in report['checks'].values())
(args.output / 'report.json').write_text(json.dumps(report, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
print('Automatisierte Zielprüfung:', 'bestanden' if report['all_automated_passed'] else 'offen/fehlgeschlagen')
print('Reale Terminalmatrix: weiterhin separat nachzuweisen')
return 0 if report['all_automated_passed'] else 1
if __name__ == '__main__':
raise SystemExit(main())