use wasm_dbms_api::prelude::SqlError;
use super::sexpr;
use crate::parser::parse;
const CASE_HEADER: &str = "=== ";
const SEPARATOR: &str = "---";
#[derive(Debug, PartialEq, Eq)]
pub(super) struct Case {
pub(super) name: String,
pub(super) line: usize,
pub(super) sql: String,
pub(super) expected: String,
}
pub(super) fn load(file: &str, source: &str) -> Vec<Case> {
struct Raw<'a> {
name: String,
line: usize,
sql: Vec<&'a str>,
expected: Vec<&'a str>,
}
let mut cases: Vec<Raw<'_>> = Vec::new();
let mut in_sql = false;
for (index, line) in source.lines().enumerate() {
let number = index + 1;
if let Some(name) = line.strip_prefix(CASE_HEADER) {
let name = name.trim().to_string();
assert!(!name.is_empty(), "{file}:{number}: empty case name");
assert!(
cases.iter().all(|case| case.name != name),
"{file}:{number}: duplicate case name `{name}`"
);
assert!(
!in_sql,
"{file}:{number}: previous case has no `---` separator"
);
cases.push(Raw {
name,
line: number,
sql: Vec::new(),
expected: Vec::new(),
});
in_sql = true;
continue;
}
let Some(case) = cases.last_mut() else {
assert!(
line.trim().is_empty() || line.starts_with('#'),
"{file}:{number}: text before the first case must be a `#` comment"
);
continue;
};
if in_sql && line == SEPARATOR {
in_sql = false;
} else if in_sql {
case.sql.push(line);
} else {
case.expected.push(line);
}
}
assert!(!in_sql, "{file}: last case has no `---` separator");
assert!(!cases.is_empty(), "{file}: no cases found");
cases
.into_iter()
.map(|raw| {
let expected = normalize(&raw.expected.join(" "));
assert!(
!expected.is_empty(),
"{file}:{line}: case `{name}` has no expectation",
line = raw.line,
name = raw.name
);
Case {
name: raw.name,
line: raw.line,
sql: raw.sql.join("\n"),
expected,
}
})
.collect()
}
fn normalize(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ")
}
pub(super) fn outcome(sql: &str) -> String {
match parse(sql) {
Ok(parsed) => sexpr::statement(&parsed.statement),
Err(SqlError::Parse { line, col, msg }) => format!("error {line}:{col}: {msg}"),
Err(SqlError::MissingWhereClause) => "error missing-where".to_string(),
Err(other) => format!("unexpected error kind: {other:?}"),
}
}
pub(super) fn run(file: &str, source: &str) {
let cases = load(file, source);
let failures: Vec<String> = cases
.iter()
.filter_map(|case| {
let actual = normalize(&outcome(&case.sql));
(actual != case.expected).then(|| {
format!(
"{file}:{line}: case `{name}`\n sql: {sql:?}\n expected: {expected}\n actual: {actual}",
line = case.line,
name = case.name,
sql = case.sql,
expected = case.expected,
)
})
})
.collect();
assert!(
failures.is_empty(),
"{count} of {total} fixture cases failed:\n\n{report}\n",
count = failures.len(),
total = cases.len(),
report = failures.join("\n\n")
);
}