use tatara_lisp::{Span, Spanned};
use crate::parse::{LOWERED_MAP, LOWERED_TEST_META};
#[derive(Clone, Copy, Debug)]
pub struct TestParts<'a> {
pub name: &'a str,
pub name_span: Span,
pub meta: Option<&'a Spanned>,
pub body: &'a Spanned,
}
#[derive(Clone, Debug, Default)]
pub struct TestMeta<'a> {
pub satisfies: Vec<(&'a str, Span)>,
pub target: Option<&'a Spanned>,
}
#[must_use]
pub fn test_parts(form: &Spanned) -> Option<TestParts<'_>> {
let items = form.as_list()?;
if items.first()?.as_symbol()? != "deftest" {
return None;
}
let name = items.get(1)?;
let (meta, body) = match items {
[_, _, body] => (None, body),
[_, _, meta, body] if is_meta(meta) => (Some(meta), body),
_ => return None,
};
Some(TestParts {
name: name.as_string()?,
name_span: name.span,
meta,
body,
})
}
#[must_use]
pub fn is_meta(form: &Spanned) -> bool {
form.as_list()
.and_then(|items| items.first())
.and_then(Spanned::as_symbol)
== Some(LOWERED_TEST_META)
}
#[must_use]
pub fn meta_map(meta: &Spanned) -> Option<&Spanned> {
match meta.as_list()? {
[head, map] if head.as_symbol() == Some(LOWERED_TEST_META) => {
let items = map.as_list()?;
(items.first()?.as_symbol()? == LOWERED_MAP).then_some(map)
}
_ => None,
}
}
#[must_use]
pub fn test_meta(meta: &Spanned) -> Option<TestMeta<'_>> {
let items = meta_map(meta)?.as_list()?;
let mut out = TestMeta::default();
for pair in items[1..].chunks(2) {
let [key, value] = pair else { return None };
match key.as_keyword()? {
"satisfies" => {
let list = value.as_list()?;
if list.first()?.as_symbol()? != "list" {
return None;
}
for control in &list[1..] {
out.satisfies.push((control.as_keyword()?, control.span));
}
}
"target" => out.target = Some(value),
_ => return None,
}
}
Some(out)
}
#[must_use]
pub fn target_key<'a>(meta: &TestMeta<'a>) -> Option<&'a str> {
meta.target.and_then(Spanned::as_symbol)
}
#[cfg(test)]
mod tests {
use super::*;
fn first(src: &str) -> Spanned {
crate::parse_program_tree(src)
.expect("parse")
.into_iter()
.next()
.expect("a form")
}
#[test]
fn a_plain_test_has_no_meta_and_its_body_is_the_third_item() {
let form = first("test \"t\"\n assert 1\nend\n");
let parts = test_parts(&form).expect("a test");
assert_eq!(parts.name, "t");
assert!(parts.meta.is_none());
assert_eq!(parts.body.to_sexp().to_string(), "(blue-assert '1 1)");
}
#[test]
fn a_declared_test_reads_its_controls_and_target() {
let form = first(
"test \"t\", {satisfies: %i[nist_ac_6 nist_cm_7], target: parse_token}\n assert true\nend\n",
);
let parts = test_parts(&form).expect("a test");
let meta = test_meta(parts.meta.expect("meta")).expect("reads");
let controls: Vec<&str> = meta.satisfies.iter().map(|(c, _)| *c).collect();
assert_eq!(controls, ["nist_ac_6", "nist_cm_7"]);
assert_eq!(target_key(&meta), Some("parse_token"));
}
fn permutations(items: &[&'static str]) -> Vec<Vec<&'static str>> {
if items.len() <= 1 {
return vec![items.to_vec()];
}
let mut out = Vec::new();
for (i, first) in items.iter().enumerate() {
let mut rest = items.to_vec();
rest.remove(i);
for mut tail in permutations(&rest) {
tail.insert(0, first);
out.push(tail);
}
}
out
}
fn spelled(controls: &[&str], target: Option<&str>, target_first: bool) -> String {
let list = match controls {
[one] => format!("[:{one}]"),
many => format!("%i[{}]", many.join(" ")),
};
let entries = match (target, target_first) {
(Some(t), true) => format!("target: {t}, satisfies: {list}"),
(Some(t), false) => format!("satisfies: {list}, target: {t}"),
(None, _) => format!("satisfies: {list}"),
};
format!("test \"t\", {{{entries}}}\n assert true\nend\n")
}
#[test]
fn every_order_of_a_declaration_lowers_to_the_canonical_tree() {
let pool = ["nist_ac_6", "nist_cm_7", "nist_sc_7", "nist_si_10"];
let mut spellings = 0;
for size in 1..=pool.len() {
let set = &pool[..size];
for target in [None, Some("parse_token")] {
let canonical = spelled(set, target, false);
let tree = first(&canonical).to_sexp().to_string();
assert!(crate::spelling_notes(&canonical)
.expect("parses")
.is_empty());
for order in permutations(set) {
for target_first in [false, true] {
let written = spelled(&order, target, target_first);
assert_eq!(first(&written).to_sexp().to_string(), tree, "{written}");
let noted = crate::spelling_notes(&written)
.expect("parses")
.iter()
.any(|n| n.spelling == crate::Spelling::TestDeclaration);
assert_eq!(noted, written != canonical, "{written}");
spellings += 1;
}
}
}
}
assert_eq!(spellings, 2 * 2 * (1 + 2 + 6 + 24));
}
#[test]
fn a_four_item_deftest_without_a_meta_head_is_not_a_test() {
let form = first("deftest(\"t\", 1, 2)\n");
assert!(test_parts(&form).is_none());
}
}