use proptest::prelude::*;
use syncbat::{EffectClass, OperationDescriptor, OperationName, RegisterOperationRowV1};
fn arb_operation_name() -> impl Strategy<Value = String> {
proptest::collection::vec("[A-Za-z0-9_-]{1,16}", 1..=4).prop_map(|segments| segments.join("."))
}
fn arb_stable_token() -> impl Strategy<Value = String> {
arb_operation_name()
}
fn arb_effect_class() -> impl Strategy<Value = EffectClass> {
prop_oneof![
Just(EffectClass::Persist),
Just(EffectClass::Inspect),
Just(EffectClass::Compute),
]
}
proptest! {
#[test]
fn operation_name_accepts_grammar_valid_names(name in arb_operation_name()) {
let parsed = OperationName::new(name.clone())
.expect("arb_operation_name should produce a grammar-valid name");
prop_assert_eq!(parsed.as_str(), name.as_str());
}
#[test]
fn operation_name_rejects_illegal_characters_anywhere(
prefix in "[A-Za-z0-9_-]{1,8}",
bad_char in prop_oneof![Just(' '), Just('/'), Just(':'), Just('@'), Just('$'), Just('+')],
suffix in "[A-Za-z0-9_-]{1,8}",
) {
let candidate = format!("{prefix}{bad_char}{suffix}");
prop_assert!(
OperationName::new(candidate.clone()).is_err(),
"OperationName::new must reject illegal character in {candidate:?}",
);
}
#[test]
fn operation_name_rejects_consecutive_dots(
a in "[A-Za-z0-9_-]{1,8}",
b in "[A-Za-z0-9_-]{1,8}",
) {
let candidate = format!("{a}..{b}");
prop_assert!(
OperationName::new(candidate.clone()).is_err(),
"OperationName::new must reject consecutive dots in {candidate:?}",
);
}
#[test]
fn operation_name_rejects_overlong_names(len in 129_usize..=256_usize) {
let candidate = "a".repeat(len);
prop_assert!(
OperationName::new(candidate.clone()).is_err(),
"OperationName::new must reject overlong name of {len} bytes",
);
}
}
proptest! {
#[test]
fn descriptor_validate_accepts_well_formed_inputs(
name in arb_operation_name(),
input_ref in arb_stable_token(),
output_ref in arb_stable_token(),
receipt_kind in arb_stable_token(),
action in arb_effect_class(),
) {
let descriptor = OperationDescriptor::owned(
name.clone(),
action,
input_ref.clone(),
output_ref.clone(),
receipt_kind.clone(),
);
descriptor
.validate()
.expect("well-formed descriptor must validate");
prop_assert_eq!(descriptor.name(), name.as_str());
prop_assert_eq!(descriptor.input_schema_ref(), input_ref.as_str());
prop_assert_eq!(descriptor.output_schema_ref(), output_ref.as_str());
prop_assert_eq!(descriptor.receipt_kind(), receipt_kind.as_str());
prop_assert_eq!(descriptor.effect, action);
}
#[test]
fn descriptor_validate_is_idempotent(
name in arb_operation_name(),
input_ref in arb_stable_token(),
output_ref in arb_stable_token(),
receipt_kind in arb_stable_token(),
) {
let descriptor = OperationDescriptor::owned(
name,
EffectClass::Inspect,
input_ref,
output_ref,
receipt_kind,
);
let first = descriptor.validate();
let second = descriptor.validate();
prop_assert_eq!(first.is_ok(), second.is_ok());
}
}
proptest! {
#[test]
fn register_operation_row_v1_roundtrips(
name in arb_operation_name(),
input_ref in arb_stable_token(),
output_ref in arb_stable_token(),
receipt_kind in arb_stable_token(),
action_disc in 0_u8..=3_u8,
superseded_name in arb_operation_name(),
) {
let descriptor = OperationDescriptor::owned(
name.clone(),
EffectClass::Persist,
input_ref,
output_ref,
receipt_kind,
);
let row = match action_disc {
0 => RegisterOperationRowV1::from_descriptor(&descriptor),
1 => RegisterOperationRowV1::update(&descriptor),
2 => RegisterOperationRowV1::delete(name.clone()),
_ => RegisterOperationRowV1::supersede(superseded_name, &descriptor),
};
let bytes = batpak::encoding::to_bytes(&row).expect("encode register row");
let decoded: RegisterOperationRowV1 =
batpak::encoding::from_bytes(&bytes).expect("decode register row");
let re_encoded = batpak::encoding::to_bytes(&decoded).expect("re-encode");
prop_assert_eq!(bytes, re_encoded);
}
#[test]
fn register_operation_row_v1_encoding_is_deterministic(
name in arb_operation_name(),
receipt_kind in arb_stable_token(),
) {
let descriptor = OperationDescriptor::owned(
name,
EffectClass::Inspect,
"alpha",
"beta",
receipt_kind,
);
let row = RegisterOperationRowV1::from_descriptor(&descriptor);
let a = batpak::encoding::to_bytes(&row).expect("encode 1");
let b = batpak::encoding::to_bytes(&row).expect("encode 2");
prop_assert_eq!(a, b);
}
}