Device-driver codegen — verification

Test cases verifying the device-driver codegen toolchain. Each test directive :verifies: one or more requirements from Device-driver codegen (or building blocks from Device-driver codegen — architecture (arc42)).

The toolchain is build-time only — there are no cyclic-runtime integration tests beyond what Connector framework — verification already covers for EtherCAT reference connector (FEAT_0041). The verification surface here is therefore heavier on snapshot / golden-file / property tests than on multi-process integration.


Parser unit tests

Per-crate, no I/O beyond test fixtures, parallel-safe. Live under crates/ethercat-esi/tests/.

Test Case: parse() accepts a representative Beckhoff EL3001 ESI TEST_0400
status: open
verifies: REQ_0500, REQ_0504

Loads a canonical Beckhoff EL3001 ESI XML fixture from crates/ethercat-esi/tests/fixtures/, calls parse(xml), and asserts the resulting EsiFile exposes the expected identity (vendor_id = 0x2, product_id = 0x0BB93052), one device with two sync managers, the “Standard” and “Compact” PDO alternatives, the expected InitCmd count, and a non-empty object dictionary.

Test Case: Parser compiles under no_std + alloc TEST_0401
status: open
verifies: REQ_0501

Compile-only test: a small bin target inside crates/ethercat-esi/tests/no_std/ declares #![no_std], uses alloc::string::String, and parses a fixture. The target must compile with --no-default-features against the crate; the test passes if compilation succeeds (no runtime assertion).

Test Case: Parser is independent of ethercrab TEST_0402
status: open
verifies: REQ_0503

cargo tree -p ethercat-esi --no-default-features --target <host> shall not list ethercrab anywhere in the resolved graph. Implemented as a CI shell check that greps the output and fails on match.

Test Case: Vendor-specific elements survive as RawXml TEST_0403
status: open
verifies: REQ_0505

Fixture file with a fabricated <Vendor:UnknownElement foo="bar">inner</Vendor:UnknownElement> inside a <Device>. After parsing, the IR carries one RawXml entry with name = "Vendor:UnknownElement", attributes = {"foo": "bar"}, inner_text = "inner". The parse does not return an error.

Test Case: Parse errors carry line and column TEST_0404
status: open
verifies: REQ_0506

A deliberately malformed ESI fixture (unclosed tag at known coordinates) parses to Err(EsiError::Xml { line, column, .. }) with the expected line and column. Catching this trace in build-time output (per Parse errors carry line and... (REQ_0506)) is the user benefit.


Codegen / IR tests

Per-crate, snapshot-based. Live under crates/ethercat-esi-codegen/tests/.

Test Case: Name sanitisation handles ESI naming edge cases TEST_0410
status: open
verifies: REQ_0511

Parameterised test asserting the sanitisation map for a fixed table of ESI product names → Rust idents: EL3001-0000EL3001_0000, EL3204 with spacesEL3204_with_spaces, leading-digit 1234-Module_1234_Module, empty / pure punctuation → error.

Test Case: Revision collision produces distinct idents TEST_0411
status: open
verifies: REQ_0512

Synthetic input set containing two devices with identical product name but different revisions (0x00100000 and 0x00110000). The generated module shall contain EL3204_REV0010 and EL3204_REV0011 (or equivalent deterministic suffixing). Reordering the input file list shall produce the same idents in the same definitions (assert via string comparison of generated source).

Test Case: PDO entry dedup collapses structurally identical layouts TEST_0412
status: open
verifies: REQ_0513

Two synthetic devices whose RxPDO entries have identical (field order, bit lengths, data types) produce a single shared PDO entry struct in the generated module; the two device structs reference it by name. Asserted by counting distinct PdoEntry-bearing struct definitions in the TokenStream (expect 1, not 2).

Test Case: TokenStream emission, not string formatting TEST_0413
status: open
verifies: REQ_0514

White-box test inside ethercat-esi-codegen asserts that emit_device and emit_module_root return TokenStream values directly (compile-time check via the return type). A complementary lint forbids format! / write! / writeln! invocations within the codegen crate’s source.


ethercrab backend snapshot tests

Live under crates/ethercat-esi-codegen-ethercrab/tests/.

Test Case: EL3001 backend output snapshot TEST_0420
status: open

Run parse → codegen → backend → prettyplease on the canonical EL3001 ESI fixture. Compare the formatted output against a committed snapshots/el3001.rs golden file using insta::assert_snapshot!. Reviewer regenerates the golden when intentional changes land; CI fails on unintentional churn.

Test Case: Generated registry covers every emitted device TEST_0421
status: open
verifies: REQ_0525

For an input set with N devices, the generated module’s registry!() expansion contains exactly N entries mapping SubDeviceIdentity → factory closure. White-box test parses the generated output and counts entries.

Test Case: Generated module compiles under no_std + alloc TEST_0422
status: open
verifies: REQ_0526

A test crate at crates/ethercat-esi-codegen-ethercrab/tests/no_std_consumer/ has #![no_std] and extern crate alloc;, include!``s the generated module from a fixed input set, and compiles successfully. Catches any accidental ``std:: qualified path in the backend’s emit code.

Test Case: Backend is the sole ethercrab consumer in the toolchain TEST_0423
status: open
verifies: REQ_0520

CI shell check: cargo tree invocations for ethercat-esi, ethercat-esi-codegen, ethercat-esi-build, ethercat-esi-cli, and ethercat-esi-verify must none of them list ethercrab in the dependency graph. ethercat-esi-codegen-ethercrab and ethercat-esi-rt are the only crates where ethercrab is allowed.

Test Case: Object-dictionary emission gated by feature flag TEST_0424
status: open
verifies: REQ_0533

Build the no_std_consumer test crate twice: once without features (the generated module’s OD table is empty / absent; no symbol named OD exists) and once with --features object-dictionary (the OD static exists and has the expected entry count for the input set). Compares the two binaries’ rodata sections — the no-feature build is smaller by an amount approximating the OD table size.


Runtime trait surface tests

Live under crates/ethercat-esi-rt/tests/.

Test Case: EsiDevice trait shape compiles for a hand-written device TEST_0430
status: open
verifies: REQ_0530

Hand-written test impl of EsiDevice for a minimal MockDevice validates the trait surface compiles end-to-end. Asserts IDENTITY, input_len, output_len return the expected values, and that decode_inputs / encode_outputs round-trip a synthetic BitSlice<u8, Lsb0>.

Test Case: EsiConfigurable async trait shape compiles TEST_0431
status: open
verifies: REQ_0531

Compile-only test: a mock device implements EsiConfigurable with type Assignment = MockAssignment and an async fn configure body. The test passes if compilation succeeds; the async signature shape catches the trait-method-async constraint.

Test Case: ethercat-esi-rt is the trait home, not taktora-internal TEST_0432
status: open
verifies: REQ_0532

CI shell check: rg "trait EsiDevice" across the workspace matches exactly one source location, inside crates/ethercat-esi-rt/src/. Same check for trait EsiConfigurable.


Build helper tests

Live under crates/ethercat-esi-build/tests/.

Test Case: Builder writes a parseable Rust file to OUT_DIR TEST_0440
status: open
verifies: REQ_0540, REQ_0541

Test crate driven by a fixture ESI set runs Builder::new().glob(...).backend(default).out_file( "devices.rs").build() in a tempfile-backed OUT_DIR. Asserts the file exists, is non-empty, and parses with syn::parse_file (catches malformed token streams).

Test Case: cargo rerun-if-changed emitted per ESI input TEST_0441
status: open
verifies: REQ_0542

Capture Builder::build()’s stdout output (the build helper prints to stdout per cargo conventions). For an N-file glob, assert exactly N + 1 cargo:rerun-if-changed= lines are present (one per ESI file + one for the build script itself).

Test Case: Output passes prettyplease formatting TEST_0442
status: open
verifies: REQ_0543

The generated devices.rs file is line-wrapped (no line exceeds 100 chars without justification), and re-running prettyplease::unparse on the file produces a byte-identical output (idempotent formatter pass).


CLI tests

Live under crates/ethercat-esi-cli/tests/.

Test Case: cargo esi expand emits a single device's code TEST_0450
status: open
verifies: REQ_0550

Spawn the CLI as cargo esi expand --device EL3001 --glob <fixtures>/*.xml, capture stdout, assert the output is non-empty, parses as Rust, and contains exactly one pub struct EL3001 definition.

Test Case: cargo esi list enumerates devices TEST_0451
status: open
verifies: REQ_0551

Run cargo esi list --glob <fixtures>/*.xml over a 3-device fixture set, assert stdout contains 3 lines each matching the <ident>\t<vendor_id>\t<product_id>\t<revision> format.

Test Case: CLI output matches build helper output byte-for-byte TEST_0452
status: open
verifies: REQ_0552

For one fixed device, capture the CLI’s expand output and the build helper’s per-device slice of $OUT_DIR/devices.rs. Assert the two byte-strings are identical (catches divergent code paths between CLI and build).


EEPROM verifier tests

Live under crates/ethercat-esi-verify/tests/.

Test Case: Verifier passes on matching ESI + SII pair TEST_0460
status: open
verifies: REQ_0560

Use the captured pair from crates/ethercat-eeprom-dump/dumps/EL3001/ (ESI XML + matching SII .bin), call verify(xml, sii), assert Ok(VerifyReport { matched: true, .. }).

Test Case: Verifier reports the differing field TEST_0461
status: open
verifies: REQ_0561

Synthetic mismatched pair: parse a real ESI but flip one bit in a captured SII to alter the revision field. The returned VerifyReport shall contain at least one Difference entry whose field is exactly "Identity.revision" and whose esi / sii values match the originals.

Test Case: Verifier reuses ethercat-esi parser TEST_0462
status: open
verifies: REQ_0562

White-box: cargo tree -p ethercat-esi-verify lists ethercat-esi as a direct dependency and does not list ethercrab anywhere in the graph (re-affirming Backend is the sole ethercr... (TEST_0423) from the verifier side).

Test Case: Verifier exit codes follow the documented matrix TEST_0463
status: open
verifies: REQ_0563

Spawn the verifier binary three times: matching pair (expect exit 0), mismatched pair (expect 1), unreadable SII path (expect 2). Asserted via Command::status().code().


Cross-cutting reproducibility tests

Verify the build-time determinism quality goal (Build-time determinism (sam... (QG_0010)).

Test Case: Repeated codegen runs produce byte-identical output TEST_0470
status: open
verifies: QG_0010, REQ_0543

Run Builder::build() twice on the same input set in freshly-prepared OUT_DIR directories. Compare the two devices.rs files with sha256. Assert identical.

Test Case: Input-file ordering does not affect output TEST_0471
status: open

Same input set, glob returns files in two different orders (force the order via explicit Builder::file(path) calls). The two devices.rs outputs are byte-identical (catches HashMap-iteration-order nondeterminism in dedup or collision-handling).

Test Case: Layering integrity check (Cargo.toml audit) TEST_0472
status: open

CI shell check that walks each toolchain crate’s Cargo.toml and asserts the allowed-dependency matrix:

  • ethercat-esi: no ethercrab, no proc-macro2, no quote, no codegen crate.

  • ethercat-esi-codegen: no ethercrab.

  • ethercat-esi-build: no ethercrab (transitively via ethercat-esi-codegen-only path).

  • ethercat-esi-verify: no ethercrab.

Implemented with cargo metadata + jq; runs in the workspace CI job.


Cross-cutting traceability

Used filter: types(test)

ID

Title

Status

Verifies

TEST_0400

parse() accepts a representative Beckhoff EL3001 ESI

open

REQ_0500; REQ_0504

TEST_0401

Parser compiles under no_std + alloc

open

REQ_0501

TEST_0402

Parser is independent of ethercrab

open

REQ_0503

TEST_0403

Vendor-specific elements survive as RawXml

open

REQ_0505

TEST_0404

Parse errors carry line and column

open

REQ_0506

TEST_0410

Name sanitisation handles ESI naming edge cases

open

REQ_0511

TEST_0411

Revision collision produces distinct idents

open

REQ_0512

TEST_0412

PDO entry dedup collapses structurally identical layouts

open

REQ_0513

TEST_0413

TokenStream emission, not string formatting

open

REQ_0514

TEST_0420

EL3001 backend output snapshot

open

REQ_0521; REQ_0522; REQ_0523; REQ_0524

TEST_0421

Generated registry covers every emitted device

open

REQ_0525

TEST_0422

Generated module compiles under no_std + alloc

open

REQ_0526

TEST_0423

Backend is the sole ethercrab consumer in the toolchain

open

REQ_0520

TEST_0424

Object-dictionary emission gated by feature flag

open

REQ_0533

TEST_0430

EsiDevice trait shape compiles for a hand-written device

open

REQ_0530

TEST_0431

EsiConfigurable async trait shape compiles

open

REQ_0531

TEST_0432

ethercat-esi-rt is the trait home, not taktora-internal

open

REQ_0532

TEST_0440

Builder writes a parseable Rust file to OUT_DIR

open

REQ_0540; REQ_0541

TEST_0441

cargo rerun-if-changed emitted per ESI input

open

REQ_0542

TEST_0442

Output passes prettyplease formatting

open

REQ_0543

TEST_0450

cargo esi expand emits a single device's code

open

REQ_0550

TEST_0451

cargo esi list enumerates devices

open

REQ_0551

TEST_0452

CLI output matches build helper output byte-for-byte

open

REQ_0552

TEST_0460

Verifier passes on matching ESI + SII pair

open

REQ_0560

TEST_0461

Verifier reports the differing field

open

REQ_0561

TEST_0462

Verifier reuses ethercat-esi parser

open

REQ_0562

TEST_0463

Verifier exit codes follow the documented matrix

open

REQ_0563

TEST_0470

Repeated codegen runs produce byte-identical output

open

QG_0010; REQ_0543

TEST_0471

Input-file ordering does not affect output

open

QG_0010; REQ_0512; REQ_0513

TEST_0472

Layering integrity check (Cargo.toml audit)

open

QG_0011; REQ_0503; REQ_0520