mod common;
use common::{deep_eq, func};
use serpent_serializer::options::Options;
use serpent_serializer::value::{Global, Key, Table, Value};
use serpent_serializer::{block, dump, dump_with, load, LoadOptions};
fn vs(s: &str) -> Value {
Value::Str(s.as_bytes().to_vec())
}
fn load_dump(t: &Value) -> Value {
let src = dump(t).unwrap();
load(&src, &LoadOptions::default()).unwrap()
}
fn load_dump_nocode(t: &Value, extra: Options) -> Value {
let opts = Options {
nocode: true,
..extra
};
let src = dump_with(t, opts).unwrap();
load(&src, &LoadOptions::default()).unwrap()
}
fn get(t: &Table, k: &Key) -> Option<Value> {
t.get(k)
}
fn assert_num(t: &Table, k: &Key, want: f64) {
let got = get(t, k);
assert!(
matches!(&got, Some(Value::Number(n)) if *n == want),
"key {k:?}: want {want}, got {got:?}"
);
}
fn assert_str(t: &Table, k: &Key, want: &[u8]) {
let got = get(t, k);
assert!(
matches!(&got, Some(Value::Str(s)) if s == want),
"key {k:?}: want {want:?}, got {got:?}"
);
}
#[test]
fn numeric_keys_all_present() {
let a = Table::new();
for i in 4..=10 {
a.set(Key::Number(i as f64), Value::Number(1.0));
}
let back = load_dump(&Value::Table(a.clone()));
let Value::Table(bt) = back else { panic!() };
for i in 4..=10 {
assert_num(&bt, &Key::Number(i as f64), 1.0);
}
}
#[test]
fn maxnum_limits_array() {
let a = Table::new();
let arr = Table::new();
for n in [7, 6, 5, 4, 3, 2, 1] {
arr.push(Value::Number(n as f64));
}
let b = Table::new();
b.push(Value::Number(1.0));
b.push(Value::Number(2.0));
a.set(Key::Str(b"a".to_vec()), Value::Table(arr));
a.set(Key::Str(b"b".to_vec()), Value::Table(b));
let opts = Options {
maxnum: Some(3),
..Options::dump()
};
let src = dump_with(&Value::Table(a), opts).unwrap();
let back = load(&src, &LoadOptions::default()).unwrap();
let Value::Table(bt) = back else { panic!() };
let Value::Table(ba) = get(&bt, &Key::Str(b"a".to_vec())).unwrap() else {
panic!()
};
let Value::Table(bb) = get(&bt, &Key::Str(b"b".to_vec())).unwrap() else {
panic!()
};
assert_eq!(ba.border(), 3);
assert_num(&ba, &Key::Number(3.0), 5.0);
assert_eq!(bb.border(), 2);
}
#[test]
fn sparse_false_keeps_size() {
let a = Table::new();
for n in 1..=4 {
a.push(Value::Number(n as f64));
}
let src_border = a.border();
let opts = Options {
sparse: false,
..Options::dump()
};
let src = dump_with(&Value::Table(a), opts).unwrap();
let back = load(&src, &LoadOptions::default()).unwrap();
let Value::Table(bt) = back else { panic!() };
assert_eq!(bt.border(), src_border);
assert_eq!(bt.border(), 4);
}
#[test]
fn shared_reference_stays_shared() {
let shared = Table::new();
shared.push(Value::Number(1.0));
let top = Table::new();
top.set(Key::Str(b"p".to_vec()), Value::Table(shared.clone()));
top.set(Key::Str(b"q".to_vec()), Value::Table(shared));
let back = load_dump(&Value::Table(top));
let Value::Table(bt) = back else { panic!() };
let Value::Table(p) = get(&bt, &Key::Str(b"p".to_vec())).unwrap() else {
panic!()
};
let Value::Table(q) = get(&bt, &Key::Str(b"q".to_vec())).unwrap() else {
panic!()
};
assert_eq!(p.id(), q.id(), "shared reference not preserved");
}
#[test]
fn self_reference_reconstructs() {
let a = Table::new();
a.set(Key::Str(b"c".to_vec()), Value::Table(a.clone()));
let back = load_dump(&Value::Table(a));
let Value::Table(bt) = back else { panic!() };
let Value::Table(c) = get(&bt, &Key::Str(b"c".to_vec())).unwrap() else {
panic!()
};
assert_eq!(c.id(), bt.id(), "self reference not reconstructed");
}
#[test]
fn table_as_key_reconstructs() {
let key_tab = Table::new();
let a = Table::new();
a.set(Key::Table(key_tab), vs("v"));
let back = load_dump(&Value::Table(a));
let Value::Table(bt) = back else { panic!() };
let e = bt.entries();
assert_eq!(e.len(), 1);
assert!(matches!(&e[0].0, Key::Table(_)));
assert!(matches!(&e[0].1, Value::Str(s) if s == b"v"));
}
#[test]
fn circular_in_selfref_section() {
let table1 = Table::new();
let inner = Table::new();
inner.set(Key::Str(b"rec1".to_vec()), Value::Table(inner.clone()));
let a = Table::new();
a.set(Key::Table(table1), Value::Table(inner));
let back = load_dump(&Value::Table(a));
let Value::Table(bt) = back else { panic!() };
let e = bt.entries();
let (_, v) = &e[0];
let Value::Table(iv) = v else { panic!() };
let Value::Table(rec) = get(iv, &Key::Str(b"rec1".to_vec())).unwrap() else {
panic!()
};
assert_eq!(rec.id(), iv.id(), "circular ref in self-ref section broken");
}
#[test]
fn mixed_array_and_boolean_keys() {
let a = Table::new();
for n in 1..=4 {
a.push(Value::Number(n as f64));
}
a.set(Key::Bool(false), Value::Number(0.0));
a.set(Key::Bool(true), Value::Number(0.0));
let src = format!(
"return {}",
serpent_serializer::line(&Value::Table(a.clone())).unwrap()
);
let src = src.strip_prefix("return ").unwrap();
let back = load(src, &LoadOptions::default()).unwrap();
let Value::Table(bt) = back else { panic!() };
assert_eq!(bt.border(), 4);
assert_num(&bt, &Key::Number(3.0), 3.0);
assert_num(&bt, &Key::Number(4.0), 4.0);
}
#[test]
fn function_value_round_trips_nocode() {
let a = Table::new();
a.set(Key::Str(b"z".to_vec()), func(Some(vec![1, 2, 3])));
a.set(Key::Str(b"x".to_vec()), Value::Number(1.0));
let back = load_dump_nocode(&Value::Table(a), Options::dump());
let Value::Table(bt) = back else { panic!() };
assert_num(&bt, &Key::Str(b"x".to_vec()), 1.0);
}
#[test]
fn master_fixture_round_trips() {
let b = Table::new();
b.set(Key::Str(b"text".to_vec()), vs("ha'ns"));
let list = Table::new();
list.set(Key::Number(1.0), vs("a"));
list.set(Key::Number(4.0), vs("f"));
list.set(Key::Number(5.0), vs("g"));
list.set(Key::Number(7.0), Value::Table(Table::new()));
list.set(Key::Str(b"3".to_vec()), Value::Number(33.0));
list.set(Key::Number(-1.0), Value::Number(-1.0));
let a = Table::new();
a.set(Key::Str(b"x".to_vec()), Value::Number(1.0));
a.set(Key::Bool(true), Value::Table(b.clone()));
a.set(Key::Bool(false), Value::Number(2.0));
a.set(Key::Str(b"true".to_vec()), vs("some value"));
a.set(Key::Str(b"list".to_vec()), Value::Table(list));
a.set(
Key::Global(Global {
name: "io.stdin".to_string(),
id: 900,
}),
Value::Number(3.0),
);
a.set(Key::Str(b"label 2".to_vec()), Value::Table(b.clone()));
a.set(Key::Table(b.clone()), Value::Number(f64::NAN));
a.set(Key::Number(f64::INFINITY), Value::Number(f64::NEG_INFINITY));
a.set(Key::Str(b"c".to_vec()), Value::Nil);
a.set(Key::Str(b"c".to_vec()), Value::Table(a.clone()));
let opts = Options {
sparse: false,
nocode: true,
..Options::dump()
};
let src = dump_with(&Value::Table(a.clone()), opts).unwrap();
let back = load(&src, &LoadOptions::default()).unwrap();
let Value::Table(bt) = back else {
panic!("not a table: {src}")
};
assert_num(&bt, &Key::Bool(false), 2.0);
match get(&bt, &Key::Number(f64::INFINITY)) {
Some(Value::Number(n)) => assert_eq!(n, f64::NEG_INFINITY),
other => panic!("huge key value: {other:?}"),
}
let Value::Table(bl) = get(&bt, &Key::Str(b"list".to_vec())).unwrap() else {
panic!()
};
assert_num(&bl, &Key::Str(b"3".to_vec()), 33.0);
assert_num(&bl, &Key::Number(-1.0), -1.0);
assert_str(&bl, &Key::Number(4.0), b"f");
let Value::Table(c) = get(&bt, &Key::Str(b"c".to_vec())).unwrap() else {
panic!()
};
assert_eq!(c.id(), bt.id());
let Value::Table(t_true) = get(&bt, &Key::Bool(true)).unwrap() else {
panic!()
};
let Value::Table(t_label) = get(&bt, &Key::Str(b"label 2".to_vec())).unwrap() else {
panic!()
};
assert_eq!(t_true.id(), t_label.id(), "shared ref b not preserved");
}
#[test]
fn block_determinism() {
let a = Table::new();
a.set(Key::Str(b"a".to_vec()), vs("a"));
a.set(Key::Str(b"b".to_vec()), vs("b"));
a.push(Value::Number(1.0));
a.push(Value::Number(2.0));
let first = block(&Value::Table(a.clone())).unwrap();
let reloaded = load(&first, &LoadOptions::default()).unwrap();
let second = block(&reloaded).unwrap();
assert_eq!(common::norm_addr(&first), common::norm_addr(&second));
assert!(deep_eq(&Value::Table(a), &reloaded));
}