blue_lang_syntax/
test_form.rs1use tatara_lisp::{Span, Spanned};
11
12use crate::parse::{LOWERED_MAP, LOWERED_TEST_META};
13
14#[derive(Clone, Copy, Debug)]
16pub struct TestParts<'a> {
17 pub name: &'a str,
18 pub name_span: Span,
19 pub meta: Option<&'a Spanned>,
21 pub body: &'a Spanned,
22}
23
24#[derive(Clone, Debug, Default)]
28pub struct TestMeta<'a> {
29 pub satisfies: Vec<(&'a str, Span)>,
30 pub target: Option<&'a Spanned>,
31}
32
33#[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#[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#[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#[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#[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 #[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}