Decision (recorded as an ADR-011 addendum): the packed-mode wire layout for a field-name-discriminator TUnion is shared-then-variant — the union's declared `fields` (disc + shared fields) first, then the variant's own fields. Reader and materializer already implemented this; LayoutBuilder was corrected from variant-only layout. Enforcement in BastUnion::parse (the choke point every consumer inherits — union roots at BastDoc::new, referenced unions at resolve_ref): - discriminator field must be declared in `fields` - `fields` must not contain duplicate names - variants must not re-declare shared fields (checked inline and through $ref resolution — parse chain now threads the doc root) - the discriminator field must be the FIRST entry in `fields` (the reader reads the disc at the union start; a later position made it dispatch on the wrong bytes — H3 item 2, probe-verified) Schemas relying on the old variant-only builder convention (variants re-declaring shared fields) are rejected with a clean Schema error naming the convention. Breaking for 0.2.0-era re-declaring schemas; announced with 0.3.x. - L5: FieldValue::Union::variant_start doc now states per-kind semantics (byte-disc: union_start + disc.offset + disc.size; field-disc: after the shared walk). - L6: roundtrip test added (poc_roundtrip.rs) — LayoutBuilder write → SequentialReader read → materialize_packed → validate_bytes over a field-disc union with a second shared field and non-redeclaring variant; pins event.type@0/seq@1/handle@5, total 10. - ADR-011: Status-block addendum recording the convention decision, the no-re-declare rule, and the breaking-constraint note. - Review #006 updated: H3/L5/L6 resolution blocks, resolution log, recommended order. Verified: 488 tests green (410+17+34+15+12, 2 pre-existing ignored), clippy -D warnings clean, wasm32 build green, cargo doc zero warnings.
713 lines
25 KiB
Rust
713 lines
25 KiB
Rust
//! POC round-trip tests adapted from the `@alkdev/alknet: alknet-typedef-poc/`
|
|
//! workspace.
|
|
//!
|
|
//! These tests re-validate the byte-identical round-trip behaviour that
|
|
//! the POC verified: fixed-size primitives, length-prefixed strings and
|
|
//! bytes, nested structs, big-endian, alignment padding, packed-layout
|
|
//! `LayoutBuilder` with `data_access` writes, and `SequentialReader`
|
|
//! walks. Each test writes values to a buffer at computed offsets and
|
|
//! reads them back, asserting both the values and (where applicable)
|
|
//! the byte positions.
|
|
//!
|
|
//! All schemas are BAST documents (`{ "$defs": { ... } }` with `kind`-
|
|
//! based vocabulary). The root type name is passed to `OffsetMap::compute`
|
|
//! / `LayoutBuilder::new` / `SequentialReader::new` / `AlkTypeEngine::compile`.
|
|
|
|
use alktype::bast::BastDoc;
|
|
use alktype::data_access;
|
|
use alktype::tunion;
|
|
use alktype::*;
|
|
use serde_json::json;
|
|
use std::collections::HashMap;
|
|
|
|
fn var_sizes(pairs: &[(&str, usize)]) -> HashMap<String, usize> {
|
|
pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn fixed_size_round_trip_via_offset_map() -> Result<(), AlkTypeError> {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{ "name": "id", "kind": "uint32" },
|
|
{ "name": "score", "kind": "float32" },
|
|
{ "name": "flag", "kind": "uint8" },
|
|
{ "name": "count", "kind": "uint16" }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let doc = BastDoc::new(&root, "S")?;
|
|
let offset_map = OffsetMap::compute(&doc)?;
|
|
let mut buffer = vec![0u8; offset_map.total_size()];
|
|
|
|
let id_range = offset_map.get("id").expect("id range");
|
|
data_access::write_u32(&mut buffer, id_range.start(), 42, "id", Endian::Little)?;
|
|
let score_range = offset_map.get("score").expect("score range");
|
|
data_access::write_f32(&mut buffer, score_range.start(), 1.5, "score", Endian::Little)?;
|
|
let flag_range = offset_map.get("flag").expect("flag range");
|
|
data_access::write_u8(&mut buffer, flag_range.start(), 1, "flag")?;
|
|
let count_range = offset_map.get("count").expect("count range");
|
|
data_access::write_u16(
|
|
&mut buffer,
|
|
count_range.start(),
|
|
1000,
|
|
"count",
|
|
Endian::Little,
|
|
)?;
|
|
|
|
assert_eq!(
|
|
data_access::read_u32(&buffer, id_range.start(), "id", Endian::Little)?,
|
|
42
|
|
);
|
|
let score = data_access::read_f32(&buffer, score_range.start(), "score", Endian::Little)?;
|
|
assert!((score - 1.5).abs() < 0.001, "score: {score}");
|
|
assert_eq!(data_access::read_u8(&buffer, flag_range.start(), "flag")?, 1);
|
|
assert_eq!(
|
|
data_access::read_u16(&buffer, count_range.start(), "count", Endian::Little)?,
|
|
1000
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn fixed_size_round_trip_via_engine_aligned() -> Result<(), AlkTypeError> {
|
|
let doc = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"endian": "little",
|
|
"fields": [
|
|
{ "name": "id", "kind": "uint32" },
|
|
{ "name": "score", "kind": "float32" },
|
|
{ "name": "flag", "kind": "uint8" }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None)?;
|
|
let offset_map = engine.offset_map().expect("aligned mode has offset_map");
|
|
let mut buffer = vec![0u8; offset_map.total_size()];
|
|
|
|
engine.write_field(&mut buffer, "id", &FieldValue::U32(42))?;
|
|
engine.write_field(&mut buffer, "score", &FieldValue::F32(1.5))?;
|
|
engine.write_field(&mut buffer, "flag", &FieldValue::U8(1))?;
|
|
|
|
assert_eq!(engine.read_field(&buffer, "id")?, FieldValue::U32(42));
|
|
let score = match engine.read_field(&buffer, "score")? {
|
|
FieldValue::F32(f) => f,
|
|
other => panic!("expected F32, got {other:?}"),
|
|
};
|
|
assert!((score - 1.5).abs() < 0.001);
|
|
assert_eq!(engine.read_field(&buffer, "flag")?, FieldValue::U8(1));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn string_round_trip_via_data_access() -> Result<(), AlkTypeError> {
|
|
let mut buffer = vec![0u8; 32];
|
|
let written = data_access::write_string(&mut buffer, 0, "hello", "name", Endian::Little)?;
|
|
assert_eq!(written, 4 + 5);
|
|
assert_eq!(buffer[0..4], 5u32.to_le_bytes());
|
|
assert_eq!(&buffer[4..9], b"hello");
|
|
assert_eq!(
|
|
data_access::read_string(&buffer, 0, "name", Endian::Little)?,
|
|
"hello"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn string_round_trip_via_engine_aligned() -> Result<(), AlkTypeError> {
|
|
let doc = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "name", "kind": "string" } ]
|
|
}
|
|
}
|
|
});
|
|
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None)?;
|
|
let offset_map = engine.offset_map().expect("aligned mode has offset_map");
|
|
let mut buffer = vec![0u8; offset_map.total_size() + 64];
|
|
engine.write_field(&mut buffer, "name", &FieldValue::String("hello"))?;
|
|
assert_eq!(
|
|
engine.read_field(&buffer, "name")?,
|
|
FieldValue::String("hello")
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn bytes_round_trip_via_data_access() -> Result<(), AlkTypeError> {
|
|
let payload = [0xAA, 0xBB, 0xCC, 0xDD];
|
|
let mut buffer = vec![0u8; 32];
|
|
let written = data_access::write_bytes(&mut buffer, 0, &payload, "data", Endian::Little)?;
|
|
assert_eq!(written, 4 + 4);
|
|
assert_eq!(buffer[0..4], 4u32.to_le_bytes());
|
|
assert_eq!(&buffer[4..8], &payload);
|
|
assert_eq!(
|
|
data_access::read_bytes(&buffer, 0, "data", Endian::Little)?,
|
|
&payload[..]
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn nested_struct_round_trip_via_offset_map() -> Result<(), AlkTypeError> {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{
|
|
"name": "header",
|
|
"kind": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{ "name": "version", "kind": "uint32" },
|
|
{ "name": "magic", "kind": "uint32" }
|
|
]
|
|
}
|
|
},
|
|
{ "name": "payload", "kind": "bytes" }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let doc = BastDoc::new(&root, "S")?;
|
|
let offset_map = OffsetMap::compute(&doc)?;
|
|
|
|
let header_version = offset_map.get("header.version").expect("header.version");
|
|
let header_magic = offset_map.get("header.magic").expect("header.magic");
|
|
let payload_prefix = offset_map.get("payload").expect("payload");
|
|
|
|
assert_eq!(header_version.start(), 0);
|
|
assert_eq!(header_magic.start(), 4);
|
|
assert_eq!(payload_prefix.start(), 8);
|
|
|
|
let data = b"body-data".to_vec();
|
|
let mut buffer = vec![0u8; offset_map.total_size() + data.len()];
|
|
data_access::write_u32(
|
|
&mut buffer,
|
|
header_version.start(),
|
|
1,
|
|
"header.version",
|
|
Endian::Little,
|
|
)?;
|
|
data_access::write_u32(
|
|
&mut buffer,
|
|
header_magic.start(),
|
|
0xCAFEBABE,
|
|
"header.magic",
|
|
Endian::Little,
|
|
)?;
|
|
data_access::write_bytes(
|
|
&mut buffer,
|
|
payload_prefix.start(),
|
|
&data,
|
|
"payload",
|
|
Endian::Little,
|
|
)?;
|
|
|
|
assert_eq!(
|
|
data_access::read_u32(
|
|
&buffer,
|
|
header_version.start(),
|
|
"header.version",
|
|
Endian::Little
|
|
)?,
|
|
1
|
|
);
|
|
assert_eq!(
|
|
data_access::read_u32(&buffer, header_magic.start(), "header.magic", Endian::Little)?,
|
|
0xCAFEBABE
|
|
);
|
|
assert_eq!(
|
|
data_access::read_bytes(&buffer, payload_prefix.start(), "payload", Endian::Little)?,
|
|
&data[..]
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn nested_struct_round_trip_via_engine_aligned() -> Result<(), AlkTypeError> {
|
|
let doc = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{
|
|
"name": "header",
|
|
"kind": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{ "name": "version", "kind": "uint8" },
|
|
{ "name": "flags", "kind": "uint8" }
|
|
]
|
|
}
|
|
},
|
|
{ "name": "payload_len", "kind": "uint32" }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Aligned, None)?;
|
|
let offset_map = engine.offset_map().expect("aligned mode");
|
|
|
|
assert_eq!(offset_map.get("header.version").unwrap().start(), 0);
|
|
assert_eq!(offset_map.get("header.flags").unwrap().start(), 1);
|
|
assert_eq!(offset_map.get("payload_len").unwrap().start(), 4);
|
|
|
|
let mut buffer = vec![0u8; offset_map.total_size()];
|
|
engine.write_field(&mut buffer, "header.version", &FieldValue::U8(1))?;
|
|
engine.write_field(&mut buffer, "header.flags", &FieldValue::U8(0x0F))?;
|
|
engine.write_field(&mut buffer, "payload_len", &FieldValue::U32(1024))?;
|
|
|
|
assert_eq!(
|
|
engine.read_field(&buffer, "header.version")?,
|
|
FieldValue::U8(1)
|
|
);
|
|
assert_eq!(
|
|
engine.read_field(&buffer, "header.flags")?,
|
|
FieldValue::U8(0x0F)
|
|
);
|
|
assert_eq!(
|
|
engine.read_field(&buffer, "payload_len")?,
|
|
FieldValue::U32(1024)
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn big_endian_round_trip_via_offset_map() -> Result<(), AlkTypeError> {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"endian": "big",
|
|
"fields": [
|
|
{ "name": "id", "kind": "uint32" },
|
|
{ "name": "offset", "kind": "float64" }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let doc = BastDoc::new(&root, "S")?;
|
|
let offset_map = OffsetMap::compute(&doc)?;
|
|
let endian = Endian::Big;
|
|
|
|
let id_range = offset_map.get("id").expect("id");
|
|
let offset_range = offset_map.get("offset").expect("offset");
|
|
|
|
assert_eq!(id_range.start(), 0);
|
|
assert_eq!(offset_range.start(), 8);
|
|
|
|
let value: f64 = std::f64::consts::PI;
|
|
let mut buffer = vec![0u8; offset_map.total_size()];
|
|
data_access::write_u32(&mut buffer, id_range.start(), 0x01020304, "id", endian)?;
|
|
data_access::write_f64(&mut buffer, offset_range.start(), value, "offset", endian)?;
|
|
|
|
assert_eq!(&buffer[0..4], &[0x01, 0x02, 0x03, 0x04]);
|
|
assert_eq!(&buffer[4..8], &[0x00, 0x00, 0x00, 0x00]);
|
|
assert_eq!(&buffer[8..16], value.to_be_bytes());
|
|
|
|
assert_eq!(
|
|
data_access::read_u32(&buffer, id_range.start(), "id", endian)?,
|
|
0x01020304
|
|
);
|
|
let read = data_access::read_f64(&buffer, offset_range.start(), "offset", endian)?;
|
|
assert!((read - value).abs() < 1e-12);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn alignment_padding_round_trip_u8_then_u32() -> Result<(), AlkTypeError> {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{ "name": "flag", "kind": "uint8" },
|
|
{ "name": "id", "kind": "uint32" }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let doc = BastDoc::new(&root, "S")?;
|
|
let offset_map = OffsetMap::compute(&doc)?;
|
|
|
|
let flag_range = offset_map.get("flag").expect("flag");
|
|
let id_range = offset_map.get("id").expect("id");
|
|
|
|
assert_eq!(flag_range.start(), 0);
|
|
assert_eq!(flag_range.end(), 1);
|
|
assert_eq!(id_range.start(), 4);
|
|
assert_eq!(id_range.end(), 8);
|
|
assert_eq!(offset_map.total_size(), 8);
|
|
|
|
let mut buffer = vec![0u8; offset_map.total_size()];
|
|
data_access::write_u8(&mut buffer, flag_range.start(), 0xAB, "flag")?;
|
|
data_access::write_u32(
|
|
&mut buffer,
|
|
id_range.start(),
|
|
0x01020304,
|
|
"id",
|
|
Endian::Little,
|
|
)?;
|
|
|
|
assert_eq!(buffer[0], 0xAB);
|
|
assert_eq!(&buffer[1..4], &[0x00, 0x00, 0x00]);
|
|
assert_eq!(&buffer[4..8], 0x01020304u32.to_le_bytes());
|
|
|
|
assert_eq!(
|
|
data_access::read_u8(&buffer, flag_range.start(), "flag")?,
|
|
0xAB
|
|
);
|
|
assert_eq!(
|
|
data_access::read_u32(&buffer, id_range.start(), "id", Endian::Little)?,
|
|
0x01020304
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn packed_layout_round_trip_via_layout_builder() -> Result<(), AlkTypeError> {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"endian": "little",
|
|
"fields": [
|
|
{ "name": "flag", "kind": "uint8" },
|
|
{ "name": "id", "kind": "uint32" },
|
|
{ "name": "payload", "kind": "string" }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let builder = LayoutBuilder::new(&root, "S")?;
|
|
let layout = builder.build(&var_sizes(&[("payload", 10)]))?;
|
|
|
|
let flag_pos = layout.get("flag").expect("flag");
|
|
let id_pos = layout.get("id").expect("id");
|
|
let payload_pos = layout.get("payload").expect("payload");
|
|
|
|
assert_eq!(flag_pos.offset, 0);
|
|
assert_eq!(id_pos.offset, 1);
|
|
assert_eq!(payload_pos.offset, 5);
|
|
assert_eq!(layout.total_size(), 19);
|
|
|
|
let payload_str = "ten bytes!";
|
|
let payload_bytes = payload_str.as_bytes();
|
|
assert_eq!(payload_bytes.len(), 10);
|
|
let mut buffer = vec![0u8; layout.total_size()];
|
|
data_access::write_u8(&mut buffer, flag_pos.offset, 0xAB, "flag")?;
|
|
data_access::write_u32(&mut buffer, id_pos.offset, 0x01020304, "id", Endian::Little)?;
|
|
data_access::write_string(
|
|
&mut buffer,
|
|
payload_pos.offset,
|
|
payload_str,
|
|
"payload",
|
|
Endian::Little,
|
|
)?;
|
|
|
|
assert_eq!(buffer[0], 0xAB);
|
|
assert_eq!(&buffer[1..5], 0x01020304u32.to_le_bytes());
|
|
assert_eq!(&buffer[5..9], 10u32.to_le_bytes());
|
|
assert_eq!(&buffer[9..19], payload_bytes);
|
|
|
|
assert_eq!(
|
|
data_access::read_u8(&buffer, flag_pos.offset, "flag")?,
|
|
0xAB
|
|
);
|
|
assert_eq!(
|
|
data_access::read_u32(&buffer, id_pos.offset, "id", Endian::Little)?,
|
|
0x01020304
|
|
);
|
|
assert_eq!(
|
|
data_access::read_string(&buffer, payload_pos.offset, "payload", Endian::Little)?,
|
|
payload_str
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn sequential_reader_round_trip_packed_buffer() -> Result<(), AlkTypeError> {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"endian": "little",
|
|
"fields": [
|
|
{ "name": "id", "kind": "uint8" },
|
|
{ "name": "name", "kind": "string" },
|
|
{ "name": "tail", "kind": "uint8" }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let builder = LayoutBuilder::new(&root, "S")?;
|
|
let payload = "hello";
|
|
let layout = builder.build(&var_sizes(&[("name", payload.len())]))?;
|
|
|
|
let mut buffer = vec![0u8; layout.total_size()];
|
|
data_access::write_u8(&mut buffer, 0, 7, "id")?;
|
|
data_access::write_string(&mut buffer, 1, payload, "name", Endian::Little)?;
|
|
let after = 1 + 4 + payload.len();
|
|
data_access::write_u8(&mut buffer, after, 99, "tail")?;
|
|
|
|
let plan = ReadPlan::compile(&root, "S")?;
|
|
let mut reader = SequentialReader::new(std::sync::Arc::new(plan));
|
|
assert_eq!(reader.endian(), Endian::Little);
|
|
assert_eq!(reader.position(), 0);
|
|
|
|
let (name, value) = reader.read_next(&buffer)?.expect("field 0");
|
|
assert_eq!(name, "id");
|
|
assert_eq!(value, FieldValue::U8(7));
|
|
assert_eq!(reader.position(), 1);
|
|
|
|
let (name, value) = reader.read_next(&buffer)?.expect("field 1");
|
|
assert_eq!(name, "name");
|
|
assert_eq!(value, FieldValue::String("hello"));
|
|
assert_eq!(reader.position(), after);
|
|
|
|
let (name, value) = reader.read_next(&buffer)?.expect("field 2");
|
|
assert_eq!(name, "tail");
|
|
assert_eq!(value, FieldValue::U8(99));
|
|
assert_eq!(reader.position(), after + 1);
|
|
|
|
assert!(reader.read_next(&buffer)?.is_none());
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn sequential_reader_read_field_walks_preceding_fields() -> Result<(), AlkTypeError> {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"endian": "little",
|
|
"fields": [
|
|
{ "name": "a", "kind": "uint8" },
|
|
{ "name": "b", "kind": "uint32" },
|
|
{ "name": "c", "kind": "uint8" }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let mut buffer = vec![0u8; 16];
|
|
data_access::write_u8(&mut buffer, 0, 1, "a")?;
|
|
data_access::write_u32(&mut buffer, 1, 0xDEADBEEF, "b", Endian::Little)?;
|
|
data_access::write_u8(&mut buffer, 5, 9, "c")?;
|
|
|
|
let plan = ReadPlan::compile(&root, "S")?;
|
|
let mut reader = SequentialReader::new(std::sync::Arc::new(plan));
|
|
let value = reader.read_field(&buffer, "c")?;
|
|
assert_eq!(value, FieldValue::U8(9));
|
|
assert_eq!(reader.position(), 6);
|
|
|
|
reader.reset();
|
|
let value = reader.read_field(&buffer, "b")?;
|
|
assert_eq!(value, FieldValue::U32(0xDEADBEEF));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn tunion_byte_offset_discriminator_dispatch() -> Result<(), AlkTypeError> {
|
|
let root = json!({
|
|
"$defs": {
|
|
"Packet": {
|
|
"kind": "union",
|
|
"discriminator": { "kind": "byte", "offset": 0, "type": "uint8" },
|
|
"mapping": {
|
|
"5": { "$ref": "#/$defs/Read" },
|
|
"6": { "$ref": "#/$defs/Write" }
|
|
}
|
|
},
|
|
"Read": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{ "name": "handle", "kind": "uint32" },
|
|
{ "name": "length", "kind": "uint32" }
|
|
]
|
|
},
|
|
"Write": {
|
|
"kind": "struct",
|
|
"fields": [
|
|
{ "name": "handle", "kind": "uint32" },
|
|
{ "name": "length", "kind": "uint32" },
|
|
{ "name": "data", "kind": "uint32" }
|
|
]
|
|
}
|
|
}
|
|
});
|
|
let doc = BastDoc::new(&root, "Packet")?;
|
|
let union_def = match doc.root_def().kind() {
|
|
alktype::bast::BastDefKind::Union(u) => u,
|
|
_ => unreachable!(),
|
|
};
|
|
let mut buffer = vec![0u8; 32];
|
|
buffer[0] = 5;
|
|
data_access::write_u32(&mut buffer, 1, 0x01020304, "Read.handle", Endian::Big)?;
|
|
data_access::write_u32(&mut buffer, 5, 4096, "Read.length", Endian::Big)?;
|
|
|
|
let dispatch = tunion::read_byte_discriminator(&buffer, union_def, Endian::Big)?;
|
|
assert_eq!(dispatch.key, "5");
|
|
assert_eq!(dispatch.variant_offset, 1);
|
|
assert_eq!(dispatch.discriminator_size, 1);
|
|
|
|
let variant = tunion::resolve_variant(union_def, &dispatch.key)?;
|
|
match variant {
|
|
alktype::bast::BastType::Ref(r) => assert_eq!(r.name(), "Read"),
|
|
other => panic!("expected Ref to Read, got {other:?}"),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn tunion_byte_offset_discriminator_size_lookup() -> Result<(), AlkTypeError> {
|
|
fn union_with(disc_type: &str) -> serde_json::Value {
|
|
json!({
|
|
"$defs": {
|
|
"U": {
|
|
"kind": "union",
|
|
"discriminator": { "kind": "byte", "type": disc_type },
|
|
"mapping": { "1": { "$ref": "#/$defs/A" } }
|
|
},
|
|
"A": { "kind": "struct", "fields": [] }
|
|
}
|
|
})
|
|
}
|
|
let u8_root = union_with("uint8");
|
|
let u16_root = union_with("uint16");
|
|
let u32_root = union_with("uint32");
|
|
let u8_doc = BastDoc::new(&u8_root, "U")?;
|
|
let u16_doc = BastDoc::new(&u16_root, "U")?;
|
|
let u32_doc = BastDoc::new(&u32_root, "U")?;
|
|
let u8_union = match u8_doc.root_def().kind() {
|
|
alktype::bast::BastDefKind::Union(u) => u,
|
|
_ => unreachable!(),
|
|
};
|
|
let u16_union = match u16_doc.root_def().kind() {
|
|
alktype::bast::BastDefKind::Union(u) => u,
|
|
_ => unreachable!(),
|
|
};
|
|
let u32_union = match u32_doc.root_def().kind() {
|
|
alktype::bast::BastDefKind::Union(u) => u,
|
|
_ => unreachable!(),
|
|
};
|
|
assert_eq!(tunion::discriminator_size(u8_union)?, 1);
|
|
assert_eq!(tunion::discriminator_size(u16_union)?, 2);
|
|
assert_eq!(tunion::discriminator_size(u32_union)?, 4);
|
|
Ok(())
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// L6 (review #006): the single roundtrip test through a field-disc union.
|
|
// Write with LayoutBuilder → read with SequentialReader → materialize →
|
|
// validate_bytes on the engine — one schema, all three packed-mode
|
|
// consumers, so the H3 wire-convention split can never reappear silently.
|
|
//
|
|
// Fixture shape: the union's `fields` carry the discriminator (`type`,
|
|
// uint8) *and* a second shared field (`seq`, uint32); the variant
|
|
// (`Read`) declares only its own field (`handle`) — it does NOT
|
|
// re-declare the discriminator or any shared field (forbidden since the
|
|
// ADR-011 addendum). The disc field is a uint8, so the mapping keys are
|
|
// stringified integers ("1" = read). The builder lays out
|
|
// shared-then-variant:
|
|
// type@0 (1B), seq@1 (4B), handle@5 (4B) — total 9.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn field_disc_union_roundtrip_build_read_materialize_validate() -> Result<(), AlkTypeError> {
|
|
let root = json!({
|
|
"$defs": {
|
|
"S": {
|
|
"kind": "struct",
|
|
"endian": "little",
|
|
"fields": [
|
|
{ "name": "event", "kind": { "$ref": "#/$defs/Event" } },
|
|
{ "name": "trailer", "kind": "uint8" }
|
|
]
|
|
},
|
|
"Event": {
|
|
"kind": "union",
|
|
"discriminator": { "kind": "field", "name": "type" },
|
|
"fields": [
|
|
{ "name": "type", "kind": "uint8" },
|
|
{ "name": "seq", "kind": "uint32" }
|
|
],
|
|
"mapping": {
|
|
"1": { "$ref": "#/$defs/Read" },
|
|
"2": { "$ref": "#/$defs/Write" }
|
|
}
|
|
},
|
|
"Read": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "handle", "kind": "uint32" } ]
|
|
},
|
|
"Write": {
|
|
"kind": "struct",
|
|
"fields": [ { "name": "handle", "kind": "uint32" } ]
|
|
}
|
|
}
|
|
});
|
|
|
|
// --- Write side: LayoutBuilder (shared-then-variant layout) --------
|
|
let builder = LayoutBuilder::new(&root, "S")?;
|
|
let layout = builder.build(&var_sizes(&[("event.__variant", 0)]))?;
|
|
assert_eq!(layout.total_size(), 9 + 1, "shared(1+4) + variant(4) + trailer(1)");
|
|
|
|
let handle_pos = layout.get("event.handle").expect("event.handle");
|
|
assert_eq!(handle_pos.offset, 5, "variant fields start after shared (type@0, seq@1)");
|
|
let disc_pos = layout.get("event.type").expect("event.type");
|
|
assert_eq!(disc_pos.offset, 0);
|
|
let seq_pos = layout.get("event.seq").expect("event.seq");
|
|
assert_eq!(seq_pos.offset, 1);
|
|
|
|
let mut buffer = vec![0u8; layout.total_size()];
|
|
data_access::write_u8(&mut buffer, 0, 1, "event.type")?; // mapping key "1"
|
|
data_access::write_u32(&mut buffer, 1, 77, "event.seq", Endian::Little)?;
|
|
data_access::write_u32(&mut buffer, 5, 4242, "event.handle", Endian::Little)?;
|
|
data_access::write_u8(&mut buffer, 9, 55, "trailer")?;
|
|
|
|
// --- Engine (packed) + reader + materializer + validator -----------
|
|
let engine = AlkTypeEngine::compile(&root, "S", LayoutMode::Packed, None)?;
|
|
|
|
// validate_bytes (materializer + ValidationPlan) accepts the buffer.
|
|
engine.validate_bytes(&buffer)?;
|
|
|
|
// SequentialReader: the union field reports the disc value and the
|
|
// variant start (after the shared walk).
|
|
let mut reader = engine.sequential_reader().expect("packed mode has reader");
|
|
let (name, value) = reader.read_next(&buffer)?.expect("event");
|
|
assert_eq!(name, "event");
|
|
let (disc, variant_start) = match &value {
|
|
FieldValue::Union { discriminator, variant_start } => (discriminator.clone(), *variant_start),
|
|
other => panic!("expected Union, got {other:?}"),
|
|
};
|
|
assert_eq!(disc, "1", "uint8 disc value 1 stringifies to the mapping key");
|
|
assert_eq!(variant_start, 5, "variant starts after the shared walk");
|
|
let (name, value) = reader.read_next(&buffer)?.expect("trailer");
|
|
assert_eq!(name, "trailer");
|
|
assert_eq!(value, FieldValue::U8(55));
|
|
|
|
// materialize_packed through the engine's plan: shared fields land
|
|
// in the object, the variant's handle flattens in.
|
|
let plan = engine_sequential_plan(&root, "S")?;
|
|
let value = alktype::materialize::materialize_packed(&plan, &buffer)?;
|
|
assert_eq!(value["event"]["type"], json!(1));
|
|
assert_eq!(value["event"]["seq"], json!(77));
|
|
assert_eq!(value["event"]["handle"], json!(4242));
|
|
assert_eq!(value["event"]["__discriminator"], json!("1"));
|
|
assert_eq!(value["trailer"], json!(55));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn engine_sequential_plan(root: &serde_json::Value, name: &str) -> Result<std::sync::Arc<ReadPlan>, AlkTypeError> {
|
|
Ok(std::sync::Arc::new(ReadPlan::compile(root, name)?))
|
|
} |