[F51] live ASB type-matrix: provision UDAs + capture wire fixtures + round-trip tests
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled

Provisioned 7 new UDAs on $TestMachine via wwtools/graccesscli
object uda add (then deployed to TestMachine_001):

  TestFloat          MxFloat        scalar
  TestFloatArray     MxFloat        array (4)
  TestDouble         MxDouble       scalar
  TestDoubleArray    MxDouble       array (4)
  TestDateTime       MxTime         scalar
  TestDuration       MxElapsedTime  scalar
  TestDurationArray  MxElapsedTime  array (4)

New crates/mxaccess/examples/asb-type-matrix.rs reads all 14 tags
(7 pre-existing + 7 new) in a single batch and dumps the live
AsbVariant bytes per tag when MX_ASB_DUMP_FIXTURES=<dir> is set.
Single-attempt register (no retry — F31 InvalidConnectionId
cool-down re-arms on every retry, making backoff
counter-productive; if the cool-down is engaged, wait 60+ seconds
without ASB activity then re-run).

Captured live evidence (single cold-start run, all 14 register
calls returned error_code=0x0000):

  TestChangingInt   type_id=4  (Int32)        length=4   payload=4
  TestAlarm001      type_id=17 (Boolean)      length=1   payload=1
  MachineCode       type_id=10 (String)       length=30  payload=30
  TestFloat         type_id=8  (Float)        length=4   payload=4
  TestDouble        type_id=9  (Double)       length=8   payload=8
  TestDateTime      type_id=11 (DateTime)     length=8   payload=8
  TestDuration      type_id=12 (ElapsedTime)  length=8   payload=8

  TestIntArray, TestBoolArray, TestStringArray, TestDateTimeArray,
  TestFloatArray, TestDoubleArray, TestDurationArray
                    type_id=0 length=0 payload=0
                    (provisioned but no value written yet)

Per-tag fixture .bin files saved under
crates/mxaccess-codec/tests/fixtures/f51-type-matrix/ — full
14-byte to 40-byte AsbVariant byte sequences (i32 type_id LE +
i32 length LE + payload bytes).

crates/mxaccess-codec/tests/f51_type_matrix_parity.rs round-trips
each scalar fixture: decode -> re-encode -> assert byte-equal +
type_id / length pin. Tests skip with [skip] message when fixtures
are absent (so the suite passes on a fresh checkout without live
captures). 7 scalar tests pass against the captured fixtures.

Array tags excluded from round-trip pinning because the live
engine returns empty payloads for unwritten arrays. Codec-side
array round-trip is covered by asb_variant's existing synthetic-
payload unit tests.

docs/galaxy-test-fixtures.md inventories all $TestMachine UDAs
(pre-existing + F51-provisioned), the graccesscli provisioning
recipe, the fixture-regeneration pattern, and the F31 cool-down
caveat.

design/followups.md F51 marked resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-06 15:27:31 -04:00
parent 8bd66bbe65
commit c7505f9570
18 changed files with 461 additions and 0 deletions
@@ -0,0 +1,105 @@
//! F51 — round-trip parity for the live ASB type matrix.
//!
//! Captured live by `cargo run -p mxaccess --example asb-type-matrix`
//! with `MX_ASB_DUMP_FIXTURES=...` set. Each fixture file holds the
//! full `AsbVariant` byte sequence (i32 type_id LE + i32 length LE +
//! payload bytes) for one tag's read response. The tests below decode
//! each fixture, re-encode, and assert byte-identical round-trip plus
//! the expected type_id / length tuple.
//!
//! Live-evidence row from the original capture:
//!
//! | Tag | type_id | length | payload bytes |
//! |---|---|---|---|
//! | TestChangingInt | 4 (Int32) | 4 | 4 |
//! | TestAlarm001 | 17 (Boolean) | 1 | 1 |
//! | MachineCode | 10 (String) | 30 | 30 |
//! | TestFloat | 8 (Float) | 4 | 4 |
//! | TestDouble | 9 (Double) | 8 | 8 |
//! | TestDateTime | 11 (DateTime) | 8 | 8 |
//! | TestDuration | 12 (ElapsedTime)| 8 | 8 |
//!
//! Array tags read empty on the live install (no value written yet)
//! so they're not in this fixture set; their codec round-trip is
//! covered by `asb_variant`'s existing unit tests with synthetic
//! payloads.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use mxaccess_codec::AsbVariant;
const FIXTURE_DIR: &str = "tests/fixtures/f51-type-matrix";
fn load_fixture(name: &str) -> Option<Vec<u8>> {
let path = format!("{FIXTURE_DIR}/{name}.bin");
match std::fs::read(&path) {
Ok(bytes) => Some(bytes),
Err(_) => {
eprintln!(
"[skip] fixture {name} not present at {path}\
capture via `cargo run -p mxaccess --example asb-type-matrix` \
with `MX_ASB_DUMP_FIXTURES={FIXTURE_DIR}` set",
);
None
}
}
}
fn assert_round_trip(fixture: &str, expected_type_id: u16, expected_length: i32) {
let Some(bytes) = load_fixture(fixture) else {
return;
};
let (decoded, consumed) = AsbVariant::decode(&bytes)
.unwrap_or_else(|e| panic!("decode {fixture}: {e:?}"));
assert_eq!(consumed, bytes.len(), "{fixture}: decode consumed != bytes.len()");
assert_eq!(decoded.type_id, expected_type_id, "{fixture}: type_id");
assert_eq!(decoded.length, expected_length, "{fixture}: length");
let re_encoded = decoded.encode();
assert_eq!(re_encoded, bytes, "{fixture}: round-trip not byte-identical");
}
#[test]
fn int32_scalar_round_trip() {
assert_round_trip("TestMachine_001_TestChangingInt", 4, 4);
}
#[test]
fn boolean_scalar_round_trip() {
assert_round_trip("TestMachine_001_TestAlarm001", 17, 1);
}
#[test]
fn string_scalar_round_trip() {
// length 30 = "TestMachine_001" (15 chars × 2 bytes UTF-16) on a
// typical install. Different fixture captures may shift the
// length if the MachineCode value differs.
let Some(bytes) = load_fixture("TestMachine_001_MachineCode") else {
return;
};
let (decoded, consumed) = AsbVariant::decode(&bytes)
.unwrap_or_else(|e| panic!("decode: {e:?}"));
assert_eq!(consumed, bytes.len());
assert_eq!(decoded.type_id, 10);
let re_encoded = decoded.encode();
assert_eq!(re_encoded, bytes);
}
#[test]
fn float_scalar_round_trip() {
assert_round_trip("TestMachine_001_TestFloat", 8, 4);
}
#[test]
fn double_scalar_round_trip() {
assert_round_trip("TestMachine_001_TestDouble", 9, 8);
}
#[test]
fn date_time_scalar_round_trip() {
assert_round_trip("TestMachine_001_TestDateTime", 11, 8);
}
#[test]
fn elapsed_time_scalar_round_trip() {
assert_round_trip("TestMachine_001_TestDuration", 12, 8);
}