Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 41 additions & 19 deletions pdl-compiler/scripts/generate_cxx_backend_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ def check_members(decl: ast.Declaration, var: str, expected: object) -> List[str

generated_tests = []
for (test_nr, test) in enumerate(tests):
if 'packed' not in test:
continue

child_packet_id = test.get('packet', packet.id)
child_packet = packet.file.packet_scope[child_packet_id]

Expand Down Expand Up @@ -308,31 +311,50 @@ def output_bytes(output: str) -> List[str]:

generated_tests = []
for (test_nr, test) in enumerate(tests):
if 'expected_error' in test:
if 'unpacked' not in test:
continue

child_packet_id = test.get('packet', packet.id)
child_packet = packet.file.packet_scope[child_packet_id]

(built_packet, intermediate_declarations) = build_packet(child_packet, 'packet', test['unpacked'])
generated_tests.append(
dedent("""\

TEST_F({serializer_test_suite}, {packet_id}_Case{test_nr}) {{
std::vector<uint8_t> expected_output {{
{output_bytes}
}};
{intermediate_declarations}
{child_packet_id}Builder packet = {built_packet};
ASSERT_EQ(packet.SerializeToBytes(), expected_output);
}}
""").format(serializer_test_suite=serializer_test_suite,
packet_id=packet.id,
child_packet_id=child_packet_id,
test_nr=test_nr,
output_bytes=indent(output_bytes(test['packed']), 2),
built_packet=built_packet,
intermediate_declarations=indent(intermediate_declarations, 1)))
if 'expected_error' in test:
generated_tests.append(
dedent("""\

TEST_F({serializer_test_suite}, {packet_id}_Invalid_Case{test_nr}) {{
{intermediate_declarations}
{child_packet_id}Builder packet = {built_packet};
std::vector<uint8_t> output;
ASSERT_FALSE(packet.Serialize(output));
}}
""").format(serializer_test_suite=serializer_test_suite,
packet_id=packet.id,
child_packet_id=child_packet_id,
test_nr=test_nr,
built_packet=built_packet,
intermediate_declarations=indent(intermediate_declarations, 1)))
else:
generated_tests.append(
dedent("""\

TEST_F({serializer_test_suite}, {packet_id}_Case{test_nr}) {{
std::vector<uint8_t> expected_output {{
{output_bytes}
}};
{intermediate_declarations}
{child_packet_id}Builder packet = {built_packet};
std::vector<uint8_t> output;
ASSERT_TRUE(packet.Serialize(output));
ASSERT_EQ(output, expected_output);
}}
""").format(serializer_test_suite=serializer_test_suite,
packet_id=packet.id,
child_packet_id=child_packet_id,
test_nr=test_nr,
output_bytes=indent(output_bytes(test['packed']), 2),
built_packet=built_packet,
intermediate_declarations=indent(intermediate_declarations, 1)))

return ''.join(generated_tests)

Expand Down
3 changes: 2 additions & 1 deletion pdl-compiler/src/backends/common/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ pub struct Packet {

#[derive(Debug, Deserialize)]
pub struct TestVector {
pub packed: String,
#[serde(default)]
pub packed: Option<String>,
#[serde(default)]
pub unpacked: Option<Value>,
pub packet: Option<String>,
Expand Down
16 changes: 12 additions & 4 deletions pdl-compiler/src/backends/java/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ fn get_test_cases(file: &str, exclude_packets: &[String]) -> Result<Vec<Packet>,
if t.packet.as_ref().is_some_and(|p| exclude_packets.contains(p)) {
return false;
}
// Skip negative serialization test vectors.
if t.packed.is_none() {
return false;
}
true
});
}
Expand Down Expand Up @@ -159,16 +163,18 @@ impl TestVector {
) -> impl FormatInto<Java> + 'a {
let maybe_child = self.packet.as_ref().unwrap_or(id);

let packed = self.packed.as_deref().unwrap();

quote_fn! {
$(java::block_comment(iter::once(format!("0x{}", &self.packed))))$['\n']
$(java::block_comment(iter::once(format!("0x{}", packed))))$['\n']
static void testEncode$test_id() {
$(Class::name_from_id(maybe_child)) packet = $(build_packet_from_fields(
self.packet.as_ref().unwrap_or(id),
self.unpacked.as_ref().unwrap().as_object().unwrap(),
decls
));
byte[] encodedPacket = packet.toBytes();
byte[] expectedBytes = $(hex_to_array(&self.packed));
byte[] expectedBytes = $(hex_to_array(packed));
assert $(&*import::ARRAYS).equals(expectedBytes, encodedPacket);
}
}
Expand All @@ -192,11 +198,12 @@ impl TestVector {
.map(|child_id| Class::name_from_id(child_id))
.unwrap_or(Class::name_from_id(id))
);
let packed = self.packed.as_deref().unwrap();

quote_fn! {
$(java::block_comment(iter::once(format!("{}", self.unpacked.as_ref().unwrap()))))$['\n']
static void testDecode$test_id() {
$(&packet_name) decodedPacket = $(&packet_name).fromBytes($(hex_to_array(&self.packed)));
$(&packet_name) decodedPacket = $(&packet_name).fromBytes($(hex_to_array(packed)));
$(self.gen_asserts_for_fields(id, decls))
}
}
Expand All @@ -212,11 +219,12 @@ impl TestVector {
.as_ref()
.map(|child_id| Class::name_from_id(child_id))
.unwrap_or(Class::name_from_id(id));
let packed = self.packed.as_deref().unwrap();

quote_fn! {
static void testDecodeInvalid$test_id() {
try {
$(&packet_name).fromBytes($(hex_to_array(&self.packed)));
$(&packet_name).fromBytes($(hex_to_array(packed)));
throw new AssertionError("Expected exception");
} catch (Exception e) {
// expected
Expand Down
185 changes: 110 additions & 75 deletions pdl-compiler/src/backends/rust/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ fn to_json<T: Serialize>(value: &T) -> syn::LitStr {
///
/// Unit variants like `"TrailingBytesError"` become `DecodeError::TrailingBytesError`.
/// Struct variants like `"LengthError"` become `DecodeError::LengthError { .. }`.
fn error_variant_pattern(variant: &str) -> proc_macro2::TokenStream {
fn decode_error_variant_pattern(variant: &str) -> proc_macro2::TokenStream {
let variant_ident = format_ident!("{}", variant);
match variant {
"UnwrapError" | "TrailingBytesError" => {
Expand All @@ -61,6 +61,12 @@ fn error_variant_pattern(variant: &str) -> proc_macro2::TokenStream {
}
}

/// Map an error variant name to a `matches!` pattern for `EncodeError`.
fn encode_error_variant_pattern(variant: &str) -> proc_macro2::TokenStream {
let variant_ident = format_ident!("{}", variant);
quote! { EncodeError::#variant_ident { .. } }
}

fn generate_unit_tests(input: &str, packet_names: &[&str]) -> Result<String, String> {
eprintln!("Reading test vectors from {input}, will use {} packets", packet_names.len());

Expand All @@ -78,84 +84,112 @@ fn generate_unit_tests(input: &str, packet_names: &[&str]) -> Result<String, Str
}
eprintln!("Generating tests for packet {test_packet}");

let packed = hexadecimal_to_vec(&test_vector.packed);
let packet_name = format_ident!("{}", test_packet);

if let Some(ref expected_error) = test_vector.expected_error {
// Invalid vector: assert that decode_full returns the expected error variant.
let parse_test_name = format_ident!(
"test_parse_invalid_{}_vector_{}_0x{}",
test_packet,
i + 1,
&test_vector.packed
);
let pattern = error_variant_pattern(expected_error);
tests.push(quote! {
#[test]
fn #parse_test_name() {
let packed = #packed;
let result = #packet_name::decode_full(&packed);
assert!(
matches!(result, Err(#pattern)),
"expected Err({}) but got {:?}",
stringify!(#pattern),
result,
);
}
});
} else if let Some(ref unpacked) = test_vector.unpacked {
// Valid vector: existing logic.
let parse_test_name = format_ident!(
"test_parse_{}_vector_{}_0x{}",
test_packet,
i + 1,
&test_vector.packed
);
let serialize_test_name = format_ident!(
"test_serialize_{}_vector_{}_0x{}",
test_packet,
i + 1,
&test_vector.packed
);
match (&test_vector.packed, &test_vector.unpacked, &test_vector.expected_error) {
(Some(packed_hex), None, Some(expected_error)) => {
// Invalid parsing vector: assert that decode_full returns the expected error variant.
let packed = hexadecimal_to_vec(packed_hex);
let parse_test_name = format_ident!(
"test_parse_invalid_{}_vector_{}_0x{}",
test_packet,
i + 1,
packed_hex
);
let pattern = decode_error_variant_pattern(expected_error);
tests.push(quote! {
#[test]
fn #parse_test_name() {
let packed = #packed;
let result = #packet_name::decode_full(&packed);
assert!(
matches!(result, Err(#pattern)),
"expected Err({}) but got {:?}",
stringify!(#pattern),
result,
);
}
});
}
(None, Some(unpacked), Some(expected_error)) => {
// Invalid serialization vector: assert that encode_to_vec returns the expected error variant.
let serialize_test_name =
format_ident!("test_serialize_invalid_{}_vector_{}", test_packet, i + 1,);
let object = unpacked.as_object().unwrap_or_else(|| {
panic!("Expected test vector object, found: {unpacked}")
});
let json = to_json(&object);
let pattern = encode_error_variant_pattern(expected_error);
tests.push(quote! {
#[test]
fn #serialize_test_name() {
let packet: #packet_name = serde_json::from_str(#json)
.expect("Could not create packet from canonical JSON data");
let result = packet.encode_to_vec();
assert!(
matches!(result, Err(#pattern)),
"expected Err({}) but got {:?}",
stringify!(#pattern),
result,
);
}
});
}
(Some(packed_hex), Some(unpacked), None) => {
// Valid vector: existing logic.
let packed = hexadecimal_to_vec(packed_hex);
let parse_test_name = format_ident!(
"test_parse_{}_vector_{}_0x{}",
test_packet,
i + 1,
packed_hex
);
let serialize_test_name = format_ident!(
"test_serialize_{}_vector_{}_0x{}",
test_packet,
i + 1,
packed_hex
);

let object = unpacked
.as_object()
.unwrap_or_else(|| panic!("Expected test vector object, found: {unpacked}"));
let assertions = object.iter().map(|(key, value)| {
let getter = format_ident!("{key}");
let expected = format_ident!("expected_{key}");
let json = to_json(&value);
quote! {
let #expected: serde_json::Value = serde_json::from_str(#json)
.expect("Could not create expected value from canonical JSON data");
assert_eq!(json!(actual.#getter()), #expected);
}
});
let object = unpacked.as_object().unwrap_or_else(|| {
panic!("Expected test vector object, found: {unpacked}")
});
let assertions = object.iter().map(|(key, value)| {
let getter = format_ident!("{key}");
let expected = format_ident!("expected_{key}");
let json = to_json(&value);
quote! {
let #expected: serde_json::Value = serde_json::from_str(#json)
.expect("Could not create expected value from canonical JSON data");
assert_eq!(json!(actual.#getter()), #expected);
}
});

let json = to_json(&object);
tests.push(quote! {
#[test]
fn #parse_test_name() {
let packed = #packed;
let actual = #packet_name::decode_full(&packed).unwrap();
assert_eq!(actual.encoded_len(), packed.len());
#(#assertions)*
}
let json = to_json(&object);
tests.push(quote! {
#[test]
fn #parse_test_name() {
let packed = #packed;
let actual = #packet_name::decode_full(&packed).unwrap();
assert_eq!(actual.encoded_len(), packed.len());
#(#assertions)*
}

#[test]
fn #serialize_test_name() {
let packet: #packet_name = serde_json::from_str(#json)
.expect("Could not create packet from canonical JSON data");
let packed: Vec<u8> = #packed;
assert_eq!(packet.encoded_len(), packed.len());
assert_eq!(packet.encode_to_vec(), Ok(packed));
}
});
} else {
panic!(
"Test vector for {test_packet} (index {i}) has neither \
'unpacked' nor 'expected_error'"
);
#[test]
fn #serialize_test_name() {
let packet: #packet_name = serde_json::from_str(#json)
.expect("Could not create packet from canonical JSON data");
let packed: Vec<u8> = #packed;
assert_eq!(packet.encoded_len(), packed.len());
assert_eq!(packet.encode_to_vec(), Ok(packed));
}
});
}
_ => {
panic!(
"Unexpected test vector combination for {test_packet} (index {i}): {test_vector:?}"
);
}
}
}
}
Expand All @@ -165,7 +199,7 @@ fn generate_unit_tests(input: &str, packet_names: &[&str]) -> Result<String, Str
#[allow(warnings, missing_docs)]
#[cfg(test)]
mod test {
use pdl_runtime::{DecodeError, Packet};
use pdl_runtime::{DecodeError, EncodeError, Packet};
use serde_json::json;
use super::*;

Expand Down Expand Up @@ -277,6 +311,7 @@ pub fn generate_tests(input_file: &str) -> Result<String, String> {
"Struct_Enum_Field",
"Struct_FixedEnum_Field",
"Struct_FixedScalar_Field",
"Struct_Reserved_Field",
"Struct_Size_Field",
"Struct_Struct_Field",
"Enum_Incomplete_Truncated_Closed",
Expand Down
Loading
Loading