mod common;
use common::norm_addr;
use serpent_serializer::options::Options;
use serpent_serializer::value::{Global, Key, Table, Value};
use serpent_serializer::{block_with, dump_with, line_with, serialize, SerError};
use std::collections::HashSet;
use std::sync::atomic::{AtomicBool, Ordering};
fn vs(s: &str) -> Value {
Value::Str(s.as_bytes().to_vec())
}
fn set(items: &[&str]) -> HashSet<String> {
items.iter().map(|s| s.to_string()).collect()
}
fn str_keys(items: &[&str]) -> Vec<Key> {
items
.iter()
.map(|s| Key::Str(s.as_bytes().to_vec()))
.collect()
}
fn mixed_table() -> Table {
let a = Table::new();
a.set(Key::Str(b"z".to_vec()), func_stub());
a.set(Key::Str(b"x".to_vec()), Value::Number(1.0));
let list = Table::new();
list.push(Value::Number(1.0));
a.set(Key::Str(b"list".to_vec()), Value::Table(list));
a.set(Key::Str(b"true".to_vec()), vs("v"));
a
}
fn func_stub() -> Value {
Value::Function(serpent_serializer::value::Func {
id: 1,
bytecode: Some(vec![9, 9]),
})
}
#[test]
fn valtypeignore_drops_function_and_table() {
let opts = Options {
valtypeignore: Some(set(&["function", "table"])),
sortkeys: true,
..Options::dump()
};
let got = dump_with(&Value::Table(mixed_table()), opts).unwrap();
assert_eq!(got, "do local _={[\"true\"]=\"v\",x=1};return _;end");
}
#[test]
fn keyallow_keeps_only_listed() {
let opts = Options {
keyallow: Some(str_keys(&["x", "list"])),
sortkeys: true,
..Options::dump()
};
let got = dump_with(&Value::Table(mixed_table()), opts).unwrap();
assert_eq!(got, "do local _={list={},x=1};return _;end");
}
#[test]
fn keyignore_drops_named_key() {
let a = Table::new();
a.set(Key::Str(b"a".to_vec()), Value::Number(1.0));
a.set(Key::Str(b"b".to_vec()), Value::Number(2.0));
let opts = Options {
keyignore: Some(str_keys(&["b"])),
sortkeys: true,
..Options::dump()
};
let got = dump_with(&Value::Table(a), opts).unwrap();
assert_eq!(got, "do local _={a=1};return _;end");
}
#[test]
fn valignore_by_scalar() {
let a = Table::new();
a.set(Key::Str(b"keep".to_vec()), Value::Number(1.0));
a.set(Key::Str(b"drop".to_vec()), Value::Number(2.0));
let opts = Options {
valignore_scalar: Some(vec![Value::Number(2.0)]),
sortkeys: true,
..Options::dump()
};
let got = dump_with(&Value::Table(a), opts).unwrap();
assert_eq!(got, "do local _={keep=1};return _;end");
}
#[test]
fn maxlength_negative_empties_table() {
let a = Table::new();
a.push(Value::Number(1.0));
a.push(Value::Number(2.0));
a.push(Value::Number(3.0));
let opts = Options {
maxlength: Some(-1),
..Options::line()
};
let got = line_with(&Value::Table(a), opts).unwrap();
assert_eq!(norm_addr(&got), "{} --[[maxlen]]");
}
#[test]
fn maxlength_positive_keeps_within_budget() {
let a = Table::new();
for n in 1..=5 {
a.push(Value::Number(n as f64));
}
let opts = Options {
maxlength: Some(6),
..Options::block()
};
let got = block_with(&Value::Table(a), opts).unwrap();
let want = "{\n 1,\n 2,\n 3,\n 4,\n 5\n} --[[table: 0xADDR]]";
assert_eq!(norm_addr(&got), want);
}
#[test]
fn custom_formatter_reshapes_output() {
let a = Table::new();
a.push(Value::Number(1.0));
a.push(Value::Number(2.0));
let opts = Options {
custom: Some(Box::new(|tag, head, body, tail, _level| {
format!("{tag}Foo{head}{body}{tail}")
})),
..Options::line()
};
let got = line_with(&Value::Table(a), opts).unwrap();
assert_eq!(norm_addr(&got), "Foo{1, 2} --[[table: 0xADDR]]");
}
#[test]
fn custom_sort_orders_keys() {
let a = Table::new();
a.set(Key::Str(b"a".to_vec()), Value::Number(1.0));
a.set(Key::Str(b"b".to_vec()), Value::Number(2.0));
a.set(Key::Str(b"c".to_vec()), Value::Number(3.0));
let opts = Options {
sortfn: Some(Box::new(
|keys: &mut Vec<Key>, _entries: &[(Key, Value)]| {
keys.sort_by_key(|k| std::cmp::Reverse(key_text(k)));
},
)),
..Options::line()
};
let got = line_with(&Value::Table(a), opts).unwrap();
assert_eq!(norm_addr(&got), "{c = 3, b = 2, a = 1} --[[table: 0xADDR]]");
}
fn key_text(k: &Key) -> String {
match k {
Key::Str(s) => String::from_utf8_lossy(s).into_owned(),
Key::Number(n) => n.to_string(),
_ => String::new(),
}
}
#[test]
fn custom_sort_can_rank_by_value() {
let a = Table::new();
a.set(Key::Str(b"a".to_vec()), Value::Number(1.0));
a.set(Key::Str(b"b".to_vec()), Value::Number(2.0));
a.set(Key::Str(b"c".to_vec()), Value::Number(3.0));
let opts = Options {
sortfn: Some(Box::new(|keys: &mut Vec<Key>, entries: &[(Key, Value)]| {
let value_of = |k: &Key| -> f64 {
entries
.iter()
.find(|(ek, _)| ek == k)
.and_then(|(_, v)| match v {
Value::Number(n) => Some(*n),
_ => None,
})
.unwrap_or(0.0)
};
keys.sort_by(|x, y| value_of(y).partial_cmp(&value_of(x)).unwrap());
})),
..Options::line()
};
let got = line_with(&Value::Table(a), opts).unwrap();
assert_eq!(norm_addr(&got), "{c = 3, b = 2, a = 1} --[[table: 0xADDR]]");
}
#[test]
fn keyallow_matches_numeric_key() {
let a = Table::new();
a.set(Key::Number(10.0), Value::Str(b"ten".to_vec()));
a.set(Key::Number(20.0), Value::Str(b"twenty".to_vec()));
let opts = Options {
keyallow: Some(vec![Key::Number(20.0)]),
sortkeys: true,
..Options::dump()
};
let got = dump_with(&Value::Table(a), opts).unwrap();
assert_eq!(got, "do local _={[20]=\"twenty\"};return _;end");
}
#[test]
fn keyignore_matches_numeric_key() {
let a = Table::new();
a.set(Key::Number(10.0), Value::Str(b"ten".to_vec()));
a.set(Key::Number(20.0), Value::Str(b"twenty".to_vec()));
let opts = Options {
keyignore: Some(vec![Key::Number(10.0)]),
sortkeys: true,
..Options::dump()
};
let got = dump_with(&Value::Table(a), opts).unwrap();
assert_eq!(got, "do local _={[20]=\"twenty\"};return _;end");
}
static CALLED: AtomicBool = AtomicBool::new(false);
#[test]
fn sort_not_called_on_numeric_only() {
let a = Table::new();
for n in 1..=5 {
a.push(Value::Number(n as f64));
}
CALLED.store(false, Ordering::Relaxed);
let opts = Options {
sortfn: Some(Box::new(
|_keys: &mut Vec<Key>, _entries: &[(Key, Value)]| {
CALLED.store(true, Ordering::Relaxed);
},
)),
sparse: false,
..Options::dump()
};
let _ = dump_with(&Value::Table(a.clone()), opts).unwrap();
assert!(
!CALLED.load(Ordering::Relaxed),
"sort ran on numeric-only table"
);
CALLED.store(false, Ordering::Relaxed);
let opts = Options {
sortfn: Some(Box::new(
|_keys: &mut Vec<Key>, _entries: &[(Key, Value)]| {
CALLED.store(true, Ordering::Relaxed);
},
)),
sparse: false,
maxnum: Some(3),
..Options::dump()
};
let _ = dump_with(&Value::Table(a), opts).unwrap();
assert!(!CALLED.load(Ordering::Relaxed), "sort ran with maxnum");
}
#[test]
fn fatal_errors_on_non_serializable() {
let a = Table::new();
a.set(
Key::Str(b"g".to_vec()),
Value::Global(Global {
name: String::new(),
id: 5,
}),
);
let opts = Options {
fatal: true,
..Options::dump()
};
let err = serialize(&Value::Table(a), &opts).unwrap_err();
let SerError::NotSerializable(value) = &err;
assert!(value.starts_with("userdata: 0x") || !value.is_empty());
assert!(err.to_string().starts_with("Can't serialize "));
}
#[test]
fn comment_level_bounds_depth() {
let inner = Table::new();
inner.set(Key::Str(b"y".to_vec()), Value::Number(2.0));
let a = Table::new();
a.set(Key::Str(b"list".to_vec()), Value::Table(inner));
a.set(Key::Str(b"x".to_vec()), Value::Number(1.0));
let opts = Options {
comment: Some(1),
nocode: true,
sortkeys: true,
..Options::block()
};
let got = block_with(&Value::Table(a), opts).unwrap();
assert_eq!(got.matches("--[[table:").count(), 1);
}
#[test]
fn nocode_emits_empty_stub() {
let a = Table::new();
a.set(Key::Str(b"f".to_vec()), func_stub());
let opts = Options {
nocode: true,
..Options::line()
};
let got = line_with(&Value::Table(a), opts).unwrap();
assert!(got.contains("function() --[[..skipped..]] end"), "{got}");
}
#[test]
fn serialize_direct_matches_dump_options() {
let a = Table::new();
a.push(Value::Number(1.0));
a.push(Value::Number(2.0));
a.push(Value::Number(3.0));
let opts = Options {
name: Some("_".to_string()),
compact: true,
sparse: true,
..Options::serialize()
};
let got = serialize(&Value::Table(a), &opts).unwrap();
assert_eq!(got, "do local _={1,2,3};return _;end");
}