testing: add an integration test for yaml-test-suite
The official YAML test suite (https://github.com/yaml/yaml-test-suite). Requires the submodule to be checked out.
This commit is contained in:
parent
da67c9a763
commit
38a81c6200
3 changed files with 281 additions and 0 deletions
3
parser/.gitmodules
vendored
Normal file
3
parser/.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "tests/yaml-test-suite"]
|
||||
path = tests/yaml-test-suite
|
||||
url = https://github.com/yaml/yaml-test-suite/
|
1
parser/tests/yaml-test-suite
Submodule
1
parser/tests/yaml-test-suite
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 534eb75451fada039442460a79b79989fc87f9c3
|
277
parser/tests/yaml-test-suite.rs
Normal file
277
parser/tests/yaml-test-suite.rs
Normal file
|
@ -0,0 +1,277 @@
|
|||
use std::{ffi::OsStr, fs, path::Path};
|
||||
|
||||
use yaml_rust::{
|
||||
parser::{Event, EventReceiver, Parser},
|
||||
scanner::{TokenType, TScalarStyle},
|
||||
ScanError,
|
||||
Yaml,
|
||||
YamlLoader,
|
||||
yaml,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn yaml_test_suite() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut error_count = 0;
|
||||
for entry in std::fs::read_dir("tests/yaml-test-suite/src")? {
|
||||
let entry = entry?;
|
||||
error_count += run_tests_from_file(&entry.path(), &entry.file_name())?;
|
||||
}
|
||||
println!("Expected errors: {}", EXPECTED_FAILURES.len());
|
||||
if error_count > 0 {
|
||||
panic!("Unexpected errors in testsuite: {}", error_count);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_tests_from_file(path: impl AsRef<Path>, file_name: &OsStr) -> Result<u32, Box<dyn std::error::Error>> {
|
||||
let test_name = path.as_ref()
|
||||
.file_name().ok_or("")?
|
||||
.to_string_lossy().strip_suffix(".yaml").ok_or("unexpected filename")?.to_owned();
|
||||
let data = fs::read_to_string(path.as_ref())?;
|
||||
let tests = YamlLoader::load_from_str(&data)?;
|
||||
let tests = tests[0].as_vec().unwrap();
|
||||
let mut error_count = 0;
|
||||
|
||||
let mut test = yaml::Hash::new();
|
||||
for (idx, test_data) in tests.iter().enumerate() {
|
||||
let desc = format!("{}-{}", test_name, idx);
|
||||
let is_xfail = EXPECTED_FAILURES.contains(&desc.as_str());
|
||||
|
||||
// Test fields except `fail` are "inherited"
|
||||
let test_data = test_data.as_hash().unwrap();
|
||||
test.remove(&Yaml::String("fail".into()));
|
||||
for (key, value) in test_data.clone() {
|
||||
test.insert(key, value);
|
||||
}
|
||||
|
||||
if let Some(error) = run_single_test(&test) {
|
||||
if !is_xfail {
|
||||
eprintln!("[{}] {}", desc, error);
|
||||
error_count += 1;
|
||||
}
|
||||
} else {
|
||||
if is_xfail {
|
||||
eprintln!("[{}] UNEXPECTED PASS", desc);
|
||||
error_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(error_count)
|
||||
}
|
||||
|
||||
fn run_single_test(test: &yaml::Hash) -> Option<String> {
|
||||
if test.get(&Yaml::String("skip".into())).is_some() {
|
||||
return None;
|
||||
}
|
||||
let source = test[&Yaml::String("yaml".into())].as_str().unwrap();
|
||||
let should_fail = test.get(&Yaml::String("fail".into())) == Some(&Yaml::Boolean(true));
|
||||
let actual_events = parse_to_events(&yaml_to_raw(source));
|
||||
if should_fail {
|
||||
if actual_events.is_ok() {
|
||||
return Some(format!("no error while expected"));
|
||||
}
|
||||
} else {
|
||||
let expected_events = yaml_to_raw(test[&Yaml::String("tree".into())].as_str().unwrap());
|
||||
match actual_events {
|
||||
Ok(events) => {
|
||||
if let Some(diff) = events_differ(events, &expected_events) {
|
||||
//dbg!(source, yaml_to_raw(source));
|
||||
return Some(format!("events differ: {}", diff));
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
//dbg!(source, yaml_to_raw(source));
|
||||
return Some(format!("unexpected error {:?}", error));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_to_events(source: &str) -> Result<Vec<String>, ScanError> {
|
||||
let mut reporter = EventReporter::new();
|
||||
Parser::new(source.chars())
|
||||
.load(&mut reporter, true)?;
|
||||
Ok(reporter.events)
|
||||
}
|
||||
|
||||
struct EventReporter {
|
||||
events: Vec<String>,
|
||||
}
|
||||
|
||||
impl EventReporter {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
events: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventReceiver for EventReporter {
|
||||
fn on_event(&mut self, ev: Event) {
|
||||
let line: String = match ev {
|
||||
Event::StreamStart => "+STR".into(),
|
||||
Event::StreamEnd => "-STR".into(),
|
||||
|
||||
Event::DocumentStart => "+DOC".into(),
|
||||
Event::DocumentEnd => "-DOC".into(),
|
||||
|
||||
Event::SequenceStart(idx) => format!("+SEQ{}", format_index(idx)),
|
||||
Event::SequenceEnd => "-SEQ".into(),
|
||||
|
||||
Event::MappingStart(idx) => format!("+MAP{}", format_index(idx)),
|
||||
Event::MappingEnd => "-MAP".into(),
|
||||
|
||||
Event::Scalar(ref text, style, idx, ref tag) => {
|
||||
let kind = match style {
|
||||
TScalarStyle::Plain => ":",
|
||||
TScalarStyle::SingleQuoted => "'",
|
||||
TScalarStyle::DoubleQuoted => r#"""#,
|
||||
TScalarStyle::Literal => "|",
|
||||
TScalarStyle::Foled => ">",
|
||||
TScalarStyle::Any => unreachable!(),
|
||||
};
|
||||
format!("=VAL{}{} {}{}",
|
||||
format_index(idx), format_tag(tag), kind, escape_text(text))
|
||||
}
|
||||
Event::Alias(idx) => format!("=ALI *{}", idx),
|
||||
Event::Nothing => return,
|
||||
};
|
||||
self.events.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
fn format_index(idx: usize) -> String {
|
||||
if idx > 0 {
|
||||
format!(" &{}", idx)
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_text(text: &str) -> String {
|
||||
let mut text = text.to_owned();
|
||||
for (ch, replacement) in [
|
||||
('\\', r#"\\"#),
|
||||
('\n', "\\n"),
|
||||
('\r', "\\r"),
|
||||
('\x08', "\\b"),
|
||||
('\t', "\\t"),
|
||||
] {
|
||||
text = text.replace(ch, replacement);
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
fn format_tag(tag: &Option<TokenType>) -> String {
|
||||
if let Some(TokenType::Tag(ns, tag)) = tag {
|
||||
let ns = match ns.as_str() {
|
||||
"!!" => "tag:yaml.org,2002:", // Wrong if this ns is overridden
|
||||
other => other,
|
||||
};
|
||||
format!(" <{}{}>", ns, tag)
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn events_differ(actual: Vec<String>, expected: &str) -> Option<String> {
|
||||
let actual = actual.iter().map(Some).chain(std::iter::repeat(None));
|
||||
let expected = expected_events(expected);
|
||||
let expected = expected.iter().map(Some).chain(std::iter::repeat(None));
|
||||
for (idx, (act, exp)) in actual.zip(expected).enumerate() {
|
||||
return match (act, exp) {
|
||||
(Some(act), Some(exp)) => {
|
||||
if act == exp {
|
||||
continue;
|
||||
} else {
|
||||
Some(format!("line {} differs: expected `{}`, found `{}`", idx, exp, act))
|
||||
}
|
||||
}
|
||||
(Some(a), None) => Some(format!("extra actual line: {:?}", a)),
|
||||
(None, Some(e)) => Some(format!("extra expected line: {:?}", e)),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
/// Replace the unprintable characters used in the YAML examples with normal
|
||||
fn yaml_to_raw(yaml: &str) -> String {
|
||||
let mut yaml = yaml.to_owned();
|
||||
for (pat, replacement) in [
|
||||
("␣", " "),
|
||||
("»", "\t"),
|
||||
("—", ""), // Tab line continuation ——»
|
||||
("←", "\r"),
|
||||
("⇔", "\u{FEFF}"),
|
||||
("↵", ""), // Trailing newline marker
|
||||
("∎\n", ""),
|
||||
] {
|
||||
yaml = yaml.replace(pat, replacement);
|
||||
}
|
||||
yaml
|
||||
}
|
||||
|
||||
/// Adapt the expectations to the yaml-rust reasonable limitations
|
||||
///
|
||||
/// Drop information on node styles (flow/block) and anchor names.
|
||||
/// Both are things that can be omitted according to spec.
|
||||
fn expected_events(expected_tree: &str) -> Vec<String> {
|
||||
let mut anchors = vec![];
|
||||
expected_tree.split("\n")
|
||||
.map(|s| s.trim_start().to_owned())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|mut s| {
|
||||
// Anchor name-to-number conversion
|
||||
if let Some(start) = s.find("&") {
|
||||
if s[..start].find(":").is_none() {
|
||||
let len = s[start..].find(" ").unwrap_or(s[start..].len());
|
||||
anchors.push(s[start+1..start + len].to_owned());
|
||||
s = s.replace(&s[start..start + len], &format!("&{}", anchors.len()));
|
||||
}
|
||||
}
|
||||
// Alias nodes name-to-number
|
||||
if s.starts_with("=ALI") {
|
||||
let start = s.find("*").unwrap();
|
||||
let name = &s[start + 1 ..];
|
||||
let idx = anchors.iter().enumerate().filter(|(_, v)| v == &name).last().unwrap().0;
|
||||
s = s.replace(&s[start..], &format!("*{}", idx + 1));
|
||||
}
|
||||
// Dropping style information
|
||||
match &*s {
|
||||
"+DOC ---" => "+DOC".into(),
|
||||
"-DOC ..." => "-DOC".into(),
|
||||
s if s.starts_with("+SEQ []") => s.replacen("+SEQ []", "+SEQ", 1),
|
||||
s if s.starts_with("+MAP {}") => s.replacen("+MAP {}", "+MAP", 1),
|
||||
"=VAL :" => "=VAL :~".into(), // FIXME: known bug
|
||||
s => s.into(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
static EXPECTED_FAILURES: &[&str] = &[
|
||||
// These seem to be API limited (not enough information on the event stream level)
|
||||
// No tag available for SEQ and MAP
|
||||
"2XXW-0",
|
||||
"35KP-0",
|
||||
"57H4-0",
|
||||
"6JWB-0",
|
||||
"735Y-0",
|
||||
"9KAX-0",
|
||||
"BU8L-0",
|
||||
"C4HZ-0",
|
||||
"EHF6-0",
|
||||
"J7PZ-0",
|
||||
"UGM3-0",
|
||||
// Cannot resolve tag namespaces
|
||||
"5TYM-0",
|
||||
"6CK3-0",
|
||||
"6WLZ-0",
|
||||
"9WXW-0",
|
||||
"CC74-0",
|
||||
"U3C3-0",
|
||||
"Z9M4-0",
|
||||
"P76L-0", // overriding the `!!` namespace!
|
||||
];
|
Loading…
Reference in a new issue