diff --git a/crates/alknet-typedef/tests/engine_integration.rs b/crates/alknet-typedef/tests/engine_integration.rs new file mode 100644 index 0000000..94db936 --- /dev/null +++ b/crates/alknet-typedef/tests/engine_integration.rs @@ -0,0 +1,371 @@ +//! Integration tests for the `TypedefEngine` public API. +//! +//! Exercises the engine across both layout modes, the convenience +//! accessors, validation convenience methods, and the aligned-mode +//! `read_field` / `write_field` round-trip for the fixed-size primitive +//! kinds and length-prefixed `String` / `Bytes`. + +use alknet_typedef::*; +use serde_json::json; + +fn mixed_fixed_struct_schema() -> serde_json::Value { + json!({ + "TypeDef:Struct": true, + "endian": "little", + "properties": { + "flag": { "TypeDef:Uint8": true }, + "id": { "TypeDef:Uint32": true }, + "score": { "TypeDef:Float32": true }, + "tag": { "TypeDef:String": true } + } + }) +} + +#[test] +fn compile_aligned_builds_engine_with_offset_map() -> Result<(), TypedefError> { + let mut schema = mixed_fixed_struct_schema(); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?; + assert_eq!(engine.mode(), LayoutMode::Aligned); + assert!(engine.offset_map().is_some()); + assert!(engine.layout_builder().is_none()); + assert!(engine.sequential_reader().is_none()); + Ok(()) +} + +#[test] +fn compile_packed_builds_engine_with_builder_and_reader() -> Result<(), TypedefError> { + let mut schema = mixed_fixed_struct_schema(); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed)?; + assert_eq!(engine.mode(), LayoutMode::Packed); + assert!(engine.offset_map().is_none()); + assert!(engine.layout_builder().is_some()); + assert!(engine.sequential_reader().is_some()); + Ok(()) +} + +#[test] +fn compile_normalizes_bare_name_refs() -> Result<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "properties": { + "child": { "$ref": "Child" } + }, + "$defs": { + "Child": { + "TypeDef:Struct": true, + "properties": { "x": { "TypeDef:Uint8": true } } + } + } + }); + let _engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed)?; + assert_eq!( + schema["properties"]["child"]["$ref"], + json!("#/$defs/Child") + ); + Ok(()) +} + +#[test] +fn compile_leaves_full_pointer_refs_unchanged() -> Result<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "properties": { + "child": { "$ref": "#/$defs/Child" } + }, + "$defs": { + "Child": { + "TypeDef:Struct": true, + "properties": { "x": { "TypeDef:Uint8": true } } + } + } + }); + let _engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed)?; + assert_eq!( + schema["properties"]["child"]["$ref"], + json!("#/$defs/Child") + ); + Ok(()) +} + +#[test] +fn compile_returns_schema_error_when_no_typedef_kind() { + let mut schema = json!({ "type": "object", "properties": {} }); + let err = TypedefEngine::compile(&mut schema, LayoutMode::Aligned).unwrap_err(); + assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}"); +} + +#[test] +fn endian_parsed_from_schema_big() -> Result<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "endian": "big", + "properties": { "id": { "TypeDef:Uint32": true } } + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed)?; + assert_eq!(engine.endian(), Endian::Big); + Ok(()) +} + +#[test] +fn endian_defaults_to_little() -> Result<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "properties": { "id": { "TypeDef:Uint32": true } } + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed)?; + assert_eq!(engine.endian(), Endian::Little); + Ok(()) +} + +#[test] +fn validate_json_accepts_valid_instance() -> Result<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "type": "object", + "properties": { + "id": { "TypeDef:Uint32": true, "type": "integer" } + }, + "required": ["id"] + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?; + assert!(engine.validate_json(&json!({"id": 42})).is_ok()); + Ok(()) +} + +#[test] +fn validate_json_rejects_invalid_instance() -> Result<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "type": "object", + "properties": { + "id": { "TypeDef:Uint32": true, "type": "integer" } + }, + "required": ["id"] + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?; + let err = engine.validate_json(&json!({"id": -1})).unwrap_err(); + assert!(matches!(err, TypedefError::Validation(_)), "got {err:?}"); + Ok(()) +} + +#[test] +fn is_valid_json_returns_bool() -> Result<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "type": "object", + "properties": { + "id": { "TypeDef:Uint32": true, "type": "integer" } + }, + "required": ["id"] + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?; + assert!(engine.is_valid_json(&json!({"id": 42}))); + assert!(!engine.is_valid_json(&json!({"id": -1}))); + Ok(()) +} + +#[test] +fn read_write_aligned_round_trips_all_fixed_size_kinds() -> Result<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "endian": "little", + "properties": { + "i8": { "TypeDef:Int8": true }, + "u8": { "TypeDef:Uint8": true }, + "i16": { "TypeDef:Int16": true }, + "u16": { "TypeDef:Uint16": true }, + "i32": { "TypeDef:Int32": true }, + "u32": { "TypeDef:Uint32": true }, + "f32": { "TypeDef:Float32": true }, + "f64": { "TypeDef:Float64": true }, + "b": { "TypeDef:Boolean": true }, + "e": { "TypeDef:Enum": true } + } + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?; + 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, "i8", &FieldValue::I8(-127))?; + engine.write_field(&mut buffer, "u8", &FieldValue::U8(0xAB))?; + engine.write_field(&mut buffer, "i16", &FieldValue::I16(-32000))?; + engine.write_field(&mut buffer, "u16", &FieldValue::U16(0xBEEF))?; + engine.write_field(&mut buffer, "i32", &FieldValue::I32(-2_000_000_007))?; + engine.write_field(&mut buffer, "u32", &FieldValue::U32(0xDEADBEEF))?; + engine.write_field(&mut buffer, "f32", &FieldValue::F32(1.5))?; + engine.write_field(&mut buffer, "f64", &FieldValue::F64(2.5))?; + engine.write_field(&mut buffer, "b", &FieldValue::Bool(true))?; + engine.write_field(&mut buffer, "e", &FieldValue::Enum(7))?; + + assert_eq!(engine.read_field(&buffer, "i8")?, FieldValue::I8(-127)); + assert_eq!(engine.read_field(&buffer, "u8")?, FieldValue::U8(0xAB)); + assert_eq!(engine.read_field(&buffer, "i16")?, FieldValue::I16(-32000)); + assert_eq!(engine.read_field(&buffer, "u16")?, FieldValue::U16(0xBEEF)); + assert_eq!(engine.read_field(&buffer, "i32")?, FieldValue::I32(-2_000_000_007)); + assert_eq!(engine.read_field(&buffer, "u32")?, FieldValue::U32(0xDEADBEEF)); + assert_eq!(engine.read_field(&buffer, "f32")?, FieldValue::F32(1.5)); + assert_eq!(engine.read_field(&buffer, "f64")?, FieldValue::F64(2.5)); + assert_eq!(engine.read_field(&buffer, "b")?, FieldValue::Bool(true)); + assert_eq!(engine.read_field(&buffer, "e")?, FieldValue::Enum(7)); + Ok(()) +} + +#[test] +fn read_write_aligned_round_trips_string() -> Result<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "properties": { + "name": { "TypeDef:String": true } + } + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?; + 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 world"))?; + assert_eq!( + engine.read_field(&buffer, "name")?, + FieldValue::String("hello world") + ); + Ok(()) +} + +#[test] +fn read_write_aligned_round_trips_bytes() -> Result<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "properties": { + "blob": { "TypeDef:Bytes": true } + } + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?; + let offset_map = engine.offset_map().expect("aligned mode has offset_map"); + let payload = b"the quick brown fox".to_vec(); + let mut buffer = vec![0u8; offset_map.total_size() + payload.len()]; + engine.write_field(&mut buffer, "blob", &FieldValue::Bytes(&payload))?; + assert_eq!( + engine.read_field(&buffer, "blob")?, + FieldValue::Bytes(&payload) + ); + Ok(()) +} + +#[test] +fn read_field_returns_access_error_in_packed_mode() -> Result<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "properties": { "id": { "TypeDef:Uint32": true } } + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed)?; + let buffer = [0u8; 4]; + let err = engine.read_field(&buffer, "id").unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); + Ok(()) +} + +#[test] +fn write_field_returns_access_error_in_packed_mode() -> Result<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "properties": { "id": { "TypeDef:Uint32": true } } + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Packed)?; + let mut buffer = [0u8; 4]; + let err = engine + .write_field(&mut buffer, "id", &FieldValue::U32(1)) + .unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); + Ok(()) +} + +#[test] +fn read_field_returns_offset_error_for_missing_path() -> Result<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "properties": { "id": { "TypeDef:Uint32": true } } + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?; + let buffer = [0u8; 8]; + let err = engine.read_field(&buffer, "missing").unwrap_err(); + assert!(matches!(err, TypedefError::Offset { .. }), "got {err:?}"); + Ok(()) +} + +#[test] +fn write_field_returns_offset_error_for_missing_path() -> Result<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "properties": { "id": { "TypeDef:Uint32": true } } + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?; + let mut buffer = [0u8; 8]; + let err = engine + .write_field(&mut buffer, "missing", &FieldValue::U32(1)) + .unwrap_err(); + assert!(matches!(err, TypedefError::Offset { .. }), "got {err:?}"); + Ok(()) +} + +#[test] +fn read_field_returns_access_error_for_composite_types() -> Result<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "properties": { + "vals": { + "TypeDef:Array": true, + "items": { "TypeDef:Uint32": true } + } + } + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?; + let buffer = [0u8; 8]; + let err = engine.read_field(&buffer, "vals").unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); + Ok(()) +} + +#[test] +fn write_field_returns_access_error_for_composite_value() -> Result<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "properties": { "id": { "TypeDef:Uint32": true } } + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?; + let mut buffer = [0u8; 8]; + let err = engine + .write_field(&mut buffer, "id", &FieldValue::Struct { start: 0, end: 4 }) + .unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); + Ok(()) +} + +#[test] +fn read_field_aligned_reads_nested_struct_byte_range() -> Result<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "properties": { + "header": { + "TypeDef:Struct": true, + "properties": { + "version": { "TypeDef:Uint8": true }, + "magic": { "TypeDef:Uint32": true } + } + } + } + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?; + let offset_map = engine.offset_map().expect("aligned mode"); + let mut buffer = vec![0u8; offset_map.total_size()]; + + engine.write_field(&mut buffer, "header.version", &FieldValue::U8(3))?; + engine.write_field(&mut buffer, "header.magic", &FieldValue::U32(0xCAFEBABE))?; + + assert_eq!( + engine.read_field(&buffer, "header.version")?, + FieldValue::U8(3) + ); + assert_eq!( + engine.read_field(&buffer, "header.magic")?, + FieldValue::U32(0xCAFEBABE) + ); + Ok(()) +} \ No newline at end of file diff --git a/crates/alknet-typedef/tests/error_paths.rs b/crates/alknet-typedef/tests/error_paths.rs new file mode 100644 index 0000000..2e92de3 --- /dev/null +++ b/crates/alknet-typedef/tests/error_paths.rs @@ -0,0 +1,407 @@ +//! Error path integration tests for `alknet-typedef`. +//! +//! Exercises the `TypedefError` variants across the crate: +//! `Access` (buffer too short, invalid UTF-8, invalid boolean byte, +//! unknown discriminator value), `Schema` (missing TypeDef kind, +//! malformed discriminator annotation), and `Offset` (missing +//! variable-length field size in `LayoutBuilder::build`). + +use alknet_typedef::data_access; +use alknet_typedef::*; +use alknet_typedef::tunion; +use serde_json::json; +use std::collections::HashMap; + +#[test] +fn read_u32_buffer_too_short_returns_access_error() { + let buffer = [0u8; 2]; + let err = data_access::read_u32(&buffer, 0, "header.id", Endian::Little).unwrap_err(); + match err { + TypedefError::Access { field_path, reason } => { + assert_eq!(field_path, "header.id"); + assert!(reason.contains("bounds"), "reason: {reason}"); + } + other => panic!("expected Access, got {other:?}"), + } +} + +#[test] +fn read_u16_buffer_too_short_returns_access_error() { + let buffer = [0u8; 1]; + let err = data_access::read_u16(&buffer, 0, "tag", Endian::Little).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); +} + +#[test] +fn read_u64_buffer_too_short_returns_access_error() { + let buffer = [0u8; 4]; + let err = data_access::read_u64(&buffer, 0, "offset", Endian::Big).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); +} + +#[test] +fn read_f32_buffer_too_short_returns_access_error() { + let buffer = [0u8; 2]; + let err = data_access::read_f32(&buffer, 0, "score", Endian::Little).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); +} + +#[test] +fn read_f64_buffer_too_short_returns_access_error() { + let buffer = [0u8; 4]; + let err = data_access::read_f64(&buffer, 0, "score", Endian::Little).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); +} + +#[test] +fn read_i32_buffer_too_short_returns_access_error() { + let buffer = [0u8; 2]; + let err = data_access::read_i32(&buffer, 0, "id", Endian::Little).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); +} + +#[test] +fn read_bool_buffer_too_short_returns_access_error() { + let buffer: [u8; 0] = []; + let err = data_access::read_bool(&buffer, 0, "flag").unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); +} + +#[test] +fn read_string_buffer_too_short_on_prefix_returns_access_error() { + let buffer = [0u8; 2]; + let err = data_access::read_string(&buffer, 0, "name", Endian::Little).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); +} + +#[test] +fn read_string_buffer_too_short_on_data_returns_access_error() { + let mut buffer = vec![0u8; 6]; + buffer[0..4].copy_from_slice(&100u32.to_le_bytes()); + let err = data_access::read_string(&buffer, 0, "name", Endian::Little).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); +} + +#[test] +fn read_bytes_buffer_too_short_on_data_returns_access_error() { + let mut buffer = vec![0u8; 5]; + buffer[0..4].copy_from_slice(&100u32.to_le_bytes()); + let err = data_access::read_bytes(&buffer, 0, "blob", Endian::Little).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); +} + +#[test] +fn read_string_invalid_utf8_returns_access_error() { + let mut buffer = vec![0u8; 16]; + let invalid = [0xFFu8, 0xFE, 0xFD]; + let _ = data_access::write_bytes(&mut buffer, 0, &invalid, "name", Endian::Little); + let err = data_access::read_string(&buffer, 0, "name", Endian::Little).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); +} + +#[test] +fn read_bool_invalid_byte_returns_access_error() { + let buffer = [0x02u8]; + let err = data_access::read_bool(&buffer, 0, "flag").unwrap_err(); + match err { + TypedefError::Access { field_path, reason } => { + assert_eq!(field_path, "flag"); + assert!(reason.contains("0x02"), "reason: {reason}"); + } + other => panic!("expected Access, got {other:?}"), + } +} + +#[test] +fn read_bool_zero_is_false() -> Result<(), TypedefError> { + let buffer = [0x00u8]; + assert!(!data_access::read_bool(&buffer, 0, "flag")?); + Ok(()) +} + +#[test] +fn read_bool_one_is_true() -> Result<(), TypedefError> { + let buffer = [0x01u8]; + assert!(data_access::read_bool(&buffer, 0, "flag")?); + Ok(()) +} + +#[test] +fn read_bool_three_is_access_error() { + let buffer = [0x03u8]; + let err = data_access::read_bool(&buffer, 0, "flag").unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); +} + +#[test] +fn write_u32_buffer_too_short_returns_access_error() { + let mut buffer = [0u8; 2]; + let err = data_access::write_u32(&mut buffer, 0, 1, "id", Endian::Little).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); +} + +#[test] +fn write_string_buffer_too_short_returns_access_error() { + let mut buffer = vec![0u8; 4]; + let err = data_access::write_string(&mut buffer, 0, "hello", "name", Endian::Little).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); +} + +#[test] +fn compile_missing_typedef_kind_returns_schema_error() { + let mut schema = json!({ "type": "object", "properties": {} }); + let err = TypedefEngine::compile(&mut schema, LayoutMode::Aligned).unwrap_err(); + assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}"); +} + +#[test] +fn offset_map_compute_missing_typedef_kind_returns_schema_error() { + let schema = json!({ "type": "object", "properties": {} }); + let err = OffsetMap::compute(&schema).unwrap_err(); + assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}"); +} + +#[test] +fn offset_map_compute_non_struct_top_level_returns_schema_error() { + let schema = json!({ "TypeDef:Uint32": true }); + let err = OffsetMap::compute(&schema).unwrap_err(); + assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}"); +} + +#[test] +fn layout_builder_new_missing_typedef_kind_returns_schema_error() { + let schema = json!({ "type": "object", "properties": {} }); + let err = LayoutBuilder::new(&schema).unwrap_err(); + assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}"); +} + +#[test] +fn layout_builder_new_non_struct_top_level_returns_schema_error() { + let schema = json!({ "TypeDef:Uint32": true }); + let err = LayoutBuilder::new(&schema).unwrap_err(); + assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}"); +} + +#[test] +fn parse_discriminator_missing_returns_schema_error() { + let schema = json!({"TypeDef:Union": true}); + let err = parse_discriminator(&schema).unwrap_err(); + assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}"); +} + +#[test] +fn parse_discriminator_field_missing_name_returns_schema_error() { + let schema = json!({ + "TypeDef:Union": true, + "discriminator": {"kind": "field"} + }); + let err = parse_discriminator(&schema).unwrap_err(); + assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}"); +} + +#[test] +fn parse_discriminator_unknown_kind_returns_schema_error() { + let schema = json!({ + "TypeDef:Union": true, + "discriminator": {"kind": "magic"} + }); + let err = parse_discriminator(&schema).unwrap_err(); + assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}"); +} + +#[test] +fn parse_discriminator_byte_invalid_type_returns_schema_error() { + let schema = json!({ + "TypeDef:Union": true, + "discriminator": {"kind": "byte", "type": "TypeDef:Float32"} + }); + let err = parse_discriminator(&schema).unwrap_err(); + assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}"); +} + +#[test] +fn read_byte_discriminator_unknown_value_returns_access_error() -> Result<(), TypedefError> { + let union_schema = json!({ + "TypeDef:Union": true, + "discriminator": {"kind": "byte", "type": "TypeDef:Uint8"}, + "mapping": {"5": {"TypeDef:Struct": true, "properties": {"x": {"TypeDef:Uint8": true}}}} + }); + let buffer = [99u8, 0x00, 0x00]; + let err = tunion::read_byte_discriminator(&buffer, &union_schema, Endian::Little).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); + Ok(()) +} + +#[test] +fn read_byte_discriminator_buffer_too_short_returns_access_error() -> Result<(), TypedefError> { + let union_schema = json!({ + "TypeDef:Union": true, + "discriminator": {"kind": "byte", "offset": 4, "type": "TypeDef:Uint32"}, + "mapping": {"5": {"TypeDef:Struct": true, "properties": {"x": {"TypeDef:Uint8": true}}}} + }); + let buffer = [0u8; 2]; + let err = tunion::read_byte_discriminator(&buffer, &union_schema, Endian::Little).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); + Ok(()) +} + +#[test] +fn read_field_discriminator_unknown_value_returns_access_error() -> Result<(), TypedefError> { + let union_schema = json!({ + "TypeDef:Union": true, + "discriminator": {"kind": "field", "name": "type"}, + "properties": {"type": {"TypeDef:Uint8": true}}, + "mapping": {"0": {"TypeDef:Struct": true, "properties": {"x": {"TypeDef:Uint8": true}}}} + }); + let mut buffer = vec![0u8; 8]; + buffer[0] = 99; + let err = tunion::read_field_discriminator(&buffer, &union_schema, 0, Endian::Little).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); + Ok(()) +} + +#[test] +fn layout_builder_missing_var_size_returns_offset_error() { + let schema = json!({ + "TypeDef:Struct": true, + "properties": { + "name": { "TypeDef:String": true } + } + }); + let builder = LayoutBuilder::new(&schema).expect("builder"); + let empty: HashMap = HashMap::new(); + let err = builder.build(&empty).unwrap_err(); + match err { + TypedefError::Offset { field_path, reason } => { + assert_eq!(field_path, "name"); + assert!(reason.contains("missing variable-length field size"), "reason: {reason}"); + } + other => panic!("expected Offset, got {other:?}"), + } +} + +#[test] +fn layout_builder_missing_array_data_size_returns_offset_error() { + let schema = json!({ + "TypeDef:Struct": true, + "properties": { + "vals": { + "TypeDef:Array": true, + "items": { "TypeDef:Uint32": true } + } + } + }); + let builder = LayoutBuilder::new(&schema).expect("builder"); + let empty: HashMap = HashMap::new(); + let err = builder.build(&empty).unwrap_err(); + assert!(matches!(err, TypedefError::Offset { .. }), "got {err:?}"); +} + +#[test] +fn layout_builder_missing_discriminator_value_returns_offset_error() { + let schema = json!({ + "TypeDef:Struct": true, + "properties": { + "payload": { + "TypeDef:Union": true, + "discriminator": {"kind": "byte", "type": "TypeDef:Uint8"}, + "mapping": {"5": {"$ref": "#/$defs/Read"}} + } + }, + "$defs": { + "Read": {"TypeDef:Struct": true, "properties": {"x": {"TypeDef:Uint8": true}}} + } + }); + let builder = LayoutBuilder::new(&schema).expect("builder"); + let empty: HashMap = HashMap::new(); + let err = builder.build(&empty).unwrap_err(); + assert!(matches!(err, TypedefError::Offset { .. }), "got {err:?}"); +} + +#[test] +fn layout_builder_unknown_discriminator_value_returns_offset_error() { + let schema = json!({ + "TypeDef:Struct": true, + "properties": { + "payload": { + "TypeDef:Union": true, + "discriminator": {"kind": "byte", "type": "TypeDef:Uint8"}, + "mapping": {"5": {"$ref": "#/$defs/Read"}} + } + }, + "$defs": { + "Read": {"TypeDef:Struct": true, "properties": {"x": {"TypeDef:Uint8": true}}} + } + }); + let builder = LayoutBuilder::new(&schema).expect("builder"); + let mut vs = HashMap::new(); + vs.insert("payload.__discriminator".to_string(), 99); + let err = builder.build(&vs).unwrap_err(); + match err { + TypedefError::Offset { reason, .. } => { + assert!(reason.contains("99"), "reason: {reason}"); + } + other => panic!("expected Offset, got {other:?}"), + } +} + +#[test] +fn sequential_reader_buffer_too_short_returns_access_error() { + let schema = json!({ + "TypeDef:Struct": true, + "properties": { + "id": { "TypeDef:Uint32": true } + } + }); + let buffer = [0u8; 2]; + let mut reader = SequentialReader::new(&schema).unwrap(); + let err = reader.read_next(&buffer).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); +} + +#[test] +fn sequential_reader_unknown_field_returns_schema_error() { + let schema = json!({ + "TypeDef:Struct": true, + "properties": { "a": { "TypeDef:Uint8": true } } + }); + let buffer = [0u8; 4]; + let mut reader = SequentialReader::new(&schema).unwrap(); + let err = reader.read_field(&buffer, "missing").unwrap_err(); + assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}"); +} + +#[test] +fn sequential_reader_new_non_struct_returns_schema_error() { + let schema = json!({ "TypeDef:Uint32": true }); + let err = SequentialReader::new(&schema).unwrap_err(); + assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}"); +} + +#[test] +fn read_string_indirect_data_region_too_short_returns_access_error() { + let mut index = [0u8; 8]; + let _ = data_access::write_u32(&mut index, 0, 100, "idx.off", Endian::Little); + let _ = data_access::write_u32(&mut index, 4, 10, "idx.len", Endian::Little); + let data_region = b"too short"; + let err = data_access::read_bytes_indirect(&index, 0, data_region, "blob", Endian::Little).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); +} + +#[test] +fn read_bytes_indirect_index_too_short_returns_access_error() { + let buffer = [0u8; 4]; + let data_region = b"anything"; + let err = data_access::read_bytes_indirect(&buffer, 0, data_region, "blob", Endian::Little).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); +} + +#[test] +fn read_string_indirect_invalid_utf8_returns_access_error() { + let data_region: &[u8] = &[0xFF, 0xFE, 0xFD]; + let mut index = [0u8; 8]; + let _ = data_access::write_u32(&mut index, 0, 0, "idx.off", Endian::Little); + let _ = data_access::write_u32(&mut index, 4, 3, "idx.len", Endian::Little); + let err = data_access::read_string_indirect(&index, 0, data_region, "name", Endian::Little).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); +} \ No newline at end of file diff --git a/crates/alknet-typedef/tests/poc_roundtrip.rs b/crates/alknet-typedef/tests/poc_roundtrip.rs new file mode 100644 index 0000000..ec09448 --- /dev/null +++ b/crates/alknet-typedef/tests/poc_roundtrip.rs @@ -0,0 +1,489 @@ +//! POC round-trip tests adapted from `/workspace/alknet-typedef-poc/`. +//! +//! 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. + +use alknet_typedef::*; +use alknet_typedef::data_access; +use alknet_typedef::tunion; +use serde_json::json; +use std::collections::HashMap; + +fn var_sizes(pairs: &[(&str, usize)]) -> HashMap { + pairs + .iter() + .map(|(k, v)| (k.to_string(), *v)) + .collect() +} + +#[test] +fn fixed_size_round_trip_via_offset_map() -> Result<(), TypedefError> { + let schema = json!({ + "TypeDef:Struct": true, + "properties": { + "id": { "TypeDef:Uint32": true }, + "score": { "TypeDef:Float32": true }, + "flag": { "TypeDef:Uint8": true }, + "count": { "TypeDef:Uint16": true } + } + }); + let offset_map = OffsetMap::compute(&schema)?; + 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<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "endian": "little", + "properties": { + "id": { "TypeDef:Uint32": true }, + "score": { "TypeDef:Float32": true }, + "flag": { "TypeDef:Uint8": true } + } + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?; + 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<(), TypedefError> { + 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<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "properties": { + "name": { "TypeDef:String": true } + } + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?; + 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<(), TypedefError> { + 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<(), TypedefError> { + let schema = json!({ + "TypeDef:Struct": true, + "properties": { + "header": { + "TypeDef:Struct": true, + "properties": { + "version": { "TypeDef:Uint32": true }, + "magic": { "TypeDef:Uint32": true } + } + }, + "payload": { "TypeDef:Bytes": true } + } + }); + let offset_map = OffsetMap::compute(&schema)?; + + 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<(), TypedefError> { + let mut schema = json!({ + "TypeDef:Struct": true, + "properties": { + "header": { + "TypeDef:Struct": true, + "properties": { + "version": { "TypeDef:Uint8": true }, + "flags": { "TypeDef:Uint8": true } + } + }, + "payload_len": { "TypeDef:Uint32": true } + } + }); + let engine = TypedefEngine::compile(&mut schema, LayoutMode::Aligned)?; + 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<(), TypedefError> { + let schema = json!({ + "TypeDef:Struct": true, + "endian": "big", + "properties": { + "id": { "TypeDef:Uint32": true }, + "offset": { "TypeDef:Float64": true } + } + }); + let offset_map = OffsetMap::compute(&schema)?; + let endian = Endian::from_schema(&schema); + assert_eq!(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<(), TypedefError> { + let schema = json!({ + "TypeDef:Struct": true, + "properties": { + "flag": { "TypeDef:Uint8": true }, + "id": { "TypeDef:Uint32": true } + } + }); + let offset_map = OffsetMap::compute(&schema)?; + + 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<(), TypedefError> { + let schema = json!({ + "TypeDef:Struct": true, + "endian": "little", + "properties": { + "flag": { "TypeDef:Uint8": true }, + "id": { "TypeDef:Uint32": true }, + "payload": { "TypeDef:String": true } + } + }); + let builder = LayoutBuilder::new(&schema)?; + 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<(), TypedefError> { + let schema = json!({ + "TypeDef:Struct": true, + "endian": "little", + "properties": { + "id": { "TypeDef:Uint8": true }, + "name": { "TypeDef:String": true }, + "tail": { "TypeDef:Uint8": true } + } + }); + let builder = LayoutBuilder::new(&schema)?; + 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 mut reader = SequentialReader::new(&schema)?; + 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<(), TypedefError> { + let schema = json!({ + "TypeDef:Struct": true, + "endian": "little", + "properties": { + "a": { "TypeDef:Uint8": true }, + "b": { "TypeDef:Uint32": true }, + "c": { "TypeDef:Uint8": true } + } + }); + 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 mut reader = SequentialReader::new(&schema)?; + 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<(), TypedefError> { + let union_schema = json!({ + "TypeDef:Union": true, + "discriminator": { + "kind": "byte", + "offset": 0, + "type": "TypeDef:Uint8" + }, + "mapping": { + "5": { "$ref": "#/$defs/Read" }, + "6": { "$ref": "#/$defs/Write" } + }, + "$defs": { + "Read": { + "TypeDef:Struct": true, + "properties": { + "handle": { "TypeDef:Uint32": true }, + "length": { "TypeDef:Uint32": true } + } + }, + "Write": { + "TypeDef:Struct": true, + "properties": { + "handle": { "TypeDef:Uint32": true }, + "length": { "TypeDef:Uint32": true }, + "data": { "TypeDef:Uint32": true } + } + } + } + }); + 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_schema, 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_schema, &dispatch.key)?; + assert_eq!( + variant.get("TypeDef:Struct").and_then(serde_json::Value::as_bool), + Some(true) + ); + Ok(()) +} + +#[test] +fn tunion_byte_offset_discriminator_size_lookup() -> Result<(), TypedefError> { + let u8_schema = json!({ + "TypeDef:Union": true, + "discriminator": {"kind": "byte", "type": "TypeDef:Uint8"}, + "mapping": {} + }); + let u16_schema = json!({ + "TypeDef:Union": true, + "discriminator": {"kind": "byte", "type": "TypeDef:Uint16"}, + "mapping": {} + }); + let u32_schema = json!({ + "TypeDef:Union": true, + "discriminator": {"kind": "byte", "type": "TypeDef:Uint32"}, + "mapping": {} + }); + assert_eq!(tunion::discriminator_size(&u8_schema)?, 1); + assert_eq!(tunion::discriminator_size(&u16_schema)?, 2); + assert_eq!(tunion::discriminator_size(&u32_schema)?, 4); + Ok(()) +} \ No newline at end of file diff --git a/crates/alknet-typedef/tests/tunion_dispatch.rs b/crates/alknet-typedef/tests/tunion_dispatch.rs new file mode 100644 index 0000000..765eab3 --- /dev/null +++ b/crates/alknet-typedef/tests/tunion_dispatch.rs @@ -0,0 +1,318 @@ +//! Integration tests for TUnion discriminator dispatch. +//! +//! Exercises both discriminator kinds end-to-end: byte-offset (SFTP +//! pattern) and field-name (typedef.ts pattern). Verifies that +//! `read_byte_discriminator` / `read_field_discriminator` produce the +//! correct mapping key and variant offset, that `resolve_variant` +//! follows `$ref` pointers, and that `discriminator_size` reports the +//! right fixed sizes. + +use alknet_typedef::data_access; +use alknet_typedef::tunion; +use alknet_typedef::{Endian, TypedefError}; +use serde_json::json; + +fn sftp_like_byte_union() -> serde_json::Value { + json!({ + "TypeDef:Union": true, + "discriminator": { + "kind": "byte", + "offset": 0, + "type": "TypeDef:Uint8" + }, + "mapping": { + "5": { "$ref": "#/$defs/Read" }, + "6": { "$ref": "#/$defs/Write" } + }, + "$defs": { + "Read": { + "TypeDef:Struct": true, + "properties": { + "handle": { "TypeDef:Uint32": true }, + "length": { "TypeDef:Uint32": true } + } + }, + "Write": { + "TypeDef:Struct": true, + "properties": { + "handle": { "TypeDef:Uint32": true }, + "length": { "TypeDef:Uint32": true }, + "data": { "TypeDef:Uint32": true } + } + } + } + }) +} + +#[test] +fn read_byte_discriminator_uint8_dispatches_to_read() -> Result<(), TypedefError> { + let union_schema = sftp_like_byte_union(); + let mut buffer = vec![0u8; 16]; + buffer[0] = 5; + data_access::write_u32(&mut buffer, 1, 0x01020304, "Read.handle", Endian::Big)?; + + let dispatch = tunion::read_byte_discriminator(&buffer, &union_schema, 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_schema, &dispatch.key)?; + assert_eq!( + variant.get("TypeDef:Struct").and_then(serde_json::Value::as_bool), + Some(true) + ); + Ok(()) +} + +#[test] +fn read_byte_discriminator_uint8_dispatches_to_write() -> Result<(), TypedefError> { + let union_schema = sftp_like_byte_union(); + let mut buffer = vec![0u8; 16]; + buffer[0] = 6; + data_access::write_u32(&mut buffer, 1, 0xDEADBEEF, "Write.handle", Endian::Big)?; + + let dispatch = tunion::read_byte_discriminator(&buffer, &union_schema, Endian::Big)?; + assert_eq!(dispatch.key, "6"); + assert_eq!(dispatch.variant_offset, 1); + assert_eq!(dispatch.discriminator_size, 1); + + let variant = tunion::resolve_variant(&union_schema, &dispatch.key)?; + let props = variant + .get("properties") + .and_then(serde_json::Value::as_object) + .expect("variant has properties"); + assert!(props.contains_key("data")); + Ok(()) +} + +#[test] +fn read_byte_discriminator_uint16_little_endian() -> Result<(), TypedefError> { + let schema = json!({ + "TypeDef:Union": true, + "discriminator": { + "kind": "byte", + "offset": 2, + "type": "TypeDef:Uint16" + }, + "mapping": { + "5": {"TypeDef:Struct": true, "properties": {"id": {"TypeDef:Uint32": true}}} + } + }); + let mut buffer = vec![0u8; 16]; + buffer[2..4].copy_from_slice(&5u16.to_le_bytes()); + let dispatch = tunion::read_byte_discriminator(&buffer, &schema, Endian::Little)?; + assert_eq!(dispatch.key, "5"); + assert_eq!(dispatch.variant_offset, 4); + assert_eq!(dispatch.discriminator_size, 2); + Ok(()) +} + +#[test] +fn read_byte_discriminator_uint32_big_endian() -> Result<(), TypedefError> { + let schema = json!({ + "TypeDef:Union": true, + "discriminator": { + "kind": "byte", + "offset": 0, + "type": "TypeDef:Uint32" + }, + "mapping": { + "101": {"TypeDef:Struct": true, "properties": {"id": {"TypeDef:Uint32": true}}} + } + }); + let mut buffer = vec![0u8; 16]; + buffer[0..4].copy_from_slice(&101u32.to_be_bytes()); + let dispatch = tunion::read_byte_discriminator(&buffer, &schema, Endian::Big)?; + assert_eq!(dispatch.key, "101"); + assert_eq!(dispatch.variant_offset, 4); + assert_eq!(dispatch.discriminator_size, 4); + Ok(()) +} + +#[test] +fn read_byte_discriminator_unknown_value_returns_access_error() -> Result<(), TypedefError> { + let union_schema = sftp_like_byte_union(); + let buffer = [99u8, 0x00, 0x00, 0x00]; + let err = tunion::read_byte_discriminator(&buffer, &union_schema, Endian::Big).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); + Ok(()) +} + +#[test] +fn read_field_discriminator_string_dispatches_to_read() -> Result<(), TypedefError> { + let union_schema = json!({ + "TypeDef:Union": true, + "discriminator": {"kind": "field", "name": "type"}, + "properties": { + "type": { "TypeDef:String": true } + }, + "mapping": { + "read": {"$ref": "#/$defs/Read"}, + "write": {"$ref": "#/$defs/Write"} + }, + "$defs": { + "Read": { + "TypeDef:Struct": true, + "properties": { + "handle": { "TypeDef:Uint32": true }, + "length": { "TypeDef:Uint32": true } + } + }, + "Write": { + "TypeDef:Struct": true, + "properties": { + "handle": { "TypeDef:Uint32": true }, + "data": { "TypeDef:Bytes": true } + } + } + } + }); + let value = "read"; + let mut buffer = vec![0u8; 32]; + data_access::write_string(&mut buffer, 0, value, "type", Endian::Little)?; + let dispatch = tunion::read_field_discriminator(&buffer, &union_schema, 0, Endian::Little)?; + assert_eq!(dispatch.key, "read"); + assert_eq!(dispatch.variant_offset, 4 + value.len()); + assert_eq!(dispatch.discriminator_size, 4 + value.len()); + + let variant = tunion::resolve_variant(&union_schema, &dispatch.key)?; + assert_eq!( + variant.get("TypeDef:Struct").and_then(serde_json::Value::as_bool), + Some(true) + ); + Ok(()) +} + +#[test] +fn read_field_discriminator_string_dispatches_to_write() -> Result<(), TypedefError> { + let union_schema = json!({ + "TypeDef:Union": true, + "discriminator": {"kind": "field", "name": "type"}, + "properties": { + "type": { "TypeDef:String": true } + }, + "mapping": { + "read": {"$ref": "#/$defs/Read"}, + "write": {"$ref": "#/$defs/Write"} + }, + "$defs": { + "Read": { + "TypeDef:Struct": true, + "properties": {"x": {"TypeDef:Uint8": true}} + }, + "Write": { + "TypeDef:Struct": true, + "properties": {"y": {"TypeDef:Uint16": true}} + } + } + }); + let value = "write"; + let mut buffer = vec![0u8; 32]; + data_access::write_string(&mut buffer, 0, value, "type", Endian::Little)?; + let dispatch = tunion::read_field_discriminator(&buffer, &union_schema, 0, Endian::Little)?; + assert_eq!(dispatch.key, "write"); + assert_eq!(dispatch.variant_offset, 4 + value.len()); + + let variant = tunion::resolve_variant(&union_schema, &dispatch.key)?; + let props = variant + .get("properties") + .and_then(serde_json::Value::as_object) + .expect("variant has properties"); + assert!(props.contains_key("y")); + assert!(!props.contains_key("x")); + Ok(()) +} + +#[test] +fn read_field_discriminator_uint8_field() -> Result<(), TypedefError> { + let union_schema = json!({ + "TypeDef:Union": true, + "discriminator": {"kind": "field", "name": "tag"}, + "properties": { + "tag": { "TypeDef:Uint8": true } + }, + "mapping": { + "0": {"TypeDef:Struct": true, "properties": {"a": {"TypeDef:Uint32": true}}}, + "1": {"TypeDef:Struct": true, "properties": {"b": {"TypeDef:Uint16": true}}} + } + }); + let mut buffer = vec![0u8; 8]; + buffer[0] = 0; + let dispatch = tunion::read_field_discriminator(&buffer, &union_schema, 0, Endian::Little)?; + assert_eq!(dispatch.key, "0"); + assert_eq!(dispatch.variant_offset, 1); + assert_eq!(dispatch.discriminator_size, 1); + + buffer[0] = 1; + let dispatch = tunion::read_field_discriminator(&buffer, &union_schema, 0, Endian::Little)?; + assert_eq!(dispatch.key, "1"); + Ok(()) +} + +#[test] +fn read_field_discriminator_unknown_value_returns_access_error() -> Result<(), TypedefError> { + let union_schema = json!({ + "TypeDef:Union": true, + "discriminator": {"kind": "field", "name": "tag"}, + "properties": { + "tag": { "TypeDef:Uint8": true } + }, + "mapping": { + "0": {"TypeDef:Struct": true, "properties": {"a": {"TypeDef:Uint32": true}}} + } + }); + let mut buffer = vec![0u8; 8]; + buffer[0] = 99; + let err = tunion::read_field_discriminator(&buffer, &union_schema, 0, Endian::Little).unwrap_err(); + assert!(matches!(err, TypedefError::Access { .. }), "got {err:?}"); + Ok(()) +} + +#[test] +fn discriminator_size_returns_correct_values() -> Result<(), TypedefError> { + let u8_schema = json!({ + "TypeDef:Union": true, + "discriminator": {"kind": "byte", "type": "TypeDef:Uint8"}, + "mapping": {} + }); + let u16_schema = json!({ + "TypeDef:Union": true, + "discriminator": {"kind": "byte", "type": "TypeDef:Uint16"}, + "mapping": {} + }); + let u32_schema = json!({ + "TypeDef:Union": true, + "discriminator": {"kind": "byte", "type": "TypeDef:Uint32"}, + "mapping": {} + }); + assert_eq!(tunion::discriminator_size(&u8_schema)?, 1); + assert_eq!(tunion::discriminator_size(&u16_schema)?, 2); + assert_eq!(tunion::discriminator_size(&u32_schema)?, 4); + Ok(()) +} + +#[test] +fn discriminator_size_field_kind_returns_schema_error() { + let schema = json!({ + "TypeDef:Union": true, + "discriminator": {"kind": "field", "name": "type"}, + "properties": {"type": {"TypeDef:Uint8": true}}, + "mapping": {} + }); + let err = tunion::discriminator_size(&schema).unwrap_err(); + assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}"); +} + +#[test] +fn resolve_variant_returns_schema_error_for_unknown_key() { + let union_schema = sftp_like_byte_union(); + let err = tunion::resolve_variant(&union_schema, "999").unwrap_err(); + assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}"); +} + +#[test] +fn parse_discriminator_missing_returns_schema_error() { + let schema = json!({"TypeDef:Union": true}); + let err = alknet_typedef::parse_discriminator(&schema).unwrap_err(); + assert!(matches!(err, TypedefError::Schema(_)), "got {err:?}"); +} \ No newline at end of file diff --git a/tasks/typedef/tests.md b/tasks/typedef/tests.md index 6981c21..0d8e700 100644 --- a/tasks/typedef/tests.md +++ b/tasks/typedef/tests.md @@ -1,7 +1,7 @@ --- id: typedef/tests name: "Write comprehensive tests for alknet-typedef: unit tests, integration tests, and POC round-trip tests" -status: pending +status: completed depends_on: [typedef/engine] scope: moderate risk: low @@ -189,4 +189,4 @@ Tests can be organized as: ## Summary -> To be filled on completion +Added 86 integration tests across 4 files in `crates/alknet-typedef/tests/` (engine_integration: 20, error_paths: 39, poc_roundtrip: 14, tunion_dispatch: 13) exercising the public API end-to-end. Combined with the pre-existing 202 unit tests, the crate now has 288 tests covering all 17 TypeDef kinds, both layout modes, TUnion byte-offset and field-name discriminators, validation, and error paths (buffer-too-short, invalid UTF-8, invalid boolean, missing var-sizes, malformed discriminators). The POC round-trip patterns — fixed-size, string, nested struct, big-endian, alignment padding, packed layout, and sequential reader — are replicated and passing. All four verification commands pass: `cargo check --all-targets`, `cargo test -p alknet-typedef`, `cargo build --workspace`, and `cargo clippy` on the new test files (pre-existing source-file clippy errors in `src/sequential_reader.rs:855` and `src/validation.rs:608/691` are unrelated to this task).