use super::*;
use crate::document::{Doc, Value};
use indexmap::IndexMap;
fn obj(pairs: &[(&str, Value)]) -> Value {
let mut m = IndexMap::new();
for (k, v) in pairs {
m.insert((*k).to_string(), v.clone());
}
Value::Object(m)
}
fn roundtrip_value(v: &Value) {
let doc = Doc::of(v).unwrap();
let raw = doc.to_raw();
let text = write_oml(&raw, 2).unwrap();
let parsed = read_oml(&text).unwrap();
let doc2 = Doc::from_raw(parsed).unwrap();
assert!(
doc.eq_doc(&doc2),
"round trip mismatch for {v:?}\n--- OML ---\n{text}"
);
}
#[test]
fn string_round_trips() {
roundtrip_value(&obj(&[("a", Value::Str("hello world".into()))]));
}
#[test]
fn string_with_escapes_round_trips() {
roundtrip_value(&obj(&[(
"a",
Value::Str("quote:\" backslash:\\ nl:\n cr:\r tab:\t ctrl:\u{1}".into()),
)]));
}
#[test]
fn integer_round_trips_positive_and_negative() {
roundtrip_value(&obj(&[
("a", Value::Int((42).into())),
("b", Value::Int((-42).into())),
("c", Value::Int((0).into())),
]));
}
#[test]
fn number_round_trips_decimal_and_whole_valued_float() {
roundtrip_value(&obj(&[
("a", Value::Float(3.15)),
("b", Value::Float(1.0)),
("c", Value::Float(-2.0)),
("d", Value::Float(1e10)),
]));
}
#[test]
fn number_round_trips_nan_and_infinities() {
let doc = Doc::of(&obj(&[
("a", Value::Float(f64::NAN)),
("b", Value::Float(f64::INFINITY)),
("c", Value::Float(f64::NEG_INFINITY)),
]))
.unwrap();
let text = write_oml(&doc.to_raw(), 2).unwrap();
assert!(text.contains("a: nan"));
assert!(text.contains("b: inf"));
assert!(text.contains("c: -inf"));
let parsed = Doc::from_raw(read_oml(&text).unwrap()).unwrap();
let root = parsed.root();
assert!(matches!(
root.child("a").unwrap().value().unwrap(),
crate::document::Scalar::Float(f) if f.is_nan()
));
assert!(matches!(
root.child("b").unwrap().value().unwrap(),
crate::document::Scalar::Float(f) if f.is_infinite() && *f > 0.0
));
assert!(matches!(
root.child("c").unwrap().value().unwrap(),
crate::document::Scalar::Float(f) if f.is_infinite() && *f < 0.0
));
}
#[test]
fn boolean_round_trips() {
roundtrip_value(&obj(&[("a", Value::Bool(true)), ("b", Value::Bool(false))]));
}
#[test]
fn null_round_trips() {
roundtrip_value(&obj(&[("a", Value::Null)]));
}
#[test]
fn date_round_trips() {
roundtrip_value(&obj(&[("a", Value::Str("2024-01-01".into()))]));
}
#[test]
fn datetime_date_then_time_lookahead_canonicalizes_missing_seconds() {
let parsed = Doc::from_raw(read_oml("a: 2024-01-01T10:30\n").unwrap()).unwrap();
let value = parsed.root().child("a").unwrap().value().unwrap();
assert!(
matches!(value, crate::document::Scalar::Datetime(s) if s == "2024-01-01T10:30:00"),
"expected canonical '2024-01-01T10:30:00', got {value:?}"
);
}
#[test]
fn time_round_trips_in_canonical_form() {
roundtrip_value(&obj(&[
("a", Value::Str("12:00:00".into())),
("b", Value::Str("12:00:00.123456".into())),
]));
}
#[test]
fn time_literal_without_seconds_canonicalizes_on_read() {
let parsed = Doc::from_raw(read_oml("a: 12:00\n").unwrap()).unwrap();
let value = parsed.root().child("a").unwrap().value().unwrap();
assert!(
matches!(value, crate::document::Scalar::Time(s) if s == "12:00:00"),
"expected canonical '12:00:00', got {value:?}"
);
}
#[test]
fn time_literal_with_short_fraction_zero_pads_on_read() {
let parsed = Doc::from_raw(read_oml("a: 12:00:00.5\n").unwrap()).unwrap();
let value = parsed.root().child("a").unwrap().value().unwrap();
assert!(
matches!(value, crate::document::Scalar::Time(s) if s == "12:00:00.500000"),
"expected canonical '12:00:00.500000', got {value:?}"
);
}
#[test]
fn bare_time_literal_with_utc_offset_reads_as_a_genuine_temporal_leaf() {
let parsed = read_oml("a: 12:00:00+05:00\n").unwrap();
let RawNode::Edges(edges) = &parsed else {
panic!("expected edges, got {parsed:?}");
};
assert_eq!(
edges[0],
(
"a".to_string(),
RawNode::Leaf(crate::document::Scalar::Time("12:00:00+05:00".to_string()))
)
);
}
#[test]
fn datetime_round_trips_with_and_without_utc_offset() {
roundtrip_value(&obj(&[
("a", Value::Str("2024-01-01T12:00:00".into())),
("b", Value::Str("2024-01-01T12:00:00+02:00".into())),
("c", Value::Str("2024-01-01T12:00:00-05:30".into())),
]));
}
#[test]
fn nested_object_round_trips() {
roundtrip_value(&obj(&[(
"a",
obj(&[("b", Value::Int((1).into())), ("c", Value::Str("x".into()))]),
)]));
}
#[test]
fn repeated_label_contiguous_round_trips() {
roundtrip_value(&obj(&[(
"tag",
Value::Array(vec![Value::Str("x".into()), Value::Str("y".into())]),
)]));
}
#[test]
fn empty_document_round_trips() {
let doc = Doc::from_raw(RawNode::Edges(vec![])).unwrap();
let text = write_oml(&doc.to_raw(), 2).unwrap();
assert_eq!(text, "");
let parsed = Doc::from_raw(read_oml(&text).unwrap()).unwrap();
assert!(doc.eq_doc(&parsed));
}
#[test]
fn bare_top_level_scalar_round_trips() {
let doc = Doc::of(&Value::Int((7).into())).unwrap();
let text = write_oml(&doc.to_raw(), 2).unwrap();
assert_eq!(text, "7");
let parsed = Doc::from_raw(read_oml(&text).unwrap()).unwrap();
assert!(doc.eq_doc(&parsed));
}
#[test]
fn interleaved_repeated_labels_round_trip_exactly_via_raw_node() {
let raw = RawNode::Edges(vec![
(
"b".to_string(),
RawNode::Leaf(crate::document::Scalar::Int((1).into())),
),
(
"c".to_string(),
RawNode::Leaf(crate::document::Scalar::Int((2).into())),
),
(
"b".to_string(),
RawNode::Leaf(crate::document::Scalar::Int((3).into())),
),
]);
let text = write_oml(&raw, 2).unwrap();
assert_eq!(text, "b: 1\nc: 2\nb: 3");
let parsed = read_oml(&text).unwrap();
assert_eq!(parsed, raw);
}
#[test]
fn write_oml_calls_the_shared_depth_guard() {
fn nest_raw(levels: usize) -> RawNode {
let mut n = RawNode::Leaf(crate::document::Scalar::Int((0).into()));
for _ in 0..levels {
n = RawNode::Edges(vec![("a".to_string(), n)]);
}
n
}
assert!(write_oml(&nest_raw(document::MAX_DEPTH), 2).is_ok());
let err = write_oml(&nest_raw(document::MAX_DEPTH + 1), 2).unwrap_err();
assert!(err.to_string().contains("maximum depth"));
let err2 = write_oml_compact(&nest_raw(document::MAX_DEPTH + 1)).unwrap_err();
assert!(err2.to_string().contains("maximum depth"));
}
#[test]
fn write_oml_preserves_datetime_utc_offset_exactly() {
let raw = RawNode::Edges(vec![(
"meeting".to_string(),
RawNode::Leaf(crate::document::Scalar::Str(
"2024-06-01T09:30:00+02:00".to_string(),
)),
)]);
let text = write_oml(&raw, 2).unwrap();
assert!(
text.contains("+02:00"),
"offset must survive the write unchanged: {text}"
);
let parsed = read_oml(&text).unwrap();
assert_eq!(parsed, raw);
}
#[test]
fn bare_time_literal_round_trips_as_a_time_not_a_quoted_string() {
let raw = RawNode::Edges(vec![(
"a".to_string(),
RawNode::Leaf(crate::document::Scalar::Time("12:00".to_string())),
)]);
let text = write_oml(&raw, 2).unwrap();
assert_eq!(text, "a: 12:00");
let expected = RawNode::Edges(vec![(
"a".to_string(),
RawNode::Leaf(crate::document::Scalar::Time("12:00:00".to_string())),
)]);
let parsed = read_oml(&text).unwrap();
assert_eq!(parsed, expected);
}
#[test]
fn plain_string_shaped_like_a_time_stays_quoted() {
let raw = RawNode::Edges(vec![(
"a".to_string(),
RawNode::Leaf(crate::document::Scalar::Str("12:00:00".to_string())),
)]);
let text = write_oml(&raw, 2).unwrap();
assert_eq!(text, "a: \"12:00:00\"");
let parsed = read_oml(&text).unwrap();
assert_eq!(parsed, raw);
}
#[test]
fn plain_string_shaped_like_a_date_stays_quoted() {
let raw = RawNode::Edges(vec![(
"d".to_string(),
RawNode::Leaf(crate::document::Scalar::Str("2024-01-01".to_string())),
)]);
let text = write_oml(&raw, 2).unwrap();
assert_eq!(text, "d: \"2024-01-01\"");
let parsed = read_oml(&text).unwrap();
assert_eq!(parsed, raw);
}
#[test]
fn genuine_temporal_leaf_writes_bare_for_date_and_datetime_too() {
let raw = RawNode::Edges(vec![
(
"d".to_string(),
RawNode::Leaf(crate::document::Scalar::Date("2024-01-01".to_string())),
),
(
"dt".to_string(),
RawNode::Leaf(crate::document::Scalar::Datetime(
"2024-01-01T12:30:00".to_string(),
)),
),
]);
let text = write_oml(&raw, 2).unwrap();
assert_eq!(text, "d: 2024-01-01\ndt: 2024-01-01T12:30:00");
assert_eq!(
write_oml_compact(&raw).unwrap(),
"d: 2024-01-01; dt: 2024-01-01T12:30:00"
);
}
#[test]
fn materialize_upgraded_date_writes_bare_through_oml() {
use crate::schema::{DATE, Field, Record, Ref, Schema};
let root = Record::new(vec![Field::required("d", DATE).unwrap()]).unwrap();
let mut env = IndexMap::new();
env.insert("Root".to_string(), root);
let schema = Schema::new(Ref::new("Root"), env).unwrap();
let input = RawNode::Edges(vec![(
"d".to_string(),
RawNode::Leaf(crate::document::Scalar::Str("2024-01-01".to_string())),
)]);
let materialized = crate::materialize::materialize(&input, Some(&schema)).unwrap();
let text = write_oml(&materialized, 2).unwrap();
assert_eq!(text, "d: 2024-01-01");
}
#[test]
fn string_escaping_is_all_occurrences_not_first_match_only() {
let s = "a\"b\"c\"d\\e\\f";
let written = write_scalar(&crate::document::Scalar::Str(s.to_string()));
assert_eq!(written, r#""a\"b\"c\"d\\e\\f""#);
assert_eq!(written.matches("\\\"").count(), 3);
assert_eq!(written.matches("\\\\").count(), 2);
}
#[test]
fn stray_character_reports_position() {
let err = read_oml("a: 1\n@").unwrap_err();
assert!(err.message.contains("stray character"));
assert_eq!(err.line, 2);
}
#[test]
fn unterminated_string_is_an_error() {
let err = read_oml("a: \"unterminated").unwrap_err();
assert!(err.message.contains("unterminated string"));
}
#[test]
fn unterminated_raw_string_is_an_error() {
let err = read_oml("a: 'unterminated").unwrap_err();
assert!(err.message.contains("unterminated raw string"));
}
#[test]
fn unterminated_multiline_string_is_an_error() {
let err = read_oml("a: \"\"\"unterminated").unwrap_err();
assert!(err.message.contains("unterminated multiline string"));
}
#[test]
fn control_character_in_string_is_rejected() {
let err = read_oml("a: \"bad\u{1}char\"").unwrap_err();
assert!(err.message.contains("control character"));
}
#[test]
fn control_character_in_multiline_string_is_rejected() {
let err = read_oml("a: \"\"\"bad\u{1}char\"\"\"").unwrap_err();
assert!(err.message.contains("control character"));
}
#[test]
fn bare_word_is_rejected_as_a_value() {
let err = read_oml("a: bareword").unwrap_err();
assert!(err.message.contains("bare word"));
}
#[test]
fn reserved_word_cannot_be_a_bare_label() {
let err = read_oml("a: { null: 1 }").unwrap_err();
assert!(err.message.contains("reserved word"));
}
#[test]
fn capitalized_reserved_words_are_bare_idents_not_keywords() {
for word in ["NAN", "NaN", "INF", "Inf", "NULL", "Null", "TRUE", "False"] {
let src = format!("a: {word}");
let err =
read_oml(&src).expect_err(&format!("expected {word:?} to be rejected as a bare word"));
assert!(
err.message.contains("bare word"),
"{word:?}: expected a bare-word error, got {:?}",
err.message
);
}
}
#[test]
fn quoted_reserved_words_are_valid_labels_and_round_trip() {
for label in ["nan", "inf", "-inf", "null", "true", "false"] {
roundtrip_value(&obj(&[(label, Value::Int((1).into()))]));
}
}
#[test]
fn invalid_date_reports_a_clear_error() {
let err = read_oml("a: 2024-02-30").unwrap_err();
assert!(err.message.contains("invalid date"));
}
#[test]
fn invalid_time_reports_a_clear_error() {
let err = read_oml("a: 25:00:00").unwrap_err();
assert!(err.message.contains("invalid time"));
}
#[test]
fn invalid_datetime_reports_a_clear_error() {
let err = read_oml("a: 2024-13-01T12:00:00").unwrap_err();
assert!(err.message.contains("invalid datetime"));
}
#[test]
fn empty_array_is_rejected() {
let err = read_oml("a: []").unwrap_err();
assert!(err.message.contains("empty array"));
}
#[test]
fn nested_array_is_rejected() {
let err = read_oml("a: [[1]]").unwrap_err();
assert!(err.message.contains("nested array"));
}
#[test]
fn missing_colon_after_label_reports_position() {
let err = read_oml("r: { a 1 }").unwrap_err();
assert!(err.message.contains("expected ':'"));
}
#[test]
fn missing_closing_brace_is_an_error() {
let err = read_oml("a: { b: 1").unwrap_err();
assert!(err.message.contains("expected '}'"));
}
#[test]
fn missing_separator_between_edges_is_an_error() {
let err = read_oml("a: 1 b: 2").unwrap_err();
assert!(err.message.contains("expected a separator"));
}
#[test]
fn trailing_content_after_document_body_is_an_error() {
let err = read_oml("1 2").unwrap_err();
assert!(err.message.contains("unexpected trailing content"));
}
#[test]
fn unquoted_field_label_must_be_a_valid_ident_or_string() {
let err = read_oml("a: 1\n5: 2").unwrap_err();
assert!(
err.message.contains("unexpected trailing content") || err.message.contains("expected")
);
}
#[test]
fn integer_digit_cap_is_enforced() {
let digits = "1".repeat(MAX_INT_DIGITS + 1);
let src = format!("a: {digits}");
let err = read_oml(&src).unwrap_err();
assert!(err.message.contains("digit"));
assert!(err.message.contains("4300"));
}
#[test]
fn integer_at_the_digit_cap_boundary_parses() {
let digits = "1".repeat(MAX_INT_DIGITS);
let src = format!("a: {digits}");
let parsed = Doc::from_raw(read_oml(&src).unwrap()).unwrap();
let value = parsed.root().child("a").unwrap().value().unwrap();
assert!(
matches!(value, crate::document::Scalar::Int(i) if i.to_string().len() == MAX_INT_DIGITS),
"got {value:?}"
);
}
#[test]
fn integer_cap_does_not_false_positive_on_a_long_identifier() {
let ident = format!("x{}", "1".repeat(MAX_INT_DIGITS + 5));
let err = read_oml(&format!("a: {ident}")).unwrap_err();
assert!(err.message.contains("bare word"));
assert!(!err.message.contains("digit"));
}
#[test]
fn beyond_i64_integer_parses_arbitrary_precision() {
let parsed = Doc::from_raw(read_oml("a: 99999999999999999999").unwrap()).unwrap();
let value = parsed.root().child("a").unwrap().value().unwrap();
assert_eq!(
value,
&crate::document::Scalar::Int(
num_bigint::BigInt::parse_bytes(b"99999999999999999999", 10).unwrap()
)
);
}
#[test]
fn invalid_unicode_escape_is_rejected() {
let err = read_oml(r#"a: "\uZZZZ""#).unwrap_err();
assert!(err.message.contains(r"invalid \u escape"));
}
#[test]
fn unpaired_high_surrogate_is_rejected() {
let err = read_oml(r#"a: "\ud800""#).unwrap_err();
assert!(err.message.contains("unpaired high surrogate"));
}
#[test]
fn unpaired_low_surrogate_is_rejected() {
let err = read_oml(r#"a: "\udc00""#).unwrap_err();
assert!(err.message.contains("unpaired low surrogate"));
}
#[test]
fn valid_surrogate_pair_decodes_to_the_combined_codepoint() {
let node = read_oml(r#"a: "😀""#).unwrap();
match node {
RawNode::Edges(edges) => {
assert_eq!(edges.len(), 1);
match &edges[0].1 {
RawNode::Leaf(crate::document::Scalar::Str(s)) => {
assert_eq!(s, "\u{1F600}");
}
other => panic!("expected a string leaf, got {other:?}"),
}
}
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn invalid_escape_character_is_rejected() {
let err = read_oml(r#"a: "\q""#).unwrap_err();
assert!(err.message.contains("invalid escape"));
}
#[test]
fn unterminated_escape_sequence_is_rejected() {
let err = read_oml("a: \"\\").unwrap_err();
assert!(err.message.contains("unterminated escape"));
}
#[test]
fn raw_string_e2_reads_with_no_escape_processing() {
let node = read_oml(r"a: 'no \n escapes here \\'").unwrap();
match node {
RawNode::Edges(edges) => match &edges[0].1 {
RawNode::Leaf(crate::document::Scalar::Str(s)) => {
assert_eq!(s, r"no \n escapes here \\");
}
other => panic!("expected a string leaf, got {other:?}"),
},
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn triple_quoted_multiline_e3_strips_opening_newline() {
let node = read_oml("a: \"\"\"\nline one\nline two\"\"\"").unwrap();
match node {
RawNode::Edges(edges) => match &edges[0].1 {
RawNode::Leaf(crate::document::Scalar::Str(s)) => {
assert_eq!(s, "line one\nline two");
}
other => panic!("expected a string leaf, got {other:?}"),
},
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn triple_quoted_multiline_e3_allows_embedded_quotes_up_to_two() {
let node = read_oml("a: \"\"\"has \"\" two quotes\"\"\"").unwrap();
match node {
RawNode::Edges(edges) => match &edges[0].1 {
RawNode::Leaf(crate::document::Scalar::Str(s)) => {
assert_eq!(s, "has \"\" two quotes");
}
other => panic!("expected a string leaf, got {other:?}"),
},
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn write_oml_compact_joins_edges_with_semicolons() {
let raw = RawNode::Edges(vec![
(
"a".to_string(),
RawNode::Leaf(crate::document::Scalar::Int((1).into())),
),
(
"b".to_string(),
RawNode::Leaf(crate::document::Scalar::Int((2).into())),
),
]);
assert_eq!(write_oml_compact(&raw).unwrap(), "a: 1; b: 2");
}
#[test]
fn write_oml_pretty_indents_nested_objects() {
let raw = RawNode::Edges(vec![(
"a".to_string(),
RawNode::Edges(vec![(
"b".to_string(),
RawNode::Leaf(crate::document::Scalar::Int((1).into())),
)]),
)]);
assert_eq!(write_oml(&raw, 2).unwrap(), "a: {\n b: 1\n}");
}
#[test]
fn write_oml_writes_empty_object_compactly_in_both_modes() {
let raw = RawNode::Edges(vec![("a".to_string(), RawNode::Edges(vec![]))]);
assert_eq!(write_oml(&raw, 2).unwrap(), "a: {}");
assert_eq!(write_oml_compact(&raw).unwrap(), "a: {}");
}
#[test]
fn labels_needing_quotes_are_quoted_on_write() {
let raw = RawNode::Edges(vec![
(
"has space".to_string(),
RawNode::Leaf(crate::document::Scalar::Int((1).into())),
),
(
"null".to_string(),
RawNode::Leaf(crate::document::Scalar::Int((2).into())),
),
(
"1leading".to_string(),
RawNode::Leaf(crate::document::Scalar::Int((3).into())),
),
]);
let text = write_oml(&raw, 2).unwrap();
assert!(text.contains("\"has space\": 1"));
assert!(text.contains("\"null\": 2"));
assert!(text.contains("\"1leading\": 3"));
}
#[test]
fn bare_identifier_label_is_written_unquoted() {
let raw = RawNode::Edges(vec![(
"under_score-and-dash".to_string(),
RawNode::Leaf(crate::document::Scalar::Int((1).into())),
)]);
assert_eq!(write_oml(&raw, 2).unwrap(), "under_score-and-dash: 1");
}
#[test]
fn comments_and_semicolons_are_accepted_as_separators() {
let node = read_oml("a: 1 # a comment\n; b: 2").unwrap();
match node {
RawNode::Edges(edges) => assert_eq!(edges.len(), 2),
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn crlf_line_endings_are_accepted_as_separators() {
let node = read_oml("a: 1\r\nb: 2").unwrap();
match node {
RawNode::Edges(edges) => assert_eq!(edges.len(), 2),
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn lone_carriage_return_is_a_stray_character() {
let err = read_oml("a: 1\rb: 2").unwrap_err();
assert!(err.message.contains("stray character"));
}
#[test]
fn utf8_bom_is_stripped() {
let node = read_oml("\u{feff}a: 1").unwrap();
match node {
RawNode::Edges(edges) => assert_eq!(edges.len(), 1),
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn bare_label_starting_with_a_multibyte_unicode_letter_scans_correctly() {
let err = read_oml("éxyz").unwrap_err();
assert!(
err.message.contains("bare word") && err.message.contains("éxyz"),
"got {err:?}"
);
}
#[test]
fn multibyte_content_inside_a_comment_does_not_corrupt_subsequent_scanning() {
let doc = read_oml("# café ☕ comment\na: 1").unwrap();
match doc {
RawNode::Edges(edges) => {
assert_eq!(
edges,
vec![("a".to_string(), RawNode::Leaf(Scalar::Int((1).into())))]
);
}
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn multibyte_string_content_round_trips_and_error_after_it_reports_correct_line() {
let doc = read_oml("a: \"café \u{1F600}\"").unwrap();
assert_eq!(
doc,
RawNode::Edges(vec![(
"a".to_string(),
RawNode::Leaf(Scalar::Str("café \u{1F600}".to_string()))
)])
);
let err = read_oml("a: \"café \u{1F600}\"\nb @\n").unwrap_err();
assert_eq!(err.line, 2, "got {err:?}");
}
#[test]
fn top_level_brace_document_is_equivalent_to_the_bare_edge_list() {
let a = read_oml("a: 1").unwrap();
let b = read_oml("{ a: 1 }").unwrap();
assert_eq!(a, b);
}
#[test]
fn array_syntax_expands_to_repeated_edges() {
let node = read_oml("tag: [\"x\", \"y\", \"z\"]").unwrap();
match node {
RawNode::Edges(edges) => {
assert_eq!(edges.len(), 3);
assert!(edges.iter().all(|(l, _)| l == "tag"));
}
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn array_syntax_allows_a_trailing_comma() {
let node = read_oml("tag: [1, 2,]").unwrap();
match node {
RawNode::Edges(edges) => assert_eq!(edges.len(), 2),
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn array_of_brace_subtrees_round_trips() {
let raw = RawNode::Edges(vec![
(
"item".to_string(),
RawNode::Edges(vec![(
"v".to_string(),
RawNode::Leaf(crate::document::Scalar::Int((1).into())),
)]),
),
(
"item".to_string(),
RawNode::Edges(vec![(
"v".to_string(),
RawNode::Leaf(crate::document::Scalar::Int((2).into())),
)]),
),
]);
let text = write_oml_compact(&raw).unwrap();
let parsed = read_oml(&text).unwrap();
assert_eq!(parsed, raw);
}
#[test]
fn read_oml_document_depth_guard_via_doc_from_raw() {
let mut src = String::new();
for _ in 0..(document::MAX_DEPTH + 1) {
src.push_str("a: {");
}
src.push('1');
for _ in 0..(document::MAX_DEPTH + 1) {
src.push('}');
}
let err = read_oml(&src).unwrap_err();
assert!(err.message.contains("maximum depth"));
}
#[test]
fn multiline_string_strips_crlf_opening_newline() {
let node = read_oml("a: \"\"\"\r\nline one\"\"\"").unwrap();
match node {
RawNode::Edges(edges) => match &edges[0].1 {
RawNode::Leaf(crate::document::Scalar::Str(s)) => assert_eq!(s, "line one"),
other => panic!("expected a string leaf, got {other:?}"),
},
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn multiline_string_decodes_escapes_inside_the_body() {
let node = read_oml("a: \"\"\"tab:\\there\"\"\"").unwrap();
match node {
RawNode::Edges(edges) => match &edges[0].1 {
RawNode::Leaf(crate::document::Scalar::Str(s)) => assert_eq!(s, "tab:\there"),
other => panic!("expected a string leaf, got {other:?}"),
},
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn multiline_string_allows_a_run_of_more_than_three_quotes_as_content() {
let node = read_oml("a: \"\"\"x\"\"\"\"").unwrap();
match node {
RawNode::Edges(edges) => match &edges[0].1 {
RawNode::Leaf(crate::document::Scalar::Str(s)) => assert_eq!(s, "x\""),
other => panic!("expected a string leaf, got {other:?}"),
},
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn escaped_surrogate_pair_decodes_to_the_combined_codepoint() {
let node = read_oml(r#"a: "😀""#).unwrap();
match node {
RawNode::Edges(edges) => match &edges[0].1 {
RawNode::Leaf(crate::document::Scalar::Str(s)) => assert_eq!(s, "\u{1F600}"),
other => panic!("expected a string leaf, got {other:?}"),
},
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn four_digits_not_followed_by_dash_is_a_plain_integer() {
let node = read_oml("a: 1234").unwrap();
match node {
RawNode::Edges(edges) => {
assert_eq!(
edges[0].1,
RawNode::Leaf(crate::document::Scalar::Int((1234).into()))
);
}
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn date_shape_missing_second_dash_falls_back_and_errors_on_trailing_content() {
let err = read_oml("a: 2024-01x").unwrap_err();
assert!(!err.message.contains("invalid date"));
}
#[test]
fn two_digit_integer_is_not_mistaken_for_a_time() {
let node = read_oml("a: 12").unwrap();
match node {
RawNode::Edges(edges) => {
assert_eq!(
edges[0].1,
RawNode::Leaf(crate::document::Scalar::Int((12).into()))
);
}
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn write_oml_compact_on_a_bare_leaf_node() {
let raw = RawNode::Leaf(crate::document::Scalar::Int((5).into()));
assert_eq!(write_oml(&raw, 2).unwrap(), "5");
assert_eq!(write_oml_compact(&raw).unwrap(), "5");
}
#[test]
fn write_oml_on_a_bare_temporal_leaf_node() {
let raw = RawNode::Leaf(crate::document::Scalar::Date("2024-01-01".to_string()));
assert_eq!(write_oml(&raw, 2).unwrap(), "2024-01-01");
assert_eq!(write_oml_compact(&raw).unwrap(), "2024-01-01");
}
#[test]
fn write_oml_pretty_on_multiple_top_level_edges() {
let raw = RawNode::Edges(vec![
(
"a".to_string(),
RawNode::Leaf(crate::document::Scalar::Int((1).into())),
),
(
"b".to_string(),
RawNode::Leaf(crate::document::Scalar::Int((2).into())),
),
]);
assert_eq!(write_oml(&raw, 2).unwrap(), "a: 1\nb: 2");
}
#[test]
fn parse_array_with_multiple_scalar_elements_and_no_trailing_comma() {
let node = read_oml("tag: [1, 2, 3]").unwrap();
match node {
RawNode::Edges(edges) => assert_eq!(edges.len(), 3),
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn parse_label_rejects_a_non_label_token() {
let err = read_oml("a: { 1: 2 }").unwrap_err();
assert!(err.message.contains("expected a label"));
}
#[test]
fn parse_value_rejects_a_bad_token_where_a_scalar_is_expected() {
let err = read_oml("a: :").unwrap_err();
assert!(err.message.contains("expected a value"));
}
#[test]
fn array_close_error_when_neither_comma_nor_bracket() {
let err = read_oml("a: [1 2]").unwrap_err();
assert!(err.message.contains("expected ',' or ']'"));
}
#[test]
fn string_decodes_solidus_backspace_and_formfeed_escapes() {
let node = read_oml(r#"a: "\/ \b \f""#).unwrap();
match node {
RawNode::Edges(edges) => match &edges[0].1 {
RawNode::Leaf(crate::document::Scalar::Str(s)) => {
assert_eq!(s, "/ \u{8} \u{c}");
}
other => panic!("expected a string leaf, got {other:?}"),
},
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn escaped_surrogate_pair_via_explicit_u_escapes_decodes_correctly() {
let node = read_oml("a: \"\\uD83D\\uDE00\"").unwrap();
match node {
RawNode::Edges(edges) => match &edges[0].1 {
RawNode::Leaf(crate::document::Scalar::Str(s)) => assert_eq!(s, "\u{1F600}"),
other => panic!("expected a string leaf, got {other:?}"),
},
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn lone_minus_not_followed_by_digit_or_inf_is_a_stray_character() {
let err = read_oml("a: -x").unwrap_err();
assert!(err.message.contains("stray character '-'"));
}
#[test]
fn decimal_number_with_an_exponent_round_trips() {
let node = read_oml("a: 1.5e10").unwrap();
match node {
RawNode::Edges(edges) => {
assert_eq!(
edges[0].1,
RawNode::Leaf(crate::document::Scalar::Float(1.5e10))
);
}
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn exponent_only_number_forms_are_recognized() {
let cases = ["1e10", "1e+10", "1e-10", "-1e5"];
for src in cases {
let node = read_oml(&format!("a: {src}")).unwrap();
match node {
RawNode::Edges(edges) => {
assert!(
matches!(edges[0].1, RawNode::Leaf(crate::document::Scalar::Float(_))),
"{src} should scan as a float"
);
}
other => panic!("expected edges, got {other:?}"),
}
}
}
#[test]
fn quoted_string_label_at_the_top_level_is_recognized_as_an_edge() {
let node = read_oml(r#""a b": 1"#).unwrap();
match node {
RawNode::Edges(edges) => {
assert_eq!(edges[0].0, "a b");
assert_eq!(
edges[0].1,
RawNode::Leaf(crate::document::Scalar::Int((1).into()))
);
}
other => panic!("expected edges, got {other:?}"),
}
}
#[test]
fn reserved_word_at_the_document_top_level_is_read_as_the_scalar_not_a_label() {
let err = read_oml("null: 1").unwrap_err();
assert!(err.message.contains("unexpected trailing content"));
}
#[test]
fn missing_separator_error_names_a_quoted_string_token() {
let err = read_oml(r#"a: 1 "b""#).unwrap_err();
assert!(err.message.contains("expected a separator"));
assert!(err.message.contains("\"b\""));
}
#[test]
fn capitalized_null_true_false_is_a_bare_ident_not_the_keyword() {
for word in ["Null", "True", "False", "NULL", "TRUE", "FALSE"] {
let raw = RawNode::Edges(vec![(
word.to_string(),
RawNode::Leaf(crate::document::Scalar::Int((1).into())),
)]);
let text = write_oml(&raw, 2).unwrap();
assert_eq!(
text,
format!("{word}: 1"),
"expected a bare unquoted label for {word:?}"
);
let parsed = read_oml(&text).unwrap();
assert_eq!(parsed, raw, "round trip mismatch for {word:?}");
let err = read_oml(&format!("a: {word}")).unwrap_err();
assert!(
err.message.contains("bare word"),
"expected a bare-word error for value {word:?}, got {err:?}"
);
}
}
#[test]
fn capitalized_nan_inf_is_a_bare_ident_not_the_keyword() {
for word in ["NAN", "INF", "NaN", "Inf"] {
let raw = RawNode::Edges(vec![(
word.to_string(),
RawNode::Leaf(crate::document::Scalar::Int((1).into())),
)]);
let text = write_oml(&raw, 2).unwrap();
assert_eq!(
text,
format!("{word}: 1"),
"expected a bare unquoted label for {word:?}"
);
let parsed = read_oml(&text).unwrap();
assert_eq!(parsed, raw, "round trip mismatch for {word:?}");
let err = read_oml(&format!("a: {word}")).unwrap_err();
assert!(
err.message.contains("bare word"),
"expected a bare-word error for value {word:?}, got {err:?}"
);
}
for word in ["-INF", "-NaN", "-Inf"] {
let err = read_oml(&format!("a: {word}")).unwrap_err();
assert!(
err.message.contains("stray character '-'"),
"expected a stray-character error for {word:?}, got {err:?}"
);
}
}
#[test]
fn bare_lowercase_nan_inf_neg_inf_cannot_be_a_label() {
for (word, expected_fragment) in [
("nan", "expected a label"),
("inf", "expected a label"),
("-inf", "expected a label"),
] {
let err = read_oml(&format!("a: {{ {word}: 1 }}")).unwrap_err();
assert!(
err.message.contains(expected_fragment),
"expected {expected_fragment:?} for label {word:?}, got {err:?}"
);
}
}
#[test]
fn quoted_reserved_spelling_is_a_valid_label_and_round_trips() {
for word in ["null", "true", "false", "nan", "inf", "-inf"] {
let raw = RawNode::Edges(vec![(
word.to_string(),
RawNode::Leaf(crate::document::Scalar::Int((1).into())),
)]);
let text = write_oml(&raw, 2).unwrap();
assert_eq!(
text,
format!("{:?}: 1", word),
"expected a quoted label for {word:?}"
);
let parsed = read_oml(&text).unwrap();
assert_eq!(parsed, raw, "round trip mismatch for quoted {word:?}");
}
}
#[test]
fn test_oml_depth_limit_boundary_and_consistency() {
use crate::oml::{read_oml, write_oml};
let valid_oml = "a: { ".repeat(199) + "z: 1" + &" }".repeat(199);
let raw = read_oml(&valid_oml).expect("should accept depth 200");
assert!(write_oml(&raw, 2).is_ok());
let invalid_oml = "a: { ".repeat(200) + "z: 1" + &" }".repeat(200);
let err = read_oml(&invalid_oml).unwrap_err();
assert!(err.to_string().contains("maximum depth"));
}
#[test]
fn test_read_oml_node_count_limit() {
use crate::document::MAX_NODES;
use crate::oml::read_oml;
let at_limit = "a: 0
"
.repeat(MAX_NODES - 1);
assert!(read_oml(&at_limit).is_ok());
let past_limit = "a: 0
"
.repeat(MAX_NODES);
let err = read_oml(&past_limit).unwrap_err();
assert!(err.to_string().contains("maximum node count"));
}