protocol/wire/test: Define `fn roundtrip` and macro

In many tests we assert that encoding and decoding gives the same
object. Let's call that a "roundrip", and also provide a proc macro to
generate such tests.
This commit is contained in:
Lorenz Leutgeb 2025-08-23 19:27:04 +02:00 committed by Erik Kundt
parent bbd1e2cff6
commit ded0d19d59
5 changed files with 71 additions and 120 deletions

7
Cargo.lock generated
View File

@ -2403,6 +2403,12 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pbkdf2"
version = "0.12.2"
@ -2893,6 +2899,7 @@ dependencies = [
"localtime",
"log",
"nonempty 0.9.0",
"paste",
"qcheck",
"qcheck-macros",
"radicle",

View File

@ -31,6 +31,7 @@ serde_json = { workspace = true, features = ["preserve_order"] }
thiserror = { workspace = true }
[dev-dependencies]
paste = "1.0.15"
qcheck = { workspace = true }
qcheck-macros = { workspace = true }
radicle = { workspace = true, features = ["test"] }

View File

@ -560,9 +560,37 @@ impl Decode for Timestamp {
}
}
#[cfg(test)]
fn roundtrip<T>(value: T)
where
T: Encode + Decode + PartialEq + Debug,
{
let encoded = value.encode_to_vec();
assert_eq!(T::decode_exact(&encoded).expect("roundtrip"), value);
}
#[cfg(test)]
#[macro_export]
macro_rules! prop_roundtrip {
($t:ty, $name:tt) => {
paste::paste! {
#[quickcheck]
fn [< prop_roundtrip_ $name:lower >](v: $t) {
$crate::wire::roundtrip(v);
}
}
};
($t:ty) => {
paste::paste! {
prop_roundtrip!($t, [< $t >]);
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use qcheck;
use qcheck_macros::quickcheck;
@ -570,101 +598,36 @@ mod tests {
use radicle::crypto::Unverified;
use radicle::storage::refs::SignedRefs;
#[quickcheck]
fn prop_u8(input: u8) {
assert_eq!(u8::decode_exact(&input.encode_to_vec()).unwrap(), input);
}
#[quickcheck]
fn prop_u16(input: u16) {
assert_eq!(u16::decode_exact(&input.encode_to_vec()).unwrap(), input);
}
#[quickcheck]
fn prop_u32(input: u32) {
assert_eq!(u32::decode_exact(&input.encode_to_vec()).unwrap(), input);
}
#[quickcheck]
fn prop_u64(input: u64) {
assert_eq!(u64::decode_exact(&input.encode_to_vec()).unwrap(), input);
}
prop_roundtrip!(u16);
prop_roundtrip!(u32);
prop_roundtrip!(u64);
prop_roundtrip!(BoundedVec<u8, 16>, vec);
prop_roundtrip!(PublicKey);
prop_roundtrip!(filter::Filter, filter);
prop_roundtrip!(RepoId);
prop_roundtrip!(Refs);
prop_roundtrip!((String, String), tuple);
prop_roundtrip!(SignedRefs<Unverified>, signed_refs);
#[quickcheck]
fn prop_string(input: String) -> qcheck::TestResult {
if input.len() > u8::MAX as usize {
return qcheck::TestResult::discard();
}
assert_eq!(String::decode_exact(&input.encode_to_vec()).unwrap(), input);
roundtrip(input);
qcheck::TestResult::passed()
}
#[quickcheck]
fn prop_vec(input: BoundedVec<String, 16>) {
assert_eq!(
BoundedVec::<String, 16>::decode_exact(&input.encode_to_vec()).unwrap(),
input
);
}
#[quickcheck]
fn prop_pubkey(input: PublicKey) {
assert_eq!(
PublicKey::decode_exact(&input.encode_to_vec()).unwrap(),
input
);
}
#[quickcheck]
fn prop_filter(input: filter::Filter) {
assert_eq!(
filter::Filter::decode_exact(&input.encode_to_vec()).unwrap(),
input
);
}
#[quickcheck]
fn prop_id(input: RepoId) {
assert_eq!(RepoId::decode_exact(&input.encode_to_vec()).unwrap(), input);
}
#[quickcheck]
fn prop_refs(input: Refs) {
assert_eq!(Refs::decode_exact(&input.encode_to_vec()).unwrap(), input);
}
#[quickcheck]
fn prop_tuple(input: (String, String)) {
assert_eq!(
<(String, String)>::decode_exact(&input.encode_to_vec()).unwrap(),
input
);
}
#[quickcheck]
fn prop_signature(input: [u8; 64]) {
let signature = Signature::from(input);
assert_eq!(
Signature::decode_exact(&signature.encode_to_vec()).unwrap(),
signature
);
roundtrip(Signature::from(input));
}
#[quickcheck]
fn prop_oid(input: [u8; 20]) {
let oid = git::Oid::try_from(input.as_slice()).unwrap();
assert_eq!(git::Oid::decode_exact(&oid.encode_to_vec()).unwrap(), oid);
}
#[quickcheck]
fn prop_signed_refs(input: SignedRefs<Unverified>) {
assert_eq!(
SignedRefs::<Unverified>::decode_exact(&input.encode_to_vec()).unwrap(),
input
);
roundtrip(git::Oid::try_from(input.as_slice()).unwrap());
}
#[test]

View File

@ -425,15 +425,20 @@ impl wire::Decode for ZeroBytes {
#[cfg(test)]
mod tests {
use super::*;
use qcheck_macros::quickcheck;
use radicle::node::device::Device;
use radicle::node::UserAgent;
use radicle::storage::refs::RefsAt;
use radicle::test::arbitrary;
use crate::deserializer::Deserializer;
use crate::wire::{self, Decode, Encode};
use radicle::test::arbitrary;
use crate::prop_roundtrip;
use crate::wire::{roundtrip, Encode as _};
use super::*;
prop_roundtrip!(Address);
prop_roundtrip!(Message);
#[test]
fn test_refs_ann_max_size() {
@ -520,14 +525,6 @@ mod tests {
.encode_to_vec();
}
#[quickcheck]
fn prop_message_encode_decode(message: Message) {
let encoded = message.encode_to_vec();
let decoded = Message::decode_exact(&encoded).unwrap();
assert_eq!(message, decoded);
}
#[test]
fn prop_message_decoder() {
fn property(items: Vec<Message>) {
@ -546,28 +543,14 @@ mod tests {
.quickcheck(property as fn(items: Vec<Message>));
}
#[test]
fn prop_zero_bytes_encode_decode() {
fn property(zeroes: wire::Size) {
if zeroes > Ping::MAX_PING_ZEROES {
return;
}
let zeroes = ZeroBytes::new(zeroes);
assert_eq!(
ZeroBytes::decode_exact(&zeroes.encode_to_vec()).unwrap(),
zeroes
);
#[quickcheck]
fn prop_zero_bytes_encode_decode(zeroes: wire::Size) -> qcheck::TestResult {
if zeroes > Ping::MAX_PING_ZEROES {
return qcheck::TestResult::discard();
}
qcheck::QuickCheck::new()
.gen(qcheck::Gen::new(16))
.quickcheck(property as fn(zeroes: wire::Size));
}
roundtrip(ZeroBytes::new(zeroes));
#[quickcheck]
fn prop_addr(addr: Address) {
assert_eq!(Address::decode_exact(&addr.encode_to_vec()).unwrap(), addr);
qcheck::TestResult::passed()
}
}

View File

@ -175,9 +175,14 @@ pub mod payload {
#[cfg(test)]
mod test {
use super::*;
use qcheck_macros::quickcheck;
use crate::prop_roundtrip;
use super::*;
prop_roundtrip!(VarInt);
impl qcheck::Arbitrary for VarInt {
fn arbitrary(g: &mut qcheck::Gen) -> Self {
let a = u16::arbitrary(g) as u64;
@ -211,16 +216,8 @@ mod test {
}
}
#[quickcheck]
fn prop_encode_decode(input: VarInt) {
let encoded = input.encode_to_vec();
let decoded: VarInt = VarInt::decode_exact(&encoded).unwrap();
assert_eq!(decoded, input);
}
#[test]
#[should_panic]
#[should_panic(expected = "overflow")]
fn test_encode_overflow() {
VarInt(u64::MAX).encode_to_vec();
}