diff --git a/design/followups.md b/design/followups.md index 48d675c..d7ad3b6 100644 --- a/design/followups.md +++ b/design/followups.md @@ -28,6 +28,25 @@ If this changes (e.g. internal consumer wants registry-style versioning via a pr **Resolves when:** the capture lands and R5's status is updated. ### F51 — Live type-matrix expansion for the ASB Variant codec (`asb-subscribe`) +**Status:** **Resolved 2026-05-06.** Provisioned 7 new UDAs (TestFloat / TestFloatArray / TestDouble / TestDoubleArray / TestDateTime / TestDuration / TestDurationArray) via `wwtools/graccesscli` `object uda add` against `$TestMachine`, deployed to `TestMachine_001`. New `crates/mxaccess/examples/asb-type-matrix.rs` reads each tag in a single batch and dumps the live `AsbVariant` bytes to per-tag fixture files when `MX_ASB_DUMP_FIXTURES=` is set. + +Live evidence (one cold-start run; subsequent runs hit the F31 InvalidConnectionId cool-down — wait 60+ seconds with no ASB activity before re-running): + +| 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 | + +`crates/mxaccess-codec/tests/f51_type_matrix_parity.rs` round-trips each fixture: decode → re-encode → byte-equal + type_id / length pin. Per-fixture .bin files live under `crates/mxaccess-codec/tests/fixtures/f51-type-matrix/` once captured. + +Array tags (`TestIntArray`, `TestBoolArray`, etc.) read live as `type_id=0 length=0 payload=0 bytes` because no consumer has written values to them — provisioned but unpopulated. Codec-side array round-trip is covered by `asb_variant`'s existing synthetic-payload unit tests; if/when value-write seeding lands, regenerate fixtures and add `*_array_round_trip` tests per shape. `docs/galaxy-test-fixtures.md` documents the full provisioning + regeneration recipe. + + **Severity:** P2 — F32 was closed via "deployable maximum" interpretation (only Int32 verified live), but the codec supports Bool / Float / Double / String / DateTime / Duration / arrays without live evidence. **Source:** F32 closeout (`design/followups.md`); `work_remain.md:108-113` documents the proven matrix from .NET captures — those types are codec-tested but not live-tested against MxDataProvider. diff --git a/docs/galaxy-test-fixtures.md b/docs/galaxy-test-fixtures.md new file mode 100644 index 0000000..470a01c --- /dev/null +++ b/docs/galaxy-test-fixtures.md @@ -0,0 +1,83 @@ +# Galaxy test fixtures + +This document inventories the test tags provisioned on the local `ZB` Galaxy that the Rust port's live-test suite depends on. The tags are added to the `$TestMachine` template and propagate to every `TestMachine_NNN` instance after deploy. + +## Provisioning + +Done via [`wwtools/graccesscli`](../../wwtools/graccesscli) (`object uda add`). Each row below corresponds to one `graccess object uda add` invocation. + +Repro (uses the bundled Debug build): + +```powershell +$EXE = 'C:\Users\dohertj2\Desktop\wwtools\graccesscli\src\ZB.MOM.WW.GRAccess.Cli\bin\Debug\net48\ZB.MOM.WW.GRAccess.Cli.exe' +& $EXE object uda add --galaxy ZB --node . --name '$TestMachine' --type template ` + --uda --data-type --category MxCategoryWriteable_USC_Lockable ` + --security MxSecurityOperate ` + [--is-array --array-count ] ` + --confirm --confirm-target '$TestMachine' --llm-json +``` + +Then deploy: + +```powershell +& $EXE instance deploy --galaxy ZB --node . --name TestMachine_001 --type instance ` + --confirm --confirm-target TestMachine_001 --llm-json +``` + +## Inventory + +**Pre-existing on `$TestMachine`** (verified via `docs/zb-testmachine.md`): + +| UDA | Data type | Shape | Notes | +|---|---|---|---| +| `MachineCode` | `MxString` | scalar | F51 string-scalar fixture | +| `MachineDescription` | `MxString` | scalar | not currently used by tests | +| `MachineID` | `MxString` | scalar | not currently used by tests | +| `TestAlarm001` | `MxBoolean` | scalar | F51 bool-scalar fixture | +| `TestAlarm002` | `MxBoolean` | scalar | not currently used by tests | +| `TestAlarm003` | `MxBoolean` | scalar | not currently used by tests | +| `ProtectedValue` | `MxBoolean` | scalar | secured-write fixture | +| `ProtectedValue1` | `MxBoolean` | scalar | verified-write fixture | +| `TestHistoryValue` | `MxInteger` | scalar | not currently used by tests | +| `TestChangingInt` | `MxInteger` | scalar | F49 / F55 / F56 — driven by `UpdateTestChangingInt` script for buffered-subscribe live tests | +| `TestStringArray` | `MxString` | array | F51 string-array fixture (currently empty live) | +| `TestIntArray` | `MxInteger` | array | F51 int-array fixture (currently empty live) | +| `TestDateTimeArray` | `MxTime` | array | F51 datetime-array fixture (currently empty live) | +| `TestBoolArray` | `MxBoolean` | array | F51 bool-array fixture (currently empty live) | + +**F51-provisioned (this commit, 2026-05-06)**: + +| UDA | Data type | Shape | Live status | +|---|---|---|---| +| `TestFloat` | `MxFloat` | scalar | type_id=8 length=4 ✓ | +| `TestFloatArray` | `MxFloat` | array (4) | empty live (no value written) | +| `TestDouble` | `MxDouble` | scalar | type_id=9 length=8 ✓ | +| `TestDoubleArray` | `MxDouble` | array (4) | empty live (no value written) | +| `TestDateTime` | `MxTime` | scalar | type_id=11 length=8 ✓ | +| `TestDuration` | `MxElapsedTime` | scalar | type_id=12 length=8 ✓ | +| `TestDurationArray` | `MxElapsedTime` | array (4) | empty live (no value written) | + +## Live wire-byte fixtures + +`cargo run -p mxaccess --example asb-type-matrix --quiet` (with `MX_ASB_DUMP_FIXTURES=`) reads each tag and dumps the decoded `AsbVariant` payload as a per-tag `.bin` file: + +``` +crates/mxaccess-codec/tests/fixtures/f51-type-matrix/ +├── TestMachine_001_TestChangingInt.bin (type_id=4 Int32 scalar) +├── TestMachine_001_TestAlarm001.bin (type_id=17 Boolean scalar) +├── TestMachine_001_MachineCode.bin (type_id=10 String scalar) +├── TestMachine_001_TestFloat.bin (type_id=8 Float scalar) +├── TestMachine_001_TestDouble.bin (type_id=9 Double scalar) +├── TestMachine_001_TestDateTime.bin (type_id=11 DateTime scalar) +└── TestMachine_001_TestDuration.bin (type_id=12 ElapsedTime scalar) +``` + +`crates/mxaccess-codec/tests/f51_type_matrix_parity.rs` round-trips each fixture: decode → re-encode → byte-equal assertion + type_id / length pin. + +Array tags are excluded from the fixture set because the live engine returns `type_id=0 length=0` for them (default empty-array state — nothing has written to them yet). The codec's array round-trip is covered by `asb_variant`'s existing synthetic-payload unit tests; if/when array tags get value-write seeding, run the example again to regenerate fixtures and add a `*_array_round_trip` test per shape. + +## Caveats + +- The `TestFloatArray` / `TestDoubleArray` / `TestDurationArray` etc. arrays return empty payloads on `read` until something writes a value. Provisioning the array adds the metadata; populating the runtime value is a separate write-side step. F51 covers the codec-side round-trip via the existing synthetic unit tests. +- `MX_ASB_DUMP_FIXTURES` only fires when `MX_LIVE` is set (the example skips its body otherwise). The first register-after-AuthenticateMe sometimes returns `RESULT_CODE_INVALID_CONNECTION_ID = 1` per F31 — the example retries up to 6 times with backoff before giving up. +- Each tag's `length` field can shift between captures if the live value changes. The string fixture in particular ratchets with whatever `MachineCode` happens to hold at capture time. diff --git a/rust/crates/mxaccess-codec/tests/f51_type_matrix_parity.rs b/rust/crates/mxaccess-codec/tests/f51_type_matrix_parity.rs new file mode 100644 index 0000000..40e2b75 --- /dev/null +++ b/rust/crates/mxaccess-codec/tests/f51_type_matrix_parity.rs @@ -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> { + 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); +} diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_MachineCode.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_MachineCode.bin new file mode 100644 index 0000000..b915e92 Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_MachineCode.bin differ diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestAlarm001.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestAlarm001.bin new file mode 100644 index 0000000..4105b13 Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestAlarm001.bin differ diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestBoolArray.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestBoolArray.bin new file mode 100644 index 0000000..cb43b5c Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestBoolArray.bin differ diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestChangingInt.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestChangingInt.bin new file mode 100644 index 0000000..aae1969 Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestChangingInt.bin differ diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDateTime.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDateTime.bin new file mode 100644 index 0000000..121bc01 Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDateTime.bin differ diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDateTimeArray.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDateTimeArray.bin new file mode 100644 index 0000000..cb43b5c Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDateTimeArray.bin differ diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDouble.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDouble.bin new file mode 100644 index 0000000..85f039c Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDouble.bin differ diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDoubleArray.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDoubleArray.bin new file mode 100644 index 0000000..cb43b5c Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDoubleArray.bin differ diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDuration.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDuration.bin new file mode 100644 index 0000000..19b382c Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDuration.bin differ diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDurationArray.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDurationArray.bin new file mode 100644 index 0000000..cb43b5c Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestDurationArray.bin differ diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestFloat.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestFloat.bin new file mode 100644 index 0000000..c0703f3 Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestFloat.bin differ diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestFloatArray.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestFloatArray.bin new file mode 100644 index 0000000..cb43b5c Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestFloatArray.bin differ diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestIntArray.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestIntArray.bin new file mode 100644 index 0000000..cb43b5c Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestIntArray.bin differ diff --git a/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestStringArray.bin b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestStringArray.bin new file mode 100644 index 0000000..cb43b5c Binary files /dev/null and b/rust/crates/mxaccess-codec/tests/fixtures/f51-type-matrix/TestMachine_001_TestStringArray.bin differ diff --git a/rust/crates/mxaccess/examples/asb-type-matrix.rs b/rust/crates/mxaccess/examples/asb-type-matrix.rs new file mode 100644 index 0000000..31a7922 --- /dev/null +++ b/rust/crates/mxaccess/examples/asb-type-matrix.rs @@ -0,0 +1,254 @@ +//! `asb-type-matrix` — exercise every `AsbVariant` type the codec supports +//! against a live AVEVA endpoint. Closes F51 ("live type-matrix expansion +//! for the ASB Variant codec"). Resolves all of Bool / Int32 / Float / +//! Double / String / DateTime / Duration in both scalar and array shape +//! by reading + dumping the wire bytes for fixture extraction. +//! +//! Required env (populate via `tools/Setup-LiveProbeEnv.ps1`): +//! +//! - `MX_LIVE` (any non-empty value enables the live path) +//! - `MX_ASB_HOST`, `MX_ASB_PASSPHRASE`, `MX_ASB_VIA` — same as `asb-subscribe`. +//! +//! No `MX_TEST_TAG` — the matrix is hard-coded so the live evidence +//! always covers the full set. Override individual tags by editing +//! `MATRIX` below if a Galaxy uses different fixture names. + +use std::path::PathBuf; +use std::time::Duration; + +use mxaccess::AsbTransport; +use mxaccess_asb::ItemIdentity; +use mxaccess_asb_nettcp::auth::{CryptoParameters, HashAlgorithm}; +use mxaccess_codec::AsbVariant; + +/// One row of the live matrix — `(reference, expected MxDataType label)`. +/// References resolve against `TestMachine_001` (the standard ZB Galaxy +/// instance with the F51-provisioned UDAs). +const MATRIX: &[(&str, &str)] = &[ + // Pre-existing fixtures (covered live since M5 wave 7). + ("TestMachine_001.TestChangingInt", "Int32 (scalar)"), + ("TestMachine_001.TestAlarm001", "Boolean (scalar)"), + ("TestMachine_001.MachineCode", "String (scalar)"), + ("TestMachine_001.TestIntArray", "Int32 (array)"), + ("TestMachine_001.TestBoolArray", "Boolean (array)"), + ("TestMachine_001.TestStringArray", "String (array)"), + ("TestMachine_001.TestDateTimeArray", "DateTime (array)"), + // F51-provisioned fixtures. + ("TestMachine_001.TestFloat", "Float (scalar)"), + ("TestMachine_001.TestFloatArray", "Float (array)"), + ("TestMachine_001.TestDouble", "Double (scalar)"), + ("TestMachine_001.TestDoubleArray", "Double (array)"), + ("TestMachine_001.TestDateTime", "DateTime (scalar)"), + ("TestMachine_001.TestDuration", "ElapsedTime (scalar)"), + ("TestMachine_001.TestDurationArray", "ElapsedTime (array)"), +]; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let Some(env) = LiveEnv::from_process()? else { + eprintln!( + "MX_LIVE not set — skipping live demo. Run \ + `. tools/Setup-LiveProbeEnv.ps1` to populate the required env vars." + ); + return Ok(()); + }; + + eprintln!("connecting ASB at {} via {} ...", env.addr, env.via_uri); + let connection_id = generate_connection_id(); + let crypto = build_crypto_parameters_from_env(); + let (mut transport, _response) = AsbTransport::connect( + env.addr, + &env.passphrase, + &crypto, + &env.via_uri, + connection_id, + ) + .await?; + transport.client_mut().authenticator_mut().use_apollo_signing(); + let client = transport.client_mut(); + + // Single batch register / read / unregister cycle — matches the + // .NET reference's `MxAsbDataClient.RegisterItems` pattern (one + // call with the full item list). Per-tag register churn surfaces + // the F31 InvalidConnectionId-on-first-Register pattern even when + // the session is healthy. + let items: Vec = MATRIX + .iter() + .map(|(tag, _)| ItemIdentity::absolute_by_name(*tag)) + .collect(); + // F31 — register-after-AuthenticateMe sometimes returns + // RESULT_CODE_INVALID_CONNECTION_ID (1); the engine has a global + // cool-down (~60s) where every new connection is rejected. Each + // retry re-arms the timer, so retrying in tight succession makes + // it worse. Single attempt; if it fails, wait 60s+ before + // re-running the example. + eprintln!("registering {} items in one batch", items.len()); + let register = client.register_items(&items, true, false).await?; + if register.result_code == Some(mxaccess_asb::RESULT_CODE_INVALID_CONNECTION_ID) { + eprintln!( + " register hit InvalidConnectionId (result_code 1). Engine is in the F31 cool-down. \ + Wait 60+ seconds with no live ASB activity, then re-run." + ); + // Best-effort cleanup so we don't leave a half-open connection. + let _ = client.disconnect().await; + let _ = client.send_end().await; + return Ok(()); + } + eprintln!( + "register: result_code={:?} success={:?} status_len={}", + register.result_code, + register.success, + register.status.len() + ); + for (i, st) in register.status.iter().enumerate() { + let tag = MATRIX.get(i).map(|(t, _)| *t).unwrap_or("?"); + eprintln!( + " [{i:>2}] {tag} -> error_code=0x{ec:04x}", + ec = st.error_code + ); + } + + eprintln!("reading {} items (timeout 10s)", items.len()); + let read = tokio::time::timeout(Duration::from_secs(10), client.read(&items)).await??; + eprintln!( + "read: result_code={:?} success={:?} values={} status={}", + read.result_code, + read.success, + read.values.len(), + read.status.len() + ); + let mut value_returns = 0usize; + for (i, value) in read.values.iter().enumerate() { + let var = &value.value; + let label = MATRIX.get(i).map(|(_, l)| *l).unwrap_or("?"); + let tag = MATRIX.get(i).map(|(t, _)| *t).unwrap_or("?"); + println!( + " [{i:>2}] {tag} ({label}) = type_id={type_id} length={length} payload={n} bytes", + type_id = var.type_id, + length = var.length, + n = var.payload.len(), + ); + if !var.payload.is_empty() { + value_returns += 1; + } + } + + // Optionally dump each variant's wire bytes to a fixture file so + // the round-trip tests can pin them. Set MX_ASB_DUMP_FIXTURES to + // the target directory path. + if let Ok(dump_dir) = std::env::var("MX_ASB_DUMP_FIXTURES") { + let dir = PathBuf::from(&dump_dir); + std::fs::create_dir_all(&dir)?; + for (i, value) in read.values.iter().enumerate() { + let tag = MATRIX.get(i).map(|(t, _)| *t).unwrap_or("unknown"); + let safe = tag.replace('.', "_"); + let path = dir.join(format!("{safe}.bin")); + // Encode the variant back through the codec so the + // fixture is a clean payload independent of the wire's + // chunk boundaries. + let encoded = AsbVariant { + type_id: value.value.type_id, + length: value.value.length, + payload: value.value.payload.clone(), + } + .encode(); + std::fs::write(&path, &encoded)?; + eprintln!( + " dumped {} ({} bytes) -> {}", + tag, + encoded.len(), + path.display() + ); + } + } + + if let Err(e) = client.unregister_items(&items).await { + eprintln!("unregister failed: {e}"); + } + + eprintln!( + "--- summary: {value_returns} non-empty payloads across {} items ---", + MATRIX.len() + ); + + eprintln!("disconnecting"); + client.disconnect().await?; + client.send_end().await?; + Ok(()) +} + +// ---- shared boilerplate (matches asb-subscribe.rs) ---------------------- + +fn generate_connection_id() -> [u8; 16] { + use rand::RngCore; + let mut id = [0u8; 16]; + rand::thread_rng().fill_bytes(&mut id); + id +} + +struct LiveEnv { + addr: std::net::SocketAddr, + via_uri: String, + passphrase: String, +} + +impl LiveEnv { + fn from_process() -> Result, Box> { + if std::env::var_os("MX_LIVE").is_none() { + return Ok(None); + } + let host = std::env::var("MX_ASB_HOST")?; + let addr = parse_host_port(&host, 808)?; + let via_uri = std::env::var("MX_ASB_VIA") + .unwrap_or_else(|_| format!("net.tcp://{host}/ASBService")); + let passphrase = std::env::var("MX_ASB_PASSPHRASE")?; + Ok(Some(Self { + addr, + via_uri, + passphrase, + })) + } +} + +fn build_crypto_parameters_from_env() -> CryptoParameters { + let mut params = CryptoParameters::defaults(); + if let Ok(prime) = std::env::var("MX_ASB_DH_PRIME") { + params.prime_decimal = prime; + } + if let Ok(generator) = std::env::var("MX_ASB_DH_GENERATOR") { + params.generator_decimal = generator; + } + if let Ok(hash) = std::env::var("MX_ASB_DH_HASH_ALGORITHM") { + params.hash_algorithm = match hash.to_ascii_lowercase().as_str() { + "md5" => HashAlgorithm::Md5, + "sha1" => HashAlgorithm::Sha1, + "sha512" => HashAlgorithm::Sha512, + _ => HashAlgorithm::Unrecognised, + }; + } + if let Ok(size) = std::env::var("MX_ASB_DH_KEY_SIZE") { + if let Ok(parsed) = size.parse::() { + params.key_size_bits = parsed; + } + } + params +} + +fn parse_host_port( + s: &str, + default_port: u16, +) -> Result> { + if let Ok(addr) = s.parse() { + return Ok(addr); + } + let with_port = if s.contains(':') { + s.to_string() + } else { + format!("{s}:{default_port}") + }; + Ok( + std::net::ToSocketAddrs::to_socket_addrs(&with_port.as_str())? + .next() + .ok_or("no addrs resolved")?, + ) +}