Skip to main content

blue_lang_syntax/
test_form.rs

1//! A `test` block as its tree: `(deftest "name" body)`, or with a declaration,
2//! `(deftest "name" (blue-test-meta (hash-map :satisfies (list :nist_ac_6)
3//! :target parse_token)) body)`.
4//!
5//! The ONE reader of the shape. The harness, the formatter, the check stage,
6//! the resolver's walk and the engine each ask here, so a test with a
7//! declaration is a test to all of them, never a stray four-item form one of
8//! them evaluates as code.
9
10use tatara_lisp::{Span, Spanned};
11
12use crate::parse::{LOWERED_MAP, LOWERED_TEST_META};
13
14/// The parts of one `deftest` form.
15#[derive(Clone, Copy, Debug)]
16pub struct TestParts<'a> {
17    pub name: &'a str,
18    pub name_span: Span,
19    /// The `(blue-test-meta …)` node, when the test declares one.
20    pub meta: Option<&'a Spanned>,
21    pub body: &'a Spanned,
22}
23
24/// What a test declares: the controls it satisfies, by keyword, and the
25/// definition it targets, as the tree holds it (a symbol, resolved to its
26/// runtime key after resolution).
27#[derive(Clone, Debug, Default)]
28pub struct TestMeta<'a> {
29    pub satisfies: Vec<(&'a str, Span)>,
30    pub target: Option<&'a Spanned>,
31}
32
33/// The parts of `form` when it is a `deftest`.
34#[must_use]
35pub fn test_parts(form: &Spanned) -> Option<TestParts<'_>> {
36    let items = form.as_list()?;
37    if items.first()?.as_symbol()? != "deftest" {
38        return None;
39    }
40    let name = items.get(1)?;
41    let (meta, body) = match items {
42        [_, _, body] => (None, body),
43        [_, _, meta, body] if is_meta(meta) => (Some(meta), body),
44        _ => return None,
45    };
46    Some(TestParts {
47        name: name.as_string()?,
48        name_span: name.span,
49        meta,
50        body,
51    })
52}
53
54/// Is `form` a `(blue-test-meta …)` node?
55#[must_use]
56pub fn is_meta(form: &Spanned) -> bool {
57    form.as_list()
58        .and_then(|items| items.first())
59        .and_then(Spanned::as_symbol)
60        == Some(LOWERED_TEST_META)
61}
62
63/// The map a `(blue-test-meta (hash-map …))` node carries.
64#[must_use]
65pub fn meta_map(meta: &Spanned) -> Option<&Spanned> {
66    match meta.as_list()? {
67        [head, map] if head.as_symbol() == Some(LOWERED_TEST_META) => {
68            let items = map.as_list()?;
69            (items.first()?.as_symbol()? == LOWERED_MAP).then_some(map)
70        }
71        _ => None,
72    }
73}
74
75/// The declaration a `(blue-test-meta …)` node holds.
76#[must_use]
77pub fn test_meta(meta: &Spanned) -> Option<TestMeta<'_>> {
78    let items = meta_map(meta)?.as_list()?;
79    let mut out = TestMeta::default();
80    for pair in items[1..].chunks(2) {
81        let [key, value] = pair else { return None };
82        match key.as_keyword()? {
83            "satisfies" => {
84                let list = value.as_list()?;
85                if list.first()?.as_symbol()? != "list" {
86                    return None;
87                }
88                for control in &list[1..] {
89                    out.satisfies.push((control.as_keyword()?, control.span));
90                }
91            }
92            "target" => out.target = Some(value),
93            _ => return None,
94        }
95    }
96    Some(out)
97}
98
99/// The key a target symbol names: `%root/parse_token` after resolution,
100/// `parse_token` as written.
101#[must_use]
102pub fn target_key<'a>(meta: &TestMeta<'a>) -> Option<&'a str> {
103    meta.target.and_then(Spanned::as_symbol)
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    fn first(src: &str) -> Spanned {
111        crate::parse_program_tree(src)
112            .expect("parse")
113            .into_iter()
114            .next()
115            .expect("a form")
116    }
117
118    #[test]
119    fn a_plain_test_has_no_meta_and_its_body_is_the_third_item() {
120        let form = first("test \"t\"\n  assert 1\nend\n");
121        let parts = test_parts(&form).expect("a test");
122        assert_eq!(parts.name, "t");
123        assert!(parts.meta.is_none());
124        assert_eq!(parts.body.to_sexp().to_string(), "(blue-assert '1 1)");
125    }
126
127    #[test]
128    fn a_declared_test_reads_its_controls_and_target() {
129        let form = first(
130            "test \"t\", {satisfies: %i[nist_ac_6 nist_cm_7], target: parse_token}\n  assert true\nend\n",
131        );
132        let parts = test_parts(&form).expect("a test");
133        let meta = test_meta(parts.meta.expect("meta")).expect("reads");
134        let controls: Vec<&str> = meta.satisfies.iter().map(|(c, _)| *c).collect();
135        assert_eq!(controls, ["nist_ac_6", "nist_cm_7"]);
136        assert_eq!(target_key(&meta), Some("parse_token"));
137    }
138
139    fn permutations(items: &[&'static str]) -> Vec<Vec<&'static str>> {
140        if items.len() <= 1 {
141            return vec![items.to_vec()];
142        }
143        let mut out = Vec::new();
144        for (i, first) in items.iter().enumerate() {
145            let mut rest = items.to_vec();
146            rest.remove(i);
147            for mut tail in permutations(&rest) {
148                tail.insert(0, first);
149                out.push(tail);
150            }
151        }
152        out
153    }
154
155    fn spelled(controls: &[&str], target: Option<&str>, target_first: bool) -> String {
156        let list = match controls {
157            [one] => format!("[:{one}]"),
158            many => format!("%i[{}]", many.join(" ")),
159        };
160        let entries = match (target, target_first) {
161            (Some(t), true) => format!("target: {t}, satisfies: {list}"),
162            (Some(t), false) => format!("satisfies: {list}, target: {t}"),
163            (None, _) => format!("satisfies: {list}"),
164        };
165        format!("test \"t\", {{{entries}}}\n  assert true\nend\n")
166    }
167
168    /// **Every spelling of one declaration is one tree.** For each set of
169    /// one to four controls, every order of the set, both orders of the keys
170    /// and with or without a target, the tree is the canonical spelling's,
171    /// and B0068's note is recorded exactly when the spelling is not the
172    /// canonical one. Exhaustive over 1 + 2 + 6 + 24 orders per class.
173    ///
174    /// Red run (2026-10-30): dropping the parser's sort made `%i[nist_cm_7
175    /// nist_ac_6]` lower to `(list :nist_cm_7 :nist_ac_6)`, a second tree.
176    #[test]
177    fn every_order_of_a_declaration_lowers_to_the_canonical_tree() {
178        let pool = ["nist_ac_6", "nist_cm_7", "nist_sc_7", "nist_si_10"];
179        let mut spellings = 0;
180        for size in 1..=pool.len() {
181            let set = &pool[..size];
182            for target in [None, Some("parse_token")] {
183                let canonical = spelled(set, target, false);
184                let tree = first(&canonical).to_sexp().to_string();
185                assert!(crate::spelling_notes(&canonical)
186                    .expect("parses")
187                    .is_empty());
188                for order in permutations(set) {
189                    for target_first in [false, true] {
190                        let written = spelled(&order, target, target_first);
191                        assert_eq!(first(&written).to_sexp().to_string(), tree, "{written}");
192                        let noted = crate::spelling_notes(&written)
193                            .expect("parses")
194                            .iter()
195                            .any(|n| n.spelling == crate::Spelling::TestDeclaration);
196                        assert_eq!(noted, written != canonical, "{written}");
197                        spellings += 1;
198                    }
199                }
200            }
201        }
202        assert_eq!(spellings, 2 * 2 * (1 + 2 + 6 + 24));
203    }
204
205    #[test]
206    fn a_four_item_deftest_without_a_meta_head_is_not_a_test() {
207        let form = first("deftest(\"t\", 1, 2)\n");
208        assert!(test_parts(&form).is_none());
209    }
210}