serpent-serializer 0.2.0

Serialize Lua values to round-trippable Lua source with cycle and shared-reference handling
Documentation
//! Randomized round-trip check.
//!
//! Generate nested values with a fixed shape and weight profile, serialize with
//! `block`, load back, and assert deep structural equality. A small seeded
//! generator keeps this dependency-free and reproducible. On failure the seed
//! prints so the case can be replayed.

mod common;

use common::deep_eq;
use serpent_serializer::value::{Key, Table, Value};
use serpent_serializer::{block, load, LoadOptions};

/// A minimal xorshift PRNG. Deterministic given a seed.
struct Rng(u64);

impl Rng {
    fn next(&mut self) -> u64 {
        let mut x = self.0;
        x ^= x << 13;
        x ^= x >> 7;
        x ^= x << 17;
        self.0 = x;
        x
    }

    /// Uniform integer in `[lo, hi]` inclusive.
    fn range(&mut self, lo: i64, hi: i64) -> i64 {
        let span = (hi - lo + 1) as u64;
        lo + (self.next() % span) as i64
    }
}

/// Generate a random value with a fixed weight profile.
///
/// About 10% nil (0 in key position), 10% false, 30% integer in the +/-1e6
/// range, 40% uppercase string up to length 100, 10% nested table to depth 3.
fn random_var(rng: &mut Rng, is_key: bool, depth: u32) -> Value {
    let key = rng.range(1, 1000);
    if key <= 100 {
        if is_key {
            Value::Number(0.0)
        } else {
            Value::Nil
        }
    } else if key <= 200 {
        Value::Bool(false)
    } else if key <= 500 {
        Value::Number(rng.range(-1_000_000, 1_000_000) as f64)
    } else if key <= 900 {
        let len = rng.range(0, 100);
        let mut s = Vec::with_capacity(len as usize);
        for _ in 0..len {
            s.push(rng.range(65, 90) as u8);
        }
        Value::Str(s)
    } else if depth > 3 || is_key {
        Value::Number(0.0)
    } else {
        let len = rng.range(0, 10);
        let t = Table::new();
        for _ in 0..len {
            if rng.range(0, 1) == 0 {
                t.push(random_var(rng, false, depth + 1));
            } else {
                let k = random_var(rng, true, depth + 1);
                let v = random_var(rng, false, depth + 1);
                if let Ok(key) = to_key(k) {
                    t.set(key, v);
                }
            }
        }
        Value::Table(t)
    }
}

fn to_key(v: Value) -> Result<Key, ()> {
    match v {
        Value::Bool(b) => Ok(Key::Bool(b)),
        Value::Number(n) => Ok(Key::Number(n)),
        Value::Str(s) => Ok(Key::Str(s)),
        Value::Table(t) => Ok(Key::Table(t)),
        _ => Err(()),
    }
}

#[test]
fn random_values_round_trip() {
    // Fixed seeds keep the run reproducible. Each seed drives 256 cases across
    // several independent streams.
    for seed in [
        0x1234_5678u64,
        0x9abc_def0,
        0xdead_beef,
        0x0f0f_0f0f,
        0xa5a5_5a5a,
        0xcafe_babe,
    ] {
        let mut rng = Rng(seed | 1);
        for i in 0..256 {
            let x = random_var(&mut rng, false, 0);
            let s = block(&x).unwrap();
            let back = load(&s, &LoadOptions::default())
                .unwrap_or_else(|e| panic!("load failed seed={seed} i={i}: {e}\n{s}"));
            assert!(
                deep_eq(&x, &back),
                "mismatch seed={seed} i={i}\nsource:\n{s}"
            );
        }
    }
}