use pack_io::{Deserialize, SerialError, Serialize, decode, encode, peek_version};
use proptest::prelude::*;
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[pack_io(version = 1)]
struct MessageV1 {
id: u64,
text: String,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[pack_io(version = 2)]
struct MessageV2 {
id: u64,
text: String,
#[pack_io(since = 2)]
timestamp: Option<u64>,
}
#[test]
fn v1_round_trips_with_itself() {
let m = MessageV1 {
id: 7,
text: "hello".into(),
};
let bytes = encode(&m).unwrap();
let back: MessageV1 = decode(&bytes).unwrap();
assert_eq!(back, m);
}
#[test]
fn v2_round_trips_with_itself() {
let m = MessageV2 {
id: 7,
text: "hello".into(),
timestamp: Some(1_700_000_000),
};
let bytes = encode(&m).unwrap();
let back: MessageV2 = decode(&bytes).unwrap();
assert_eq!(back, m);
}
#[test]
fn v2_round_trips_with_none_timestamp() {
let m = MessageV2 {
id: 7,
text: "hello".into(),
timestamp: None,
};
let bytes = encode(&m).unwrap();
let back: MessageV2 = decode(&bytes).unwrap();
assert_eq!(back, m);
}
#[test]
fn v1_encoded_decodes_as_v2_with_default_timestamp() {
let v1 = MessageV1 {
id: 100,
text: "from v1".into(),
};
let bytes = encode(&v1).unwrap();
let v2: MessageV2 = decode(&bytes).unwrap();
assert_eq!(v2.id, 100);
assert_eq!(v2.text, "from v1");
assert_eq!(v2.timestamp, None);
}
#[test]
fn v2_encoded_decodes_as_v1_ignoring_trailing_field() {
let v2 = MessageV2 {
id: 200,
text: "from v2".into(),
timestamp: Some(42),
};
let bytes = encode(&v2).unwrap();
let v1: MessageV1 = decode(&bytes).unwrap();
assert_eq!(v1.id, 200);
assert_eq!(v1.text, "from v2");
}
#[test]
fn peek_version_returns_writer_version() {
let v1 = MessageV1 {
id: 1,
text: "v1".into(),
};
let v2 = MessageV2 {
id: 1,
text: "v2".into(),
timestamp: Some(99),
};
assert_eq!(peek_version(&encode(&v1).unwrap()).unwrap(), 1);
assert_eq!(peek_version(&encode(&v2).unwrap()).unwrap(), 2);
}
#[test]
fn peek_version_on_empty_input_is_unexpected_eof() {
let err = peek_version(&[]).expect_err("empty input has no version");
assert!(matches!(err, SerialError::UnexpectedEof { .. }));
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[pack_io(version = 1)]
struct AccountV1 {
id: u64,
legacy_token: String, }
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[pack_io(version = 3)]
struct AccountV3 {
id: u64,
#[pack_io(deprecated = 3)]
legacy_token: String, #[pack_io(since = 2)]
handle: String, #[pack_io(since = 3)]
bio: String, }
#[test]
fn deprecated_field_is_omitted_at_or_after_removal_version() {
let v3 = AccountV3 {
id: 1,
legacy_token: "ignored".into(), handle: "alice".into(),
bio: "hi".into(),
};
let bytes = encode(&v3).unwrap();
let back: AccountV3 = decode(&bytes).unwrap();
assert_eq!(back.id, 1);
assert_eq!(back.legacy_token, String::new()); assert_eq!(back.handle, "alice");
assert_eq!(back.bio, "hi");
}
#[test]
fn v1_payload_decodes_as_v3_with_defaults_for_new_fields() {
let v1 = AccountV1 {
id: 5,
legacy_token: "token-xyz".into(),
};
let bytes = encode(&v1).unwrap();
let v3: AccountV3 = decode(&bytes).unwrap();
assert_eq!(v3.id, 5);
assert_eq!(v3.legacy_token, "token-xyz"); assert_eq!(v3.handle, String::new()); assert_eq!(v3.bio, String::new()); }
#[test]
fn v3_payload_decodes_as_v1_ignoring_new_fields() {
let v3 = AccountV3 {
id: 9,
legacy_token: "ignored on encode".into(),
handle: "bob".into(),
bio: "world".into(),
};
let bytes = encode(&v3).unwrap();
let v1: AccountV1 = decode(&bytes).unwrap();
assert_eq!(v1.id, 9);
assert_eq!(v1.legacy_token, "bob");
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Plain {
a: u32,
b: u32,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[pack_io(version = 1)]
struct WithPlain {
label: String,
plain: Plain,
}
#[test]
fn versioned_struct_containing_plain_struct_round_trips() {
let value = WithPlain {
label: "wrap".into(),
plain: Plain { a: 1, b: 2 },
};
let bytes = encode(&value).unwrap();
let back: WithPlain = decode(&bytes).unwrap();
assert_eq!(back, value);
}
proptest! {
#[test]
fn proptest_v1_is_readable_by_v2(id: u64, text: String) {
let bytes = encode(&MessageV1 { id, text: text.clone() }).unwrap();
let v2: MessageV2 = decode(&bytes).unwrap();
prop_assert_eq!(v2.id, id);
prop_assert_eq!(v2.text, text);
prop_assert_eq!(v2.timestamp, None);
}
#[test]
fn proptest_v2_is_readable_by_v1(id: u64, text: String, ts: Option<u64>) {
let bytes = encode(&MessageV2 { id, text: text.clone(), timestamp: ts }).unwrap();
let v1: MessageV1 = decode(&bytes).unwrap();
prop_assert_eq!(v1.id, id);
prop_assert_eq!(v1.text, text);
}
#[test]
fn proptest_peek_version_matches_writer(id: u64, text: String) {
let b1 = encode(&MessageV1 { id, text: text.clone() }).unwrap();
let b2 = encode(&MessageV2 { id, text, timestamp: None }).unwrap();
prop_assert_eq!(peek_version(&b1).unwrap(), 1);
prop_assert_eq!(peek_version(&b2).unwrap(), 2);
}
}
#[test]
fn hostile_body_length_is_rejected() {
let mut bytes = Vec::new();
bytes.push(0x01); bytes.extend_from_slice(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]);
let err = decode::<MessageV1>(&bytes).expect_err("hostile body length");
assert!(matches!(err, SerialError::InvalidLength { .. }));
}