use std::collections::hash_map::{Entry, HashMap};
use crate::{
fatal,
tester::{Status, TestCmd, Tests},
};
pub(crate) fn parse_tests<'a>(comment_prefix: Option<&str>, test_str: &'a str) -> Tests<'a> {
let lines = test_str.lines().collect::<Vec<_>>();
let mut tests = HashMap::new();
let mut line_off = 0;
let mut ignore_if = None;
while line_off < lines.len() {
let indent = indent_level(&lines, line_off);
if indent == lines[line_off].len() {
line_off += 1;
continue;
}
if let Some(cp) = comment_prefix {
if lines[line_off][indent..].starts_with(cp) {
line_off += 1;
continue;
}
}
let (test_name, val) = key_val(&lines, line_off, indent);
if test_name == "ignore-if" {
if ignore_if.is_some() {
fatal(&format!(
"'ignore-if' is specified more than once, line {}.",
line_off
))
}
ignore_if = Some(val.into());
line_off += 1;
continue;
}
if !val.is_empty() {
fatal(&format!(
"Test name '{}' can't have a value on line {}.",
test_name, line_off
));
}
match tests.entry(test_name.to_lowercase()) {
Entry::Occupied(_) => fatal(&format!(
"Command name '{}' is specified more than once, line {}.",
test_name, line_off
)),
Entry::Vacant(e) => {
line_off += 1;
let mut testcmd = TestCmd::default();
while line_off < lines.len() {
let sub_indent = indent_level(&lines, line_off);
if sub_indent == lines[line_off].len() {
line_off += 1;
continue;
}
if sub_indent == indent {
break;
}
let (end_line_off, key, val) =
key_multiline_val(comment_prefix, &lines, line_off, sub_indent);
line_off = end_line_off;
match key {
"env-var" => {
let val_str = val.join("\n");
match val_str.find('=') {
Some(i) => {
let key = val_str[..i].trim().to_owned();
let var = val_str[i + 1..].trim().to_owned();
testcmd.env.insert(key, var);
}
None => {
fatal(&format!(
"'{}' is not in the format '<key>=<string>' on line {}",
val_str, line_off
));
}
}
}
"exec-arg" => {
let val_str = val.join("\n");
testcmd.args.push(val_str);
}
"status" | "rerun-if-status" => {
let val_str = val.join("\n");
let status = match val_str.to_lowercase().as_str() {
"success" => Status::Success,
"error" => Status::Error,
"signal" => Status::Signal,
x => {
if let Ok(i) = x.parse::<i32>() {
Status::Int(i)
} else {
fatal(&format!(
"Unknown status '{}' on line {}",
val_str, line_off
));
}
}
};
match key {
"status" => {
testcmd.status = status;
}
"rerun-if-status" => {
testcmd.rerun_if_status = Some(status);
}
_ => {
unreachable!();
}
}
}
"stdin" => {
testcmd.stdin = Some(val.join("\n"));
}
"stderr" => {
testcmd.stderr = val;
}
"stdout" => {
testcmd.stdout = val;
}
"rerun-if-stderr" => {
testcmd.rerun_if_stderr = Some(val);
}
"rerun-if-stdout" => {
testcmd.rerun_if_stdout = Some(val);
}
_ => fatal(&format!("Unknown key '{}' on line {}.", key, line_off)),
}
}
e.insert(testcmd);
}
}
}
Tests { ignore_if, tests }
}
fn indent_level(lines: &[&str], line_off: usize) -> usize {
lines[line_off]
.chars()
.take_while(|c| c.is_whitespace())
.count()
}
fn key_val<'a>(lines: &[&'a str], line_off: usize, indent: usize) -> (&'a str, &'a str) {
let line = lines[line_off];
let key_len = line[indent..]
.chars()
.take_while(|c| !(c.is_whitespace() || c == &':'))
.count();
let key = &line[indent..indent + key_len];
let mut content_start = indent + key_len;
content_start += line[content_start..]
.chars()
.take_while(|c| c.is_whitespace())
.count();
match line[content_start..].chars().next() {
Some(':') => content_start += ':'.len_utf8(),
_ => fatal(&format!(
"Invalid key terminator at line {}.\n {}",
line_off, line
)),
}
content_start += line[content_start..]
.chars()
.take_while(|c| c.is_whitespace())
.count();
(key, &line[content_start..])
}
fn key_multiline_val<'a>(
comment_prefix: Option<&str>,
lines: &[&'a str],
mut line_off: usize,
indent: usize,
) -> (usize, &'a str, Vec<&'a str>) {
let (key, first_line_val) = key_val(lines, line_off, indent);
line_off += 1;
let mut val = vec![first_line_val];
if line_off < lines.len() {
let sub_indent = indent_level(lines, line_off);
while line_off < lines.len() {
let cur_indent = indent_level(lines, line_off);
if cur_indent == lines[line_off].len() {
val.push("");
line_off += 1;
continue;
}
if cur_indent <= indent {
break;
}
if let Some(cp) = comment_prefix {
if lines[line_off][sub_indent..].starts_with(cp) {
line_off += 1;
continue;
}
}
val.push(&lines[line_off][sub_indent..]);
line_off += 1;
}
}
while !val.is_empty() && val[val.len() - 1].is_empty() {
val.pop();
}
while !val.is_empty() && val[0].is_empty() {
val.remove(0);
}
(line_off, key, val)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_key_multiline() {
assert_eq!(key_multiline_val(None, &["x:", ""], 0, 0), (2, "x", vec![]));
assert_eq!(
key_multiline_val(None, &["x: y", " z", "a"], 0, 0),
(2, "x", vec!["y", "z"])
);
assert_eq!(
key_multiline_val(None, &["x:", " z", "a"], 0, 0),
(2, "x", vec!["z"])
);
assert_eq!(
key_multiline_val(None, &["x:", " z ", " a ", " ", "b"], 0, 0),
(4, "x", vec!["z ", "a "])
);
assert_eq!(
key_multiline_val(None, &["x:", " z ", " a ", " ", " b"], 0, 0),
(5, "x", vec!["z ", " a ", "", "b"])
);
assert_eq!(
key_multiline_val(
Some("#"),
&["x:", " z ", " a ", " # c2", " ", " b"],
0,
0
),
(6, "x", vec!["z ", " a ", "", "b"])
);
}
}