Racket port phases 3-5: full parity, C# tree retirement prep (ADR 0002) - #30
Merged
Merged
Conversation
Core runtime: frozen wire-contract records with typed camelCase serializers, evidence pipeline with bounded stores and per-target context rings, the runtime state machine (target-first mutation gates with rollback, first-cause-wins cancellation, wall-clock deadline rejection of late successes, bounded history, drain), and the target-oriented facade (power/serial/flash/diagnostics operations, preflight, real-ECU readiness). Diagnostics: ISO 15765-2 codec and endpoint over an in-process simulated CAN bus with per-port pump threads, the ISO 14229 UDS client (P2/P2* timing, NRC 0x78 pending), the behavioral simulated ECU (sessions with S3 timeout, security access attempt counting, DID read/write, erase/verify routines, RequestDownload/TransferData/Exit block sequencing), the transport-independent flash engine (CRC32 verify, audited steps) and the simulator bench (power/serial/flash sharing virtual board state). 42 ported/contract tests green locally; port bugs found by the port itself: escaped-string corruption in tolerant JSON reading (regression test added), '() used as empty-queue marker (truthy in Racket), left-shifts where C# encoders right-shift.
ISO 13400-2 frames over TCP: routing activation handshake, alive-check answering, buffered frame reassembly; UDP vehicle discovery with loopback-then-broadcast; the simulated DoIP entity serving the shared behavioral UDS processor; and the DoIP UDS channel behind the same diag-channel contract, so the flash engine drives CAN and DoIP unchanged. DoIP workflow and channel tests green; 44 ported tests total green locally.
benchpilotd: loopback HTTP/1.1 JSON API over the runtime with per-user
token auth, admit/stopping middleware, route table over flat thunks,
bounded-drain shutdown and autostart quiet mode (stdio detach).
benchpilot CLI: the full command surface (status/doctor, operations and
observations with evidence/cancel, preflight/validate, power, flash,
serial, UDS, DoIP discover, shutdown) with the stable exit-code table,
one-retry daemon autostart and indented/compact JSON output per --json.
End-to-end through real HTTP: doctor, validate, power on, flash,
serial wait, UDS DID read, power check, history, power off, shutdown —
all green through the CLI against the daemon. Port-side findings:
hasheq never matches dynamically-built string keys (equal?-hash the
header table), Racket 0/1 integers are truthy like everything else,
case only matches symbol datums, and #rx lacks \d and {n} (use #px).
44 unit tests + the CLI/daemon flow green locally.
Generic raw-TCP SCPI power supply with profile command templates, IO-gated connection lifecycle, safety shutoff on transport/protocol errors, and bounded response timeouts (TCP input ports as sync events). ScpiPowerTests ported: the full profile-driven flow against a fake SCPI server and the timeout-triggers-safety-off path. 46 tests green. Port finding: hasheq's eq?-based keys silently fail for dynamically built strings (the fake server's command table never matched); use equal-based hash for anything with runtime-built keys.
SEGGER J-Link Commander backend: generated command files through the installed JLink.exe/JLinkExe with process timeout, probe enumeration that never touches the target MCU (ShowEmuList only), executable resolution across Program Files/SEGGER and /opt/SEGGER paths, and factory validation parity (device required, speed/timeout ranges, binAddress hex-string format). J-Link and SCPI drivers complete phase 4's TCP/process-backed drivers; SocketCAN/PCAN (Linux/Win FFI) and system-serial (POSIX/Win32 FFI) follow when hardware is available per ADR 0002's Phase 4 gate.
The derivers table was hasheq (eq?-based), so the plan's dynamically built 'xor0x5a' key never matched the table entry on Linux CI. Use equal-based hash for any table with runtime-built string keys.
Deriver names arrive as plan strings from other modules; hasheq's eq?
lookup misses cross-module string literals, so CI failed 3 flash/DoIP
tests that passed locally only by compiled-literal-sharing luck. Also
name the fake SCPI server's silence sentinel correctly ('no-reply)
instead of relying on the same lookup miss.
The key-derivers table was hasheq (eq?-based), so the plan's dynamically built 'xor0x5a' key never matched. Changed to equal-based hash and removed the hash-union dependency that silently failed.
- gen:can-bus: port of ICanBus; the ISO-TP endpoint and both diagnostics channels now attach to any transport through the generic - SocketCAN (Linux libc FFI) and PCAN-Basic (Windows kernel32 FFI) bus transports; wire-frame codec is pure and unit-tested everywhere - system-serial: SystemSerialChannel port with POSIX termios and Windows CommAPI backends behind one bounded line-buffer state machine - can-iso-tp resource factory with C#-parity transport selection; DoIP factory fixed to the C# driver name and settings contract - daemon composes the same seven factories as the C# RuntimeHost - J-Link probe discovery ported (ShowEmuList parse + deterministic USB selection) and wired into the health check - SCPI health check performs the real identify query; factory rejects multiline command templates; driver settings read symbol-keyed JSON (string keys never matched parsed profiles) - 25 new hardware-free driver tests; suite at 69 green
- updater.rkt: release-feed check, archive download, SHA256SUMS verification (sha256sum/shasum/certutil), OS-tar extraction (bsdtar reads zip on Windows), active-operation refusal, graceful daemon shutdown with bounded wait + pkill/taskkill fallback, in-place executable swap with .old backups - contracts: update-check-result / update-result API records - cli: update --check|update replaces the not_supported stub; exit codes match the C# table (4 on failure) - fixed raco-subprocess value order in jlink (stdout pipe arrived first) and switched driver health details to equal-based hashes so cross-module string lookups resolve - cli main: current-command-line-arguments is a vector - 5 SelfUpdater tests ported; suite at 74 green
- mcp/server.rkt: stdio JSON-RPC server with newline-delimited messages, 26 tools proxying the same /api/v1 surface as the CLI (bench, operations, observations, power, flash, serial, UDS/DoIP), full inputSchema per tool, tools/call errors surfaced as isError content - reuses the CLI's api-call/resolve-endpoint/autostart (now exported) - 6-test smoke suite drives initialize/tools/list/tools/call against an in-process resident daemon; the C# tree had no MCP tests
… gates) E2EEnvironment/SharedDaemonFixture port: stages benchpilot + benchpilotd side by side as wrappers, spawns the real CLI and resident daemon as processes over loopback, and drives the full product contract: - ShutdownTests (2): graceful stop releases the endpoint; idempotent not_running - AutostartTests (2): first command starts the resident daemon; doctor never does (exit 4) - AuthAndDiagnosticsTests (8): version contract, healthz/status runtime version, 401 without/wrong token, doctor health report, UDS/DoIP routes through the daemon, history records completed mutations - FullLoopTests (4): the README simulator loop through the CLI, unmatched wait exit 1 with failure-window + context evidence, runtime deadline exit 6 with deadlineMs, validate not-ready exit 1 Bugs the suite flushed out and fixes: - http.rkt: query-string regex read group four instead of three, so any request with a query died silently (empty reply) — E2E was the first harness to send query parameters - daemon: /shutdown now goes through the same drain-and-exit thread as SIGTERM - cli: error exit codes compared string codes against symbol datums (deadline/busy never matched); structured error fields (deadlineMs, busyScope...) survive to the printed JSON; doctor handles the network error subtype before the base class; autostart spawns staged .rkt daemons via the racket binary and consumes all four subprocess values - doip: discovery hardens the broadcast leg (empty result on hosts without a route) and fixes the udp-receive!* arity for this Racket
…html (phase 5d) - html-report.rkt: one self-contained static page (no scripts, no network) covering bench status, readiness checks with pass/fail badges, operation/observation history and the evidence recorded for the newest completed mutation and observation - cli: benchpilot report [--format html] [--out PATH] [--target ID] - 4 renderer tests (sections, escaping, static contract); suite at 95
…phase 5e) - scripts/build-dist.sh: per-RID distributions — Windows ships three embed-dlls single-file executables, Unix a full raco distribution tree (bin/ + lib/); committed launcher entries under packaging/launchers/ so the packaged CLI/daemon/MCP behave exactly like the E2E-tested wrappers - install.sh installs the tree under ~/.benchpilot/lib/benchpilot/<ver> and writes thin bin wrappers; the self-updater follows wrappers back to the real launchers and swaps them in place - Debian package carries the distribution tree under /opt/benchpilot with /usr/bin wrappers (deb verify step runs the real binary) - release matrix: win-x64, linux-x64/arm64, osx-x64/arm64 on native runners; win-arm64 dropped (Racket publishes no Windows ARM64 builds); Homebrew formula installs to libexec + bin wrappers, scoop is 64bit - ci.yml is now racket-only (unit + driver + MCP + E2E + smoke on ubuntu/windows); smoke scripts boot the real daemon and CLI - docs: README requirements/build/report sections, CHANGELOG 0.6.0, ROADMAP port-complete note, site milestone; benchpilot-version 0.6.0
The Racket port reached full contract parity: same frozen JSON API, CLI exit-code table and the black-box E2E suite — now ported and green. The 0.5.x C# sources, their test projects and the .NET solution files leave with this commit; v0.6.0 ships from the Racket tree only.
- the serial factory test no longer asserts health ok=false (a host may genuinely expose the configured port); the contract stays 'never opened anything' - the doip E2E check reports the response body on failure - DoIP discovery treats every socket leg as best-effort (open/bind/send/ receive), so hosts without broadcast routes or with blocked sockets get an empty result instead of a 500
main holds the same phases 0-2 content as this branch (PR #29 squash); conflicts resolve to this branch, which is a strict superset built on the same tree.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Completes the ADR 0002 port on
racket-port-2:can-busgeneric (ICanBus port), SocketCAN (Linux FFI), PCAN-Basic (Windows FFI),system-serial(POSIX termios + Windows CommAPI),can-iso-tpfactory with C#-parity transport selection, 7-factory daemon composition, J-Link probe discovery + deterministic USB selection, SCPI real identify health check. 25 hardware-free driver tests.SelfUpdaterport +update --check|updateon the CLI (feed check, SHA256 verify via OS tools, OS-tar extraction, active-op refusal, graceful daemon stop + kill fallback, in-place swap).benchpilot report --format html— static self-contained evidence report.Suite: 95 tests green locally (unit + drivers + MCP smoke + E2E), including 16 E2E process tests on this machine.
Bugs the E2E suite flushed out (all fixed here)
shutdownnever drained/exited (only blocked new work).Deliberate deviations (documented in code/CHANGELOG)
bin/+lib/distribution tree; installers (install.sh, deb, Homebrew) wrap the real launchers so the packaged runtime resolves and self-update keeps swapping in place.After merge (separate steps)
workflow_dispatchthe Release workflow on main to validate the 5-RID packaging without publishing.v0.6.0→ the release ships and the C# tree removal lands with it (this PR already retiressrc/+tests/? — no: C# removal is staged for the merge commit itself if CI is green, per ADR 0002's phase 5 gate 'C# tree removed; release v0.6.0').