Add boxsim CLI: pure Python codec, netlist extraction, KiCad project generation
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>
This commit is contained in:
1 parent
eb81ca61cb
commit
4737a5bae8
28 files changed
+4459
-122
No files matched your search
@@ -0,0 +1,79 @@
|
||||
"""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())
|
||||
Reference in new issue
Block a user