use super::classify::{classify_value_start, is_float_literal, try_parse_integer};
use super::insert::insert_value;
use super::validate::is_valid_key;
use super::value_start::ValueStart;
use crate::error::Span;
use crate::value::{ObjectMap, Value};
const S: Span = Span::EMPTY;
#[test]
fn valid_keys_accepted() {
assert!(is_valid_key("port"));
assert!(is_valid_key("a1"));
assert!(is_valid_key("kebab-case"));
assert!(is_valid_key("snake_case"));
assert!(is_valid_key("with#hash"));
assert!(is_valid_key("has space"));
assert!(is_valid_key("first name"));
}
#[test]
fn invalid_keys_rejected() {
assert!(!is_valid_key(""));
assert!(!is_valid_key("with[bracket"));
assert!(!is_valid_key("with]bracket"));
assert!(!is_valid_key("with{brace"));
assert!(!is_valid_key("with}brace"));
assert!(is_valid_key("with:colon"));
assert!(is_valid_key("with.dot"));
assert!(!is_valid_key("with,comma"));
assert!(!is_valid_key("with(paren"));
assert!(!is_valid_key("with)paren"));
}
#[test]
fn paths_validated_segment_by_segment_via_insert() {
let mut t = ObjectMap::default();
assert!(insert_value(&mut t, "a.b.c", Value::Null, 1, S).is_ok());
let mut t = ObjectMap::default();
assert!(insert_value(&mut t, "a", Value::Null, 1, S).is_ok());
let mut t = ObjectMap::default();
assert!(insert_value(&mut t, "a..b", Value::Null, 1, S).is_err());
let mut t = ObjectMap::default();
assert!(insert_value(&mut t, "a.b.", Value::Null, 1, S).is_err());
let mut t = ObjectMap::default();
assert!(insert_value(&mut t, ".", Value::Null, 1, S).is_err());
}
#[test]
fn classify_scalar() {
match classify_value_start("hello", 1, S, false).unwrap() {
ValueStart::Scalar(s) => assert_eq!(s, "hello"),
_ => panic!("expected Scalar"),
}
}
#[test]
fn classify_keywords() {
assert!(matches!(
classify_value_start("null", 1, S, false).unwrap(),
ValueStart::Null
));
assert!(matches!(
classify_value_start("true", 1, S, false).unwrap(),
ValueStart::Bool(true)
));
assert!(matches!(
classify_value_start("false", 1, S, false).unwrap(),
ValueStart::Bool(false)
));
}
#[test]
fn classify_case_sensitive_keywords() {
match classify_value_start("True", 1, S, false).unwrap() {
ValueStart::Scalar(s) => assert_eq!(s, "True"),
_ => panic!("expected Scalar"),
}
match classify_value_start("NULL", 1, S, false).unwrap() {
ValueStart::Scalar(s) => assert_eq!(s, "NULL"),
_ => panic!("expected Scalar"),
}
}
#[test]
fn classify_open_compounds() {
assert!(matches!(
classify_value_start("{", 1, S, false).unwrap(),
ValueStart::OpenObject
));
assert!(matches!(
classify_value_start("[", 1, S, false).unwrap(),
ValueStart::OpenArray
));
}
#[test]
fn classify_empty_inline_compounds() {
assert!(matches!(
classify_value_start("{}", 1, S, false).unwrap(),
ValueStart::EmptyObject
));
assert!(matches!(
classify_value_start("[]", 1, S, false).unwrap(),
ValueStart::EmptyArray
));
assert!(matches!(
classify_value_start("{ }", 1, S, false).unwrap(),
ValueStart::EmptyObject
));
assert!(matches!(
classify_value_start("[ ]", 1, S, false).unwrap(),
ValueStart::EmptyArray
));
}
#[test]
fn classify_inline_nonempty_accepted() {
match classify_value_start("{a: 1}", 1, S, false).unwrap() {
ValueStart::InlineValue(v) => {
assert!(v.as_object().is_some());
let obj = v.as_object().unwrap();
assert_eq!(obj.get("a"), Some(&Value::Integer("1".into())));
}
other => panic!(
"expected InlineValue, got {:?}",
std::mem::discriminant(&other)
),
}
match classify_value_start("[1, 2]", 1, S, false).unwrap() {
ValueStart::InlineValue(v) => {
let arr = v.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0], Value::Integer("1".into()));
assert_eq!(arr[1], Value::Integer("2".into()));
}
other => panic!(
"expected InlineValue, got {:?}",
std::mem::discriminant(&other)
),
}
}
#[test]
fn insert_simple_pair() {
let mut t = ObjectMap::default();
insert_value(&mut t, "port", Value::String("8080".into()), 1, S).unwrap();
assert_eq!(t.get("port"), Some(&Value::String("8080".into())));
}
#[test]
fn insert_dotted_path_creates_intermediate_objects() {
let mut t = ObjectMap::default();
insert_value(&mut t, "a.b.c", Value::String("x".into()), 1, S).unwrap();
let a = t.get("a").unwrap().as_object().unwrap();
let b = a.get("b").unwrap().as_object().unwrap();
assert_eq!(b.get("c"), Some(&Value::String("x".into())));
}
#[test]
fn insert_duplicate_rejected() {
let mut t = ObjectMap::default();
insert_value(&mut t, "x", Value::String("1".into()), 1, S).unwrap();
let err = insert_value(&mut t, "x", Value::String("2".into()), 2, S);
assert!(err.is_err());
}
#[test]
fn insert_scalar_then_nested_path_rejected() {
let mut t = ObjectMap::default();
insert_value(&mut t, "a", Value::String("leaf".into()), 1, S).unwrap();
let err = insert_value(&mut t, "a.b", Value::String("x".into()), 2, S);
assert!(err.is_err());
}
#[test]
fn insert_trims_key_segments() {
let mut t = ObjectMap::default();
insert_value(&mut t, " port ", Value::String("80".into()), 1, S).unwrap();
assert_eq!(t.get("port"), Some(&Value::String("80".into())));
}
#[test]
fn insert_trims_dotted_key_segments() {
let mut t = ObjectMap::default();
insert_value(&mut t, " a . b . c ", Value::String("x".into()), 1, S).unwrap();
let a = t.get("a").unwrap().as_object().unwrap();
let b = a.get("b").unwrap().as_object().unwrap();
assert_eq!(b.get("c"), Some(&Value::String("x".into())));
}
#[test]
fn integer_decimal_basic() {
assert_eq!(try_parse_integer("42"), Some(42));
assert_eq!(try_parse_integer("0"), Some(0));
assert_eq!(try_parse_integer("-7"), Some(-7));
assert_eq!(try_parse_integer("+5"), Some(5));
}
#[test]
fn integer_decimal_underscores() {
assert_eq!(try_parse_integer("1_000"), Some(1000));
assert_eq!(try_parse_integer("1_000_000"), Some(1_000_000));
}
#[test]
fn integer_hex() {
assert_eq!(try_parse_integer("0xFF"), Some(255));
assert_eq!(try_parse_integer("0x1a"), Some(26));
assert_eq!(try_parse_integer("-0x10"), Some(-16));
}
#[test]
fn integer_octal() {
assert_eq!(try_parse_integer("0o77"), Some(63));
assert_eq!(try_parse_integer("0o10"), Some(8));
}
#[test]
fn integer_binary() {
assert_eq!(try_parse_integer("0b1010"), Some(10));
assert_eq!(try_parse_integer("0b0"), Some(0));
}
#[test]
fn integer_rejects_bad_forms() {
assert_eq!(try_parse_integer(""), None);
assert_eq!(try_parse_integer("+"), None);
assert_eq!(try_parse_integer("-"), None);
assert_eq!(try_parse_integer("0x"), None);
assert_eq!(try_parse_integer("0o"), None);
assert_eq!(try_parse_integer("0b"), None);
assert_eq!(try_parse_integer("_42"), None);
assert_eq!(try_parse_integer("42_"), None);
assert_eq!(try_parse_integer("4__2"), None);
assert_eq!(try_parse_integer("0x_ff"), None);
assert_eq!(try_parse_integer("abc"), None);
assert_eq!(try_parse_integer("hello"), None);
}
#[test]
fn integer_overflow_returns_none() {
assert_eq!(try_parse_integer("9223372036854775808"), None);
}
#[test]
fn integer_i64_min() {
assert_eq!(try_parse_integer("-9223372036854775808"), Some(i64::MIN));
}
#[test]
fn integer_i64_min_negative_prefixed_radixes() {
assert_eq!(try_parse_integer("-0x8000000000000000"), Some(i64::MIN));
assert_eq!(
try_parse_integer("-0o1000000000000000000000"),
Some(i64::MIN)
);
assert_eq!(
try_parse_integer("-0b1000000000000000000000000000000000000000000000000000000000000000"),
Some(i64::MIN)
);
}
#[test]
fn integer_prefixed_boundary_overflow_returns_none() {
assert_eq!(try_parse_integer("-0x8000000000000001"), None);
assert_eq!(try_parse_integer("-0o1000000000000000000001"), None);
assert_eq!(
try_parse_integer("-0b1000000000000000000000000000000000000000000000000000000000000001"),
None
);
assert_eq!(try_parse_integer("0x8000000000000000"), None);
assert_eq!(try_parse_integer("0o1000000000000000000000"), None);
assert_eq!(
try_parse_integer("0b1000000000000000000000000000000000000000000000000000000000000000"),
None
);
}
#[test]
fn parse_negative_prefixed_i64_min_end_to_end() {
for literal in [
"-0x8000000000000000",
"-0o1000000000000000000000",
"-0b1000000000000000000000000000000000000000000000000000000000000000",
] {
let doc = format!("x: {literal}");
let v = crate::parse(&doc).unwrap();
let obj = v.as_object().unwrap();
assert_eq!(
obj.get("x"),
Some(&Value::Integer("-9223372036854775808".into())),
"literal {literal}"
);
}
}
#[test]
fn parse_prefixed_overflow_falls_to_string_end_to_end() {
for literal in ["-0x8000000000000001", "0x8000000000000000"] {
let doc = format!("x: {literal}");
let v = crate::parse(&doc).unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("x"), Some(&Value::String(literal.into())));
}
}
#[test]
fn parse_strict_negative_prefixed_i64_min() {
let err = crate::parse_strict("x: -0x8000000000000000").unwrap_err();
match err {
crate::error::Error::Structured(crate::error::ErrorKind::LossyScalar { body, .. }) => {
assert_eq!(body, "-0x8000000000000000");
}
other => panic!("expected LossyScalar, got {other:?}"),
}
}
#[test]
fn float_with_decimal_point() {
assert!(is_float_literal("3.14"));
assert!(is_float_literal("0.0"));
assert!(is_float_literal("-3.14"));
assert!(is_float_literal("+3.14"));
}
#[test]
fn float_with_exponent_no_dot() {
assert!(is_float_literal("1e10"));
assert!(is_float_literal("1E10"));
assert!(is_float_literal("-1e10"));
assert!(is_float_literal("1e+10"));
assert!(is_float_literal("1e-10"));
}
#[test]
fn float_with_dot_and_exponent() {
assert!(is_float_literal("3.14e10"));
assert!(is_float_literal("1.0E-3"));
}
#[test]
fn float_with_underscores() {
assert!(is_float_literal("1_000.5"));
assert!(is_float_literal("1.000_5"));
}
#[test]
fn float_rejects_bad_forms() {
assert!(!is_float_literal("1."));
assert!(!is_float_literal(".5"));
assert!(!is_float_literal("42"));
assert!(!is_float_literal("1e"));
assert!(!is_float_literal("1e+"));
assert!(!is_float_literal("abc"));
}
#[test]
fn classify_infers_integer() {
match classify_value_start("42", 1, S, false).unwrap() {
ValueStart::Integer(s) => assert_eq!(s, "42"),
other => panic!("expected Integer, got {:?}", std::mem::discriminant(&other)),
}
match classify_value_start("-7", 1, S, false).unwrap() {
ValueStart::Integer(s) => assert_eq!(s, "-7"),
other => panic!("expected Integer, got {:?}", std::mem::discriminant(&other)),
}
match classify_value_start("0xFF", 1, S, false).unwrap() {
ValueStart::Integer(s) => assert_eq!(s, "255"),
other => panic!("expected Integer, got {:?}", std::mem::discriminant(&other)),
}
}
#[test]
fn classify_infers_float() {
match classify_value_start("3.14", 1, S, false).unwrap() {
ValueStart::Float(_) => {}
other => panic!("expected Float, got {:?}", std::mem::discriminant(&other)),
}
match classify_value_start("1e10", 1, S, false).unwrap() {
ValueStart::Float(_) => {}
other => panic!("expected Float, got {:?}", std::mem::discriminant(&other)),
}
}
#[test]
fn classify_integer_overflow_falls_to_string() {
match classify_value_start("9223372036854775808", 1, S, false).unwrap() {
ValueStart::Scalar(s) => assert_eq!(s, "9223372036854775808"),
other => panic!(
"expected Scalar (String), got {:?}",
std::mem::discriminant(&other)
),
}
}
#[test]
fn parse_inline_object_single_pair() {
let v = crate::parse("a: {name: alice}").unwrap();
let obj = v.as_object().unwrap();
let a = obj.get("a").unwrap().as_object().unwrap();
assert_eq!(a.get("name"), Some(&Value::String("alice".into())));
}
#[test]
fn parse_inline_object_multiple_pairs() {
let v = crate::parse("server: {host: localhost, port: 8080, tls: true}").unwrap();
let obj = v.as_object().unwrap();
let server = obj.get("server").unwrap().as_object().unwrap();
assert_eq!(server.get("host"), Some(&Value::String("localhost".into())));
assert_eq!(server.get("port"), Some(&Value::Integer("8080".into())));
assert_eq!(server.get("tls"), Some(&Value::Bool(true)));
}
#[test]
fn parse_inline_array_integers() {
let v = crate::parse("a: [1, 2, 3]").unwrap();
let obj = v.as_object().unwrap();
let a = obj.get("a").unwrap().as_array().unwrap();
assert_eq!(a[0], Value::Integer("1".into()));
assert_eq!(a[1], Value::Integer("2".into()));
assert_eq!(a[2], Value::Integer("3".into()));
}
#[test]
fn parse_inline_nested_objects() {
let v = crate::parse("cfg: {outer: {middle: {inner: deep}}}").unwrap();
let obj = v.as_object().unwrap();
let cfg = obj.get("cfg").unwrap().as_object().unwrap();
let outer = cfg.get("outer").unwrap().as_object().unwrap();
let middle = outer.get("middle").unwrap().as_object().unwrap();
assert_eq!(middle.get("inner"), Some(&Value::String("deep".into())));
}
#[test]
fn parse_inline_midvalue_brace_is_literal() {
let v = crate::parse("cfg: {a: hello{world, b: x}").unwrap();
let obj = v.as_object().unwrap();
let cfg = obj.get("cfg").unwrap().as_object().unwrap();
assert_eq!(cfg.get("a"), Some(&Value::String("hello{world".into())));
assert_eq!(cfg.get("b"), Some(&Value::String("x".into())));
}
#[test]
fn parse_inline_closer_scan_value_positions_and_raw_items() {
let v = crate::parse("x: [{b: 1}]").unwrap();
let arr = v.as_object().unwrap().get("x").unwrap().as_array().unwrap();
assert_eq!(
arr[0].as_object().unwrap().get("b"),
Some(&Value::Integer("1".into()))
);
let v = crate::parse("x: [[1]]").unwrap();
let arr = v.as_object().unwrap().get("x").unwrap().as_array().unwrap();
let inner = arr[0].as_array().unwrap();
assert_eq!(inner[0], Value::Integer("1".into()));
let v = crate::parse("{a: [{b: 1, c: 2}]}").unwrap();
let a = v.as_object().unwrap().get("a").unwrap().as_array().unwrap();
assert_eq!(
a[0].as_object().unwrap().get("c"),
Some(&Value::Integer("2".into()))
);
match crate::parse("x: [:: {abc}]") {
Err(crate::Error::Structured(crate::error::ErrorKind::UnterminatedInlineCompound {
..
})) => {}
other => panic!("expected UnterminatedInlineCompound, got {other:?}"),
}
match crate::parse("[:: {abc}]") {
Err(crate::Error::Structured(crate::error::ErrorKind::UnterminatedInlineCompound {
..
})) => {}
other => panic!("expected UnterminatedInlineCompound, got {other:?}"),
}
let v = crate::parse("x: [:: \\{abc\\}]").unwrap();
let arr = v.as_object().unwrap().get("x").unwrap().as_array().unwrap();
assert_eq!(arr[0], Value::String(":: {abc}".into()));
let v = crate::parse("{k: \"v\", arr: [{b: 1}]}").unwrap();
let obj = v.as_object().unwrap();
let arr = obj.get("arr").unwrap().as_array().unwrap();
assert_eq!(
arr[0].as_object().unwrap().get("b"),
Some(&Value::Integer("1".into()))
);
assert!(crate::parse("{a: \"x] y\", b: 1}").is_err());
let v = crate::parse("[{b: 1}]").unwrap();
assert_eq!(
v.as_array().unwrap()[0].as_object().unwrap().get("b"),
Some(&Value::Integer("1".into()))
);
}
#[test]
fn parse_inline_escape_comma() {
let v = crate::parse("a: {greeting: hello\\, world}").unwrap();
let obj = v.as_object().unwrap();
let a = obj.get("a").unwrap().as_object().unwrap();
assert_eq!(
a.get("greeting"),
Some(&Value::String("hello, world".into()))
);
}
#[test]
fn parse_inline_empty_value() {
let v = crate::parse("a: {x:, y: 1}").unwrap();
let obj = v.as_object().unwrap();
let a = obj.get("a").unwrap().as_object().unwrap();
assert_eq!(a.get("x"), Some(&Value::String("".into())));
assert_eq!(a.get("y"), Some(&Value::Integer("1".into())));
}
#[test]
fn parse_inline_trailing_comma() {
let v = crate::parse("a: {name: alice, age: 30,}").unwrap();
let obj = v.as_object().unwrap();
let a = obj.get("a").unwrap().as_object().unwrap();
assert_eq!(a.get("name"), Some(&Value::String("alice".into())));
assert_eq!(a.get("age"), Some(&Value::Integer("30".into())));
}
#[test]
fn parse_inline_no_whitespace() {
let v = crate::parse("a: {x:1,y:2,z:3}").unwrap();
let obj = v.as_object().unwrap();
let a = obj.get("a").unwrap().as_object().unwrap();
assert_eq!(a.get("x"), Some(&Value::Integer("1".into())));
assert_eq!(a.get("y"), Some(&Value::Integer("2".into())));
assert_eq!(a.get("z"), Some(&Value::Integer("3".into())));
}
#[test]
fn parse_inline_dotted_keys() {
let v = crate::parse("cfg: {a.b: 1, a.c: 2}").unwrap();
let obj = v.as_object().unwrap();
let cfg = obj.get("cfg").unwrap().as_object().unwrap();
let a = cfg.get("a").unwrap().as_object().unwrap();
assert_eq!(a.get("b"), Some(&Value::Integer("1".into())));
assert_eq!(a.get("c"), Some(&Value::Integer("2".into())));
}
#[test]
fn parse_inline_escape_newline() {
let v = crate::parse("multiline: {body: line1\\nline2\\nline3}").unwrap();
let obj = v.as_object().unwrap();
let ml = obj.get("multiline").unwrap().as_object().unwrap();
assert_eq!(
ml.get("body"),
Some(&Value::String("line1\nline2\nline3".into()))
);
}
#[test]
fn parse_inline_escape_forces_string_not_float() {
let v = crate::parse("cfg: {v: 1\\.0}").unwrap();
let obj = v.as_object().unwrap();
let cfg = obj.get("cfg").unwrap().as_object().unwrap();
assert_eq!(cfg.get("v"), Some(&Value::String("1.0".into())));
}
#[test]
fn parse_inline_escape_forces_string_not_float_exponent() {
let v = crate::parse("cfg: {v: 1\\.e2}").unwrap();
let obj = v.as_object().unwrap();
let cfg = obj.get("cfg").unwrap().as_object().unwrap();
assert_eq!(cfg.get("v"), Some(&Value::String("1.e2".into())));
}
#[test]
fn parse_inline_escape_forces_string_in_array_item() {
let v = crate::parse("cfg: [1\\.0]").unwrap();
let obj = v.as_object().unwrap();
let arr = obj.get("cfg").unwrap().as_array().unwrap();
assert_eq!(arr[0], Value::String("1.0".into()));
}
#[test]
fn parse_inline_unescaped_float_still_classifies_float() {
let v = crate::parse("cfg: {v: 1.5}").unwrap();
let obj = v.as_object().unwrap();
let cfg = obj.get("cfg").unwrap().as_object().unwrap();
assert_eq!(cfg.get("v"), Some(&Value::Float("1.5".into())));
}
#[test]
fn parse_inline_keywords_still_classify() {
let v = crate::parse("cfg: {t: true, f: false, n: null}").unwrap();
let obj = v.as_object().unwrap();
let cfg = obj.get("cfg").unwrap().as_object().unwrap();
assert_eq!(cfg.get("t"), Some(&Value::Bool(true)));
assert_eq!(cfg.get("f"), Some(&Value::Bool(false)));
assert_eq!(cfg.get("n"), Some(&Value::Null));
}
#[test]
fn parse_float_positive_overflow_to_string() {
let v = crate::parse("v: 1e9999").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("v"), Some(&Value::String("1e9999".into())));
}
#[test]
fn parse_float_negative_overflow_to_string() {
let v = crate::parse("v: -1e9999").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("v"), Some(&Value::String("-1e9999".into())));
}
#[test]
fn parse_float_underflow_to_positive_zero() {
let v = crate::parse("v: 1e-9999").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("v"), Some(&Value::Float("0.0".into())));
}
#[test]
fn parse_float_negative_underflow_to_negative_zero() {
let v = crate::parse("v: -1e-9999").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("v"), Some(&Value::Float("-0.0".into())));
}
#[test]
fn parse_float_finite_literals_still_float() {
let v = crate::parse("a: 3.14\nb: 1e6\nc: 1e-2").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("a"), Some(&Value::Float("3.14".into())));
assert_eq!(obj.get("b"), Some(&Value::Float("1000000.0".into())));
assert_eq!(obj.get("c"), Some(&Value::Float("0.01".into())));
}
#[test]
fn parse_strict_float_overflow_to_string_same_as_lax() {
let v = crate::parse_strict("v: 1e9999").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("v"), Some(&Value::String("1e9999".into())));
let v = crate::parse_strict("v: -1e9999").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("v"), Some(&Value::String("-1e9999".into())));
}
#[test]
fn parse_strict_float_underflow_is_lossy_written_form() {
match crate::parse_strict("v: 1e-9999") {
Err(crate::Error::Structured(crate::ErrorKind::LossyScalar { .. })) => {}
other => panic!("expected LossyScalar, got {:?}", other),
}
}
#[test]
fn parse_inline_float_positive_overflow_to_string() {
let v = crate::parse("v: {x: 1e9999}").unwrap();
let obj = v.as_object().unwrap();
let inner = obj.get("v").unwrap().as_object().unwrap();
assert_eq!(inner.get("x"), Some(&Value::String("1e9999".into())));
}
#[test]
fn parse_inline_float_negative_overflow_to_string() {
let v = crate::parse("v: {x: -1e9999}").unwrap();
let obj = v.as_object().unwrap();
let inner = obj.get("v").unwrap().as_object().unwrap();
assert_eq!(inner.get("x"), Some(&Value::String("-1e9999".into())));
}
#[test]
fn parse_inline_float_underflow_to_positive_zero() {
let v = crate::parse("v: {x: 1e-9999}").unwrap();
let obj = v.as_object().unwrap();
let inner = obj.get("v").unwrap().as_object().unwrap();
assert_eq!(inner.get("x"), Some(&Value::Float("0.0".into())));
}
#[test]
fn parse_inline_float_negative_underflow_to_negative_zero() {
let v = crate::parse("v: {x: -1e-9999}").unwrap();
let obj = v.as_object().unwrap();
let inner = obj.get("v").unwrap().as_object().unwrap();
assert_eq!(inner.get("x"), Some(&Value::Float("-0.0".into())));
}
#[test]
fn parse_strict_inline_float_overflow_to_string_same_as_lax() {
let v = crate::parse_strict("v: {x: 1e9999}").unwrap();
let obj = v.as_object().unwrap();
let inner = obj.get("v").unwrap().as_object().unwrap();
assert_eq!(inner.get("x"), Some(&Value::String("1e9999".into())));
}
#[test]
fn parse_float_zero_canonical_roundtrip() {
let v = crate::parse("z: 1e-9999\nnz: -1e-9999\n").unwrap();
let out = crate::emit_canonical(&v).unwrap();
assert_eq!(out, "z: 0.0\nnz: -0.0\n");
assert_eq!(crate::parse(&out).unwrap(), v);
}
#[test]
fn parse_float_overflow_string_canonical_roundtrip() {
let v = crate::parse("v: 1e9999\nw: -1e9999\n").unwrap();
let out = crate::emit_canonical(&v).unwrap();
assert_eq!(out, "v:: 1e9999\nw:: -1e9999\n");
assert_eq!(crate::parse(&out).unwrap(), v);
}
#[test]
fn parse_inline_nested_arrays() {
let v = crate::parse("matrix: [[1, 2], [3, 4], [5, 6]]").unwrap();
let obj = v.as_object().unwrap();
let matrix = obj.get("matrix").unwrap().as_array().unwrap();
assert_eq!(matrix.len(), 3);
let first = matrix[0].as_array().unwrap();
assert_eq!(first[0], Value::Integer("1".into()));
assert_eq!(first[1], Value::Integer("2".into()));
}
#[test]
fn parse_inline_mixed_nested() {
let v = crate::parse("users: [{name: alice, age: 30}, {name: bob, age: 25}]").unwrap();
let obj = v.as_object().unwrap();
let users = obj.get("users").unwrap().as_array().unwrap();
assert_eq!(users.len(), 2);
let alice = users[0].as_object().unwrap();
assert_eq!(alice.get("name"), Some(&Value::String("alice".into())));
assert_eq!(alice.get("age"), Some(&Value::Integer("30".into())));
}
#[test]
fn parse_top_level_inline_object() {
let v = crate::parse("{a: 1, b: hello}").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("a"), Some(&Value::Integer("1".into())));
assert_eq!(obj.get("b"), Some(&Value::String("hello".into())));
}
#[test]
fn parse_top_level_inline_array() {
let v = crate::parse("[1, 2, 3]").unwrap();
let arr = v.as_array().unwrap();
assert_eq!(arr.len(), 3);
assert_eq!(arr[0], Value::Integer("1".into()));
}
#[test]
fn parse_top_level_explicit_object() {
let v = crate::parse("{\na: 1\nb: 2\n}").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("a"), Some(&Value::Integer("1".into())));
assert_eq!(obj.get("b"), Some(&Value::Integer("2".into())));
}
#[test]
fn parse_top_level_explicit_array() {
let v = crate::parse("[\nfoo\nbar\n]").unwrap();
let arr = v.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0], Value::String("foo".into()));
assert_eq!(arr[1], Value::String("bar".into()));
}
#[test]
fn parse_orphan_after_top_level_inline() {
let err = crate::parse("{a: 1}\norphan: line").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::OrphanLineAfterTopLevelInline { .. }) => {}
other => panic!("expected OrphanLineAfterTopLevelInline, got: {}", other),
}
}
#[test]
fn parse_inline_unterminated_object() {
let err = crate::parse("cfg: {a: 1, b: 2").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::UnterminatedInlineCompound { .. }) => {}
other => panic!("expected UnterminatedInlineCompound, got: {}", other),
}
}
#[test]
fn parse_inline_double_comma() {
let err = crate::parse("arr: [1,, 2]").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::MalformedInlineCompound { .. }) => {}
other => panic!("expected MalformedInlineCompound, got: {}", other),
}
}
#[test]
fn parse_inline_bad_escape() {
let err = crate::parse("cfg: {a: foo\\t}").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::BadEscapeSequence { .. }) => {}
other => panic!("expected BadEscapeSequence, got: {}", other),
}
}
#[test]
fn parse_inline_backslash_at_eol() {
let err = crate::parse("cfg: {a: foo\\").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::BadEscapeSequence { .. }) => {}
other => panic!("expected BadEscapeSequence, got: {}", other),
}
}
#[test]
fn parse_unicode_escape_basic_inline() {
let v = crate::parse("cfg: {a: \\u0041}").unwrap();
let obj = v.as_object().unwrap();
let cfg = obj.get("cfg").unwrap().as_object().unwrap();
assert_eq!(cfg.get("a"), Some(&Value::String("A".into())));
}
#[test]
fn parse_unicode_escape_not_greedy_inline() {
let v = crate::parse("cfg: {a: \\u00411}").unwrap();
let obj = v.as_object().unwrap();
let cfg = obj.get("cfg").unwrap().as_object().unwrap();
assert_eq!(cfg.get("a"), Some(&Value::String("A1".into())));
}
#[test]
fn parse_unicode_escape_not_greedy_in_key() {
let v = crate::parse("k\\u00411: v").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("kA1"), Some(&Value::String("v".into())));
}
#[test]
fn parse_unicode_escape_hex_case_insensitive() {
let v = crate::parse("cfg: {a: \\u00e9, b: \\u00E9, c: \\uAbCd}").unwrap();
let obj = v.as_object().unwrap();
let cfg = obj.get("cfg").unwrap().as_object().unwrap();
assert_eq!(cfg.get("a"), Some(&Value::String("\u{e9}".into())));
assert_eq!(cfg.get("b"), Some(&Value::String("\u{e9}".into())));
assert_eq!(cfg.get("c"), Some(&Value::String("\u{ABCD}".into())));
}
#[test]
fn parse_unicode_escape_too_few_digits() {
let err = crate::parse("cfg: {a: \\u12}").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::BadEscapeSequence { .. }) => {}
other => panic!("expected BadEscapeSequence, got: {}", other),
}
}
#[test]
fn parse_unicode_escape_too_few_digits_at_value_end() {
let err = crate::parse("cfg: [\\u12]").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::BadEscapeSequence { .. }) => {}
other => panic!("expected BadEscapeSequence, got: {}", other),
}
}
#[test]
fn parse_unicode_escape_non_hex_before_fourth_digit() {
let err = crate::parse("cfg: {a: \\u12g4}").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::BadEscapeSequence { .. }) => {}
other => panic!("expected BadEscapeSequence, got: {}", other),
}
}
#[test]
fn parse_unicode_escape_surrogate_pair() {
let v = crate::parse("cfg: {a: \\uD83D\\uDE00}").unwrap();
let obj = v.as_object().unwrap();
let cfg = obj.get("cfg").unwrap().as_object().unwrap();
assert_eq!(cfg.get("a"), Some(&Value::String("\u{1F600}".into())));
}
#[test]
fn parse_unicode_escape_lone_high_surrogate() {
let err = crate::parse("cfg: {a: \\uD800}").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::BadEscapeSequence { .. }) => {}
other => panic!("expected BadEscapeSequence, got: {}", other),
}
}
#[test]
fn parse_unicode_escape_lone_high_surrogate_before_literal() {
let err = crate::parse("cfg: {a: \\uD800x}").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::BadEscapeSequence { .. }) => {}
other => panic!("expected BadEscapeSequence, got: {}", other),
}
}
#[test]
fn parse_unicode_escape_high_surrogate_then_non_low_escape() {
let err = crate::parse("cfg: {a: \\uD800\\u0041}").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::BadEscapeSequence { .. }) => {}
other => panic!("expected BadEscapeSequence, got: {}", other),
}
}
#[test]
fn parse_unicode_escape_high_surrogate_then_high_surrogate() {
let err = crate::parse("cfg: {a: \\uD800\\uD801}").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::BadEscapeSequence { .. }) => {}
other => panic!("expected BadEscapeSequence, got: {}", other),
}
}
#[test]
fn parse_unicode_escape_lone_low_surrogate() {
let err = crate::parse("cfg: {a: \\uDC00}").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::BadEscapeSequence { .. }) => {}
other => panic!("expected BadEscapeSequence, got: {}", other),
}
}
#[test]
fn parse_unicode_escape_boundaries_around_surrogate_range() {
let v = crate::parse("cfg: {a: \\uD7FF, b: \\uE000}").unwrap();
let obj = v.as_object().unwrap();
let cfg = obj.get("cfg").unwrap().as_object().unwrap();
assert_eq!(cfg.get("a"), Some(&Value::String("\u{D7FF}".into())));
assert_eq!(cfg.get("b"), Some(&Value::String("\u{E000}".into())));
}
#[test]
fn parse_unicode_escape_in_key() {
let v = crate::parse("\\u0041b: v").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("Ab"), Some(&Value::String("v".into())));
}
#[test]
fn parse_unicode_escape_decoded_dot_is_not_structural() {
let v = crate::parse("a\\u002Eb: v").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("a.b"), Some(&Value::String("v".into())));
assert!(obj.get("a").is_none());
}
#[test]
fn parse_unicode_escape_decoded_colon_is_not_structural() {
let v = crate::parse("\\u003A: v").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get(":"), Some(&Value::String("v".into())));
}
#[test]
fn parse_unicode_escape_malformed_in_key_still_errors() {
let err = crate::parse("a\\u12.b: v").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::BadEscapeSequence { .. }) => {}
other => panic!("expected BadEscapeSequence, got: {}", other),
}
}
#[test]
fn parse_unicode_escape_not_processed_in_plain_body() {
let v = crate::parse("note: \\u0041").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("note"), Some(&Value::String("\\u0041".into())));
}
#[test]
fn parse_unicode_escape_not_processed_in_multiline_string() {
let v = crate::parse("note: (\n\\u0041\n)").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("note"), Some(&Value::String("\\u0041".into())));
}
#[test]
fn parse_uppercase_u_is_not_unicode_escape() {
let err = crate::parse("cfg: {a: \\U0041}").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::BadEscapeSequence { .. }) => {}
other => panic!("expected BadEscapeSequence, got: {}", other),
}
}
#[test]
fn parse_unicode_escape_forces_string_not_integer() {
let v = crate::parse("cfg: {v: \\u0030}").unwrap();
let obj = v.as_object().unwrap();
let cfg = obj.get("cfg").unwrap().as_object().unwrap();
assert_eq!(cfg.get("v"), Some(&Value::String("0".into())));
}
#[test]
fn parse_named_escapes_still_work_regression() {
let v = crate::parse(
"cfg: {a: \\\\, b: \\,, c: \\}, d: \\], e: \\{, f: \\[, g: \\n, h: \\r, i: \\., j: \\:}",
)
.unwrap();
let obj = v.as_object().unwrap();
let cfg = obj.get("cfg").unwrap().as_object().unwrap();
assert_eq!(cfg.get("a"), Some(&Value::String("\\".into())));
assert_eq!(cfg.get("b"), Some(&Value::String(",".into())));
assert_eq!(cfg.get("c"), Some(&Value::String("}".into())));
assert_eq!(cfg.get("d"), Some(&Value::String("]".into())));
assert_eq!(cfg.get("e"), Some(&Value::String("{".into())));
assert_eq!(cfg.get("f"), Some(&Value::String("[".into())));
assert_eq!(cfg.get("g"), Some(&Value::String("\n".into())));
assert_eq!(cfg.get("h"), Some(&Value::String("\r".into())));
assert_eq!(cfg.get("i"), Some(&Value::String(".".into())));
assert_eq!(cfg.get("j"), Some(&Value::String(":".into())));
}
#[test]
fn parse_unicode_escape_high_then_malformed_low_errors() {
let err = crate::parse("cfg: {a: \\uD800\\uZZ}").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::BadEscapeSequence { .. }) => {}
other => panic!("expected BadEscapeSequence, got: {}", other),
}
}
#[test]
fn parse_unicode_escape_preserves_decoded_edge_whitespace() {
let v = crate::parse("cfg: {v: \\u0009A\\u0009}").unwrap();
let obj = v.as_object().unwrap();
let cfg = obj.get("cfg").unwrap().as_object().unwrap();
assert_eq!(cfg.get("v"), Some(&Value::String("\tA\t".into())));
}
#[test]
fn parse_unicode_escape_interior_whitespace_preserved() {
let v = crate::parse("cfg: {v: A\\nB}").unwrap();
let obj = v.as_object().unwrap();
let cfg = obj.get("cfg").unwrap().as_object().unwrap();
assert_eq!(cfg.get("v"), Some(&Value::String("A\nB".into())));
}
use super::inline::{decode_key_segment, process_escapes};
use super::validate::{check_key, KeyValidity};
use crate::error::{Error, ErrorKind};
#[test]
fn check_key_quoted_segments() {
use KeyValidity::{Empty, Invalid, Valid};
assert_eq!(check_key("port"), Valid);
assert_eq!(check_key("\"a b\""), Valid);
assert_eq!(check_key("`it's \"quoted\"`"), Valid);
assert_eq!(check_key("\"a,b{c}d[e]:f.g\""), Valid);
assert_eq!(check_key("\" a \""), Valid);
assert_eq!(check_key("\" \""), Valid);
assert_eq!(check_key("\"\""), Empty);
assert_eq!(check_key("''"), Empty);
assert_eq!(check_key("``"), Empty);
assert_eq!(check_key("\"a\"b"), Invalid);
assert_eq!(check_key("\"a\" \"b\""), Invalid);
assert_eq!(check_key("'\"unbalanced"), Invalid);
assert_eq!(check_key("\u{1}a"), Invalid);
assert_eq!(check_key("\u{7F}"), Invalid);
assert!(is_valid_key("a\u{B}b"));
assert!(is_valid_key("a\u{C}b"));
assert_eq!(check_key("\"\u{1}\""), Invalid);
assert_eq!(check_key(r#""a\.b\u{41}""#), Valid);
}
#[test]
fn process_escapes_quote_escapes() {
assert_eq!(process_escapes(r#"a"b"#, 1, S).unwrap(), "a\"b");
assert_eq!(process_escapes(r"a\'b", 1, S).unwrap(), "a'b");
assert_eq!(process_escapes(r"a`b", 1, S).unwrap(), "a`b");
assert_eq!(process_escapes(r"a\:\.b", 1, S).unwrap(), "a:.b");
}
#[test]
fn decode_key_segment_quoted() {
assert_eq!(decode_key_segment("\"a b\"", 1, S).unwrap(), "a b");
assert_eq!(
decode_key_segment("`it's \"quoted\"`", 1, S).unwrap(),
"it's \"quoted\""
);
assert_eq!(decode_key_segment("\" a \"", 1, S).unwrap(), " a ");
assert_eq!(decode_key_segment(r#""a\:b""#, 1, S).unwrap(), "a:b");
assert_eq!(decode_key_segment(r#""a\u0041b""#, 1, S).unwrap(), "aAb");
}
#[test]
fn parse_quoted_keys() {
let v = crate::parse("\"a\": 1").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("a"), Some(&Value::Integer("1".into())));
let v = crate::parse("\" \": 1").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get(" "), Some(&Value::Integer("1".into())));
let e = crate::parse("\"\": 1").unwrap_err();
assert!(matches!(e, Error::Structured(ErrorKind::EmptyKey { .. })));
let e = crate::parse("\"a\"b: 1").unwrap_err();
assert!(matches!(e, Error::Structured(ErrorKind::InvalidKey { .. })));
let v = crate::parse("port\": 1").unwrap();
assert_eq!(
v.as_object().unwrap().get("port\""),
Some(&Value::Integer("1".into()))
);
let v = crate::parse("a\"b: 1").unwrap();
assert_eq!(
v.as_object().unwrap().get("a\"b"),
Some(&Value::Integer("1".into()))
);
let v = crate::parse("cfg: {v: say \"hi\"}").unwrap();
let cfg = v
.as_object()
.unwrap()
.get("cfg")
.unwrap()
.as_object()
.unwrap();
assert_eq!(cfg.get("v"), Some(&Value::String("say \"hi\"".into())));
}
use super::inline::find_matching_close;
use super::inline::find_unescaped_colon_inline;
use super::inline::has_quote_bytes;
use super::inline::split_top_level;
use super::inline::ColonScan;
use super::inline::InlineBody;
use super::inline::InlineBounds;
use super::inline::{key_is_single_segment, scan_unescaped_colon, split_key_path};
#[test]
fn quoted_colon_scan_finds_colon_outside_spans() {
assert_eq!(scan_unescaped_colon("a: 1"), ColonScan::Found(1));
assert_eq!(scan_unescaped_colon("\"a: b\": 1"), ColonScan::Found(6));
assert_eq!(scan_unescaped_colon("'a' : 1"), ColonScan::Found(4));
assert_eq!(scan_unescaped_colon("`a:b`: 1"), ColonScan::Found(5));
assert_eq!(scan_unescaped_colon("a.\"b:c\".d: 1"), ColonScan::Found(9));
assert_eq!(scan_unescaped_colon("a . \"b\": 1"), ColonScan::Found(7));
}
#[test]
fn quoted_colon_scan_unterminated() {
assert_eq!(
scan_unescaped_colon("\"unterm: 1"),
ColonScan::UnterminatedQuote
);
assert_eq!(
scan_unescaped_colon("a.\"unterm"),
ColonScan::UnterminatedQuote
);
}
#[test]
fn quoted_colon_scan_absent_and_escapes() {
assert_eq!(scan_unescaped_colon("no colon"), ColonScan::Absent);
assert_eq!(scan_unescaped_colon("a\\:b: 1"), ColonScan::Found(4));
assert_eq!(scan_unescaped_colon("\"a\\\"b\": 1"), ColonScan::Found(6));
assert_eq!(scan_unescaped_colon("\"a\"b: 1"), ColonScan::Found(4));
}
#[test]
fn r10f1_colon_scan_value_side_quotes_are_not_key_quotes() {
assert_eq!(scan_unescaped_colon("a: \"b:c\""), ColonScan::Found(1));
assert_eq!(
scan_unescaped_colon("a: \"unterminated"),
ColonScan::Found(1)
);
assert_eq!(
scan_unescaped_colon("a: b.\"unterm: 2\""),
ColonScan::Found(1)
);
assert_eq!(
scan_unescaped_colon("a: b.c.\"x:y\".d"),
ColonScan::Found(1)
);
assert_eq!(
scan_unescaped_colon("a: b.\"unterm: 2"),
ColonScan::Found(1)
);
assert_eq!(scan_unescaped_colon("ab\"c:d: 1"), ColonScan::Found(4));
assert_eq!(scan_unescaped_colon("a: b\"c: d"), ColonScan::Found(1));
assert_eq!(scan_unescaped_colon("a.\"x:y\".b: 1"), ColonScan::Found(9));
assert_eq!(scan_unescaped_colon("\"a\".\"b\": 1"), ColonScan::Found(7));
assert_eq!(scan_unescaped_colon("a\\\"b: 1"), ColonScan::Found(4));
assert_eq!(scan_unescaped_colon("a . \"b\": 1"), ColonScan::Found(8));
}
#[test]
fn r10f1_colon_scan_no_candidate_corners() {
assert_eq!(scan_unescaped_colon("a b"), ColonScan::Absent);
assert_eq!(scan_unescaped_colon("a\\:b"), ColonScan::Absent);
assert_eq!(
scan_unescaped_colon("\"unterm"),
ColonScan::UnterminatedQuote
);
assert_eq!(
scan_unescaped_colon("a.\"unterm"),
ColonScan::UnterminatedQuote
);
assert_eq!(scan_unescaped_colon("\"a\" b"), ColonScan::Absent);
}
#[test]
fn quoted_split_key_path_keeps_quotes_in_slices() {
assert_eq!(split_key_path("a.\"b.c\".d"), vec!["a", "\"b.c\"", "d"]);
assert_eq!(split_key_path("\"a\".\"b\""), vec!["\"a\"", "\"b\""]);
assert_eq!(split_key_path("a\\.b"), vec!["a\\.b"]);
assert_eq!(split_key_path("a. \"b\""), vec!["a", " \"b\""]);
assert_eq!(split_key_path("\"a.b\""), vec!["\"a.b\""]);
}
#[test]
fn quoted_key_is_single_segment() {
assert!(key_is_single_segment("\"a.b\""));
assert!(!key_is_single_segment("a.\"b\".c"));
assert!(key_is_single_segment("\"unterm"));
}
#[test]
fn quoted_find_unescaped_colon_inline() {
assert_eq!(find_unescaped_colon_inline("\"a}b\": 1"), Some(5));
assert_eq!(find_unescaped_colon_inline("\"a: 1"), None);
assert_eq!(find_unescaped_colon_inline("a: \"b}"), Some(1));
}
#[test]
fn quoted_split_top_level_object_mode() {
assert_eq!(
split_top_level(
"\"a}b\": 1, c: 2",
1,
S,
InlineBody::Object,
InlineBounds::for_input("\"a}b\": 1, c: 2"),
has_quote_bytes("\"a}b\": 1, c: 2".as_bytes())
)
.unwrap(),
vec!["\"a}b\": 1", " c: 2"]
);
assert_eq!(
split_top_level(
"a: \"x,y\", b: 2",
1,
S,
InlineBody::Object,
InlineBounds::for_input("a: \"x,y\", b: 2"),
has_quote_bytes("a: \"x,y\", b: 2".as_bytes())
)
.unwrap(),
vec!["a: \"x", "y\"", " b: 2"]
);
match split_top_level(
"\"a: 1",
1,
S,
InlineBody::Object,
InlineBounds::for_input("\"a: 1"),
has_quote_bytes("\"a: 1".as_bytes()),
) {
Err(crate::Error::Structured(crate::ErrorKind::UnterminatedInlineCompound { .. })) => {}
other => panic!(
"expected UnterminatedInlineCompound, got: {:?}",
other.err()
),
}
}
#[test]
fn quoted_split_top_level_array_mode_ignores_quotes() {
assert_eq!(
split_top_level(
"\"a,b\", c",
1,
S,
InlineBody::Array,
InlineBounds::for_input("\"a,b\", c"),
has_quote_bytes("\"a,b\", c".as_bytes())
)
.unwrap(),
vec!["\"a", "b\"", " c"]
);
}
#[test]
fn quoted_find_matching_close_object_mode() {
let input = "{\"a}b\": 1}";
assert_eq!(
find_matching_close(input, b'{', b'}'),
Some(input.len() - 1)
);
assert_eq!(find_matching_close("{\"a: 1}", b'{', b'}'), None);
let input = "{a: 1, \"b}c\": 2}";
assert_eq!(
find_matching_close(input, b'{', b'}'),
Some(input.len() - 1)
);
assert_eq!(find_matching_close("[\"a]b\"]", b'[', b']'), Some(3));
}
#[test]
fn quoted_find_matching_close_triple_nested() {
let input = r#"{a: {b: {"c}d": 1}}}"#;
assert_eq!(
find_matching_close(input, b'{', b'}'),
Some(input.len() - 1)
);
let input = r#"{b: {"c}d": 1}}"#;
assert_eq!(
find_matching_close(input, b'{', b'}'),
Some(input.len() - 1)
);
}
#[test]
fn r3f1_split_top_level_trailing_ws_after_comma_no_phantom_segment() {
for tail in [" ", "\t", "\u{00a0}"] {
let body = format!("\"a\": 1,{tail}");
assert_eq!(
split_top_level(
&body,
1,
S,
InlineBody::Object,
InlineBounds::for_input(&body),
has_quote_bytes(body.as_bytes()),
)
.unwrap(),
vec!["\"a\": 1"]
);
}
assert_eq!(
split_top_level(
"\"a\": 1,",
1,
S,
InlineBody::Object,
InlineBounds::for_input("\"a\": 1,"),
has_quote_bytes("\"a\": 1,".as_bytes())
)
.unwrap(),
vec!["\"a\": 1", ""]
);
assert_eq!(
split_top_level(
"\"a b\": 1, ",
1,
S,
InlineBody::Object,
InlineBounds::for_input("\"a b\": 1, "),
has_quote_bytes("\"a b\": 1, ".as_bytes())
)
.unwrap(),
vec!["\"a b\": 1"]
);
assert_eq!(
split_top_level(
"a: \"x\", ",
1,
S,
InlineBody::Object,
InlineBounds::for_input("a: \"x\", "),
has_quote_bytes("a: \"x\", ".as_bytes())
)
.unwrap(),
vec!["a: \"x\""]
);
}
#[test]
fn r3f1_split_top_level_dotted_key_trailing_ws_raw_last_segment() {
for tail in [" ", "\t"] {
let body = format!(" \"a\": 1, b.{tail}");
let seg2 = format!(" b.{tail}");
assert_eq!(
split_top_level(
&body,
1,
S,
InlineBody::Object,
InlineBounds::for_input(&body),
has_quote_bytes(body.as_bytes()),
)
.unwrap(),
vec![" \"a\": 1", seg2.as_str()]
);
}
assert_eq!(
split_top_level(
" \"a\". ",
1,
S,
InlineBody::Object,
InlineBounds::for_input(" \"a\". "),
has_quote_bytes(" \"a\". ".as_bytes()),
)
.unwrap(),
vec![" \"a\". "]
);
assert_eq!(
split_top_level(
"a. b : 1",
1,
S,
InlineBody::Object,
InlineBounds::for_input("a. b : 1"),
has_quote_bytes("a. b : 1".as_bytes())
)
.unwrap(),
vec!["a. b : 1"]
);
match crate::parse("{\"a\": 1, b. }") {
Err(crate::Error::Structured(crate::ErrorKind::MalformedInlineCompound { .. })) => {}
other => panic!("expected MalformedInlineCompound, got {other:?}"),
}
}
#[test]
fn r3f1_find_matching_close_eof_after_trailing_ws() {
assert_eq!(find_matching_close("{\"a\": 1, ", b'{', b'}'), None);
assert_eq!(find_matching_close("{\"a\": 1, }", b'{', b'}'), Some(9));
}
#[test]
fn r3f1_scan_inline_closer_eof_after_trailing_ws() {
use super::inline::{scan_inline_closer, InlineCloserScan};
assert!(matches!(
scan_inline_closer("{\"a\": 1, ", b'{', b'}', 1, S),
InlineCloserScan::NotFound
));
assert!(matches!(
scan_inline_closer("{\"a\": 1, }", b'{', b'}', 1, S),
InlineCloserScan::Found(9)
));
}
#[test]
fn r3f1_find_unescaped_colon_inline_eof_after_dotted_ws() {
assert_eq!(find_unescaped_colon_inline("a. "), None);
assert_eq!(find_unescaped_colon_inline("a. : 1"), Some(3));
}
#[test]
fn quoted_key_with_spaces() {
let v = crate::parse("\"a b\": 1").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("a b"), Some(&Value::Integer("1".into())));
}
#[test]
fn quoted_key_backtick_with_inner_quotes() {
let v = crate::parse("`it's \"quoted\"`: 1").unwrap();
let obj = v.as_object().unwrap();
let key = obj.keys().next().unwrap().clone();
assert_eq!(key, "it's \"quoted\"");
assert_eq!(key.len(), 13);
}
#[test]
fn quoted_key_interior_not_trimmed() {
let v = crate::parse("\" a \": 1").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get(" a "), Some(&Value::Integer("1".into())));
}
#[test]
fn quoted_segment_in_dotted_path() {
let v = crate::parse("a.\"b.c\".d: 1").unwrap();
let obj = v.as_object().unwrap();
let a = obj.get("a").unwrap().as_object().unwrap();
let mid = a.get("b.c").unwrap().as_object().unwrap();
assert_eq!(mid.get("d"), Some(&Value::Integer("1".into())));
}
#[test]
fn quoted_adjacent_segments_decode() {
let v = crate::parse("\"a\".\"b\": 1").unwrap();
let obj = v.as_object().unwrap();
let a = obj.get("a").unwrap().as_object().unwrap();
assert_eq!(a.get("b"), Some(&Value::Integer("1".into())));
}
#[test]
fn quoted_key_comma_inside_inline_object() {
let v = crate::parse("k: {\"a,b\": 1, c: 2}").unwrap();
let k = v
.as_object()
.unwrap()
.get("k")
.unwrap()
.as_object()
.unwrap();
assert_eq!(k.len(), 2);
assert_eq!(k.get("a,b"), Some(&Value::Integer("1".into())));
assert_eq!(k.get("c"), Some(&Value::Integer("2".into())));
}
#[test]
fn quoted_key_brace_inside_inline_object() {
let v = crate::parse("k: {\"a}b\": 1, c: 2}").unwrap();
let k = v
.as_object()
.unwrap()
.get("k")
.unwrap()
.as_object()
.unwrap();
assert_eq!(k.len(), 2);
assert_eq!(k.get("a}b"), Some(&Value::Integer("1".into())));
assert_eq!(k.get("c"), Some(&Value::Integer("2".into())));
}
#[test]
fn quoted_key_brace_inside_root_inline_object() {
let v = crate::parse("{\"a}b\": 1, c: 2}").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.len(), 2);
assert_eq!(obj.get("a}b"), Some(&Value::Integer("1".into())));
assert_eq!(obj.get("c"), Some(&Value::Integer("2".into())));
}
#[test]
fn quoted_key_unterminated_inline_root_is_unterminated_compound() {
let err = crate::parse("{\"a: 1}").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::UnterminatedInlineCompound { .. }) => {}
other => panic!("expected UnterminatedInlineCompound, got: {}", other),
}
}
#[test]
fn quoted_key_unterminated_after_established_object() {
let err = crate::parse("y: 1\n'unterminated: 1").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::UnterminatedQuotedKey { .. }) => {}
other => panic!("expected UnterminatedQuotedKey, got: {}", other),
}
}
#[test]
fn quoted_key_unterminated_in_root_pair_falls_to_array() {
let v = crate::parse("'tis the season: fa").unwrap();
let arr = v.as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0], Value::String("'tis the season: fa".into()));
}
#[test]
fn quoted_root_object_key() {
let v = crate::parse("\"port\": 1").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("port"), Some(&Value::Integer("1".into())));
}
#[test]
fn quoted_key_unterminated_in_established_object_line() {
let err = crate::parse("cfg:\n a: 1\n \"unterminated: 1").unwrap_err();
match err {
crate::Error::Structured(crate::ErrorKind::UnterminatedQuotedKey { .. }) => {}
other => panic!("expected UnterminatedQuotedKey, got: {}", other),
}
}
#[test]
fn r3f2_find_matching_close_array_nested_quoted_keys() {
use super::inline::find_matching_close;
assert_eq!(
find_matching_close("[{\"x]y\": 1},2]", b'[', b']'),
Some(13)
);
assert_eq!(
find_matching_close("[{\"x}y\": 1},2]", b'[', b']'),
Some(13)
);
assert_eq!(find_matching_close("[{\"x]y\": 1", b'[', b']'), None);
assert_eq!(
find_matching_close("[[{\"x]y\": 1}]]", b'[', b']'),
Some(13)
);
}
#[test]
fn r3f2_scan_inline_closer_array_nested_quoted_keys() {
use super::inline::{scan_inline_closer, InlineCloserScan};
assert!(matches!(
scan_inline_closer("[{\"x]y\": 1},2]", b'[', b']', 1, S),
InlineCloserScan::Found(13)
));
assert!(matches!(
scan_inline_closer("[{\"x}y\": 1},2]", b'[', b']', 1, S),
InlineCloserScan::Found(13)
));
assert!(matches!(
scan_inline_closer("[[{\"x]y\": 1}]]", b'[', b']', 1, S),
InlineCloserScan::Found(13)
));
assert!(matches!(
scan_inline_closer("{\"x}y\": 1}", b'{', b'}', 1, S),
InlineCloserScan::Found(9)
));
}
#[test]
fn r3f2_split_top_level_array_body_quoted_key_object() {
use super::inline::{split_top_level, InlineBody};
assert_eq!(
split_top_level(
"{\"x]y\": 1},2",
1,
S,
InlineBody::Array,
InlineBounds::for_input("{\"x]y\": 1},2"),
has_quote_bytes("{\"x]y\": 1},2".as_bytes())
)
.unwrap(),
vec!["{\"x]y\": 1}", "2"]
);
}
#[test]
fn r3f4_split_top_level_midvalue_balanced_brace_splits_at_inner_comma() {
use super::inline::{split_top_level, InlineBody};
assert_eq!(
split_top_level(
"a: x{y,z}, b: 2",
1,
S,
InlineBody::Object,
InlineBounds::for_input("a: x{y,z}, b: 2"),
has_quote_bytes("a: x{y,z}, b: 2".as_bytes())
)
.unwrap(),
vec!["a: x{y", "z}", " b: 2"]
);
}
#[test]
fn r3f4_split_top_level_midvalue_brace_quote_aware_slow_path() {
use super::inline::{split_top_level, InlineBody};
assert_eq!(
split_top_level(
"k: \"v\", a: x{y,z}, b: 2",
1,
S,
InlineBody::Object,
InlineBounds::for_input("k: \"v\", a: x{y,z}, b: 2"),
has_quote_bytes("k: \"v\", a: x{y,z}, b: 2".as_bytes())
)
.unwrap(),
vec!["k: \"v\"", " a: x{y", "z}", " b: 2"]
);
}
#[test]
fn r3f4_split_top_level_array_body_midvalue_brace_splits_at_inner_comma() {
use super::inline::{split_top_level, InlineBody};
assert_eq!(
split_top_level(
"x{y,z}, 2",
1,
S,
InlineBody::Array,
InlineBounds::for_input("x{y,z}, 2"),
has_quote_bytes("x{y,z}, 2".as_bytes())
)
.unwrap(),
vec!["x{y", "z}", " 2"]
);
}
#[test]
fn r3f4_split_top_level_genuine_value_start_compounds_guard() {
use super::inline::{split_top_level, InlineBody};
assert_eq!(
split_top_level(
"{a: 1}, 2",
1,
S,
InlineBody::Array,
InlineBounds::for_input("{a: 1}, 2"),
has_quote_bytes("{a: 1}, 2".as_bytes())
)
.unwrap(),
vec!["{a: 1}", " 2"]
);
assert_eq!(
split_top_level(
"a: {y: 1}, b: 2",
1,
S,
InlineBody::Object,
InlineBounds::for_input("a: {y: 1}, b: 2"),
has_quote_bytes("a: {y: 1}, b: 2".as_bytes())
)
.unwrap(),
vec!["a: {y: 1}", " b: 2"]
);
}
#[test]
fn r3f4_scan_inline_closer_midvalue_balanced_brace_is_body_closer() {
use super::inline::{scan_inline_closer, InlineCloserScan};
assert!(matches!(
scan_inline_closer("{a: x{y,z}, b: 2}", b'{', b'}', 1, S),
InlineCloserScan::Found(9)
));
}
#[test]
fn r3f4_scan_inline_closer_crossed_bracket_not_found() {
use super::inline::{scan_inline_closer, InlineCloserScan};
assert!(matches!(
scan_inline_closer("{a: x[y,z], b: 2}", b'{', b'}', 1, S),
InlineCloserScan::NotFound
));
}
#[test]
fn r3f4_scan_inline_closer_genuine_compounds_guard() {
use super::inline::{scan_inline_closer, InlineCloserScan};
assert!(matches!(
scan_inline_closer("{a: {y: 1}, b: 2}", b'{', b'}', 1, S),
InlineCloserScan::Found(16)
));
assert!(matches!(
scan_inline_closer("[{a: 1}, 2]", b'[', b']', 1, S),
InlineCloserScan::Found(10)
));
}
#[test]
fn memo_bounds_are_a_pure_memo_of_the_live_dispatches() {
use super::inline::{
find_matching_close, scan_inline_closer, scan_inline_closer_with_bounds, InlineCloserScan,
};
let hostile = [
"{a: {]}{, ,x{x,}",
"{a: {[][x]}]}n\"}",
"{a: {x ]}, 2}",
"{a: {]a{ :x}}",
"{a: [a],{:[,:,}]}",
"{a: [ a:,:[],{:[}, ]}",
"{a: [],{[:[,}]}",
"{a: [],[:[},a[{ {,a ]}",
"[[][text]",
"[[]['text]",
"[{}[text]",
"{a: [[][text]}",
"{a: [[][text], q: '}",
];
let check = |body: &str| {
let bytes = body.as_bytes();
let (open, close) = if bytes[0] == b'[' {
(b'[', b']')
} else {
(b'{', b'}')
};
let mut pairs = Vec::new();
let verdict = scan_inline_closer_with_bounds(body, open, close, 0, S, &mut pairs);
if !matches!(verdict, InlineCloserScan::Found(_)) {
assert!(
pairs.is_empty(),
"bounds recorded on a non-Found gate: {body:?} {pairs:?}"
);
return;
}
for &(o, c) in &pairs {
let ob = bytes[o];
let (so, sc) = if ob == b'[' {
(b'[', b']')
} else {
(b'{', b'}')
};
for (shape_name, span) in [("suffix", &body[o..]), ("bounded", &body[o..=c])] {
assert!(
matches!(
scan_inline_closer(span, so, sc, 0, S),
InlineCloserScan::Found(f) if f == c - o
),
"scan ({shape_name}) disagrees with the memo at {o}..{c} in {body:?}"
);
assert_eq!(
find_matching_close(span, so, sc),
Some(c - o),
"find ({shape_name}) disagrees with the memo at {o}..{c} in {body:?}"
);
}
}
};
for b in hostile {
check(b);
}
let alpha: &[u8] = b"{[]}a:,.'\"` ";
let mut buf = [0u8; 5];
for len in 1..=5usize {
let total = alpha.len().pow(len as u32);
for mut idx in 0..total {
for d in (0..len).rev() {
buf[d] = alpha[idx % alpha.len()];
idx /= alpha.len();
}
let body = std::str::from_utf8(&buf[..len]).unwrap();
if body.starts_with('{') || body.starts_with('[') {
check(body);
}
}
}
}
#[test]
fn r9f1_crossed_closer_fuzz_inputs_reclassified_by_key_position() {
let cases = [
"k: {a: [a],{:[,:,}]}",
"k: {a: [ a:,:[],{:[}, ]}",
"k: {a: [],{[:[,}]}",
];
for input in cases {
let expect_unterminated = |res: Result<(), crate::Error>| {
assert!(
matches!(
res,
Err(crate::Error::Structured(
crate::ErrorKind::UnterminatedInlineCompound { .. }
))
),
"input {input:?}: expected UnterminatedInlineCompound, got {res:?}"
);
};
expect_unterminated(crate::parse(input).map(|_| ()));
expect_unterminated(crate::parse_strict(input).map(|_| ()));
expect_unterminated(crate::from_str::<serde_json::Value>(input).map(|_| ()));
expect_unterminated(crate::parse_events(input, |_| {}).map(|_| ()));
}
}
#[test]
fn r9f1_key_bracket_is_invalid_key_not_phantom_compound() {
use crate::ErrorKind;
let cases = [
"{[a: 1}",
"{{a: 1}",
"{x: 0, [a: 1}",
"{x: 0, {a: 1}",
"{x: 0, [a: 1, q: '}",
];
for input in cases {
let expect_invalid_key = |res: Result<(), crate::Error>| match res {
Err(crate::Error::Structured(ErrorKind::InvalidKey { key, .. })) => {
assert!(
key.starts_with('[') || key.starts_with('{'),
"input {input:?}: unexpected offending key {key:?}"
);
}
other => panic!("input {input:?}: expected InvalidKey, got {other:?}"),
};
expect_invalid_key(crate::parse(input).map(|_| ()));
expect_invalid_key(crate::parse_strict(input).map(|_| ()));
expect_invalid_key(crate::from_str::<serde_json::Value>(input).map(|_| ()));
expect_invalid_key(crate::parse_events(input, |_| {}).map(|_| ()));
}
}
#[test]
fn r9f1_key_bracket_positive_controls_unchanged() {
use crate::Value;
let v = crate::parse(r"{x: 0, \[a: 1}").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("x"), Some(&Value::Integer("0".into())));
assert_eq!(obj.get("[a"), Some(&Value::Integer("1".into())));
let v = crate::parse(r"{\[a: 1}").unwrap();
assert_eq!(
v.as_object().unwrap().get("[a"),
Some(&Value::Integer("1".into()))
);
let v = crate::parse("{x: 0, a: [1]}").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("x"), Some(&Value::Integer("0".into())));
assert_eq!(
obj.get("a"),
Some(&Value::Array(vec![Value::Integer("1".into())]))
);
let v = crate::parse("{x: 0, a: {b: 2}}").unwrap();
let inner = v
.as_object()
.unwrap()
.get("a")
.unwrap()
.as_object()
.unwrap();
assert_eq!(inner.get("b"), Some(&Value::Integer("2".into())));
let v = crate::parse("{\"a}b\": 1, c: 2}").unwrap();
let obj = v.as_object().unwrap();
assert_eq!(obj.get("a}b"), Some(&Value::Integer("1".into())));
assert_eq!(obj.get("c"), Some(&Value::Integer("2".into())));
let v = crate::parse("{\"[x]\": 1}").unwrap();
assert_eq!(
v.as_object().unwrap().get("[x]"),
Some(&Value::Integer("1".into()))
);
assert!(crate::parse_events(r"{x: 0, \[a: 1}", |_| {}).is_ok());
assert!(crate::parse_events("{x: 0, a: [1]}", |_| {}).is_ok());
assert!(crate::parse_events("{\"a}b\": 1, c: 2}", |_| {}).is_ok());
}
#[test]
fn r9f1_forbidden_bracket_and_bad_escape_precedence() {
let expect_bad_escape = |res: Result<(), crate::Error>| {
assert!(
matches!(
res,
Err(crate::Error::Structured(
crate::ErrorKind::BadEscapeSequence { .. }
))
),
"expected BadEscapeSequence, got {res:?}"
);
};
expect_bad_escape(crate::parse(r"{[a\q: 1}").map(|_| ()));
expect_bad_escape(crate::parse_strict(r"{[a\q: 1}").map(|_| ()));
expect_bad_escape(crate::from_str::<serde_json::Value>(r"{[a\q: 1}").map(|_| ()));
expect_bad_escape(crate::parse_events(r"{[a\q: 1}", |_| {}).map(|_| ()));
let err = crate::parse(r"a\q[ : 1").expect_err("must be InvalidKey");
assert!(
matches!(
err,
crate::Error::Structured(crate::ErrorKind::InvalidKey { .. })
),
"expected InvalidKey, got {err:?}"
);
}
#[test]
fn r9f1_find_unescaped_colon_inline_ignores_key_depth() {
assert_eq!(find_unescaped_colon_inline("[a: 1"), Some(2));
assert_eq!(find_unescaped_colon_inline("{a: 1"), Some(2));
assert_eq!(find_unescaped_colon_inline("\"[a\": 1"), Some(4));
assert_eq!(find_unescaped_colon_inline("a: {b: 1}"), Some(1));
assert_eq!(find_unescaped_colon_inline("a: [1, {c: 2}]"), Some(1));
}
#[test]
fn r8f2_fast_in_key_residue_no_longer_recategorizes_crossed_closer() {
let input = "k: {a: [],[:[},a[{ {,a ]}";
let expect_unterminated = |res: Result<(), crate::Error>| {
assert!(
matches!(
res,
Err(crate::Error::Structured(
crate::ErrorKind::UnterminatedInlineCompound { .. }
))
),
"input {input:?}: expected UnterminatedInlineCompound"
);
};
expect_unterminated(crate::parse(input).map(|_| ()));
expect_unterminated(crate::parse_strict(input).map(|_| ()));
expect_unterminated(crate::parse_events(input, |_| {}).map(|_| ()));
}
#[test]
fn r8f2_closed_empty_array_consumes_value_start_across_modes() {
let cases = [
"[[][text]",
"[[]['text]",
"[{}[text]",
"{a: [[][text]}",
"{a: [[][text], q: '}",
];
for input in cases {
let expect_malformed = |res: Result<(), crate::Error>| {
assert!(
matches!(
res,
Err(crate::Error::Structured(
crate::ErrorKind::MalformedInlineCompound { .. }
))
),
"input {input:?}: expected MalformedInlineCompound"
);
};
expect_malformed(crate::parse(input).map(|_| ()));
expect_malformed(crate::parse_strict(input).map(|_| ()));
expect_malformed(crate::parse_events(input, |_| {}).map(|_| ()));
}
}
#[path = "../../benches/fixtures_ix.rs"]
mod ix_fixtures;
#[test]
fn ix_probe_r8f6_fixture_shapes_are_valid() {
use crate::Value;
let doc = ix_fixtures::wide_line_arr_tiny(64);
let v = crate::parse(&doc).expect("wide_arr_tiny must parse");
let arr = v.as_object().unwrap().get("k").unwrap().as_array().unwrap();
assert_eq!(arr.len(), 64);
assert!(arr
.iter()
.all(|it| matches!(it, Value::Object(o) if o.is_empty())));
let doc = ix_fixtures::wide_line_obj_tiny(64);
let v = crate::parse(&doc).expect("wide_obj_tiny must parse");
let obj = v
.as_object()
.unwrap()
.get("k")
.unwrap()
.as_object()
.unwrap();
assert_eq!(obj.len(), 64);
assert!(obj
.iter()
.all(|(_, it)| matches!(it, Value::Object(o) if o.is_empty())));
let doc = ix_fixtures::wide_line_two_level(64);
let v = crate::parse(&doc).expect("two_level must parse");
let arr = v.as_object().unwrap().get("k").unwrap().as_array().unwrap();
assert_eq!(arr.len(), 64);
for it in arr {
let inner = it.as_array().expect("two-level item must be an Array");
assert_eq!(inner.len(), 1);
assert!(matches!(inner[0], Value::Object(ref o) if o.is_empty()));
}
let doc = ix_fixtures::many_inline_trees(50);
let v = crate::parse(&doc).expect("many_trees must parse");
let root = v.as_object().unwrap();
assert_eq!(root.len(), 50);
for (k, it) in root {
let arr = it
.as_array()
.unwrap_or_else(|| panic!("{k}: expected Array"));
assert_eq!(arr.len(), 2, "{k}: expected 2 items");
}
let doc = ix_fixtures::deep_chain(32);
let v = crate::parse(&doc).expect("deep_chain must parse");
let mut depth = 0usize;
let mut cur = &v;
while let Some(obj) = cur.as_object() {
let key = if depth == 0 { "k" } else { "a" };
cur = obj
.get(key)
.unwrap_or_else(|| panic!("deep_chain spine key {key} missing"));
depth += 1;
if cur.as_object().is_none() {
break;
}
}
assert_eq!(depth, 33, "deep_chain spine depth drift");
assert!(ix_fixtures::wide_line_arr_tiny(4096).len() > 12_000);
}
fn ix_print_counters(
name: &str,
path: &str,
doc_bytes: usize,
s: &super::inline::ix_probe::Snapshot,
) {
println!(
"IX\t{name}\t{path}\tdoc_bytes={doc_bytes}\tbodies={}\tbody_bytes={}\tpairs={}\tpairs_max={}\tkc={}/{}\tkc_steps={}\tkc_steps_max={}\toca={}/{}\toca_steps={}\toca_steps_max={}\tsort={}\tsort_elems={}\tsort_cmps={}",
s.bodies, s.body_bytes, s.pairs_total, s.pairs_max,
s.kc_hits, s.kc_calls, s.kc_steps, s.kc_steps_max,
s.oca_hits, s.oca_calls, s.oca_steps, s.oca_steps_max,
s.sort_calls, s.sort_elems, s.sort_cmps,
);
}
#[test]
fn ix_probe_r8f6_index_counters() {
use std::fmt::Write as _;
let shapes: Vec<(&str, String)> = vec![
("wide_arr_tiny_64", ix_fixtures::wide_line_arr_tiny(64)),
("wide_arr_tiny_1024", ix_fixtures::wide_line_arr_tiny(1024)),
("wide_arr_tiny_4096", ix_fixtures::wide_line_arr_tiny(4096)),
(
"wide_arr_small_1024",
ix_fixtures::wide_line_arr_small(1024),
),
("wide_obj_tiny_1024", ix_fixtures::wide_line_obj_tiny(1024)),
("two_level_512", ix_fixtures::wide_line_two_level(512)),
("many_trees_2000", ix_fixtures::many_inline_trees(2000)),
("deep_chain_32", ix_fixtures::deep_chain(32)),
("deep_chain_96", ix_fixtures::deep_chain(96)),
("inline_doc_50k", {
let mut out = String::with_capacity(51_200);
let mut i = 0u32;
while out.len() < 50_000 {
let _ = writeln!(
out,
"k{i}: {{a: {i}, b: text item {i}, c: [{i}, {}, {i}], d:: raw {i}, e: {{deep: {}.5}}}}",
i + 1,
i % 10
);
i += 1;
}
out
}),
("synth_50k", ix_fixtures::medium_50k()),
];
for (name, doc) in &shapes {
let doc_bytes = doc.len();
super::inline::ix_probe::reset();
let v = crate::parse(doc).unwrap_or_else(|e| panic!("{name}: owned parse failed: {e}"));
let s = super::inline::ix_probe::snapshot();
assert!(!v.as_object().expect(name).is_empty());
ix_print_counters(name, "P", doc_bytes, &s);
super::inline::ix_probe::reset();
let mut events = 0usize;
crate::parse_events(doc, |_ev| {
events += 1;
})
.unwrap_or_else(|e| panic!("{name}: thin parse failed: {e}"));
let s = super::inline::ix_probe::snapshot();
assert!(events > 0);
ix_print_counters(name, "E", doc_bytes, &s);
}
}
fn ix_run_parse(input: &str) -> bool {
std::hint::black_box(crate::parse(input).is_ok())
}
fn ix_run_events(input: &str) -> bool {
std::hint::black_box(crate::parse_events(input, |_ev| {}).is_ok())
}
#[test]
#[ignore]
fn ix_probe_r8f6_wall_clock() {
use std::fmt::Write as _;
use std::hint::black_box;
use std::time::{Duration, Instant};
let shapes: Vec<(&str, String)> = vec![
("wide_arr_tiny_1024", ix_fixtures::wide_line_arr_tiny(1024)),
("wide_arr_tiny_4096", ix_fixtures::wide_line_arr_tiny(4096)),
(
"wide_arr_small_1024",
ix_fixtures::wide_line_arr_small(1024),
),
("wide_obj_tiny_1024", ix_fixtures::wide_line_obj_tiny(1024)),
("two_level_512", ix_fixtures::wide_line_two_level(512)),
("many_trees_2000", ix_fixtures::many_inline_trees(2000)),
("deep_chain_96", ix_fixtures::deep_chain(96)),
("inline_doc_50k", {
let mut out = String::with_capacity(51_200);
let mut i = 0u32;
while out.len() < 50_000 {
let _ = writeln!(
out,
"k{i}: {{a: {i}, b: text item {i}, c: [{i}, {}, {i}], d:: raw {i}, e: {{deep: {}.5}}}}",
i + 1,
i % 10
);
i += 1;
}
out
}),
("synth_50k", ix_fixtures::medium_50k()),
];
for (name, doc) in &shapes {
for (path, run) in [
("P", ix_run_parse as fn(&str) -> bool),
("E", ix_run_events),
] {
for _ in 0..2 {
run(doc);
}
let mut iters: u64 = 1;
loop {
let t = Instant::now();
for _ in 0..iters {
black_box(run(doc));
}
if t.elapsed() >= Duration::from_millis(40) || iters >= (1 << 22) {
break;
}
iters *= 2;
}
println!("SCEN {name}:{path} iters={iters}");
for batch in 0..9u32 {
let t = Instant::now();
for _ in 0..iters {
black_box(run(doc));
}
println!(
"SCEN {name}:{path} batch={batch} nanos={}",
t.elapsed().as_nanos() as u64
);
}
}
}
let line = ix_fixtures::wide_line_arr_small(1024);
let body = line.trim_end_matches('\n');
let body = &body["k: ".len()..];
let mut pairs = Vec::new();
let verdict = super::inline::scan_inline_closer_with_bounds(body, b'[', b']', 0, S, &mut pairs);
assert!(matches!(verdict, super::inline::InlineCloserScan::Found(_)));
assert_eq!(
pairs.len(),
1024,
"wide_arr_small body must record 1024 pairs"
);
let bounds = InlineBounds::over(body, &pairs);
let hit_slice = &body[1..6];
assert_eq!(bounds.known_closer(hit_slice), Some(4));
let miss_slice = &body[3..8];
assert_eq!(bounds.known_closer(miss_slice), None);
for (label, slice, expect) in [("hit", hit_slice, 4usize), ("miss", miss_slice, 0usize)] {
let mut iters: u64 = 1;
loop {
let t = Instant::now();
for _ in 0..iters {
black_box(bounds.known_closer(black_box(slice)));
}
if t.elapsed() >= Duration::from_millis(40) || iters >= (1 << 24) {
break;
}
iters *= 2;
}
let mut best: u128 = u128::MAX;
for _ in 0..9u32 {
let t = Instant::now();
for _ in 0..iters {
black_box(bounds.known_closer(black_box(slice)));
}
best = best.min(t.elapsed().as_nanos());
}
println!("MIKC C=1024 kind={label} expect={expect} iters={iters} min_batch_nanos={best} ns_per_op={}", best as f64 / iters as f64);
}
for (order_label, table) in [
(
"asc",
(0..1024).map(|i| (i * 6, i * 6 + 4)).collect::<Vec<_>>(),
),
(
"reverse",
(0..1024)
.rev()
.map(|i| (i * 6, i * 6 + 4))
.collect::<Vec<_>>(),
),
] {
let mut iters: u64 = 1;
loop {
let t = Instant::now();
for _ in 0..iters {
let mut t2 = table.clone();
t2.sort_unstable_by_key(|p| p.0);
black_box(&t2);
}
if t.elapsed() >= Duration::from_millis(40) || iters >= (1 << 20) {
break;
}
iters *= 2;
}
let mut best: u128 = u128::MAX;
for _ in 0..9u32 {
let t = Instant::now();
for _ in 0..iters {
let mut t2 = table.clone();
t2.sort_unstable_by_key(|p| p.0);
black_box(&t2);
}
best = best.min(t.elapsed().as_nanos());
}
println!("MISORT C=1024 order={order_label} iters={iters} min_batch_nanos={best} ns_per_sort_incl_clone={}", best as f64 / iters as f64);
}
}
#[test]
#[ignore]
fn ix_probe_r8f6_memo_lookup_ab() {
use std::hint::black_box;
use std::time::{Duration, Instant};
use super::inline::ix_probe;
let shapes: Vec<(&str, String)> = vec![
("wide_arr_tiny_1024", ix_fixtures::wide_line_arr_tiny(1024)),
("wide_arr_tiny_4096", ix_fixtures::wide_line_arr_tiny(4096)),
(
"wide_arr_small_1024",
ix_fixtures::wide_line_arr_small(1024),
),
("wide_obj_tiny_1024", ix_fixtures::wide_line_obj_tiny(1024)),
("two_level_512", ix_fixtures::wide_line_two_level(512)),
("many_trees_2000", ix_fixtures::many_inline_trees(2000)),
("deep_chain_96", ix_fixtures::deep_chain(96)),
];
for (name, doc) in &shapes {
ix_probe::reset();
let on = crate::parse(doc).unwrap_or_else(|e| panic!("{name}: memo parse failed: {e}"));
let lookups_before_bypass = {
let s = ix_probe::snapshot();
s.kc_calls + s.oca_calls
};
let bypassed = ix_probe::set_bypass(true);
let off = crate::parse(doc).unwrap_or_else(|e| panic!("{name}: bypass parse failed: {e}"));
let lookup_calls_while_bypassed = {
let s = ix_probe::snapshot();
s.kc_calls + s.oca_calls - lookups_before_bypass
};
drop(bypassed);
assert_eq!(
format!("{on:?}"),
format!("{off:?}"),
"{name}: bypass changed the parse result"
);
assert_eq!(
lookup_calls_while_bypassed, 0,
"{name}: bypass did not engage"
);
for (label, bypass) in [("on", false), ("off", true)] {
let _mode = ix_probe::set_bypass(bypass);
for _ in 0..2 {
black_box(crate::parse(doc).is_ok());
}
let mut iters: u64 = 1;
loop {
let t = Instant::now();
for _ in 0..iters {
black_box(crate::parse(doc).is_ok());
}
let e = t.elapsed();
if e >= Duration::from_millis(40) || iters >= (1 << 22) {
break;
}
iters *= 2;
}
for batch in 0..9u32 {
let t = Instant::now();
for _ in 0..iters {
black_box(crate::parse(doc).is_ok());
}
let e = t.elapsed();
println!(
"AB {name} memo={label} iters={iters} batch={batch} nanos={}",
e.as_nanos() as u64
);
}
}
}
}
#[test]
fn r10f1_deep_chain_leaf_shapes_are_valid() {
use crate::Value;
for (depth, leaf_bytes) in [(1usize, 8usize), (4, 64), (32, 512), (100, 16)] {
let doc = ix_fixtures::deep_chain_leaf(depth, leaf_bytes);
let mut v = crate::parse(&doc).expect("family document must parse");
let mut levels = 0usize;
while let Some(obj) = v.as_object() {
let key = if levels == 0 { "k" } else { "a" };
v = obj
.get(key)
.unwrap_or_else(|| panic!("spine key {key} missing at level {levels}"))
.clone();
levels += 1;
}
assert_eq!(levels, depth + 1, "spine depth at depth={depth}");
match &v {
Value::String(s) => assert_eq!(s.len(), leaf_bytes, "leaf at depth={depth}"),
other => panic!("leaf must be a String, got {other:?}"),
}
}
assert!(ix_fixtures::deep_chain_leaf(64, 4096).len() > 4096 + 64 * 5);
}
#[test]
fn r10f1_root_quotes_threaded_over_quotefree_descendants() {
use crate::Value;
let doc = "{\"a,b\": {c: {d: 1, e: 2}}, g: [1, [2, 3]]}";
let v = crate::parse(doc).expect("threaded doc must parse");
let root = v.as_object().unwrap();
let quoted = root.get("a,b").expect("quoted key must survive whole");
let quoted = quoted.as_object().expect("quoted key maps to object");
let c = quoted.get("c").unwrap().as_object().unwrap();
assert_eq!(c.get("d"), Some(&Value::Integer("1".into())));
assert_eq!(c.get("e"), Some(&Value::Integer("2".into())));
let g = root.get("g").unwrap().as_array().unwrap();
assert_eq!(g[0], Value::Integer("1".into()));
assert_eq!(g[1].as_array().unwrap()[0], Value::Integer("2".into()));
crate::parse_strict(doc).expect("strict must accept");
crate::from_str::<serde_json::Value>(doc).expect("serde must accept");
let mut events = 0usize;
crate::parse_events(doc, |_| {
events += 1;
})
.expect("events must accept");
assert!(events > 0);
}
#[test]
fn r10f1_quote_prescan_cost_no_depth_multiplier() {
let m = 512usize;
let measure = |depth: usize| {
let doc = ix_fixtures::deep_chain_leaf(depth, m);
super::inline::ix_probe::reset();
let v = crate::parse(&doc).expect("family document must parse");
assert!(v.as_object().is_some());
super::inline::ix_probe::snapshot().hq_bytes
};
let d1 = measure(1);
let d32 = measure(32);
assert!(d1 >= m as u64, "root-body prescan must happen: d1={d1}");
assert!(
d32 < 2 * d1 + 32 * 64,
"prescan bytes must not multiply by depth: d1={d1} d32={d32}"
);
assert!(d32 < 8 * m as u64, "absolute bound: d32={d32} m={m}");
}
#[test]
fn r10f1_quote_prescan_cost_linear_in_leaf() {
let measure = |m: usize| {
let doc = ix_fixtures::deep_chain_leaf(8, m);
super::inline::ix_probe::reset();
let _ = crate::parse(&doc).expect("family document must parse");
super::inline::ix_probe::snapshot().hq_bytes
};
let small = measure(512);
let large = measure(8192);
assert!(small >= 512, "root prescan must happen: {small}");
assert!(small < 8 * 512, "small bound: {small}");
assert!(
large >= 8192,
"prescan must still scale with the leaf: {large}"
);
assert!(large < 8 * 8192, "large bound: {large}");
let ratio = large as f64 / small as f64;
assert!(
ratio > 8.0 && ratio < 32.0,
"growth must be ~linear in M: ratio={ratio}"
);
}
#[test]
fn ix_probe_r10f1_quote_prescan_counters() {
let shapes = [
(1usize, 512usize),
(4, 512),
(8, 512),
(32, 512),
(64, 512),
(8, 8192),
(8, 65536),
];
for (depth, leaf) in shapes {
let doc = ix_fixtures::deep_chain_leaf(depth, leaf);
for (path, run) in [
(
"P",
Box::new(|doc: &str| {
let _ = crate::parse(doc);
}) as Box<dyn Fn(&str)>,
),
(
"E",
Box::new(|doc: &str| {
let _ = crate::parse_events(doc, |_| {});
}) as Box<dyn Fn(&str)>,
),
] {
super::inline::ix_probe::reset();
run(&doc);
let s = super::inline::ix_probe::snapshot();
println!(
"HQ\tdeep_chain_leaf\t{path}\tdepth={depth}\tleaf={leaf}\tdoc_bytes={}\thq_calls={}\thq_bytes={}\thq_max={}",
doc.len(),
s.hq_calls,
s.hq_bytes,
s.hq_max
);
}
}
}
#[test]
fn r11f1_splitq_last_segment_missing_separator_matches_fast_machine() {
use crate::ErrorKind;
let expect_malformed = |input: &str, res: Result<(), crate::Error>| {
assert!(
matches!(
res,
Err(crate::Error::Structured(
ErrorKind::MalformedInlineCompound { .. }
))
),
"input {input:?}: expected MalformedInlineCompound, got {res:?}"
);
};
let all_entry_points = |input: &str| {
expect_malformed(input, crate::parse(input).map(|_| ()));
expect_malformed(input, crate::parse_strict(input).map(|_| ()));
expect_malformed(
input,
crate::from_str::<serde_json::Value>(input).map(|_| ()),
);
expect_malformed(input, crate::parse_events(input, |_| {}).map(|_| ()));
};
for input in [
r"{a: {b. }}", r"{a: {b. }, q: '}", r"{q: ', a: {b. }}", r#"{"a": {b. }}"#, r"[{b. }, ']", ] {
all_entry_points(input);
}
for tail in [" ", "\t", "\u{2000}"] {
let mut doc = String::from("{a: {b.");
doc.push_str(tail);
doc.push_str("}, q: '}");
all_entry_points(&doc);
}
for q in ["'", "\"", "`"] {
let doc = format!(r"{{a: {{b. }}, q: {q}}}");
all_entry_points(&doc);
}
for input in [r"{a: {b.}, q: '}", r"{a: {b.}}"] {
all_entry_points(input);
}
let ok_doc = r"{a: {b: 1, }, q: '}";
let v = crate::parse(ok_doc).expect("trailing-comma control must parse");
let root = v.as_object().unwrap();
let a = root.get("a").unwrap().as_object().unwrap();
assert_eq!(a.get("b"), Some(&Value::Integer("1".into())));
assert_eq!(root.get("q"), Some(&Value::String("'".into())));
crate::parse_strict(ok_doc).expect("strict must accept");
crate::from_str::<serde_json::Value>(ok_doc).expect("serde must accept");
crate::parse_events(ok_doc, |_| {}).expect("events must accept");
for input in [r"{a.: 1}", r#"{"a": 1, b.: 2}"#] {
for res in [
crate::parse(input).map(|_| ()),
crate::parse_strict(input).map(|_| ()),
crate::from_str::<serde_json::Value>(input).map(|_| ()),
crate::parse_events(input, |_| {}).map(|_| ()),
] {
assert!(
matches!(
res,
Err(crate::Error::Structured(ErrorKind::EmptyKey { .. }))
),
"input {input:?}: expected EmptyKey, got {res:?}"
);
}
}
}