use noyalib::{Mapping, Number, Value, from_str, from_value, to_string, to_value};
use proptest::prelude::*;
fn arb_number() -> impl Strategy<Value = Number> {
prop_oneof![
any::<i64>().prop_map(Number::Integer),
any::<f64>()
.prop_filter("finite floats only", |f| f.is_finite())
.prop_map(Number::Float),
]
}
fn arb_scalar_value() -> impl Strategy<Value = Value> {
prop_oneof![
Just(Value::Null),
any::<bool>().prop_map(Value::Bool),
arb_number().prop_map(Value::Number),
"[a-zA-Z0-9_]{0,20}".prop_map(Value::String),
]
}
fn arb_value() -> impl Strategy<Value = Value> {
arb_scalar_value().prop_recursive(
3, 64, 10, |inner| {
prop_oneof![
prop::collection::vec(inner.clone(), 0..5).prop_map(Value::Sequence),
prop::collection::vec(
("[a-zA-Z][a-zA-Z0-9_]{0,10}".prop_map(String::from), inner),
0..5
)
.prop_map(|pairs| {
let mut map = Mapping::new();
for (k, v) in pairs {
let _ = map.insert(k, v);
}
Value::Mapping(map)
}),
]
},
)
}
proptest! {
#[test]
fn roundtrip_value(value in arb_value()) {
let yaml = to_string(&value).expect("serialization should succeed");
let parsed: Value = from_str(&yaml).expect("deserialization should succeed");
prop_assert!(values_equal(&value, &parsed),
"Roundtrip failed:\nOriginal: {:?}\nYAML: {}\nParsed: {:?}",
value, yaml, parsed);
}
#[test]
fn roundtrip_to_from_value(value in arb_value()) {
let serialized = to_value(&value).expect("to_value should succeed");
let deserialized: Value = from_value(&serialized).expect("from_value should succeed");
prop_assert!(values_equal(&value, &deserialized),
"to_value/from_value roundtrip failed:\nOriginal: {:?}\nSerialized: {:?}\nDeserialized: {:?}",
value, serialized, deserialized);
}
#[test]
fn roundtrip_integer(n in any::<i64>()) {
let value = Value::Number(Number::Integer(n));
let yaml = to_string(&value).expect("serialization should succeed");
let parsed: Value = from_str(&yaml).expect("deserialization should succeed");
prop_assert_eq!(parsed.as_i64(), Some(n));
}
#[test]
fn roundtrip_bool(b in any::<bool>()) {
let value = Value::Bool(b);
let yaml = to_string(&value).expect("serialization should succeed");
let parsed: Value = from_str(&yaml).expect("deserialization should succeed");
prop_assert_eq!(parsed.as_bool(), Some(b));
}
#[test]
fn roundtrip_simple_string(s in "[a-zA-Z0-9_]{1,50}") {
let value = Value::String(s.clone());
let yaml = to_string(&value).expect("serialization should succeed");
let parsed: Value = from_str(&yaml).expect("deserialization should succeed");
prop_assert_eq!(parsed.as_str(), Some(s.as_str()));
}
#[test]
fn roundtrip_null(_dummy in Just(())) {
let value = Value::Null;
let yaml = to_string(&value).expect("serialization should succeed");
let parsed: Value = from_str(&yaml).expect("deserialization should succeed");
prop_assert!(parsed.is_null());
}
}
proptest! {
#[test]
fn number_integer_as_i64(n in any::<i64>()) {
let num = Number::Integer(n);
prop_assert_eq!(num.as_i64(), Some(n));
}
#[test]
fn number_integer_as_u64(n in 0i64..=i64::MAX) {
let num = Number::Integer(n);
prop_assert_eq!(num.as_u64(), Some(n as u64));
}
#[test]
fn number_float_as_f64(f in any::<f64>().prop_filter("finite", |f| f.is_finite())) {
let num = Number::Float(f);
prop_assert!((num.as_f64() - f).abs() < f64::EPSILON || num.as_f64() == f);
}
#[test]
fn number_cmp_reflexive(n in arb_number()) {
prop_assert_eq!(n.cmp(&n), std::cmp::Ordering::Equal);
}
#[test]
fn number_hash_consistent_integers(a in any::<i64>(), b in any::<i64>()) {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let na = Number::Integer(a);
let nb = Number::Integer(b);
if na == nb {
let mut ha = DefaultHasher::new();
let mut hb = DefaultHasher::new();
na.hash(&mut ha);
nb.hash(&mut hb);
prop_assert_eq!(ha.finish(), hb.finish());
}
}
#[test]
fn number_hash_identical_floats(f in any::<f64>().prop_filter("finite", |f| f.is_finite())) {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let n1 = Number::Float(f);
let n2 = Number::Float(f);
let mut h1 = DefaultHasher::new();
let mut h2 = DefaultHasher::new();
n1.hash(&mut h1);
n2.hash(&mut h2);
prop_assert_eq!(h1.finish(), h2.finish());
}
}
proptest! {
#[test]
fn value_eq_reflexive(value in arb_value()) {
let cloned = value.clone();
prop_assert_eq!(value, cloned);
}
#[test]
fn value_cmp_reflexive(value in arb_value()) {
prop_assert_eq!(value.cmp(&value), std::cmp::Ordering::Equal);
}
#[test]
fn value_hash_consistent(a in arb_value(), b in arb_value()) {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
if a == b {
let mut ha = DefaultHasher::new();
let mut hb = DefaultHasher::new();
a.hash(&mut ha);
b.hash(&mut hb);
prop_assert_eq!(ha.finish(), hb.finish());
}
}
#[test]
fn value_clone_equals(value in arb_value()) {
let cloned = value.clone();
prop_assert_eq!(value, cloned);
}
}
fn hash_one<T: std::hash::Hash>(t: &T) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
let mut h = DefaultHasher::new();
t.hash(&mut h);
h.finish()
}
#[test]
fn number_zero_negative_zero_eq_hash() {
let p = Number::Float(0.0);
let n = Number::Float(-0.0);
assert_eq!(p, n, "+0.0 == -0.0 by IEEE 754");
assert_eq!(hash_one(&p), hash_one(&n), "Eq/Hash invariant");
}
#[test]
fn number_nan_eq_hash() {
let a = Number::Float(f64::NAN);
let b = Number::Float(f64::from_bits(0x7FF8_0000_0000_0042));
assert_eq!(a, b, "all NaNs compare equal under our reflexive-Eq policy");
assert_eq!(hash_one(&a), hash_one(&b), "Eq/Hash invariant");
}
#[test]
fn value_zero_negative_zero_eq_hash() {
let p = Value::Number(Number::Float(0.0));
let n = Value::Number(Number::Float(-0.0));
assert_eq!(p, n);
assert_eq!(hash_one(&p), hash_one(&n));
}
proptest! {
#[test]
fn mapping_insert_get(
key in "[a-zA-Z][a-zA-Z0-9_]{0,10}",
value in arb_scalar_value()
) {
let mut map = Mapping::new();
let _ = map.insert(key.clone(), value.clone());
prop_assert_eq!(map.get(&key), Some(&value));
}
#[test]
fn mapping_remove(
key in "[a-zA-Z][a-zA-Z0-9_]{0,10}",
value in arb_scalar_value()
) {
let mut map = Mapping::new();
let _ = map.insert(key.clone(), value);
let _ = map.shift_remove(&key);
prop_assert!(map.get(&key).is_none());
}
#[test]
fn mapping_length(
pairs in prop::collection::vec(
("[a-zA-Z][a-zA-Z0-9]{0,5}".prop_map(String::from), arb_scalar_value()),
0..10
)
) {
let mut map = Mapping::new();
let mut unique_keys = std::collections::HashSet::new();
for (k, v) in pairs {
let _ = map.insert(k.clone(), v);
let _ = unique_keys.insert(k);
}
prop_assert_eq!(map.len(), unique_keys.len());
}
}
proptest! {
#[test]
fn merge_with_empty(base in arb_value()) {
let mut merged = base.clone();
merged.merge(Value::Mapping(Mapping::new()));
if base.is_mapping() {
prop_assert!(values_equal(&merged, &base));
}
}
#[test]
fn merge_into_empty(
pairs in prop::collection::vec(
("[a-zA-Z][a-zA-Z0-9]{0,5}".prop_map(String::from), arb_scalar_value()),
1..5
)
) {
let mut base = Value::Mapping(Mapping::new());
let mut other = Mapping::new();
for (k, v) in pairs.iter() {
let _ = other.insert(k.clone(), v.clone());
}
let other_clone = other.clone();
base.merge(Value::Mapping(other));
for (k, v) in other_clone.iter() {
prop_assert_eq!(base.get(k.as_str()), Some(v));
}
}
}
proptest! {
#[test]
fn get_path_simple(
key in "[a-zA-Z][a-zA-Z0-9_]{0,10}",
value in arb_scalar_value()
) {
let mut map = Mapping::new();
let _ = map.insert(key.clone(), value.clone());
let v = Value::Mapping(map);
prop_assert_eq!(v.get_path(&key), Some(&value));
}
#[test]
fn get_path_nonexistent(
key in "[a-zA-Z][a-zA-Z0-9_]{0,10}",
value in arb_scalar_value()
) {
let mut map = Mapping::new();
let _ = map.insert(key, value);
let v = Value::Mapping(map);
prop_assert!(v.get_path("nonexistent_key_xyz").is_none());
}
}
fn values_equal(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Null, Value::Null) => true,
(Value::Bool(a), Value::Bool(b)) => a == b,
(Value::Number(Number::Integer(a)), Value::Number(Number::Integer(b))) => a == b,
(Value::Number(Number::Float(a)), Value::Number(Number::Float(b))) => {
(a - b).abs() < 1e-10 || (a.is_nan() && b.is_nan())
}
(Value::Number(Number::Integer(a)), Value::Number(Number::Float(b))) => {
(*a as f64 - b).abs() < 1e-10
}
(Value::Number(Number::Float(a)), Value::Number(Number::Integer(b))) => {
(a - *b as f64).abs() < 1e-10
}
(Value::String(a), Value::String(b)) => a == b,
(Value::Sequence(a), Value::Sequence(b)) => {
a.len() == b.len() && a.iter().zip(b.iter()).all(|(a, b)| values_equal(a, b))
}
(Value::Mapping(a), Value::Mapping(b)) => {
a.len() == b.len()
&& a.iter()
.all(|(k, v)| b.get(k).is_some_and(|bv| values_equal(v, bv)))
}
(Value::Tagged(a), Value::Tagged(b)) => {
a.tag() == b.tag() && values_equal(a.value(), b.value())
}
_ => false,
}
}