Replaces the bpj2ini.py prototype with a packaged tool. The A2F codec is now reimplemented in Python, so neither Boxsim.exe nor a CPU emulator is needed to decode a container; the emulator survives as a development-only oracle that the codec is differentially tested against. Verified: - 134 containers decode byte-identically to Boxsim's own decoder, 270 pre-2.0 files rejected by both implementations (tools/difftest.py --self-test) - Default.BPJ: 22 components, 14 nets, no dangling terminal - 22 of 22 parts resolve to library symbols; one series inferred from a sibling, two DC resistance deviations reported - ERC clean and the netlist KiCad extracts from the generated sheet is an identical partition to the one derived from the Boxsim grid - container build byte-identical to the host build - 32 tests passing, ruff clean Decode time went 69 s -> 14.5 s (binding emulator hooks to trap addresses) -> 1.6 s (pure Python). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
80 lines
2.7 KiB
Python
80 lines
2.7 KiB
Python
"""Codec tests. The heavy differential sweep against Boxsim.exe lives in
|
|
tools/difftest.py; these guard regressions against the digests it recorded.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from boxsim import a2f, container
|
|
from boxsim.krypt import KryptPRNG
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def test_keystream_is_stable():
|
|
"""Regression guard on the keystream every Boxsim file is scrambled with.
|
|
|
|
The value is not independently derived, it is the keystream under which all
|
|
134 containers in the distribution decode byte-identically to Boxsim's own
|
|
decoder (see tools/difftest.py). Its worth here is catching an accidental
|
|
change to the generator, not proving the generator correct.
|
|
"""
|
|
ks = KryptPRNG().keystream(8)
|
|
assert ks.hex() == "ebe14331cb3183e1"
|
|
|
|
|
|
def test_rejects_foreign_file():
|
|
with pytest.raises(container.ContainerError, match="not a MyIni2CK file"):
|
|
container.decode(b"not a boxsim file at all")
|
|
|
|
|
|
def test_truncated_stream_fails_loudly():
|
|
raw = (ROOT / "boxsim_dl" / "BoxSim 2.1 DE (Stand 12.2025)"
|
|
/ "Projekte" / "Default.BPJ")
|
|
if not raw.exists():
|
|
pytest.skip("distribution not present")
|
|
data = raw.read_bytes()
|
|
with pytest.raises(a2f.A2FError):
|
|
container.decode(data[: len(data) // 2])
|
|
|
|
|
|
def test_padding_allowance_is_bounded():
|
|
# A stream of zeros must not decode forever; it has to hit the limit.
|
|
with pytest.raises(a2f.A2FError):
|
|
a2f.expand(b"MyIni2CK", skip_magic=8)
|
|
|
|
|
|
@pytest.mark.needs_dist
|
|
def test_matches_oracle_digests(manifest: dict):
|
|
"""Every container the oracle decoded must still decode identically."""
|
|
files = manifest["files"]
|
|
assert files, "manifest has no entries"
|
|
checked = 0
|
|
for rel, expected in files.items():
|
|
path = ROOT / rel
|
|
if not path.exists():
|
|
pytest.fail(f"manifest references missing file {rel}")
|
|
payload = container.decode(path.read_bytes())
|
|
digest = hashlib.sha256(payload).hexdigest()
|
|
assert digest == expected["sha256"], f"{rel} digest changed"
|
|
assert len(payload) == expected["len"], f"{rel} length changed"
|
|
checked += 1
|
|
assert checked == len(files)
|
|
|
|
|
|
@pytest.mark.needs_dist
|
|
def test_pre20_containers_are_rejected(manifest: dict):
|
|
"""The BoxSim 1.2 files use an older container and must stay rejected."""
|
|
rejected = manifest["rejected_not_myini2ck"]
|
|
assert rejected, "manifest recorded no rejected files"
|
|
for rel in rejected:
|
|
path = ROOT / rel
|
|
if not path.exists():
|
|
continue
|
|
with pytest.raises(container.ContainerError):
|
|
container.decode(path.read_bytes())
|