CANopen device-driver codegen — verification

Test cases verifying the CANopen device-driver codegen toolchain. Each test directive :verifies: one or more requirements from CANopen device-driver codegen (or building blocks / quality goals from CANopen 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 CAN (SocketCAN) reference c... (FEAT_0046). The verification surface here is therefore heavier on snapshot / golden-file / property tests than on multi-process integration. Mirrors the structure of Device-driver codegen — verification so reviewers can read both verification pages 1:1.


OD-core unit tests

Per-crate, no I/O beyond synthetic inputs, parallel-safe. Live under crates/fieldbus-od-core/tests/.

Test Case: Identity, DictEntry, PdoEntry round-trip TEST_0600
status: open
verifies: REQ_0702

Unit tests construct each public type with synthetic values, clone, and compare for structural equality. No I/O. Confirms the public surface compiles and supports the trait derives expected by OD type surface (REQ_0702).

Test Case: fieldbus-od-core has no transport deps TEST_0601
status: open
verifies: REQ_0700

CI shell check: cargo tree -p fieldbus-od-core --no-default-features shall not list ethercrab, socketcan, taktora-connector-can, taktora-connector-ethercat, or any I/O-bearing crate in the resolved graph. Fails the CI job on any match.

Test Case: fieldbus-od-core compiles under no_std + alloc TEST_0602
status: open
verifies: REQ_0701

Compile-only test: a small bin target inside crates/fieldbus-od-core/tests/no_std/ declares #![no_std], uses alloc::vec::Vec, constructs each type. Compiles with --no-default-features; the test passes if compilation succeeds.

Test Case: ethercat-esi re-exports lifted types TEST_0603
status: open
verifies: REQ_0703, REQ_0704

Compile-only test: a test crate under crates/ethercat-esi/tests/reexport/ writes use ethercat_esi::{Identity, DictEntry, PdoEntry, PdoMap}; and constructs each. Compiles if and only if the re-export façade is in place.


EDS parser tests

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

Test Case: parse() accepts a representative Maxon EPOS4 EDS TEST_0610
status: open

Loads a canonical Maxon EPOS4 EDS fixture from crates/canopen-eds/tests/fixtures/, calls parse(text), and asserts the resulting EdsFile exposes the expected identity (vendor_id = 0x000000FB), one device, two declared TPDOs and two declared RPDOs, the matching communication parameter records, and a non-empty object dictionary.

Test Case: Parser compiles under no_std + alloc TEST_0611
status: open
verifies: REQ_0721

Compile-only test: a small bin target inside crates/canopen-eds/tests/no_std/ declares #![no_std], parses a fixture EDS string baked in as include_str!. Must compile with --no-default-features against the crate.

Test Case: Parser is independent of codegen and transport TEST_0612
status: open
verifies: REQ_0721

CI shell check: cargo tree -p canopen-eds shall not list canopen-eds-codegen, canopen-eds-rt, taktora-connector-can, socketcan, or ethercrab anywhere in the resolved graph.

Test Case: Unknown sections survive as RawSection TEST_0613
status: open
verifies: REQ_0724

Fixture EDS containing a fabricated [ManufacturerSpecific_FF] section with two keys. After parsing, the IR carries one RawSection entry whose name is exactly the section header and whose keys lists both key/value pairs. The parse does not return an error.

Test Case: Parse errors carry line and column TEST_0614
status: open
verifies: REQ_0723

A deliberately malformed EDS fixture (missing = at a known line / column) parses to Err(EdsError::Syntax { line, column, .. }) with the expected values. Catching this trace in build-time output is the user benefit.

Test Case: Liberal-quirk parsing emits warnings without failing TEST_0615
status: open
verifies: REQ_0725

Fixture EDS carrying every quirk listed in Liberal parsing — warn and ... (REQ_0725): leading UTF-8 BOM, CRLF line endings, trailing whitespace on a value, ; comment after a value, redundant whitespace around =, LineFeed=0 exporter key. The parse returns Ok(EdsFile); EdsFile::warnings lists one entry per triggered quirk with the expected EdsWarning::kind.


Codegen / IR tests

Per-crate, snapshot-based. Live under crates/canopen-eds-codegen/tests/.

Test Case: Name sanitisation handles EDS naming edge cases TEST_0620
status: open
verifies: REQ_0731

Parameterised test asserting the sanitisation map for a fixed table of EDS ProductName values → Rust idents: EPOS4 50/5EPOS4_50_5, Module-V2Module_V2, 1234 Drive_1234_Drive, empty / pure punctuation → error.

Test Case: Revision collision produces distinct idents TEST_0621
status: open
verifies: REQ_0732

Synthetic input set with two EDS files sharing ProductName but differing in RevisionNumber (0x01400000 and 0x01410000). The generated module shall contain two distinct device structs with deterministic suffixes (e.g. EPOS4_REV0140 and EPOS4_REV0141). Reordering the input file list shall produce the same idents (string-compare of generated source).

Test Case: PDO entry dedup collapses structurally identical layouts TEST_0622
status: open
verifies: REQ_0733

Two synthetic devices whose RPDO entries have identical (bit_len, data_type) tuple lists but different field names produce a single shared PDO entry struct in the generated module. Asserted by counting distinct PDO-entry struct definitions in the TokenStream (expect 1, not 2).

Test Case: TokenStream emission, not string formatting TEST_0623
status: open
verifies: REQ_0730, REQ_0734

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

Test Case: One EDS file equals one device TEST_0624
status: open
verifies: REQ_0735

For an N-file fixture set, the call generate(&files, &TaktoraBackend::default()) produces a module with exactly N distinct device-struct definitions and one shared registry table.


taktora backend snapshot tests

Live under crates/canopen-eds-codegen-taktora/tests/.

Test Case: EPOS4 backend output snapshot TEST_0630
status: open

Run parse → codegen → backend → prettyplease on the canonical EPOS4 EDS fixture. Compare the formatted output against a committed snapshots/epos4.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_0631
status: open
verifies: REQ_0745

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

Test Case: Generated module compiles under no_std + alloc TEST_0632
status: open
verifies: REQ_0748

A test crate at crates/canopen-eds-codegen-taktora/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 canopen-eds-rt consumer in the toolchain TEST_0633
status: open
verifies: REQ_0740

CI shell check: cargo tree invocations for canopen-eds, canopen-eds-codegen, canopen-eds-build, canopen-eds-cli, and canopen-eds-verify must none of them list canopen-eds-rt in the dependency graph. canopen-eds-codegen-taktora is the only crate where canopen-eds-rt is allowed.

Test Case: Object-dictionary emission gated by feature flag TEST_0634
status: open
verifies: REQ_0747

Build the no_std_consumer test crate twice: once without features (the generated module exposes no OD symbol) and once with --features object-dictionary (the OD static exists with 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.

Test Case: Dummy entries skipped in PDO struct fields TEST_0635
status: open
verifies: REQ_0744

Fixture EDS declares a PDO mapping containing one real 0x6040:00 entry, one Dummy32 entry, and one real 0x607A:00 entry. The generated PDO payload struct contains exactly two fields (the two real entries). The generated encode body writes zeros in the bit range covered by the dummy; the generated decode body skips it.

Test Case: Bring-up SDO writes emitted from EDS TEST_0636
status: open
verifies: REQ_0746

Snapshot test on the generated impl CanOpenConfigurable body for the EPOS4 fixture asserts the expected sequence of sdo.write(index, sub, value) calls: PDO comm params first, then PDO mapping params, then optional NMT start. Sequence and target indices match the EDS values exactly.


Runtime trait surface tests

Live under crates/canopen-eds-rt/tests/.

Test Case: CanOpenDevice trait shape compiles for a hand-written device TEST_0640
status: open

Hand-written test impl of CanOpenDevice for a minimal MockDevice validates the trait surface compiles end-to-end. Asserts IDENTITY is reachable, node_id / set_node_id round-trip, nmt_state / set_nmt_state round-trip, on_rpdo is callable on &mut self with idx: u8 in 0..=3, drain_tpdos is callable with a TpdoSink, and each error path returns the expected CanOpenError variant (PdoLenMismatch, NmtStateViolation, AbortCode, TransportFailed).

Test Case: CanOpenConfigurable async trait shape compiles TEST_0641
status: open
verifies: REQ_0751

Compile-only test: a mock device implements CanOpenConfigurable and an async fn configure body driving a mock SdoClient. The test passes if compilation succeeds; catches any trait-method-async surface drift.

Test Case: canopen-eds-rt is the trait home, not taktora-internal TEST_0642
status: open
verifies: REQ_0752

CI shell check: rg "trait CanOpenDevice" across the workspace matches exactly one source location, inside crates/canopen-eds-rt/src/. Same check for trait CanOpenConfigurable.

Test Case: PdoOut payload uses heapless::Vec<u8, 8> TEST_0643
status: open
verifies: REQ_0753

Compile-time check: static_assertions::assert_type_eq_all! on PdoOut::payload’s type vs heapless::Vec<u8, 8>. A change to the buffer type (e.g. lifting to a const generic for CAN-FD) requires this test to be updated deliberately rather than passing accidentally.

Test Case: RPDO rejected outside Operational state TEST_0644
status: open
verifies: REQ_0756

Integration test against a generated device fixture: set nmt_state(NmtState::PreOperational), call on_rpdo with a valid frame, assert the returned error is CanOpenError::NmtStateViolation. Switch to NmtState::Operational and confirm the same frame succeeds.


Build helper tests

Live under crates/canopen-eds-build/tests/.

Test Case: Builder writes a parseable Rust file to OUT_DIR TEST_0650
status: open
verifies: REQ_0760, REQ_0761

Test crate driven by a fixture EDS 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 EDS input TEST_0651
status: open
verifies: REQ_0762

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

Test Case: Output passes prettyplease formatting TEST_0652
status: open
verifies: REQ_0763

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

Test Case: Parser warnings surface as cargo:warning lines TEST_0653
status: open
verifies: REQ_0764

Drive the builder against a fixture set including the liberal-quirk EDS from Liberal-quirk parsing emits... (TEST_0615). Capture stdout, assert that one cargo:warning= line appears per quirk warning raised by the parser, and that the build still exits Ok.


CLI tests

Live under crates/canopen-eds-cli/tests/.

Test Case: cargo eds expand emits a single device's code TEST_0660
status: open
verifies: REQ_0770

Spawn the CLI as cargo eds expand --device EPOS4 --glob <fixtures>/*.eds, capture stdout, assert the output is non-empty, parses as Rust, and contains exactly one pub struct EPOS4_REV0140 (or equivalent rev-suffixed) definition.

Test Case: cargo eds list enumerates devices TEST_0661
status: open
verifies: REQ_0771

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

Test Case: CLI output matches build helper output byte-for-byte TEST_0662
status: open
verifies: REQ_0772

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).


Verifier tests

Live under crates/canopen-eds-verify/tests/.

Test Case: Verifier passes on matching EDS + dump pair TEST_0670
status: open
verifies: REQ_0780

Use the captured pair from crates/canopen-eds-verify/tests/fixtures/EPOS4/ (EDS file + matching epos4.dump.json), call verify(eds, dump), assert Ok(VerifyReport { matched: true, .. }).

Test Case: Verifier reports the differing field TEST_0671
status: open
verifies: REQ_0781

Synthetic mismatched pair: real EDS, mutated JSON dump altering the 0x1018:02 product_code field. The returned VerifyReport shall contain at least one Difference entry whose field is exactly "Identity.product_code" and whose eds / dump values match the originals.

Test Case: Verifier reuses canopen-eds parser TEST_0672
status: open
verifies: REQ_0782

White-box: cargo tree -p canopen-eds-verify lists canopen-eds as a direct dependency and does not list canopen-eds-codegen, canopen-eds-rt, taktora-connector-can, or socketcan anywhere in the graph.

Test Case: Verifier exit codes follow the documented matrix TEST_0673
status: open
verifies: REQ_0783

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

Test Case: Verifier rejects unknown schema version TEST_0674
status: open
verifies: REQ_0784

Fixture JSON dump with "schema": "taktora.canopen.sdo-dump.v2". The verifier shall reject the input with a parse error (exit code 2) before any field comparison runs.


Cross-cutting reproducibility tests

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

Test Case: Repeated codegen runs produce byte-identical output TEST_0680
status: open
verifies: QG_0014, REQ_0763

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_0681
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_0682
status: open

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

  • fieldbus-od-core: no ethercrab, no socketcan, no taktora-connector-*, no canopen-eds-rt.

  • canopen-eds: no canopen-eds-codegen, no canopen-eds-rt, no socketcan, no ethercrab.

  • canopen-eds-codegen: no canopen-eds-rt, no transport crates.

  • canopen-eds-build, canopen-eds-cli: no canopen-eds-rt.

  • canopen-eds-verify: no canopen-eds-codegen, no canopen-eds-rt, no transport crates.

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


Cross-cutting traceability

Used filter: types(test)

ID

Title

Status

Verifies

TEST_0600

Identity, DictEntry, PdoEntry round-trip

open

REQ_0702

TEST_0601

fieldbus-od-core has no transport deps

open

REQ_0700

TEST_0602

fieldbus-od-core compiles under no_std + alloc

open

REQ_0701

TEST_0603

ethercat-esi re-exports lifted types

open

REQ_0703; REQ_0704

TEST_0610

parse() accepts a representative Maxon EPOS4 EDS

open

REQ_0720; REQ_0722; REQ_0726

TEST_0611

Parser compiles under no_std + alloc

open

REQ_0721

TEST_0612

Parser is independent of codegen and transport

open

REQ_0721

TEST_0613

Unknown sections survive as RawSection

open

REQ_0724

TEST_0614

Parse errors carry line and column

open

REQ_0723

TEST_0615

Liberal-quirk parsing emits warnings without failing

open

REQ_0725

TEST_0620

Name sanitisation handles EDS naming edge cases

open

REQ_0731

TEST_0621

Revision collision produces distinct idents

open

REQ_0732

TEST_0622

PDO entry dedup collapses structurally identical layouts

open

REQ_0733

TEST_0623

TokenStream emission, not string formatting

open

REQ_0730; REQ_0734

TEST_0624

One EDS file equals one device

open

REQ_0735

TEST_0630

EPOS4 backend output snapshot

open

REQ_0741; REQ_0742; REQ_0743; REQ_0744

TEST_0631

Generated registry covers every emitted device

open

REQ_0745

TEST_0632

Generated module compiles under no_std + alloc

open

REQ_0748

TEST_0633

Backend is the sole canopen-eds-rt consumer in the toolchain

open

REQ_0740

TEST_0634

Object-dictionary emission gated by feature flag

open

REQ_0747

TEST_0635

Dummy entries skipped in PDO struct fields

open

REQ_0744

TEST_0636

Bring-up SDO writes emitted from EDS

open

REQ_0746

TEST_0640

CanOpenDevice trait shape compiles for a hand-written device

open

REQ_0750; REQ_0754; REQ_0755

TEST_0641

CanOpenConfigurable async trait shape compiles

open

REQ_0751

TEST_0642

canopen-eds-rt is the trait home, not taktora-internal

open

REQ_0752

TEST_0643

PdoOut payload uses heapless::Vec<u8, 8>

open

REQ_0753

TEST_0644

RPDO rejected outside Operational state

open

REQ_0756

TEST_0650

Builder writes a parseable Rust file to OUT_DIR

open

REQ_0760; REQ_0761

TEST_0651

cargo rerun-if-changed emitted per EDS input

open

REQ_0762

TEST_0652

Output passes prettyplease formatting

open

REQ_0763

TEST_0653

Parser warnings surface as cargo:warning lines

open

REQ_0764

TEST_0660

cargo eds expand emits a single device's code

open

REQ_0770

TEST_0661

cargo eds list enumerates devices

open

REQ_0771

TEST_0662

CLI output matches build helper output byte-for-byte

open

REQ_0772

TEST_0670

Verifier passes on matching EDS + dump pair

open

REQ_0780

TEST_0671

Verifier reports the differing field

open

REQ_0781

TEST_0672

Verifier reuses canopen-eds parser

open

REQ_0782

TEST_0673

Verifier exit codes follow the documented matrix

open

REQ_0783

TEST_0674

Verifier rejects unknown schema version

open

REQ_0784

TEST_0680

Repeated codegen runs produce byte-identical output

open

QG_0014; REQ_0763

TEST_0681

Input-file ordering does not affect output

open

QG_0014; REQ_0732; REQ_0733

TEST_0682

Layering integrity check (Cargo.toml audit)

open

QG_0015; REQ_0721; REQ_0740