mod common;
use common::deep_eq;
use serpent_serializer::load::LoadError;
use serpent_serializer::value::{Key, Table, Value};
use serpent_serializer::{block, dump, line, load, LoadOptions};
#[test]
fn load_number() {
let src = line(&Value::Number(10.0)).unwrap();
let v = load(&src, &LoadOptions::default()).unwrap();
assert!(matches!(v, Value::Number(n) if n == 10.0));
}
#[test]
fn load_boolean() {
let src = line(&Value::Bool(true)).unwrap();
let v = load(&src, &LoadOptions::default()).unwrap();
assert!(matches!(v, Value::Bool(true)));
}
#[test]
fn load_pretty_table() {
let t = Table::new();
t.push(Value::Number(3.0));
t.push(Value::Number(4.0));
let src = line(&Value::Table(t)).unwrap();
let v = load(&src, &LoadOptions::default()).unwrap();
let Value::Table(back) = v else {
panic!("not a table")
};
let e = back.entries();
assert_eq!(e.len(), 2);
assert!(matches!(e[0].1, Value::Number(n) if n == 3.0));
assert!(matches!(e[1].1, Value::Number(n) if n == 4.0));
}
#[test]
fn load_dumped_table() {
let t = Table::new();
t.push(Value::Number(3.0));
t.push(Value::Number(4.0));
let src = dump(&Value::Table(t)).unwrap();
let v = load(&src, &LoadOptions::default()).unwrap();
let Value::Table(back) = v else {
panic!("not a table")
};
assert_eq!(back.border(), 2);
}
#[test]
fn safe_rejects_calls() {
let r = load("{a = math.random()}", &LoadOptions::default());
assert_eq!(r.unwrap_err(), LoadError::UnsafeCall);
}
#[test]
fn unsafe_allows_calls() {
let opts = LoadOptions { safe: Some(false) };
let r = load("{a = math.random()}", &opts);
assert!(r.is_ok());
}
#[test]
fn keyword_prefix_names_load_as_identifiers() {
for src in ["double", "done", "doit", "normal", "math.huge"] {
let v = load(src, &LoadOptions::default()).unwrap();
let Value::Global(g) = v else {
panic!("expected global for {src}")
};
assert_eq!(g.name, src);
}
for src in [
"do local _={}; returnx = _; return _ end",
"do local _={}; localx[1] = _; return _ end",
] {
let v = load(src, &LoadOptions::default()).unwrap();
assert!(matches!(v, Value::Table(_)));
}
}
#[test]
fn bracketed_global_access_returns_syntax_errors() {
let opts = LoadOptions { safe: Some(false) };
for src in ["foo[", "foo[bar"] {
let r = load(src, &opts);
assert!(r.is_err(), "expected error for {src}, got {r:?}");
}
}
#[test]
fn skipped_calls_ignore_parentheses_inside_strings() {
let opts = LoadOptions { safe: Some(false) };
let v = load("{a = math.random(')')}", &opts).unwrap();
let Value::Table(t) = v else {
panic!("expected table")
};
assert_eq!(t.get(&Key::Str(b"a".to_vec())), Some(Value::Nil));
}
#[test]
fn safe_rejects_do_error() {
let r = load("do error('not allowed') end", &LoadOptions::default());
assert_eq!(r.unwrap_err(), LoadError::UnsafeCall);
}
#[test]
fn rejects_trailing_call_after_expression() {
let r = load("{a=1}; os.execute(\"x\")", &LoadOptions::default());
assert!(r.is_err(), "expected error, got {r:?}");
}
#[test]
fn rejects_do_block_without_end() {
let r = load("do local _={}; return _", &LoadOptions::default());
assert!(r.is_err(), "expected error, got {r:?}");
}
#[test]
fn sandbox_allows_global_assignment() {
for src in [
"do print = error end",
"do _G.print = error end",
"do _G._G.print = error end",
"do _G = nil _G.print = error end",
] {
let r = load(src, &LoadOptions::default());
assert!(r.is_ok(), "expected ok for {src}, got {r:?}");
}
}
#[test]
fn block_round_trips() {
let t = Table::new();
t.set(Key::Str(b"a".to_vec()), Value::Str(b"a".to_vec()));
t.set(Key::Str(b"b".to_vec()), Value::Str(b"b".to_vec()));
t.push(Value::Number(1.0));
t.push(Value::Number(2.0));
let src = block(&Value::Table(t.clone())).unwrap();
let back = load(&src, &LoadOptions::default()).unwrap();
assert!(deep_eq(&Value::Table(t), &back));
}
#[test]
fn leading_comment_in_table_parses() {
let v = load("{--[[x]] 1, 2}", &LoadOptions::default()).unwrap();
let Value::Table(t) = v else { panic!() };
let e = t.entries();
assert_eq!(e.len(), 2);
assert!(matches!(e[0].1, Value::Number(n) if n == 1.0));
assert!(matches!(e[1].1, Value::Number(n) if n == 2.0));
}
#[test]
fn empty_table_with_only_comment_parses() {
let v = load("{--[[x]]}", &LoadOptions::default()).unwrap();
let Value::Table(t) = v else { panic!() };
assert!(t.is_empty());
}
#[test]
fn string_escaping_round_trips() {
let t = Table::new();
t.set(Key::Str(b"str".to_vec()), Value::Str(b"\"\n'\\\0".to_vec()));
let src = block(&Value::Table(t.clone())).unwrap();
let back = load(&src, &LoadOptions::default()).unwrap();
let Value::Table(bt) = back else {
panic!("not table")
};
let got = bt.get(&Key::Str(b"str".to_vec())).unwrap();
assert!(matches!(got, Value::Str(ref s) if s == b"\"\n'\\\0"));
}
#[test]
fn function_stub_rejects_missing_body_end() {
let r = load("function()", &LoadOptions::default());
assert!(r.is_err(), "expected error, got {r:?}");
}
#[test]
fn function_stub_rejects_open_if_block() {
let r = load("function() if true then", &LoadOptions::default());
assert!(r.is_err(), "expected error, got {r:?}");
}
#[test]
fn function_stub_rejects_if_end_without_body_end() {
let r = load("function() if true then end", &LoadOptions::default());
assert!(r.is_err(), "expected error, got {r:?}");
}
#[test]
fn function_stub_skips_opaque_body_tokens() {
for src in [
"function() local s = 'do' end",
"function() local x = specif() end",
"function() local name = doer end",
"function() -- do\n end",
"function() --[[do]] end",
"function() local s = [[do]] end",
"function() local s = [==[ do ]==] end",
"function() --[==[ x ]==] end",
"function() local s = \"\\\"do\\\"\" end",
] {
let r = load(src, &LoadOptions::default());
assert_eq!(r, Ok(Value::Nil), "expected nil for {src}");
}
}