use std::{
fmt::{Debug, Display},
str::FromStr,
};
use lexe_crypto::ed25519;
use lexe_hex::hex;
use proptest::{
arbitrary::{Arbitrary, any},
prop_assert_eq, proptest,
strategy::Strategy,
test_runner::Config,
};
use serde::{Serialize, de::DeserializeOwned};
pub fn bcs_roundtrip_proptest<T>()
where
T: Arbitrary + PartialEq + Serialize + DeserializeOwned,
{
proptest!(|(value1: T)| {
let bytes1 = bcs::to_bytes(&value1).unwrap();
bcs_roundtrip_ok(&bytes1, &value1);
});
}
#[track_caller]
pub fn bcs_roundtrip_ok<T>(expected_bytes: &[u8], expected_value: &T)
where
T: Debug + PartialEq + Serialize + DeserializeOwned,
{
let actual_bytes = bcs::to_bytes(expected_value).unwrap();
if actual_bytes != expected_bytes {
assert_eq!(hex::encode(&actual_bytes), hex::encode(expected_bytes));
}
let actual_value = bcs::from_bytes::<T>(expected_bytes).unwrap();
assert_eq!(&actual_value, expected_value);
}
pub fn json_value_roundtrip_proptest<T>()
where
T: Arbitrary + PartialEq + Serialize + DeserializeOwned,
{
json_value_custom(any::<T>(), Config::default());
}
pub fn json_value_custom<S, T>(strategy: S, config: Config)
where
S: Strategy<Value = T>,
T: PartialEq + Serialize + DeserializeOwned + Debug,
{
proptest!(config, |(value1 in strategy)| {
let json_value1 = serde_json::to_value(&value1).unwrap();
let value2 = serde_json::from_value(json_value1.clone()).unwrap();
let json_value2 = serde_json::to_value(&value2).unwrap();
prop_assert_eq!(&value1, &value2);
prop_assert_eq!(&json_value1, &json_value2);
});
}
pub fn json_string_roundtrip_proptest<T>()
where
T: Arbitrary + PartialEq + Serialize + DeserializeOwned,
{
json_string_custom(any::<T>(), Config::default());
}
pub fn json_string_custom<S, T>(strategy: S, config: Config)
where
S: Strategy<Value = T>,
T: PartialEq + Serialize + DeserializeOwned + Debug,
{
proptest!(config, |(value1 in strategy)| {
let json_value1 = serde_json::to_string(&value1).unwrap();
let value2 = serde_json::from_str::<T>(&json_value1).unwrap();
prop_assert_eq!(&value1, &value2);
});
}
pub fn query_string_roundtrip_proptest<T>()
where
T: Arbitrary + PartialEq + Serialize + DeserializeOwned,
{
proptest!(|(value1: T)| {
let query_str1 = serde_urlencoded::to_string(&value1).unwrap();
let value2 = serde_urlencoded::from_str(&query_str1).unwrap();
let query_str2 = serde_urlencoded::to_string(&value2).unwrap();
prop_assert_eq!(&value1, &value2);
prop_assert_eq!(&query_str1, &query_str2);
});
}
pub fn signed_roundtrip_proptest<T>()
where
T: Arbitrary + PartialEq + Serialize + DeserializeOwned + ed25519::Signable,
{
proptest!(|(seed: [u8; 32], value: T)| {
let key_pair = ed25519::KeyPair::from_seed(&seed);
let pubkey = key_pair.public_key();
let (ser_value, signed_value) = key_pair.sign_struct(&value).unwrap();
let signed_value2 = pubkey.verify_self_signed_struct(&ser_value).unwrap();
let (ser_value2, _) = key_pair.sign_struct(signed_value2.inner()).unwrap();
prop_assert_eq!(signed_value, signed_value2.as_ref());
prop_assert_eq!(&ser_value, &ser_value2);
});
}
pub fn fromstr_display_roundtrip_proptest<T>()
where
T: Arbitrary + PartialEq + FromStr + Display,
<T as FromStr>::Err: Debug,
{
fromstr_display_custom(any::<T>(), Config::default());
}
pub fn fromstr_display_custom<S, T>(strategy: S, config: Config)
where
S: Strategy<Value = T>,
T: PartialEq + FromStr + Display + Debug,
<T as FromStr>::Err: Debug,
{
proptest!(config, |(value1 in strategy)| {
let value2 = T::from_str(&value1.to_string()).unwrap();
prop_assert_eq!(value1, value2)
});
}
pub fn fromstr_json_string_equiv<T>()
where
T: Arbitrary + PartialEq + Debug,
T: FromStr + Display,
T: Serialize + DeserializeOwned,
<T as FromStr>::Err: Debug,
{
fromstr_json_string_equiv_custom(any::<T>(), Config::default())
}
pub fn fromstr_json_string_equiv_custom<S, T>(strategy: S, config: Config)
where
S: Strategy<Value = T>,
T: PartialEq + Debug,
T: FromStr + Display,
T: Serialize + DeserializeOwned,
<T as FromStr>::Err: Debug,
{
proptest!(config, |(value in strategy)| {
let ser_display = value.to_string();
let ser_json = serde_json::to_string(&value).unwrap();
prop_assert_eq!(&format!("\"{ser_display}\""), &ser_json);
let value_fromstr = T::from_str(&ser_display).unwrap();
let value_json = serde_json::from_str::<T>(&ser_json).unwrap();
prop_assert_eq!(&value_fromstr, &value);
prop_assert_eq!(&value_json, &value);
});
}
pub fn json_unit_enum_backwards_compat<T>(expected_ser: &str)
where
T: Clone + PartialEq + Debug,
T: Serialize + DeserializeOwned,
T: strum::VariantArray,
{
let expected_ser = if expected_ser.is_empty() {
"[]"
} else {
expected_ser
};
let expected_de = T::VARIANTS.to_vec();
let actual_ser = serde_json::to_string(&expected_de).unwrap();
let actual_de = serde_json::from_str::<Vec<T>>(expected_ser).unwrap();
if actual_ser != expected_ser {
panic!(
"\n\
This enum's JSON serialization has changed or a new variant has \n\
been added/deleted: \n\
\n\
actual_ser: '{actual_ser}' \n\
expected_ser: '{expected_ser}' \n\
\n\
It is not safe to remove or rename a variant, as this breaks \n\
backwards compatibility! Our service won't be able to read data \n\
persisted in the past! You will need a data migration to do this \n\
safely. \n\
\n\
However, if you've just added a new variant, then this is OK. Just \n\
update `expected_ser` as below: \n\
\n\
```\n\
let expected_ser = r#\"{actual_ser}\"#;\n\
```\n\
"
);
}
assert_eq!(actual_de, expected_de);
}
pub fn json_unit_enum_backwards_compat_with_unknown<T>(
known_variants: &'static [T],
expected_ser: &str,
) where
T: Clone + PartialEq + Debug,
T: Serialize + DeserializeOwned,
{
let expected_ser = if expected_ser.is_empty() {
"[]"
} else {
expected_ser
};
let actual_ser = serde_json::to_string(&known_variants).unwrap();
let actual_de = serde_json::from_str::<Vec<T>>(expected_ser).unwrap();
if actual_ser != expected_ser {
panic!(
"\n\
KNOWN_VARIANTS serialization has changed: \n\
\n\
actual: '{actual_ser}' \n\
expected: '{expected_ser}' \n\
\n\
RENAMING or REMOVING variants is NOT SAFE. Unlike unit enums, \n\
deserialization won't fail - the old string silently becomes \n\
Unknown(...), losing semantic meaning. You need a data migration. \n\
\n\
If you only ADDED a new variant, that's OK. Update expected_ser: \n\
\n\
```\n\
let expected_ser = r#\"{actual_ser}\"#;\n\
```\n\
"
);
}
assert_eq!(
actual_de, known_variants,
"\n\
Deserializing expected_ser doesn't reproduce KNOWN_VARIANTS. \n\
\n\
This suggests the enum's Serialize/Deserialize impls don't roundtrip \n\
correctly, or expected_ser was corrupted. Check 1 should have caught \n\
renames/removals, so this indicates a different problem. \n\
"
);
}