#![expect(
clippy::panic,
clippy::expect_used,
clippy::indexing_slicing,
clippy::too_many_lines,
clippy::wildcard_enum_match_arm,
reason = "test code"
)]
use std::path::PathBuf;
use std::time::Duration;
use rlsp_yaml_parser::loader::{LoadError, LoaderBuilder, load};
use rlsp_yaml_parser::node::{Document, Node};
use rlsp_yaml_parser::{CollectionStyle, ScalarStyle, Span};
use rstest::rstest;
use super::{ConformanceCase, load_cases_from_file};
#[derive(Debug, Clone)]
enum ExpectedNode {
Scalar {
value: String,
style: StyleVariant,
anchor: Option<String>,
tag: Option<String>,
},
Mapping {
entries: Vec<(Self, Self)>,
style: CollectionStyle,
anchor: Option<String>,
tag: Option<String>,
},
Sequence {
items: Vec<Self>,
style: CollectionStyle,
anchor: Option<String>,
tag: Option<String>,
},
Alias {
name: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StyleVariant {
Plain,
SingleQuoted,
DoubleQuoted,
Literal,
Folded,
}
impl StyleVariant {
const fn matches(self, style: ScalarStyle) -> bool {
matches!(
(self, style),
(Self::Plain, ScalarStyle::Plain)
| (Self::SingleQuoted, ScalarStyle::SingleQuoted)
| (Self::DoubleQuoted, ScalarStyle::DoubleQuoted)
| (Self::Literal, ScalarStyle::Literal(_))
| (Self::Folded, ScalarStyle::Folded(_))
)
}
}
#[derive(Debug)]
struct ExpectedDocument {
root: ExpectedNode,
}
fn unescape_tree_value(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(ch) = chars.next() {
if ch == '\\' {
match chars.next() {
Some('0') => out.push('\0'),
Some('a') => out.push('\x07'),
Some('b') => out.push('\x08'),
Some('e') => out.push('\x1B'),
Some('f') => out.push('\x0C'),
Some('n') => out.push('\n'),
Some('t') => out.push('\t'),
Some('v') => out.push('\x0B'),
Some('r') => out.push('\r'),
Some('\\') | None => out.push('\\'),
Some(other) => {
out.push('\\');
out.push(other);
}
}
} else {
out.push(ch);
}
}
out
}
#[derive(Debug)]
enum TreeToken {
StreamStart,
StreamEnd,
DocStart,
DocEnd,
SeqStart {
anchor: Option<String>,
tag: Option<String>,
style: CollectionStyle,
},
SeqEnd,
MapStart {
anchor: Option<String>,
tag: Option<String>,
style: CollectionStyle,
},
MapEnd,
Scalar {
anchor: Option<String>,
tag: Option<String>,
style: StyleVariant,
value: String,
},
Alias {
name: String,
},
}
fn parse_tree_line(line: &str) -> Option<TreeToken> {
let line = line.trim_start();
if line.trim_end().is_empty() {
return None;
}
let line_trimmed = line.trim_end();
if line_trimmed == "+STR" {
return Some(TreeToken::StreamStart);
}
if line_trimmed == "-STR" {
return Some(TreeToken::StreamEnd);
}
if line_trimmed.starts_with("+DOC") {
return Some(TreeToken::DocStart);
}
if line_trimmed.starts_with("-DOC") {
return Some(TreeToken::DocEnd);
}
if line_trimmed.starts_with("-SEQ") {
return Some(TreeToken::SeqEnd);
}
if line_trimmed.starts_with("-MAP") {
return Some(TreeToken::MapEnd);
}
if let Some(rest) = line_trimmed.strip_prefix("+SEQ") {
let rest = rest.trim();
let style = if rest.starts_with("[]") {
CollectionStyle::Flow
} else {
CollectionStyle::Block
};
let rest = rest.trim_start_matches("[]").trim();
let (anchor, tag, _) = parse_optional_anchor_tag(rest);
return Some(TreeToken::SeqStart { anchor, tag, style });
}
if let Some(rest) = line_trimmed.strip_prefix("+MAP") {
let rest = rest.trim();
let style = if rest.starts_with("{}") {
CollectionStyle::Flow
} else {
CollectionStyle::Block
};
let rest = rest.trim_start_matches("{}").trim();
let (anchor, tag, _) = parse_optional_anchor_tag(rest);
return Some(TreeToken::MapStart { anchor, tag, style });
}
if let Some(rest) = line_trimmed.strip_prefix("=ALI") {
let name = rest
.trim()
.strip_prefix('*')
.unwrap_or_else(|| rest.trim())
.to_string();
return Some(TreeToken::Alias { name });
}
if let Some(rest) = line.strip_prefix("=VAL") {
let rest = rest.trim_start();
let (anchor, tag, rest) = parse_optional_anchor_tag(rest);
let (style, raw_value) = match rest.chars().next() {
Some(':') => (StyleVariant::Plain, &rest[1..]),
Some('\'') => (StyleVariant::SingleQuoted, &rest[1..]),
Some('"') => (StyleVariant::DoubleQuoted, &rest[1..]),
Some('|') => (StyleVariant::Literal, &rest[1..]),
Some('>') => (StyleVariant::Folded, &rest[1..]),
_ => return None,
};
let value = unescape_tree_value(raw_value);
return Some(TreeToken::Scalar {
anchor,
tag,
style,
value,
});
}
None
}
fn parse_optional_anchor_tag(s: &str) -> (Option<String>, Option<String>, &str) {
let mut rest = s;
let mut anchor: Option<String> = None;
let mut tag: Option<String> = None;
if rest.starts_with('&') {
let end = rest.find(' ').unwrap_or(rest.len());
anchor = Some(rest[1..end].to_string());
rest = rest[end..].trim_start();
}
if rest.starts_with('<') {
if let Some(close) = rest.find('>') {
tag = Some(rest[1..close].to_string());
rest = rest[close + 1..].trim_start();
}
}
(anchor, tag, rest)
}
fn parse_expected_documents(tree: &str) -> Option<Vec<ExpectedDocument>> {
let tokens: Vec<TreeToken> = tree.lines().filter_map(parse_tree_line).collect();
let mut docs: Vec<ExpectedDocument> = Vec::new();
let mut pos = 0;
if matches!(tokens.get(pos), Some(TreeToken::StreamStart)) {
pos += 1;
}
while pos < tokens.len() {
match &tokens[pos] {
TreeToken::DocStart => {
pos += 1;
if let Some((node, next_pos)) = parse_expected_node(&tokens, pos) {
docs.push(ExpectedDocument { root: node });
pos = next_pos;
} else {
return None;
}
if matches!(tokens.get(pos), Some(TreeToken::DocEnd)) {
pos += 1;
}
}
TreeToken::StreamEnd => break,
_ => {
pos += 1;
}
}
}
Some(docs)
}
fn parse_expected_node(tokens: &[TreeToken], pos: usize) -> Option<(ExpectedNode, usize)> {
let token = tokens.get(pos)?;
match token {
TreeToken::Scalar {
anchor,
tag,
style,
value,
} => Some((
ExpectedNode::Scalar {
value: value.clone(),
style: *style,
anchor: anchor.clone(),
tag: tag.clone(),
},
pos + 1,
)),
TreeToken::Alias { name } => Some((ExpectedNode::Alias { name: name.clone() }, pos + 1)),
TreeToken::SeqStart { anchor, tag, style } => {
let anchor = anchor.clone();
let tag = tag.clone();
let style = *style;
let mut items: Vec<ExpectedNode> = Vec::new();
let mut cur = pos + 1;
loop {
match tokens.get(cur) {
Some(TreeToken::SeqEnd) => {
cur += 1;
break;
}
None => return None,
_ => {
let (item, next) = parse_expected_node(tokens, cur)?;
items.push(item);
cur = next;
}
}
}
Some((
ExpectedNode::Sequence {
items,
style,
anchor,
tag,
},
cur,
))
}
TreeToken::MapStart { anchor, tag, style } => {
let anchor = anchor.clone();
let tag = tag.clone();
let style = *style;
let mut entries: Vec<(ExpectedNode, ExpectedNode)> = Vec::new();
let mut cur = pos + 1;
loop {
match tokens.get(cur) {
Some(TreeToken::MapEnd) => {
cur += 1;
break;
}
None => return None,
_ => {
let (key, next_k) = parse_expected_node(tokens, cur)?;
let (val, next_v) = parse_expected_node(tokens, next_k)?;
entries.push((key, val));
cur = next_v;
}
}
}
Some((
ExpectedNode::Mapping {
entries,
style,
anchor,
tag,
},
cur,
))
}
_ => None,
}
}
fn assert_node(actual: &Node<Span>, expected: &ExpectedNode, path: &str) {
match (actual, expected) {
(
Node::Scalar {
value, style, tag, ..
},
ExpectedNode::Scalar {
value: exp_value,
style: exp_style,
anchor: exp_anchor,
tag: exp_tag,
},
) => {
assert_eq!(
value, exp_value,
"{path}: scalar value mismatch: got {value:?}, expected {exp_value:?}"
);
assert!(
exp_style.matches(*style),
"{path}: scalar style mismatch: got {style:?}, expected {exp_style:?}"
);
assert_eq!(
actual.anchor(),
exp_anchor.as_deref(),
"{path}: scalar anchor mismatch: got {:?}, expected {exp_anchor:?}",
actual.anchor()
);
if let Some(exp) = exp_tag {
let tag_ok = tag.as_deref() == Some(exp.as_str())
|| (exp == "!" && tag.as_deref() == Some("tag:yaml.org,2002:str"));
assert!(
tag_ok,
"{path}: scalar tag mismatch: got {tag:?}, expected {exp_tag:?}"
);
}
}
(Node::Alias { name, .. }, ExpectedNode::Alias { name: exp_name }) => {
assert_eq!(
name, exp_name,
"{path}: alias name mismatch: got {name:?}, expected {exp_name:?}"
);
}
(
Node::Sequence {
items, style, tag, ..
},
ExpectedNode::Sequence {
items: exp_items,
style: exp_style,
anchor: exp_anchor,
tag: exp_tag,
},
) => {
assert_eq!(
*style, *exp_style,
"{path}: sequence style mismatch: got {style:?}, expected {exp_style:?}"
);
assert_eq!(
actual.anchor(),
exp_anchor.as_deref(),
"{path}: sequence anchor mismatch"
);
if let Some(exp) = exp_tag {
assert_eq!(
tag.as_deref(),
Some(exp.as_str()),
"{path}: sequence tag mismatch"
);
}
assert_eq!(
items.len(),
exp_items.len(),
"{path}: sequence length mismatch: got {}, expected {}",
items.len(),
exp_items.len()
);
for (i, (item, exp_item)) in items.iter().zip(exp_items.iter()).enumerate() {
assert_node(item, exp_item, &format!("{path}[{i}]"));
}
}
(
Node::Mapping {
entries,
style,
tag,
..
},
ExpectedNode::Mapping {
entries: exp_entries,
style: exp_style,
anchor: exp_anchor,
tag: exp_tag,
},
) => {
assert_eq!(
*style, *exp_style,
"{path}: mapping style mismatch: got {style:?}, expected {exp_style:?}"
);
assert_eq!(
actual.anchor(),
exp_anchor.as_deref(),
"{path}: mapping anchor mismatch"
);
if let Some(exp) = exp_tag {
assert_eq!(
tag.as_deref(),
Some(exp.as_str()),
"{path}: mapping tag mismatch"
);
}
assert_eq!(
entries.len(),
exp_entries.len(),
"{path}: mapping entry count mismatch: got {}, expected {}",
entries.len(),
exp_entries.len()
);
for (i, ((k, v), (exp_k, exp_v))) in entries.iter().zip(exp_entries.iter()).enumerate()
{
assert_node(k, exp_k, &format!("{path}.key[{i}]"));
assert_node(v, exp_v, &format!("{path}.val[{i}]"));
}
}
_ => {
panic!(
"{path}: node kind mismatch: got {:?} but expected {:?}",
node_kind_name(actual),
expected_kind_name(expected)
);
}
}
}
#[expect(clippy::missing_const_for_fn, reason = "test helper")]
fn node_kind_name(node: &Node<Span>) -> &'static str {
match node {
Node::Scalar { .. } => "Scalar",
Node::Mapping { .. } => "Mapping",
Node::Sequence { .. } => "Sequence",
Node::Alias { .. } => "Alias",
}
}
#[expect(clippy::missing_const_for_fn, reason = "test helper")]
fn expected_kind_name(node: &ExpectedNode) -> &'static str {
match node {
ExpectedNode::Scalar { .. } => "Scalar",
ExpectedNode::Mapping { .. } => "Mapping",
ExpectedNode::Sequence { .. } => "Sequence",
ExpectedNode::Alias { .. } => "Alias",
}
}
fn assert_documents(docs: &[Document<Span>], expected: &[ExpectedDocument], tag: &str) {
assert_eq!(
docs.len(),
expected.len(),
"{tag}: document count mismatch: got {}, expected {}",
docs.len(),
expected.len()
);
for (i, (doc, exp_doc)) in docs.iter().zip(expected.iter()).enumerate() {
assert_node(&doc.root, &exp_doc.root, &format!("{tag}[doc {i}]"));
}
}
#[test]
fn spike_sequence_of_mappings_loads_correctly() {
let yaml = "\
-
name: Mark McGwire
hr: 65
avg: 0.278
-
name: Sammy Sosa
hr: 63
avg: 0.288
";
let docs = load(yaml).expect("load failed");
assert_eq!(docs.len(), 1, "expected 1 document");
assert!(
matches!(&docs[0].root, Node::Sequence { .. }),
"expected Sequence root, got: {:?}",
node_kind_name(&docs[0].root)
);
}
#[rstest]
#[timeout(Duration::from_secs(5))]
pub fn yaml_test_suite(#[files("../tests/yaml-test-suite/src/*.yaml")] path: PathBuf) {
let cases = load_cases_from_file(&path);
if cases.is_empty() {
return;
}
for case in &cases {
assert_case(case);
}
}
fn assert_case(case: &ConformanceCase) {
let tag = format!("{}[{}] {}", case.file, case.index, case.name);
if case.fail {
return;
}
let docs = load(&case.yaml).unwrap_or_else(|e| panic!("{tag}: load() returned error: {e}"));
if let Some(tree_expected) = case.tree.as_deref().and_then(parse_expected_documents) {
assert_documents(&docs, &tree_expected, &tag);
}
}
#[test]
fn empty_yaml_loads_to_zero_documents() {
let docs = load("").expect("load failed");
assert_eq!(docs.len(), 0);
}
#[test]
fn null_scalar_produces_empty_string_value() {
let docs = load("key:\n").expect("load failed");
assert_eq!(docs.len(), 1);
match &docs[0].root {
Node::Mapping { entries, .. } => {
assert_eq!(entries.len(), 1);
let (_, val) = &entries[0];
assert!(
matches!(val, Node::Scalar { value, style, .. }
if value.is_empty() && *style == ScalarStyle::Plain),
"expected empty plain scalar, got: {val:?}"
);
}
other => panic!("expected Mapping root, got: {other:?}"),
}
}
#[test]
fn alias_not_expanded_in_lossless_mode() {
let yaml = "a: &anchor foo\nb: *anchor\n";
let docs = load(yaml).expect("load failed");
assert_eq!(docs.len(), 1);
match &docs[0].root {
Node::Mapping { entries, .. } => {
assert_eq!(entries.len(), 2);
let (_, val_b) = &entries[1];
assert!(
matches!(val_b, Node::Alias { name, .. } if name == "anchor"),
"expected Alias(anchor), got: {val_b:?}"
);
}
other => panic!("expected Mapping root, got: {other:?}"),
}
}
#[test]
fn lossless_mode_preserves_undefined_alias() {
let docs = load("key: *undefined\n").expect("load should succeed in lossless mode");
assert_eq!(docs.len(), 1);
match &docs[0].root {
Node::Mapping { entries, .. } => {
let (_, val) = &entries[0];
assert!(
matches!(val, Node::Alias { name, .. } if name == "undefined"),
"expected Alias(undefined), got: {val:?}"
);
}
other => panic!("expected Mapping, got: {other:?}"),
}
}
#[test]
fn undefined_alias_in_resolved_mode_returns_error() {
let result = LoaderBuilder::new()
.resolved()
.build()
.load("key: *undefined\n");
assert!(
matches!(result, Err(LoadError::UndefinedAlias { ref name }) if name == "undefined"),
"expected UndefinedAlias, got: {result:?}"
);
}
#[test]
fn nesting_depth_limit_exceeded_returns_error() {
let yaml = "- - - value\n";
let result = LoaderBuilder::new().max_nesting_depth(2).build().load(yaml);
assert!(
matches!(
result,
Err(LoadError::NestingDepthLimitExceeded { limit: 2 })
),
"expected NestingDepthLimitExceeded(2), got: {result:?}"
);
}
#[test]
fn anchor_count_limit_exceeded_returns_error() {
let yaml = "&a foo: &b bar: &c baz\n";
let result = LoaderBuilder::new().max_anchors(2).build().load(yaml);
assert!(
matches!(
result,
Err(LoadError::AnchorCountLimitExceeded { limit: 2 })
),
"expected AnchorCountLimitExceeded(2), got: {result:?}"
);
}
#[test]
fn multi_document_yaml_with_explicit_markers() {
let yaml = "---\nfoo\n---\nbar\n";
let docs = load(yaml).expect("load failed");
assert_eq!(docs.len(), 2);
assert!(
matches!(&docs[0].root, Node::Scalar { value, .. } if value == "foo"),
"expected scalar 'foo', got: {:?}",
&docs[0].root
);
assert!(
matches!(&docs[1].root, Node::Scalar { value, .. } if value == "bar"),
"expected scalar 'bar', got: {:?}",
&docs[1].root
);
}
#[cfg(test)]
mod tree_parser_tests {
use super::*;
#[test]
fn plain_scalar_token_parsed_correctly() {
let token = parse_tree_line("=VAL :hello").unwrap();
assert!(
matches!(
&token,
TreeToken::Scalar {
style: StyleVariant::Plain,
value,
anchor: None,
tag: None,
} if value == "hello"
),
"got: {token:?}"
);
}
#[test]
fn single_quoted_scalar_token_parsed_correctly() {
let token = parse_tree_line("=VAL 'world").unwrap();
assert!(
matches!(
&token,
TreeToken::Scalar {
style: StyleVariant::SingleQuoted,
value,
anchor: None,
tag: None,
} if value == "world"
),
"got: {token:?}"
);
}
#[test]
fn double_quoted_scalar_token_parsed_correctly() {
let token = parse_tree_line("=VAL \"text").unwrap();
assert!(
matches!(
&token,
TreeToken::Scalar {
style: StyleVariant::DoubleQuoted,
value,
anchor: None,
tag: None,
} if value == "text"
),
"got: {token:?}"
);
}
#[test]
fn literal_block_scalar_token_parsed_correctly() {
let token = parse_tree_line("=VAL |content").unwrap();
assert!(
matches!(
&token,
TreeToken::Scalar {
style: StyleVariant::Literal,
value,
..
} if value == "content"
),
"got: {token:?}"
);
}
#[test]
fn folded_block_scalar_token_parsed_correctly() {
let token = parse_tree_line("=VAL >content").unwrap();
assert!(
matches!(
&token,
TreeToken::Scalar {
style: StyleVariant::Folded,
value,
..
} if value == "content"
),
"got: {token:?}"
);
}
#[test]
fn scalar_with_anchor_parsed_correctly() {
let token = parse_tree_line("=VAL &myanchor :value").unwrap();
assert!(
matches!(
&token,
TreeToken::Scalar {
anchor: Some(anchor),
style: StyleVariant::Plain,
value,
tag: None,
} if anchor == "myanchor" && value == "value"
),
"got: {token:?}"
);
}
#[test]
fn scalar_with_tag_parsed_correctly() {
let token = parse_tree_line("=VAL <tag:str> :value").unwrap();
assert!(
matches!(
&token,
TreeToken::Scalar {
tag: Some(tag),
style: StyleVariant::Plain,
value,
anchor: None,
} if tag == "tag:str" && value == "value"
),
"got: {token:?}"
);
}
#[test]
fn scalar_with_anchor_and_tag_parsed_correctly() {
let token = parse_tree_line("=VAL &a <tag:str> :val").unwrap();
assert!(
matches!(
&token,
TreeToken::Scalar {
anchor: Some(anchor),
tag: Some(tag),
style: StyleVariant::Plain,
value,
} if anchor == "a" && tag == "tag:str" && value == "val"
),
"got: {token:?}"
);
}
#[test]
fn alias_token_parsed_correctly() {
let token = parse_tree_line("=ALI *myanchor").unwrap();
assert!(
matches!(&token, TreeToken::Alias { name } if name == "myanchor"),
"got: {token:?}"
);
}
#[test]
fn mapping_start_token_parsed_correctly() {
let token = parse_tree_line("+MAP").unwrap();
assert!(
matches!(
&token,
TreeToken::MapStart {
anchor: None,
tag: None,
style: CollectionStyle::Block,
}
),
"got: {token:?}"
);
}
#[test]
fn mapping_start_with_anchor_parsed_correctly() {
let token = parse_tree_line("+MAP &mapanchor").unwrap();
assert!(
matches!(
&token,
TreeToken::MapStart {
anchor: Some(a),
tag: None,
style: CollectionStyle::Block,
} if a == "mapanchor"
),
"got: {token:?}"
);
}
#[test]
fn sequence_start_with_tag_parsed_correctly() {
let token = parse_tree_line("+SEQ <tag:seq>").unwrap();
assert!(
matches!(
&token,
TreeToken::SeqStart {
tag: Some(t),
anchor: None,
style: CollectionStyle::Block,
} if t == "tag:seq"
),
"got: {token:?}"
);
}
#[test]
fn document_start_with_explicit_marker_recognized() {
let token = parse_tree_line("+DOC ---").unwrap();
assert!(matches!(token, TreeToken::DocStart), "got: {token:?}");
}
#[test]
fn document_start_without_marker_recognized() {
let token = parse_tree_line("+DOC").unwrap();
assert!(matches!(token, TreeToken::DocStart), "got: {token:?}");
}
#[test]
fn empty_scalar_value_parsed_correctly() {
let token = parse_tree_line("=VAL :").unwrap();
assert!(
matches!(
&token,
TreeToken::Scalar {
style: StyleVariant::Plain,
value,
anchor: None,
tag: None,
} if value.is_empty()
),
"got: {token:?}"
);
}
}