Skip to content

Commit 8a963aa

Browse files
nerdsanerita-aga
andauthored
Refactor temper-spec for guideline compliance (#84)
* Refactor temper-spec for guideline compliance * Remove unwrap from guard translation * Rename test modules for CI integrity scan --------- Co-authored-by: rita-aga <rita.mirai@gmail.com>
1 parent 9c84a6f commit 8a963aa

35 files changed

Lines changed: 4399 additions & 4186 deletions

crates/temper-spec/src/automaton/lint.rs

Lines changed: 159 additions & 382 deletions
Large diffs are not rendered by default.
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
use super::*;
2+
use crate::automaton::parse_automaton;
3+
use std::collections::BTreeMap;
4+
5+
#[test]
6+
fn lint_rejects_unknown_state_var_type() {
7+
let src = r#"
8+
[automaton]
9+
name = "Task"
10+
states = ["Draft", "Done"]
11+
initial = "Draft"
12+
13+
[[state]]
14+
name = "mystery"
15+
type = "mystery_type"
16+
initial = "0"
17+
18+
[[action]]
19+
name = "Complete"
20+
from = ["Draft"]
21+
to = "Done"
22+
"#;
23+
let automaton = parse_automaton(src).expect("parse");
24+
let findings = lint_automaton(&automaton);
25+
assert!(
26+
findings
27+
.iter()
28+
.any(|finding| finding.code == "unknown_state_var_type"
29+
&& finding.severity == LintSeverity::Error)
30+
);
31+
}
32+
33+
#[test]
34+
fn lint_rejects_unknown_guard_and_effect_variables() {
35+
let src = r#"
36+
[automaton]
37+
name = "Task"
38+
states = ["Draft", "Done"]
39+
initial = "Draft"
40+
41+
[[state]]
42+
name = "approved"
43+
type = "bool"
44+
initial = "false"
45+
46+
[[action]]
47+
name = "Complete"
48+
from = ["Draft"]
49+
to = "Done"
50+
guard = "is_true phantom"
51+
effect = "set ghost true"
52+
"#;
53+
let automaton = parse_automaton(src).expect("parse");
54+
let findings = lint_automaton(&automaton);
55+
assert!(
56+
findings
57+
.iter()
58+
.any(|finding| finding.code == "guard_unknown_var"
59+
&& finding.severity == LintSeverity::Error)
60+
);
61+
assert!(
62+
findings
63+
.iter()
64+
.any(|finding| finding.code == "effect_unknown_var"
65+
&& finding.severity == LintSeverity::Error)
66+
);
67+
}
68+
69+
#[test]
70+
fn lint_warns_for_missing_to_on_internal_action() {
71+
let src = r#"
72+
[automaton]
73+
name = "Task"
74+
states = ["Draft", "Done"]
75+
initial = "Draft"
76+
77+
[[action]]
78+
name = "Nop"
79+
kind = "internal"
80+
from = ["Draft"]
81+
"#;
82+
let automaton = parse_automaton(src).expect("parse");
83+
let findings = lint_automaton(&automaton);
84+
assert!(
85+
findings
86+
.iter()
87+
.any(|finding| finding.code == "action_missing_to"
88+
&& finding.severity == LintSeverity::Warning)
89+
);
90+
}
91+
92+
#[test]
93+
fn lint_allows_missing_to_for_output_action() {
94+
let src = r#"
95+
[automaton]
96+
name = "Task"
97+
states = ["Draft", "Done"]
98+
initial = "Draft"
99+
100+
[[action]]
101+
name = "EmitAudit"
102+
kind = "output"
103+
from = ["Draft"]
104+
effect = "emit audit"
105+
"#;
106+
let automaton = parse_automaton(src).expect("parse");
107+
let findings = lint_automaton(&automaton);
108+
assert!(
109+
!findings
110+
.iter()
111+
.any(|finding| finding.code == "action_missing_to")
112+
);
113+
}
114+
115+
fn parse(src: &str) -> Automaton {
116+
parse_automaton(src).expect("parse")
117+
}
118+
119+
#[test]
120+
fn bundle_lint_rejects_missing_spawn_target() {
121+
let parent = parse(
122+
r#"
123+
[automaton]
124+
name = "Plan"
125+
states = ["Draft"]
126+
initial = "Draft"
127+
128+
[[action]]
129+
name = "AddTask"
130+
from = ["Draft"]
131+
effect = [{ type = "spawn", entity_type = "Task", entity_id_source = "{uuid}", initial_action = "Create" }]
132+
"#,
133+
);
134+
135+
let bundle = BTreeMap::from([("Plan".to_string(), parent)]);
136+
let findings = lint_automata_bundle(&bundle);
137+
assert!(findings.iter().any(|finding| {
138+
finding.code == "spawn_target_missing"
139+
&& finding.entity == "Plan"
140+
&& finding.severity == LintSeverity::Error
141+
}));
142+
}
143+
144+
#[test]
145+
fn bundle_lint_rejects_missing_spawn_initial_action() {
146+
let parent = parse(
147+
r#"
148+
[automaton]
149+
name = "Plan"
150+
states = ["Draft"]
151+
initial = "Draft"
152+
153+
[[action]]
154+
name = "AddTask"
155+
from = ["Draft"]
156+
effect = [{ type = "spawn", entity_type = "Task", entity_id_source = "{uuid}", initial_action = "Create" }]
157+
"#,
158+
);
159+
let child = parse(
160+
r#"
161+
[automaton]
162+
name = "Task"
163+
states = ["Open", "Done"]
164+
initial = "Open"
165+
166+
[[action]]
167+
name = "Complete"
168+
from = ["Open"]
169+
to = "Done"
170+
"#,
171+
);
172+
173+
let bundle = BTreeMap::from([("Plan".to_string(), parent), ("Task".to_string(), child)]);
174+
let findings = lint_automata_bundle(&bundle);
175+
assert!(findings.iter().any(|finding| {
176+
finding.code == "spawn_initial_action_missing" && finding.entity == "Plan"
177+
}));
178+
}
179+
180+
#[test]
181+
fn bundle_lint_rejects_spawn_initial_action_not_enabled_from_initial() {
182+
let parent = parse(
183+
r#"
184+
[automaton]
185+
name = "Plan"
186+
states = ["Draft"]
187+
initial = "Draft"
188+
189+
[[action]]
190+
name = "AddTask"
191+
from = ["Draft"]
192+
effect = [{ type = "spawn", entity_type = "Task", entity_id_source = "{uuid}", initial_action = "Create" }]
193+
"#,
194+
);
195+
let child = parse(
196+
r#"
197+
[automaton]
198+
name = "Task"
199+
states = ["Open", "InProgress"]
200+
initial = "Open"
201+
202+
[[action]]
203+
name = "Create"
204+
from = ["InProgress"]
205+
"#,
206+
);
207+
208+
let bundle = BTreeMap::from([("Plan".to_string(), parent), ("Task".to_string(), child)]);
209+
let findings = lint_automata_bundle(&bundle);
210+
assert!(
211+
findings
212+
.iter()
213+
.any(|finding| { finding.code == "spawn_initial_action_not_from_initial_state" })
214+
);
215+
}
216+
217+
#[test]
218+
fn bundle_lint_rejects_unmapped_spawn_params() {
219+
let parent = parse(
220+
r#"
221+
[automaton]
222+
name = "Plan"
223+
states = ["Draft"]
224+
initial = "Draft"
225+
226+
[[action]]
227+
name = "AddTask"
228+
from = ["Draft"]
229+
params = ["title"]
230+
effect = [{ type = "spawn", entity_type = "Task", entity_id_source = "{uuid}", initial_action = "Create" }]
231+
"#,
232+
);
233+
let child = parse(
234+
r#"
235+
[automaton]
236+
name = "Task"
237+
states = ["Open"]
238+
initial = "Open"
239+
240+
[[action]]
241+
name = "Create"
242+
from = ["Open"]
243+
params = ["title", "description", "plan_id"]
244+
"#,
245+
);
246+
247+
let bundle = BTreeMap::from([("Plan".to_string(), parent), ("Task".to_string(), child)]);
248+
let findings = lint_automata_bundle(&bundle);
249+
assert!(findings.iter().any(|finding| {
250+
finding.code == "spawn_initial_action_params_unmapped"
251+
&& finding.entity == "Plan"
252+
&& finding.message.contains("description")
253+
}));
254+
}
255+
256+
#[test]
257+
fn bundle_lint_accepts_valid_spawn_contract() {
258+
let parent = parse(
259+
r#"
260+
[automaton]
261+
name = "Plan"
262+
states = ["Active"]
263+
initial = "Active"
264+
265+
[[action]]
266+
name = "AddTask"
267+
from = ["Active"]
268+
params = ["title", "description"]
269+
effect = [{ type = "spawn", entity_type = "Task", entity_id_source = "{uuid}", initial_action = "Create" }]
270+
"#,
271+
);
272+
let child = parse(
273+
r#"
274+
[automaton]
275+
name = "Task"
276+
states = ["Open"]
277+
initial = "Open"
278+
279+
[[action]]
280+
name = "Create"
281+
from = ["Open"]
282+
params = ["title", "description", "plan_id"]
283+
"#,
284+
);
285+
286+
let bundle = BTreeMap::from([("Plan".to_string(), parent), ("Task".to_string(), child)]);
287+
let findings = lint_automata_bundle(&bundle);
288+
assert!(
289+
findings.is_empty(),
290+
"expected no bundle lint findings, got: {findings:?}"
291+
);
292+
}

0 commit comments

Comments
 (0)