use crate::ErrorKind;
use crate::Span;
use crate::Table;
macro_rules! is_number_error {
($kind:expr) => {
matches!(
$kind,
ErrorKind::InvalidInteger(_)
| ErrorKind::InvalidFloat(_)
| ErrorKind::InvalidDateTime(_)
)
};
}
struct TestCtx {
arena: crate::arena::Arena,
}
impl TestCtx {
fn new() -> Self {
Self {
arena: crate::arena::Arena::new(),
}
}
fn parse_ok<'a>(&'a self, input: &'a str) -> Table<'a> {
crate::parser::parse(input, &self.arena)
.unwrap_or_else(|e| panic!("parse failed for {input:?}: {e}"))
.into_table()
}
fn parse_err<'a>(&'a self, input: &'a str) -> crate::Error {
match crate::parser::parse(input, &self.arena) {
Ok(_) => panic!("For input `{input}` expected error but parsed successfully"),
Err(err) => err,
}
}
}
#[test]
fn basic_scalar_values() {
let ctx = TestCtx::new();
let v = ctx.parse_ok("");
assert!(v.is_empty());
let v = ctx.parse_ok("a = \"hello\"");
assert_eq!(v["a"].as_str(), Some("hello"));
let v = ctx.parse_ok("a = 42");
assert_eq!(v["a"].as_i64(), Some(42));
let v = ctx.parse_ok("a = -100");
assert_eq!(v["a"].as_i64(), Some(-100));
let v = ctx.parse_ok("a = 3.15");
let f = v["a"].as_f64().unwrap();
assert!((f - 3.15).abs() < f64::EPSILON);
let v = ctx.parse_ok("a = true");
assert_eq!(v["a"].as_bool(), Some(true));
let v = ctx.parse_ok("a = false");
assert_eq!(v["a"].as_bool(), Some(false));
let v = ctx.parse_ok("a = 1\nb = 2\nc = 3");
assert_eq!(v.len(), 3);
assert_eq!(v["a"].as_i64(), Some(1));
assert_eq!(v["c"].as_i64(), Some(3));
}
#[test]
fn string_escapes() {
let ctx = TestCtx::new();
let cases = [
(r#"a = "line1\nline2""#, "line1\nline2"),
(r#"a = "col1\tcol2""#, "col1\tcol2"),
(r#"a = "path\\to""#, "path\\to"),
(r#"a = "say \"hi\"""#, "say \"hi\""),
(r#"a = "\u0041""#, "A"),
(r#"a = "\U00000041""#, "A"),
];
for (input, expected) in cases {
let v = ctx.parse_ok(input);
assert_eq!(v["a"].as_str(), Some(expected), "input: {input}");
}
}
#[test]
fn string_types() {
let ctx = TestCtx::new();
let cases = [
("a = \"\"\"\nhello\nworld\"\"\"", "hello\nworld"),
("a = '''\nhello\nworld'''", "hello\nworld"),
(r#"a = 'no\escape'"#, "no\\escape"),
(r#"a = """#, ""),
];
for (input, expected) in cases {
let v = ctx.parse_ok(input);
assert_eq!(v["a"].as_str(), Some(expected), "input: {input}");
}
}
#[test]
fn number_formats() {
let ctx = TestCtx::new();
let int_cases = [
("a = 0xDEAD", 0xDEAD),
("a = 0o777", 0o777),
("a = 0b1010", 0b1010),
("a = 1_000_000", 1_000_000),
];
for (input, expected) in int_cases {
let v = ctx.parse_ok(input);
assert_eq!(v["a"].as_i64(), Some(expected), "input: {input}");
}
let float_cases = [
("a = inf", f64::INFINITY, false),
("a = -inf", f64::NEG_INFINITY, false),
("a = nan", f64::NAN, true),
("a = -nan", f64::NAN, true),
];
for (input, expected, is_nan) in float_cases {
let v = ctx.parse_ok(input);
let f = v["a"].as_f64().unwrap();
if is_nan {
assert!(f.is_nan(), "input: {input}");
} else {
assert_eq!(f, expected, "input: {input}");
}
}
let float_approx_cases = [
("a = 1e10", 1e10, 1.0),
("a = 1.5E-3", 1.5e-3, 1e-10),
("a = 1_000.5", 1000.5, f64::EPSILON),
];
for (input, expected, epsilon) in float_approx_cases {
let v = ctx.parse_ok(input);
let f = v["a"].as_f64().unwrap();
assert!((f - expected).abs() < epsilon, "input: {input}");
}
}
#[test]
fn arrays() {
let ctx = TestCtx::new();
let v = ctx.parse_ok("a = [1, 2, 3]");
assert_eq!(v["a"].as_array().unwrap().len(), 3);
assert_eq!(v["a"][0].as_i64(), Some(1));
assert_eq!(v["a"][2].as_i64(), Some(3));
let v = ctx.parse_ok("a = []");
assert!(v["a"].as_array().unwrap().is_empty());
let v = ctx.parse_ok("a = [[1, 2], [3, 4]]");
assert_eq!(v["a"].as_array().unwrap().len(), 2);
assert_eq!(v["a"][0].as_array().unwrap().len(), 2);
}
#[test]
fn split_keys_error() {
let ctx = TestCtx::new();
ctx.parse_err("a.\nb = 1");
ctx.parse_err("a\n.b = 1");
ctx.parse_err("a\n.\nb = 1");
ctx.parse_err("[a\n.\nb]\nc = 1");
ctx.parse_err("[a\n.b]\nc = 1");
ctx.parse_err("[a.\nb]\nc = 1");
ctx.parse_err("a={a\n.b=1}");
ctx.parse_err("a={a.\nb=1}");
ctx.parse_err("a={a\n.\nb=1}");
}
#[test]
fn inline_tables() {
let ctx = TestCtx::new();
let v = ctx.parse_ok("a = {x = 1, y = 2}");
assert_eq!(v["a"].as_table().unwrap().len(), 2);
assert_eq!(v["a"]["x"].as_i64(), Some(1));
assert_eq!(v["a"]["y"].as_i64(), Some(2));
let v = ctx.parse_ok("a = {}");
assert!(v["a"].as_table().unwrap().is_empty());
let v = ctx.parse_ok("a = {b = {c = 1}}");
assert_eq!(v["a"]["b"]["c"].as_i64(), Some(1));
let v = ctx.parse_ok("a = [{x = 1}, {x = 2}]");
assert_eq!(v["a"].as_array().unwrap().len(), 2);
assert_eq!(v["a"][0]["x"].as_i64(), Some(1));
}
#[test]
fn table_headers_and_structure() {
let ctx = TestCtx::new();
let v = ctx.parse_ok("[table]\nkey = 1");
assert_eq!(v["table"]["key"].as_i64(), Some(1));
let v = ctx.parse_ok("[a]\nx = 1\n[b]\ny = 2");
assert_eq!(v["a"]["x"].as_i64(), Some(1));
assert_eq!(v["b"]["y"].as_i64(), Some(2));
let v = ctx.parse_ok("[a.b.c]\nkey = 1");
assert_eq!(v["a"]["b"]["c"]["key"].as_i64(), Some(1));
let v = ctx.parse_ok("a.b.c = 1");
assert_eq!(v["a"]["b"]["c"].as_i64(), Some(1));
let v = ctx.parse_ok("a.x = 1\na.y = 2");
assert_eq!(v["a"]["x"].as_i64(), Some(1));
assert_eq!(v["a"]["y"].as_i64(), Some(2));
let v = ctx.parse_ok("[[items]]\nname = \"a\"\n[[items]]\nname = \"b\"");
assert_eq!(v["items"].as_array().unwrap().len(), 2);
assert_eq!(v["items"][0]["name"].as_str(), Some("a"));
assert_eq!(v["items"][1]["name"].as_str(), Some("b"));
let v = ctx.parse_ok("[[fruit]]\nname = \"apple\"\n[fruit.physical]\ncolor = \"red\"");
assert_eq!(v["fruit"][0]["name"].as_str(), Some("apple"));
assert_eq!(v["fruit"][0]["physical"]["color"].as_str(), Some("red"));
let v = ctx.parse_ok("[a.b]\nx = 1\n[a]\ny = 2");
assert_eq!(v["a"]["y"].as_i64(), Some(2));
assert_eq!(v["a"]["b"]["x"].as_i64(), Some(1));
}
#[test]
fn table_indexing_thresholds() {
let ctx = TestCtx::new();
let v = ctx.parse_ok("a = 1\nb = 2\nc = 3\nd = 4\ne = 5");
assert_eq!(v.len(), 5);
assert_eq!(v["e"].as_i64(), Some(5));
let v = ctx.parse_ok("a = 1\nb = 2\nc = 3\nd = 4\ne = 5\nf = 6");
assert_eq!(v.len(), 6);
assert_eq!(v["a"].as_i64(), Some(1));
assert_eq!(v["f"].as_i64(), Some(6));
let v = ctx.parse_ok("a = 1\nb = 2\nc = 3\nd = 4\ne = 5\nf = 6\ng = 7");
assert_eq!(v.len(), 7);
assert_eq!(v["g"].as_i64(), Some(7));
let mut lines = Vec::new();
for i in 0..20 {
lines.push(format!("key{i} = {i}"));
}
let input = lines.join("\n");
let v = ctx.parse_ok(&input);
assert_eq!(v.len(), 20);
assert_eq!(v["key0"].as_i64(), Some(0));
assert_eq!(v["key19"].as_i64(), Some(19));
let mut lines = vec!["[sub]".to_string()];
for i in 0..6 {
lines.push(format!("k{i} = {i}"));
}
let input = lines.join("\n");
let v = ctx.parse_ok(&input);
assert_eq!(v["sub"].as_table().unwrap().len(), 6);
assert_eq!(v["sub"]["k5"].as_i64(), Some(5));
}
#[test]
fn parse_errors() {
let ctx = TestCtx::new();
let e = ctx.parse_err("a = 1\na = 2");
assert!(matches!(e.kind(), ErrorKind::DuplicateKey { .. }));
let e = ctx.parse_err("a = \"unterminated");
assert!(matches!(e.kind(), ErrorKind::UnterminatedString('"')));
let e = ctx.parse_err(r#"a = "\z""#);
assert!(matches!(e.kind(), ErrorKind::InvalidEscape('z')));
let e = ctx.parse_err("[t]\na = 1\n[t]\nb = 2");
assert!(matches!(e.kind(), ErrorKind::DuplicateTable { .. }));
let e = ctx.parse_err("a = ");
assert!(matches!(e.kind(), ErrorKind::UnexpectedEof));
let e = ctx.parse_err("a = 0x");
assert!(is_number_error!(e.kind()));
let e = ctx.parse_err("a = 1\n[a]\nb = 2");
assert!(matches!(
e.kind(),
ErrorKind::DuplicateTable { .. } | ErrorKind::DuplicateKey { .. }
));
let e = ctx.parse_err("a = {x = 1}\n[a]\ny = 2");
assert!(matches!(
e.kind(),
ErrorKind::DuplicateTable { .. } | ErrorKind::DuplicateKey { .. }
));
}
#[test]
fn error_span_in_range() {
let ctx = TestCtx::new();
let cases = [
"a = 0.",
"a = 0.e",
"a = 0.E",
"a = 0.0E",
"a = 0.0e",
"a = 0.0e-",
"a = 0.0e+",
"a = ",
"key =",
];
for input in cases {
let e = ctx.parse_err(input);
let span = e.span();
assert!(
(span.start as usize) <= input.len() && (span.end as usize) <= input.len(),
"span {span:?} out of range for {input:?} (len {})",
input.len()
);
assert!(
span.start <= span.end,
"inverted span {span:?} for {input:?}"
);
}
}
#[test]
fn quoted_keys_and_spans() {
let ctx = TestCtx::new();
let v = ctx.parse_ok(r#""quoted key" = 1"#);
assert_eq!(v["quoted key"].as_i64(), Some(1));
let v = ctx.parse_ok(r#""key\nwith\nnewlines" = 1"#);
assert_eq!(v["key\nwith\nnewlines"].as_i64(), Some(1));
let v = ctx.parse_ok("'literal key' = 1");
assert_eq!(v["literal key"].as_i64(), Some(1));
let input = "key = 42";
let v = ctx.parse_ok(input);
let span = v.get("key").unwrap().span_unchecked();
assert_eq!(&input[span.start as usize..span.end as usize], "42");
let input = "key = \"hello\"";
let v = ctx.parse_ok(input);
let span = v.get("key").unwrap().span_unchecked();
assert_eq!(&input[span.start as usize..span.end as usize], "\"hello\"");
}
#[test]
fn comments_and_whitespace() {
let ctx = TestCtx::new();
let v = ctx.parse_ok("# comment\na = 1 # inline comment\n# another");
assert_eq!(v["a"].as_i64(), Some(1));
let v = ctx.parse_ok("\n\n\na = 1\n\n\n");
assert_eq!(v["a"].as_i64(), Some(1));
}
#[test]
fn value_into_kind() {
let ctx = TestCtx::new();
let mut v = ctx.parse_ok("a = \"hello\"\nb = 42\nc = [1, 2]\nd = {x = 1}");
let (_, a) = v.remove_entry("a").unwrap();
assert_eq!(a.as_str().unwrap(), "hello");
let (_, b) = v.remove_entry("b").unwrap();
assert_eq!(b.as_i64().unwrap(), 42);
let (_, c) = v.remove_entry("c").unwrap();
assert_eq!(c.as_array().unwrap().len(), 2);
let (_, d) = v.remove_entry("d").unwrap();
assert_eq!(d.as_table().unwrap().len(), 1);
}
#[test]
fn recursion_depth_at_limit() {
let depth = super::MAX_RECURSION_DEPTH as usize;
let ctx = TestCtx::new();
let mut input = String::from("a = ");
for _ in 0..depth {
input.push('[');
}
input.push('1');
for _ in 0..depth {
input.push(']');
}
ctx.parse_ok(&input);
let mut input = String::from("a = ");
for _ in 0..depth {
input.push_str("{b= ");
}
input.push('1');
for _ in 0..depth {
input.push('}');
}
ctx.parse_ok(&input);
let mut input = String::from("a = ");
for _ in 0..depth / 2 {
input.push_str("[{b= ");
}
input.push('1');
for _ in 0..depth / 2 {
input.push_str("}]");
}
ctx.parse_ok(&input);
}
#[test]
fn recursion_depth_over_limit() {
let depth = super::MAX_RECURSION_DEPTH as usize;
let ctx = TestCtx::new();
let mut input = String::from("a = ");
for _ in 0..=depth {
input.push('[');
}
input.push('1');
for _ in 0..=depth {
input.push(']');
}
let e = ctx.parse_err(&input);
assert!(matches!(
e.kind(),
ErrorKind::OutOfRange {
ty: &"Max recursion depth exceeded",
..
}
));
let mut input = String::from("a = ");
for _ in 0..=depth {
input.push_str("{b= ");
}
input.push('1');
for _ in 0..=depth {
input.push('}');
}
let e = ctx.parse_err(&input);
assert!(matches!(
e.kind(),
ErrorKind::OutOfRange {
ty: &"Max recursion depth exceeded",
..
}
));
let mut input = String::from("a = ");
for _ in 0..=depth / 2 {
input.push_str("[{b= ");
}
input.push('1');
for _ in 0..=depth / 2 {
input.push_str("}]");
}
let e = ctx.parse_err(&input);
assert!(matches!(
e.kind(),
ErrorKind::OutOfRange {
ty: &"Max recursion depth exceeded",
..
}
));
}
#[test]
fn mixed_content() {
let ctx = TestCtx::new();
let input = r#"
title = "TOML Example"
enabled = true
count = 100
ratio = 0.5
[database]
server = "192.168.1.1"
ports = [8001, 8001, 8002]
enabled = true
[servers.alpha]
ip = "10.0.0.1"
[servers.beta]
ip = "10.0.0.2"
[[products]]
name = "Hammer"
sku = 738594937
[[products]]
name = "Nail"
sku = 284758393
"#;
let v = ctx.parse_ok(input);
assert_eq!(v["title"].as_str(), Some("TOML Example"));
assert_eq!(v["count"].as_i64(), Some(100));
assert_eq!(v["database"]["ports"].as_array().unwrap().len(), 3);
assert_eq!(v["servers"]["alpha"]["ip"].as_str(), Some("10.0.0.1"));
assert_eq!(v["products"].as_array().unwrap().len(), 2);
assert_eq!(v["products"][0]["name"].as_str(), Some("Hammer"));
}
#[test]
fn utf8_bom_is_skipped() {
let ctx = TestCtx::new();
let v = ctx.parse_ok("\u{FEFF}");
assert!(v.is_empty());
let v = ctx.parse_ok("\u{FEFF}a = 1");
assert_eq!(v["a"].as_i64(), Some(1));
let v = ctx.parse_ok("\u{FEFF}[section]\nkey = \"val\"");
assert_eq!(v["section"]["key"].as_str(), Some("val"));
}
#[test]
fn crlf_handling() {
let ctx = TestCtx::new();
let v = ctx.parse_ok("a = 1\r\nb = 2\r\n");
assert_eq!(v["a"].as_i64(), Some(1), "input: a = 1\\r\\nb = 2\\r\\n");
assert_eq!(v["b"].as_i64(), Some(2), "input: a = 1\\r\\nb = 2\\r\\n");
let valid_str_cases = [
("a = \"\"\"\r\nhello\r\nworld\"\"\"", "hello\r\nworld"),
("a = \"\"\"\\\r\n trimmed\"\"\"", "trimmed"),
("a = \"\"\"\\\n\r\n trimmed\"\"\"", "trimmed"),
("a = \"\"\"\r\ncontent\"\"\"", "content"),
("a = \"\"\"\r\nline1\r\nline2\"\"\"", "line1\r\nline2"),
];
for (input, expected) in valid_str_cases {
let v = ctx.parse_ok(input);
assert_eq!(v["a"].as_str(), Some(expected), "input: {input}");
}
let v = ctx.parse_ok("[table]\r\nkey = 1\r\n");
assert_eq!(
v["table"]["key"].as_i64(),
Some(1),
"input: [table]\\r\\nkey = 1\\r\\n"
);
let e = ctx.parse_err("a = \"hello\rworld\"");
assert!(
matches!(e.kind(), ErrorKind::InvalidCharInString('\r')),
"input: a = \"hello\\rworld\""
);
let e = ctx.parse_err("a = \"hello\r\nworld\"");
assert!(
matches!(e.kind(), ErrorKind::UnterminatedString('"')),
"input: a = \"hello\\r\\nworld\""
);
let e = ctx.parse_err("a = [ \r ]");
assert!(
matches!(e.kind(), ErrorKind::Unexpected('\r')),
"bare CR in array: {:?}",
e.kind(),
);
let e = ctx.parse_err("a = { \r }");
assert!(
matches!(
e.kind(),
ErrorKind::Wanted {
expected: &"a table key",
found: &"a carriage return",
}
),
"bare CR in inline table: {:?}",
e.kind(),
);
let e = ctx.parse_err("a = 1\r");
assert!(
matches!(
e.kind(),
ErrorKind::Wanted {
expected: &"newline",
found: &"a carriage return",
}
),
"bare CR after key-value: {:?}",
e.kind(),
);
let e = ctx.parse_err("[a]\r");
assert!(
matches!(
e.kind(),
ErrorKind::Wanted {
expected: &"newline",
found: &"a carriage return",
}
),
"bare CR after table header: {:?}",
e.kind(),
);
}
#[test]
fn escape_sequences() {
let ctx = TestCtx::new();
let valid_cases = [
(r#"a = "\b\f""#, "\x08\x0C"),
(r#"a = "\e""#, "\x1B"),
(r#"a = "\x41""#, "A"),
(r#"a = "\r""#, "\r"),
];
for (input, expected) in valid_cases {
let v = ctx.parse_ok(input);
assert_eq!(v["a"].as_str(), Some(expected), "input: {input}");
}
let e = ctx.parse_err(r#"a = "\uGGGG""#);
assert!(
matches!(e.kind(), ErrorKind::InvalidHexEscape('G')),
"input: a = \"\\uGGGG\""
);
let e = ctx.parse_err(r#"a = "\UFFFFFFFF""#);
assert!(
matches!(e.kind(), ErrorKind::InvalidEscapeValue(_)),
"input: a = \"\\UFFFFFFFF\""
);
let e = ctx.parse_err("a = \"\\u00");
assert!(
matches!(e.kind(), ErrorKind::UnterminatedString('"')),
"input: a = \"\\u00"
);
let e = ctx.parse_err(r#"a = "\xGG""#);
assert!(
matches!(e.kind(), ErrorKind::InvalidHexEscape('G')),
"input: a = \"\\xGG\""
);
}
#[test]
fn multiline_string_edge_cases() {
let ctx = TestCtx::new();
let valid_cases = [
("a = \"\"\"\\\n trimmed\"\"\"", "trimmed"),
("a = \"\"\"\\ \n trimmed\"\"\"", "trimmed"),
("a = \"\"\"\\\t\n trimmed\"\"\"", "trimmed"),
("a = \"\"\"\\\r\n trimmed\"\"\"", "trimmed"),
("a = \"\"\"\\\n\n\n trimmed\"\"\"", "trimmed"),
("a = \"\"\"content\"\"\"\"\"", "content\"\""),
("a = \"\"\"content\"\"\"\"", "content\""),
("a = \"\"\"he said \"hi\" ok\"\"\"", "he said \"hi\" ok"),
("a = \"\"\"two \"\" here\"\"\"", "two \"\" here"),
];
for (input, expected) in valid_cases {
let v = ctx.parse_ok(input);
assert_eq!(v["a"].as_str(), Some(expected), "input: {input}");
}
let e = ctx.parse_err("a = \"\"\"\\ x\"\"\"");
assert!(
matches!(e.kind(), ErrorKind::InvalidEscape(' ')),
"input: a = \"\"\"\\ x\"\"\""
);
let e = ctx.parse_err("a = \"\"\"\\\r\r\n\"\"\"");
assert!(
matches!(e.kind(), ErrorKind::InvalidCharInString('\r')),
"bare CR after line-ending backslash: {:?}",
e.kind(),
);
}
#[test]
fn number_valid_edge_cases() {
let ctx = TestCtx::new();
let int_cases = [
("a = 0xDEAD_BEEF", 0xDEAD_BEEF),
("a = 0o755", 0o755),
("a = 0b1111_0000", 0b1111_0000),
("a = +42", 42),
];
for (input, expected) in int_cases {
let v = ctx.parse_ok(input);
assert_eq!(v["a"].as_i64(), Some(expected), "input: {input}");
}
let v = ctx.parse_ok("a = +3.15");
let f = v["a"].as_f64().unwrap();
assert!((f - 3.15).abs() < f64::EPSILON, "input: a = +3.15");
let v = ctx.parse_ok("a = +inf");
assert_eq!(v["a"].as_f64(), Some(f64::INFINITY), "input: a = +inf");
let v = ctx.parse_ok("a = +nan");
assert!(v["a"].as_f64().unwrap().is_nan(), "input: a = +nan");
}
#[test]
fn number_format_errors() {
let ctx = TestCtx::new();
let error_cases = [
"a = +0xFF",
"a = +",
"a = 0o89",
"a = 0b102",
"a = 0x",
"a = 0o",
"a = 0b",
"a = 0x_FF",
"a = 0xFF_",
"a = 0o77_",
"a = 0b11_",
"a = 0xF__F",
"a = 0o7__7",
"a = 0b1__0",
"a = 01",
"a = 123_",
"a = 1__2",
];
for input in error_cases {
let e = ctx.parse_err(input);
assert!(is_number_error!(e.kind()), "input: {input}");
}
}
#[test]
fn float_valid_edge_cases() {
let ctx = TestCtx::new();
let cases = [
("a = 5e2", 500.0, 0.01),
("a = -1.5e-3", -1.5e-3, 1e-10),
("a = 1_000.5_00", 1000.5, f64::EPSILON),
("a = 1e+5", 1e5, 0.01),
("a = 1.5E+3", 1.5e3, 0.01),
];
for (input, expected, epsilon) in cases {
let v = ctx.parse_ok(input);
let f = v["a"].as_f64().unwrap();
assert!((f - expected).abs() < epsilon, "input: {input}");
}
}
#[test]
fn float_format_errors() {
let ctx = TestCtx::new();
let error_cases = [
"a = 00.5",
"a = 1.",
"a = 1.5_",
"a = --1.0",
"a = --1",
"a = -+1",
"a = +-1",
"a = +-1.0",
"a = +-1E",
"a = --1E",
"a = ++1E",
"a = -00.5",
"a = 50E+-1",
"a = 50E-+1",
"a = 1._5",
"a = 1e\nb = 2",
];
for input in error_cases {
let e = ctx.parse_err(input);
assert!(is_number_error!(e.kind()), "input: {input}");
}
}
#[test]
fn structural_errors() {
let ctx = TestCtx::new();
let e = ctx.parse_err("a = 1\na.b = 2");
assert!(matches!(e.kind(), ErrorKind::DottedKeyInvalidType { .. }));
let e = ctx.parse_err("a = {b = 1}\na.c = 2");
assert!(matches!(
e.kind(),
ErrorKind::DuplicateTable { .. }
| ErrorKind::DuplicateKey { .. }
| ErrorKind::DottedKeyInvalidType { .. }
));
let e = ctx.parse_err("[a]\nb = 1\n[[a]]");
assert!(matches!(e.kind(), ErrorKind::RedefineAsArray { .. }));
let e = ctx.parse_err("[a]\nb = 1\n[a]\nc = 2");
assert!(matches!(e.kind(), ErrorKind::DuplicateTable { .. }));
let e = ctx.parse_err("\"\"\"key\"\"\" = 1");
assert!(matches!(e.kind(), ErrorKind::MultilineStringKey));
let e = ctx.parse_err("'''key''' = 1");
assert!(matches!(e.kind(), ErrorKind::MultilineStringKey));
let e = ctx.parse_err("a = not_a_keyword");
assert!(matches!(e.kind(), ErrorKind::UnquotedString));
let e = ctx.parse_err("a = ");
assert!(matches!(e.kind(), ErrorKind::UnexpectedEof));
let e = ctx.parse_err("key 1");
assert!(matches!(e.kind(), ErrorKind::Wanted { .. }));
let e = ctx.parse_err("a = \"line1\nline2\"");
assert!(matches!(e.kind(), ErrorKind::UnterminatedString('"')));
let e = ctx.parse_err("a = 'line1\nline2'");
assert!(matches!(e.kind(), ErrorKind::UnterminatedString('\'')));
let e = ctx.parse_err("a = \"hello\x7Fworld\"");
assert!(matches!(e.kind(), ErrorKind::InvalidCharInString(_)));
}
#[test]
fn inline_table_edge_cases() {
let ctx = TestCtx::new();
let v = ctx.parse_ok("a = {b.c = 1}");
assert_eq!(v["a"]["b"]["c"].as_i64(), Some(1), "input: a = {{b.c = 1}}");
let table_len_cases = [
("a = {x = 1, y = 2,}", 2),
("a = {\n x = 1,\n y = 2\n}", 2),
("a = {\n x = 1, # comment\n y = 2\n}", 2),
];
for (input, expected_len) in table_len_cases {
let v = ctx.parse_ok(input);
assert_eq!(
v["a"].as_table().unwrap().len(),
expected_len,
"input: {input}"
);
}
}
#[test]
fn array_edge_cases() {
let ctx = TestCtx::new();
let cases = [
("a = [\n 1, # first\n 2, # second\n 3\n]", 3),
("a = [1, 2, 3,]", 3),
];
for (input, expected_len) in cases {
let v = ctx.parse_ok(input);
assert_eq!(
v["a"].as_array().unwrap().len(),
expected_len,
"input: {input}"
);
}
}
#[test]
fn dotted_key_features() {
let ctx = TestCtx::new();
let v = ctx.parse_ok("a.b.c.d = 1");
assert_eq!(v["a"]["b"]["c"]["d"].as_i64(), Some(1));
let v = ctx.parse_ok("a.b = 1\na.c = 2");
assert_eq!(v["a"]["b"].as_i64(), Some(1));
assert_eq!(v["a"]["c"].as_i64(), Some(2));
let v = ctx.parse_ok("[a]\nb.c = 1\nb.d = 2");
assert_eq!(v["a"]["b"]["c"].as_i64(), Some(1));
assert_eq!(v["a"]["b"]["d"].as_i64(), Some(2));
}
#[test]
fn aot_and_implicit_tables() {
let ctx = TestCtx::new();
let v = ctx.parse_ok("[[a]]\nb = 1\n[a.c]\nd = 2\n[[a]]\nb = 3");
assert_eq!(v["a"].as_array().unwrap().len(), 2);
assert_eq!(v["a"][0]["b"].as_i64(), Some(1));
assert_eq!(v["a"][0]["c"]["d"].as_i64(), Some(2));
let v = ctx.parse_ok("[[a]]\nb = 1\n[a.c]\nd = 2");
assert_eq!(v["a"][0]["b"].as_i64(), Some(1));
assert_eq!(v["a"][0]["c"]["d"].as_i64(), Some(2));
let v = ctx.parse_ok("[a.b]\nc = 1\n[a]\nd = 2");
assert_eq!(v["a"]["d"].as_i64(), Some(2));
assert_eq!(v["a"]["b"]["c"].as_i64(), Some(1));
}
#[test]
fn integer_boundary_values() {
let ctx = TestCtx::new();
let cases = [
("a = -9223372036854775808", i64::MIN),
("a = 9223372036854775807", i64::MAX),
];
for (input, expected) in cases {
let v = ctx.parse_ok(input);
assert_eq!(v["a"].as_i64(), Some(expected), "input: {input}");
}
}
#[test]
fn integer_overflow_errors() {
let ctx = TestCtx::new();
let error_cases = [
"a = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
"a = 99999999999999999999999999999999999999999",
];
for input in error_cases {
let e = ctx.parse_err(input);
assert!(is_number_error!(e.kind()), "input: {input}");
}
let ok_cases = [
("a = 0xFFFFFFFFFFFFFFFF", 0xFFFFFFFFFFFFFFFFi128),
("a = 99999999999999999999", 99999999999999999999i128),
];
for (input, expected) in ok_cases {
let v = ctx.parse_ok(input);
assert_eq!(v["a"].as_i128(), Some(expected), "input: {input}");
}
}
#[test]
fn more_parse_errors() {
let ctx = TestCtx::new();
let e = ctx.parse_err("a = {x = 1, x = 2}");
assert!(matches!(e.kind(), ErrorKind::DuplicateKey { .. }));
let e = ctx.parse_err("[a]\nb = 1\nb = 2");
assert!(matches!(e.kind(), ErrorKind::DuplicateKey { .. }));
let e = ctx.parse_err("a = ]");
assert!(matches!(e.kind(), ErrorKind::Unexpected(']')));
let e = ctx.parse_err("a = }");
assert!(matches!(e.kind(), ErrorKind::Unexpected('}')));
let e = ctx.parse_err("a = -_bad");
assert!(is_number_error!(e.kind()));
let e = ctx.parse_err("[");
assert!(matches!(e.kind(), ErrorKind::Wanted { .. }));
let e = ctx.parse_err("= 1");
assert!(matches!(e.kind(), ErrorKind::Wanted { .. }));
let e = ctx.parse_err("a = 1\r");
assert!(matches!(
e.kind(),
ErrorKind::Wanted { .. } | ErrorKind::Unexpected('\r')
));
let e = ctx.parse_err("[table\na = 1");
assert!(matches!(e.kind(), ErrorKind::Wanted { .. }));
let e = ctx.parse_err("a = {x = 1");
assert!(matches!(e.kind(), ErrorKind::UnclosedInlineTable));
let e = ctx.parse_err("a = [1, 2");
assert!(matches!(e.kind(), ErrorKind::UnclosedArray));
let e = ctx.parse_err("a = 1 b = 2");
assert!(matches!(e.kind(), ErrorKind::Wanted { .. }));
let e = ctx.parse_err("a = {b = 1}\na.b.c = 2");
assert!(matches!(
e.kind(),
ErrorKind::DuplicateKey { .. } | ErrorKind::DottedKeyInvalidType { .. }
));
let e = ctx.parse_err("a = 1\n[a.b]");
assert!(matches!(e.kind(), ErrorKind::DuplicateKey { .. }));
let e = ctx.parse_err("[a]\nb = 1\n[a.b]\nc = 2");
assert!(matches!(
e.kind(),
ErrorKind::DottedKeyInvalidType { .. } | ErrorKind::DuplicateKey { .. }
));
let e = ctx.parse_err("a.b = 1\n[a.b]\nc = 2");
assert!(matches!(
e.kind(),
ErrorKind::DuplicateKey { .. } | ErrorKind::DuplicateTable { .. }
));
let e = ctx.parse_err("a = {b = 1}\n[a.c]");
assert!(matches!(e.kind(), ErrorKind::DuplicateKey { .. }));
let e = ctx.parse_err("a = 1\n[[a]]");
assert!(matches!(e.kind(), ErrorKind::DuplicateKey { .. }));
let e = ctx.parse_err("a.b = 1\n[a]\nc = 2");
assert!(matches!(
e.kind(),
ErrorKind::DuplicateTable { .. } | ErrorKind::DuplicateKey { .. }
));
let e = ctx.parse_err(r#""unterminated = 1"#);
assert!(matches!(e.kind(), ErrorKind::UnterminatedString('"')));
let e = ctx.parse_err("'unterminated = 1");
assert!(matches!(e.kind(), ErrorKind::UnterminatedString('\'')));
let e = ctx.parse_err("= 1");
assert!(matches!(
e.kind(),
ErrorKind::Wanted {
expected: &"a table key",
..
}
));
let e = ctx.parse_err("[a]\n, = 1");
assert!(matches!(e.kind(), ErrorKind::Wanted { .. }));
let e = ctx.parse_err("a = \"hello\r\nworld\"");
assert!(matches!(e.kind(), ErrorKind::UnterminatedString('"')));
}
#[test]
fn crlf_in_intermediate_contexts() {
let ctx = TestCtx::new();
let array_cases = [
("a = [\r\n1\r\n]", 1),
("a = [\r\n1,\r\n2\r\n]", 2),
("a = [\r\n 1, # comment\r\n 2\r\n]", 2),
];
for (input, expected_len) in array_cases {
let v = ctx.parse_ok(input);
assert_eq!(
v["a"].as_array().unwrap().len(),
expected_len,
"input: {input}"
);
}
let v = ctx.parse_ok("a = {\r\nx = 1\r\n}");
assert_eq!(v["a"]["x"].as_i64(), Some(1));
let ml_cases = [
("a = \"\"\"\\ \r\n trimmed\"\"\"", "trimmed"),
("a = \"\"\"\\\t\r\n trimmed\"\"\"", "trimmed"),
("a = \"\"\"\\\r\n\r\n trimmed\"\"\"", "trimmed"),
];
for (input, expected) in ml_cases {
let v = ctx.parse_ok(input);
assert_eq!(v["a"].as_str(), Some(expected), "input: {input}");
}
}
#[test]
fn scan_token_desc_branches() {
let ctx = TestCtx::new();
let header_key_cases: [(&str, &str); 4] = [
("[#]", "a comment"),
("[.]", "a period"),
("[:]", "a colon"),
("[+]", "a plus"),
];
for (input, expected_found) in header_key_cases {
let e = ctx.parse_err(input);
assert!(
matches!(
e.kind(),
ErrorKind::Wanted { found, .. } if *found == expected_found
),
"input: {input}, got: {:?}",
e.kind()
);
}
let e = ctx.parse_err("a = {x = 1 y = 2}");
assert!(
matches!(e.kind(), ErrorKind::MissingInlineTableComma),
"input: a = {{x = 1 y = 2}}, got: {:?}",
e.kind()
);
let e = ctx.parse_err("a = {x = 1 @}");
assert!(
matches!(e.kind(), ErrorKind::MissingInlineTableComma),
"input: a = {{x = 1 @}}, got: {:?}",
e.kind()
);
}
#[test]
fn string_eof_edge_cases() {
let ctx = TestCtx::new();
let eof_cases = ["a = \"", "a = \"\\", "a = \"\"\"", "a = \"\"\"\\"];
for input in eof_cases {
let e = ctx.parse_err(input);
assert!(
matches!(e.kind(), ErrorKind::UnterminatedString('"')),
"input: {input}"
);
}
}
#[test]
fn number_identifier_not_inf_nan() {
let ctx = TestCtx::new();
let cases = ["a = -infix", "a = -nah", "a = -infinity", "a = -nano"];
for input in cases {
let e = ctx.parse_err(input);
assert!(is_number_error!(e.kind()), "input: {input}");
}
}
#[test]
fn error_messages_and_spans() {
let ctx = TestCtx::new();
let e = ctx.parse_err("a = +0xFF");
assert!(matches!(
e.kind(),
ErrorKind::InvalidInteger("signs are not allowed on prefixed integers")
));
let e = ctx.parse_err("a = -0b1");
assert!(matches!(
e.kind(),
ErrorKind::InvalidInteger("signs are not allowed on prefixed integers")
));
let e = ctx.parse_err("a = -0o7");
assert!(matches!(
e.kind(),
ErrorKind::InvalidInteger("signs are not allowed on prefixed integers")
));
let e = ctx.parse_err("a = -.7");
assert!(matches!(
e.kind(),
ErrorKind::InvalidInteger("expected digit after sign")
));
let e = ctx.parse_err("a = +_2");
assert!(matches!(
e.kind(),
ErrorKind::InvalidInteger("expected digit after sign")
));
let e = ctx.parse_err("a = -_bad");
assert!(matches!(
e.kind(),
ErrorKind::InvalidInteger("expected digit after sign")
));
let e = ctx.parse_err("key = +");
assert!(matches!(
e.kind(),
ErrorKind::InvalidInteger("expected digit after sign")
));
let e = ctx.parse_err("a = .1");
assert!(matches!(e.kind(), ErrorKind::Unexpected('.')));
let e = ctx.parse_err("a = ]");
assert!(matches!(e.kind(), ErrorKind::Unexpected(']')));
let e = ctx.parse_err("a = }");
assert!(matches!(e.kind(), ErrorKind::Unexpected('}')));
let e = ctx.parse_err("a = hello");
assert!(matches!(e.kind(), ErrorKind::UnquotedString));
let e = ctx.parse_err("a = not_a_keyword");
assert!(matches!(e.kind(), ErrorKind::UnquotedString));
let e = ctx.parse_err("a = True");
assert!(matches!(e.kind(), ErrorKind::UnquotedString));
let e = ctx.parse_err("a = 0xABGD");
assert!(matches!(
e.kind(),
ErrorKind::InvalidInteger("invalid digit for hexadecimal")
));
assert_eq!(e.span(), Span::new(8, 9), "should point at 'G'");
let e = ctx.parse_err("a = 0o89");
assert!(matches!(
e.kind(),
ErrorKind::InvalidInteger("invalid digit for octal")
));
assert_eq!(e.span(), Span::new(6, 7), "should point at '8'");
let e = ctx.parse_err("a = 0b102");
assert!(matches!(
e.kind(),
ErrorKind::InvalidInteger("invalid digit for binary")
));
assert_eq!(e.span(), Span::new(8, 9), "should point at '2'");
let e = ctx.parse_err("a = 1__2");
assert!(matches!(
e.kind(),
ErrorKind::InvalidInteger("underscores must be between two digits")
));
assert_eq!(
e.span(),
Span::new(6, 7),
"should point at second underscore"
);
let e = ctx.parse_err("a = 123_");
assert!(matches!(
e.kind(),
ErrorKind::InvalidInteger("underscores must be between two digits")
));
assert_eq!(
e.span(),
Span::new(7, 8),
"should point at trailing underscore"
);
let e = ctx.parse_err("a = 0xFF_");
assert!(matches!(
e.kind(),
ErrorKind::InvalidInteger("underscores must be between two digits")
));
assert_eq!(
e.span(),
Span::new(8, 9),
"should point at trailing underscore in hex"
);
let e = ctx.parse_err("a = 1a2");
assert!(matches!(
e.kind(),
ErrorKind::InvalidInteger("contains non-digit character")
));
assert_eq!(e.span(), Span::new(5, 6), "should point at 'a'");
let e = ctx.parse_err("a = 01");
assert!(matches!(
e.kind(),
ErrorKind::InvalidInteger("leading zeros are not allowed")
));
let e = ctx.parse_err("a = 2016-9-09T09:09:09Z");
assert!(matches!(
e.kind(),
ErrorKind::InvalidDateTime("expected 2-digit month")
));
let e = ctx.parse_err("a = 25-01-01T00:00:00Z");
assert!(matches!(
e.kind(),
ErrorKind::InvalidDateTime("expected 4-digit year")
));
let e = ctx.parse_err("a = 2016-13-01");
assert!(matches!(
e.kind(),
ErrorKind::InvalidDateTime("month is out of range")
));
let e = ctx.parse_err("a = 2016-02-30");
assert!(matches!(
e.kind(),
ErrorKind::InvalidDateTime("day is out of range")
));
let e = ctx.parse_err("a = 2016-09-09T25:00:00");
assert!(matches!(
e.kind(),
ErrorKind::InvalidDateTime("hour is out of range")
));
let e = ctx.parse_err("a = 2016-09-09T09:60:00");
assert!(matches!(
e.kind(),
ErrorKind::InvalidDateTime("minute is out of range")
));
}
#[test]
fn integer_overflow_specific_paths() {
let ctx = TestCtx::new();
let e = ctx.parse_err("a = 170141183460469231731687303715884105728");
assert!(is_number_error!(e.kind()), "input: i128::MAX + 1");
let e = ctx.parse_err("a = -170141183460469231731687303715884105729");
assert!(is_number_error!(e.kind()), "input: -(i128::MIN) - 1");
let v = ctx.parse_ok("a = 9223372036854775808");
assert_eq!(v["a"].as_i128(), Some(9223372036854775808i128));
let v = ctx.parse_ok("a = -9223372036854775809");
assert_eq!(v["a"].as_i128(), Some(-9223372036854775809i128));
let hex_invalid = ["a = 0xGG", "a = 0xZZ"];
for input in hex_invalid {
let e = ctx.parse_err(input);
assert!(is_number_error!(e.kind()), "input: {input}");
}
let e = ctx.parse_err("a = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
assert!(is_number_error!(e.kind()), "input: 0x 33 F's");
let v = ctx.parse_ok("a = 0xFFFFFFFFFFFFFFFFF");
assert_eq!(v["a"].as_i128(), Some(0xFFFFFFFFFFFFFFFFFi128));
let v = ctx.parse_ok("a = 0x10000000000000000");
assert_eq!(v["a"].as_i128(), Some(0x10000000000000000i128));
let e = ctx.parse_err("a = 0o4000000000000000000000000000000000000000000");
assert!(is_number_error!(e.kind()), "input: 0o > i128::MAX");
let e = ctx.parse_err(&format!("a = 0b1{}", "1".repeat(128)));
assert!(is_number_error!(e.kind()), "input: 0b 129 ones");
}
#[test]
fn float_validation_edge_cases() {
let ctx = TestCtx::new();
let integer_part_cases = ["a = 1_.5", "a = 1__.5"];
for input in integer_part_cases {
let e = ctx.parse_err(input);
assert!(is_number_error!(e.kind()), "input: {input}");
}
let exponent_cases = ["a = 1e+_5", "a = 1E+_5", "a = 1e+5_"];
for input in exponent_cases {
let e = ctx.parse_err(input);
assert!(is_number_error!(e.kind()), "input: {input}");
}
let non_finite_cases = ["a = 1e999", "a = 1e9999", "a = 1.0e999", "a = -1e999"];
for input in non_finite_cases {
let e = ctx.parse_err(input);
assert!(is_number_error!(e.kind()), "input: {input}");
}
}
#[test]
fn inline_table_error_paths() {
let ctx = TestCtx::new();
let e = ctx.parse_err("a = {#bad\r}");
assert!(
matches!(e.kind(), ErrorKind::Wanted { .. }),
"input: bare CR after comment in inline table start"
);
let e = ctx.parse_err("a = {!}");
assert!(
matches!(
e.kind(),
ErrorKind::Wanted {
expected: &"a table key",
..
}
),
"input: a = {{!}}"
);
let e = ctx.parse_err("a = {x #bad\r= 1}");
assert!(
matches!(e.kind(), ErrorKind::Wanted { .. }),
"input: bare CR after key in inline table"
);
let e = ctx.parse_err("a = {x = 1, x.y = 2}");
assert!(
matches!(e.kind(), ErrorKind::DottedKeyInvalidType { .. }),
"input: dotted key on integer in inline table"
);
let e = ctx.parse_err("a = {x.! = 1}");
assert!(
matches!(
e.kind(),
ErrorKind::Wanted {
expected: &"a table key",
..
}
),
"input: invalid key after dot in inline table"
);
let e = ctx.parse_err("a = {x}");
assert!(
matches!(
e.kind(),
ErrorKind::Wanted {
expected: &"an equals",
..
}
),
"input: a = {{x}}"
);
let e = ctx.parse_err("a = {x = 1 #bad\r}");
assert!(
matches!(e.kind(), ErrorKind::Wanted { .. }),
"input: bare CR after value in inline table"
);
let e = ctx.parse_err("a = {x = 1, #bad\r}");
assert!(
matches!(e.kind(), ErrorKind::Wanted { .. }),
"input: bare CR after comma in inline table"
);
}
#[test]
fn array_error_paths() {
let ctx = TestCtx::new();
let e = ctx.parse_err("a = [#bad\r]");
assert!(
matches!(e.kind(), ErrorKind::Wanted { .. }),
"input: bare CR after comment at array start"
);
let e = ctx.parse_err("a = [1 #bad\r]");
assert!(
matches!(e.kind(), ErrorKind::Wanted { .. }),
"input: bare CR after comment after array value"
);
let e = ctx.parse_err("a = [1, #bad\r]");
assert!(
matches!(e.kind(), ErrorKind::Wanted { .. }),
"input: bare CR after comment after comma in array"
);
}
#[test]
fn parse_document_error_paths() {
let ctx = TestCtx::new();
let e = ctx.parse_err("#bad\r");
assert!(
matches!(e.kind(), ErrorKind::Wanted { .. }),
"input: bare CR after top-level comment"
);
let e = ctx.parse_err("a = 1\n#bad\r");
assert!(
matches!(e.kind(), ErrorKind::Wanted { .. }),
"input: bare CR after comment following key-value"
);
let e = ctx.parse_err("\r");
assert!(
matches!(e.kind(), ErrorKind::Unexpected('\r')),
"input: bare CR at document top level"
);
let e = ctx.parse_err("a = 1\n\r");
assert!(
matches!(e.kind(), ErrorKind::Unexpected('\r')),
"input: bare CR after newline at document top level"
);
}
#[test]
fn table_header_error_paths() {
let ctx = TestCtx::new();
let e = ctx.parse_err("[a.]\nk = 1");
assert!(
matches!(
e.kind(),
ErrorKind::Wanted {
expected: &"a table key",
..
}
),
"input: [a.]"
);
let e = ctx.parse_err("[[a]\nk = 1");
assert!(
matches!(
e.kind(),
ErrorKind::Wanted {
expected: &"a right bracket",
..
}
),
"input: [[a] missing second bracket"
);
let v = ctx.parse_ok("[a] # comment\nk = 1");
assert_eq!(v["a"]["k"].as_i64(), Some(1), "input: [a] # comment");
let e = ctx.parse_err("[a]x");
assert!(
matches!(
e.kind(),
ErrorKind::Wanted {
expected: &"newline",
..
}
),
"input: [a]x"
);
let e = ctx.parse_err("[a]#bad\r");
assert!(
matches!(e.kind(), ErrorKind::Wanted { .. }),
"input: [a]#bad\\r"
);
}
#[test]
fn key_value_error_paths() {
let ctx = TestCtx::new();
let e = ctx.parse_err("x.! = 1");
assert!(
matches!(
e.kind(),
ErrorKind::Wanted {
expected: &"a table key",
..
}
),
"input: x.! = 1"
);
let e = ctx.parse_err("[t]\nx.= = 1");
assert!(
matches!(
e.kind(),
ErrorKind::Wanted {
expected: &"a table key",
..
}
),
"input: x.= = 1"
);
let e = ctx.parse_err("a = 1 #bad\r");
assert!(
matches!(e.kind(), ErrorKind::Wanted { .. }),
"input: a = 1 #bad\\r"
);
}
#[test]
fn hex_numbers_with_zero_digits() {
let ctx = TestCtx::new();
let cases: [(&str, i64); 6] = [
("a = 0x10", 0x10),
("a = 0x0F", 0x0F),
("a = 0x00", 0x00),
("a = 0x0a", 0x0a),
("a = 0xab", 0xab),
("a = 0xAbCd", 0xABCD),
];
for (input, expected) in cases {
let v = ctx.parse_ok(input);
assert_eq!(v["a"].as_i64(), Some(expected), "input: {input}");
}
}
#[test]
fn integer_base_max_boundary() {
let ctx = TestCtx::new();
let v = ctx.parse_ok("a = 0x7FFFFFFFFFFFFFFF");
assert_eq!(v["a"].as_i64(), Some(i64::MAX));
let v = ctx.parse_ok("a = 0x8000000000000000");
assert_eq!(v["a"].as_i128(), Some(0x8000000000000000i128));
assert_eq!(v["a"].as_i64(), None);
let v = ctx.parse_ok("a = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
assert_eq!(v["a"].as_i128(), Some(i128::MAX));
let e = ctx.parse_err("a = 0x80000000000000000000000000000000");
assert!(is_number_error!(e.kind()), "hex i128::MAX+1");
let v = ctx.parse_ok("a = 0o777777777777777777777");
assert_eq!(v["a"].as_i64(), Some(i64::MAX));
let v = ctx.parse_ok("a = 0o1000000000000000000000");
assert_eq!(v["a"].as_i128(), Some(0o1000000000000000000000i128));
let v = ctx.parse_ok("a = 0b0111111111111111111111111111111111111111111111111111111111111111");
assert_eq!(v["a"].as_i64(), Some(i64::MAX));
let v = ctx.parse_ok("a = 0b1000000000000000000000000000000000000000000000000000000000000000");
assert_eq!(v["a"].as_i128(), Some(1i128 << 63));
}
#[test]
fn literal_string_span() {
let ctx = TestCtx::new();
let input = "key = 'hello'";
let v = ctx.parse_ok(input);
let span = v.get("key").unwrap().span_unchecked();
assert_eq!(
&input[span.start as usize..span.end as usize],
"'hello'",
"literal string span"
);
let input = "key = ''";
let v = ctx.parse_ok(input);
let span = v.get("key").unwrap().span_unchecked();
assert_eq!(span.start, 6, "empty literal string span start");
assert_eq!(span.end, 8, "empty literal string span end");
}
#[test]
fn multiline_string_spans() {
let ctx = TestCtx::new();
let input = "key = \"\"\"\nhello\nworld\"\"\"";
let v = ctx.parse_ok(input);
let span = v.get("key").unwrap().span_unchecked();
assert_eq!(
&input[span.start as usize..span.end as usize],
"\"\"\"\nhello\nworld\"\"\"",
"multiline basic string span"
);
let input = "key = \"\"\"\r\nhello\"\"\"";
let v = ctx.parse_ok(input);
let span = v.get("key").unwrap().span_unchecked();
assert_eq!(
&input[span.start as usize..span.end as usize],
"\"\"\"\r\nhello\"\"\"",
"multiline basic string span with CRLF opening"
);
let input = "key = \"\"\"hello\"\"\"";
let v = ctx.parse_ok(input);
let span = v.get("key").unwrap().span_unchecked();
assert_eq!(
&input[span.start as usize..span.end as usize],
"\"\"\"hello\"\"\"",
"multiline basic string span without leading newline"
);
let input = "key = '''\nhello\nworld'''";
let v = ctx.parse_ok(input);
let span = v.get("key").unwrap().span_unchecked();
assert_eq!(
&input[span.start as usize..span.end as usize],
"'''\nhello\nworld'''",
"multiline literal string span"
);
let input = "key = \"\"";
let v = ctx.parse_ok(input);
let span = v.get("key").unwrap().span_unchecked();
assert_eq!(span.start, 6, "empty basic string span start");
assert_eq!(span.end, 8, "empty basic string span end");
}
#[test]
fn backslash_whitespace_in_nonmultiline_string() {
let ctx = TestCtx::new();
let e = ctx.parse_err("a = \"hello\\ world\"");
assert!(
matches!(e.kind(), ErrorKind::InvalidEscape(' ')),
"backslash-space in basic string: {:?}",
e.kind()
);
let e = ctx.parse_err("a = \"hello\\\tworld\"");
assert!(
matches!(e.kind(), ErrorKind::InvalidEscape('\t')),
"backslash-tab in basic string: {:?}",
e.kind()
);
}
#[test]
fn lowercase_hex_escapes() {
let ctx = TestCtx::new();
let cases = [
(r#"a = "\u0061""#, "a"), (r#"a = "\u00ff""#, "\u{ff}"), (r#"a = "\U0001f600""#, "\u{1f600}"), (r#"a = "\x61""#, "a"), ];
for (input, expected) in cases {
let v = ctx.parse_ok(input);
assert_eq!(
v.get("a").unwrap().as_str(),
Some(expected),
"input: {input}"
);
}
}
#[test]
fn nan_sign_preservation() {
let ctx = TestCtx::new();
let v = ctx.parse_ok("a = nan");
let f = v["a"].as_f64().unwrap();
assert!(f.is_nan());
assert!(f.is_sign_positive(), "nan should be positive");
let v = ctx.parse_ok("a = -nan");
let f = v["a"].as_f64().unwrap();
assert!(f.is_nan());
assert!(f.is_sign_negative(), "-nan should be negative");
let v = ctx.parse_ok("a = +nan");
let f = v["a"].as_f64().unwrap();
assert!(f.is_nan());
assert!(f.is_sign_positive(), "+nan should be positive");
}
#[test]
fn indexed_table_nonexistent_key() {
let ctx = TestCtx::new();
let v = ctx.parse_ok("a = 1\nb = 2\nc = 3\nd = 4\ne = 5\nf = 6");
assert!(
v.get("nonexistent").is_none(),
"nonexistent key in 6-entry table"
);
assert_eq!(v["a"].as_i64(), Some(1));
assert_eq!(v["b"].as_i64(), Some(2));
assert_eq!(v["c"].as_i64(), Some(3));
assert_eq!(v["d"].as_i64(), Some(4));
assert_eq!(v["e"].as_i64(), Some(5));
assert_eq!(v["f"].as_i64(), Some(6));
let v = ctx.parse_ok("a = 1\nb = 2\nc = 3\nd = 4\ne = 5\nf = 6\ng = 7");
assert!(
v.get("nonexistent").is_none(),
"nonexistent key in 7-entry table"
);
assert_eq!(v["a"].as_i64(), Some(1));
assert_eq!(v["g"].as_i64(), Some(7));
}
#[test]
fn long_string_exercises_swar_path() {
let ctx = TestCtx::new();
let cases = [
(r#"a = "abcdefghijklmnop""#, "abcdefghijklmnop"),
("a = \"abcdefgh\\nrest\"", "abcdefgh\nrest"),
(r#"a = "abcdefghij""#, "abcdefghij"),
("a = \"abc\tdefghijklmno\"", "abc\tdefghijklmno"),
];
for (input, expected) in cases {
let v = ctx.parse_ok(input);
assert_eq!(v["a"].as_str(), Some(expected), "input: {input}");
}
let v = ctx.parse_ok("a = 'abcdefghijklmnop'");
assert_eq!(v["a"].as_str(), Some("abcdefghijklmnop"));
}
#[test]
fn integer_plus_sign_in_decimal() {
let ctx = TestCtx::new();
let cases: [(&str, i64); 3] = [
("a = +0", 0),
("a = +1", 1),
("a = +9223372036854775807", i64::MAX),
];
for (input, expected) in cases {
let v = ctx.parse_ok(input);
assert_eq!(v["a"].as_i64(), Some(expected), "input: {input}");
}
}
#[test]
fn octal_digit_validation() {
let ctx = TestCtx::new();
let cases: [(&str, i64); 3] = [("a = 0o0", 0), ("a = 0o7", 7), ("a = 0o10", 8)];
for (input, expected) in cases {
let v = ctx.parse_ok(input);
assert_eq!(v["a"].as_i64(), Some(expected), "input: {input}");
}
let e = ctx.parse_err("a = 0o8");
assert!(is_number_error!(e.kind()), "octal digit 8");
let e = ctx.parse_err("a = 0o9");
assert!(is_number_error!(e.kind()), "octal digit 9");
}
#[test]
fn invalid_true_false_literals() {
let ctx = TestCtx::new();
for input in [
"a = tru",
"a = toast",
"a = true2",
"a = fals",
"a = foobar",
"a = false2",
"a = t2",
"a = f2",
] {
let e = ctx.parse_err(input);
assert!(
matches!(e.kind(), ErrorKind::UnquotedString),
"input: {input}, got: {:?}",
e.kind()
);
}
}
#[test]
fn datetime_values() {
let ctx = TestCtx::new();
let v = ctx.parse_ok("a = 2023-06-15T12:30:45Z");
assert!(v["a"].as_datetime().is_some());
assert_eq!(v["a"].as_datetime().unwrap().date().unwrap().year, 2023);
let v = ctx.parse_ok("a = 2023-06-15");
assert!(v["a"].as_datetime().is_some());
let v = ctx.parse_ok("a = 2023-06-15T12:30:45+05:30");
assert!(v["a"].as_datetime().is_some());
}
#[test]
fn scan_token_whitespace_and_string() {
let ctx = TestCtx::new();
let e = ctx.parse_err("a = {x = 1 ");
assert!(
matches!(e.kind(), ErrorKind::UnclosedInlineTable),
"input: truncated inline table with trailing whitespace"
);
let e = ctx.parse_err("[\"key\"\n]");
assert!(
matches!(e.kind(), ErrorKind::Wanted { .. }),
"input: string where bracket expected"
);
}
#[test]
fn duplicate_key_in_indexed_table() {
let ctx = TestCtx::new();
let mut lines = Vec::new();
for i in 0..7 {
lines.push(format!("k{i} = {i}"));
}
lines.push("k0 = 99".to_string()); let input = lines.join("\n");
let e = ctx.parse_err(&input);
assert!(
matches!(e.kind(), ErrorKind::DuplicateKey { .. }),
"duplicate in indexed table: {:?}",
e.kind()
);
let mut pairs = Vec::new();
for i in 0..6 {
pairs.push(format!("k{i} = {i}"));
}
let input = format!("a = {{{}, k0 = 99}}", pairs.join(", "));
let e = ctx.parse_err(&input);
assert!(
matches!(e.kind(), ErrorKind::DuplicateKey { .. }),
"duplicate in inline table: {:?}",
e.kind()
);
}
#[test]
fn crlf_line_endings() {
let ctx = TestCtx::new();
let v = ctx.parse_ok("a = 1\r\nb = 2\r\n");
assert_eq!(v["a"].as_i64(), Some(1));
assert_eq!(v["b"].as_i64(), Some(2));
let v = ctx.parse_ok("a = 1\r\n\r\n\r\nb = 2\r\n");
assert_eq!(v["a"].as_i64(), Some(1));
assert_eq!(v["b"].as_i64(), Some(2));
let v = ctx.parse_ok("[t]\r\nx = 'hello'\r\n\r\n[u]\r\ny = 42\r\n");
assert_eq!(v["t"]["x"].as_str(), Some("hello"));
assert_eq!(v["u"]["y"].as_i64(), Some(42));
let v = ctx.parse_ok("[[items]]\r\nval = 1\r\n\r\n[[items]]\r\nval = 2\r\n");
assert_eq!(v["items"][0]["val"].as_i64(), Some(1));
assert_eq!(v["items"][1]["val"].as_i64(), Some(2));
let v = ctx.parse_ok("# comment\r\n\r\na = 1\r\n");
assert_eq!(v["a"].as_i64(), Some(1));
let v = ctx.parse_ok("t = {\r\n a = 1,\r\n b = 2\r\n}\r\n");
assert_eq!(v["t"]["a"].as_i64(), Some(1));
assert_eq!(v["t"]["b"].as_i64(), Some(2));
let v = ctx.parse_ok("a = [\r\n 1,\r\n 2,\r\n 3\r\n]\r\n");
let arr = v["a"].as_array().unwrap();
assert_eq!(arr.len(), 3);
}
#[test]
fn parse_error_token_descriptions() {
let ctx = TestCtx::new();
let e = ctx.parse_err("key ");
assert!(matches!(e.kind(), ErrorKind::Wanted { .. }));
let e = ctx.parse_err("key : value");
assert!(matches!(e.kind(), ErrorKind::Wanted { .. }));
let e = ctx.parse_err("key = +");
assert!(is_number_error!(e.kind()));
let e = ctx.parse_err("t = { a = 1 ");
assert!(matches!(e.kind(), ErrorKind::UnclosedInlineTable));
let e = ctx.parse_err("a = [1, 2 ");
assert!(matches!(e.kind(), ErrorKind::UnclosedArray));
}
#[test]
fn error_paths() {
let ctx = TestCtx::new();
let path = |e: crate::Error| -> String {
let components = e.path().expect("expected non-empty path");
use std::fmt::Write;
let mut out = String::new();
for (i, c) in components.iter().enumerate() {
match c {
crate::error::PathComponent::Key(k) => {
if i > 0 {
out.push('.');
}
out.push_str(k.name);
}
crate::error::PathComponent::Index(idx) => {
write!(out, "[{idx}]").unwrap();
}
}
}
out
};
let e = ctx.parse_err("a = 1\na = 2");
assert_eq!(path(e), "a");
let e = ctx.parse_err("[fruit.apple.taste]\n[fruit]\napple.taste.sweet = true");
assert_eq!(path(e), "fruit.apple.taste");
let e = ctx.parse_err("[a]\nb = 1\n[a]\nc = 2");
assert_eq!(path(e), "a");
let e = ctx.parse_err("[section]\na = 1\na = 2");
assert_eq!(path(e), "section.a");
let e = ctx.parse_err(
"[[servers]]\nname = 'a'\n[[servers]]\nname = 'b'\n[[servers]]\nname = 'c'\nname = 'd'",
);
assert_eq!(path(e), "servers[2].name");
let e = ctx.parse_err("[[a]]\n[[a.b]]\nx = 1\n[[a.b]]\nx = 2\nx = 3");
assert_eq!(path(e), "a[0].b[1].x");
let e = ctx.parse_err("[a.]");
assert_eq!(path(e), "a");
}
#[cfg(feature = "from-toml")]
mod recovering {
use crate::arena::Arena;
#[test]
fn valid_input_no_errors() {
let arena = Arena::new();
let doc = crate::parser::parse_recoverable("key = 'value'\nnum = 42\n", &arena);
assert!(doc.errors().is_empty());
assert_eq!(doc["key"].as_str(), Some("value"));
assert_eq!(doc["num"].as_i64(), Some(42));
}
#[test]
fn multiple_value_errors() {
let input = "good = 1\nbad =\nalso_good = 2\nworse = [}\n";
let arena = Arena::new();
let doc = crate::parser::parse_recoverable(input, &arena);
assert!(doc.errors().len() >= 2);
assert_eq!(doc["good"].as_i64(), Some(1));
assert_eq!(doc["also_good"].as_i64(), Some(2));
}
#[test]
fn bad_table_header_recovery() {
let input = "[valid]\na = 1\n[bad.]\n[another]\nb = 2\n";
let arena = Arena::new();
let doc = crate::parser::parse_recoverable(input, &arena);
assert!(!doc.errors().is_empty());
assert_eq!(doc["valid"].as_table().unwrap()["a"].as_i64(), Some(1));
assert_eq!(doc["another"].as_table().unwrap()["b"].as_i64(), Some(2));
}
#[test]
fn error_limit() {
let mut input = String::new();
for _ in 0..50 {
input.push_str("bad =\n");
}
let arena = Arena::new();
let doc = crate::parser::parse_recoverable(&input, &arena);
assert_eq!(
doc.errors().len(),
crate::parser::Parser::MAX_RECOVER_ERRORS
);
}
#[test]
fn eof_mid_error() {
let arena = Arena::new();
let doc = crate::parser::parse_recoverable("[incomplete", &arena);
assert!(!doc.errors().is_empty());
}
#[test]
fn bare_cr_recovery() {
let input = "a = 1\n\rb = 2\nc = 3\n";
let arena = Arena::new();
let doc = crate::parser::parse_recoverable(input, &arena);
assert!(!doc.errors().is_empty());
assert_eq!(doc["a"].as_i64(), Some(1));
assert_eq!(doc["c"].as_i64(), Some(3));
}
#[test]
fn multiline_inline_table_error() {
let input = "a = {\n b = 1,\n c = \n}\nd = 2\n";
let arena = Arena::new();
let doc = crate::parser::parse_recoverable(input, &arena);
assert!(!doc.errors().is_empty());
assert_eq!(doc["d"].as_i64(), Some(2));
}
#[test]
fn multiline_array_error() {
let input = "a = [\n 1,\n bad,\n 3\n]\nb = 2\n";
let arena = Arena::new();
let doc = crate::parser::parse_recoverable(input, &arena);
assert!(!doc.errors().is_empty());
assert_eq!(doc["b"].as_i64(), Some(2));
}
#[test]
fn nested_inline_structure_error() {
let input = "a = {b = [1, {c = }, 3]}\nd = 2\n";
let arena = Arena::new();
let doc = crate::parser::parse_recoverable(input, &arena);
assert!(!doc.errors().is_empty());
assert_eq!(doc["d"].as_i64(), Some(2));
}
#[test]
fn string_with_brackets_not_confused() {
let input = "a = \"hello [\"\nb = \"world }\"\nc = 3\n";
let arena = Arena::new();
let doc = crate::parser::parse_recoverable(input, &arena);
assert!(doc.errors().is_empty());
assert_eq!(doc["a"].as_str(), Some("hello ["));
assert_eq!(doc["b"].as_str(), Some("world }"));
assert_eq!(doc["c"].as_i64(), Some(3));
}
#[test]
fn unclosed_inline_table_at_eof() {
let input = "a = 1\nb = {c = \n";
let arena = Arena::new();
let doc = crate::parser::parse_recoverable(input, &arena);
assert!(!doc.errors().is_empty());
assert_eq!(doc["a"].as_i64(), Some(1));
}
#[test]
fn unclosed_array_at_eof() {
let input = "a = 1\nb = [1, 2\n";
let arena = Arena::new();
let doc = crate::parser::parse_recoverable(input, &arena);
assert!(!doc.errors().is_empty());
assert_eq!(doc["a"].as_i64(), Some(1));
}
#[test]
fn multiline_array_single_bad_element() {
let input = "before = true\narr = [\n 1,\n ,\n 3,\n]\nafter = true\n";
let arena = Arena::new();
let doc = crate::parser::parse_recoverable(input, &arena);
assert!(!doc.errors().is_empty());
assert_eq!(doc["before"].as_bool(), Some(true));
assert_eq!(doc["after"].as_bool(), Some(true));
}
#[test]
fn error_inside_multiline_string_does_not_cascade() {
let input = "a = 1\nb = \"unterminated\nc = 2\nd = 3\n";
let arena = Arena::new();
let doc = crate::parser::parse_recoverable(input, &arena);
assert!(!doc.errors().is_empty());
assert_eq!(doc["a"].as_i64(), Some(1));
assert_eq!(doc["d"].as_i64(), Some(3));
}
#[test]
fn multiple_sections_with_errors() {
let input = "\
[s1]
a = 1
b = {bad
[s2]
c = 2
d = [broken
[s3]
e = 3
";
let arena = Arena::new();
let doc = crate::parser::parse_recoverable(input, &arena);
assert!(doc.errors().len() >= 2);
assert_eq!(doc["s1"].as_table().unwrap()["a"].as_i64(), Some(1));
assert_eq!(doc["s2"].as_table().unwrap()["c"].as_i64(), Some(2));
assert_eq!(doc["s3"].as_table().unwrap()["e"].as_i64(), Some(3));
}
}