jmri_core.testing
Test-time utilities shared across driver packages.
Kept here rather than under each driver's tests/ because these
formats — recorded-bus replay files being the current example — are
one shared contract across drivers. A divergent per-driver shape
would defeat the point of any protocol-agnostic capture tooling
that reads them.
The modules here have no runtime hardware dependencies and are safe to import from production code (though there's no reason to).
1"""Test-time utilities shared across driver packages. 2 3Kept here rather than under each driver's ``tests/`` because these 4formats — recorded-bus replay files being the current example — are 5one shared contract across drivers. A divergent per-driver shape 6would defeat the point of any protocol-agnostic capture tooling 7that reads them. 8 9The modules here have no runtime hardware dependencies and are safe 10to import from production code (though there's no reason to). 11""" 12from jmri_core.testing.bus_replay import ( 13 BusFrame, 14 Direction, 15 iter_direction, 16 read_capture, 17 write_capture, 18) 19 20__all__ = [ 21 "BusFrame", 22 "Direction", 23 "iter_direction", 24 "read_capture", 25 "write_capture", 26]
47@dataclass(frozen=True, slots=True) 48class BusFrame: 49 """One captured frame.""" 50 51 t: float 52 direction: Direction 53 frame: bytes 54 55 def to_jsonl(self) -> str: 56 return json.dumps({ 57 "t": round(self.t, 6), 58 "dir": self.direction, 59 "frame_hex": self.frame.hex(), 60 }) 61 62 @classmethod 63 def from_jsonl(cls, line: str) -> BusFrame: 64 obj = json.loads(line) 65 direction = obj.get("dir", "in") 66 if direction not in ("in", "out"): 67 raise ValueError( 68 f"invalid direction {direction!r}; must be 'in' or 'out'" 69 ) 70 return cls( 71 t=float(obj["t"]), 72 direction=direction, # type: ignore[arg-type] 73 frame=bytes.fromhex(obj["frame_hex"]), 74 )
One captured frame.
62 @classmethod 63 def from_jsonl(cls, line: str) -> BusFrame: 64 obj = json.loads(line) 65 direction = obj.get("dir", "in") 66 if direction not in ("in", "out"): 67 raise ValueError( 68 f"invalid direction {direction!r}; must be 'in' or 'out'" 69 ) 70 return cls( 71 t=float(obj["t"]), 72 direction=direction, # type: ignore[arg-type] 73 frame=bytes.fromhex(obj["frame_hex"]), 74 )
99def iter_direction( 100 frames: Iterable[BusFrame], direction: Direction, 101) -> Iterator[BusFrame]: 102 """Filter capture frames by direction. Handy when a test only 103 cares about one side of the conversation (e.g. "assert every 104 ``in`` frame decodes cleanly").""" 105 for f in frames: 106 if f.direction == direction: 107 yield f
Filter capture frames by direction. Handy when a test only
cares about one side of the conversation (e.g. "assert every
in frame decodes cleanly").
85def read_capture(path: str | pathlib.Path) -> list[BusFrame]: 86 """Load a capture. Blank lines and ``#``-prefixed comments are 87 silently skipped so hand-authored fixtures can carry notes.""" 88 p = pathlib.Path(path) 89 out: list[BusFrame] = [] 90 with p.open("r", encoding="utf-8") as f: 91 for raw_line in f: 92 line = raw_line.strip() 93 if not line or line.startswith("#"): 94 continue 95 out.append(BusFrame.from_jsonl(line)) 96 return out
Load a capture. Blank lines and #-prefixed comments are
silently skipped so hand-authored fixtures can carry notes.
77def write_capture(path: str | pathlib.Path, frames: Iterable[BusFrame]) -> None: 78 """Serialize *frames* to *path* as JSONL. Overwrites.""" 79 p = pathlib.Path(path) 80 with p.open("w", encoding="utf-8") as f: 81 for frame in frames: 82 f.write(frame.to_jsonl() + "\n")
Serialize frames to path as JSONL. Overwrites.