Skip to main content

intent_codegen/
test_harness.rs

1//! Contract test harness generator.
2//!
3//! Translates `test` blocks from IntentLang specs into executable test
4//! modules in the target language. The generated tests verify that an
5//! implementation honors the spec's contracts (requires/ensures).
6
7use intent_parser::ast;
8
9use crate::Language;
10
11/// Generate a contract test harness from a parsed intent file.
12///
13/// Returns a test module string (e.g., `#[cfg(test)] mod contract_tests { ... }`
14/// for Rust), or an empty string if the spec contains no test blocks.
15pub fn generate(file: &ast::File, lang: Language) -> String {
16    match lang {
17        Language::Rust => crate::rust_tests::generate(file),
18        _ => String::new(), // not yet supported
19    }
20}
21
22/// List the expected test function names that the harness generates.
23///
24/// Used by `intent-implement` to validate that the LLM output includes
25/// all contract tests.
26pub fn expected_test_names(file: &ast::File) -> Vec<String> {
27    file.items
28        .iter()
29        .filter_map(|i| match i {
30            ast::TopLevelItem::Test(t) => Some(format!("test_{}", slugify(&t.name))),
31            _ => None,
32        })
33        .collect()
34}
35
36/// Slugify a test name for use as a function name.
37/// "successful transfer" -> "successful_transfer"
38pub fn slugify(name: &str) -> String {
39    name.chars()
40        .map(|c| {
41            if c.is_alphanumeric() {
42                c.to_ascii_lowercase()
43            } else {
44                '_'
45            }
46        })
47        .collect::<String>()
48        .split('_')
49        .filter(|s| !s.is_empty())
50        .collect::<Vec<_>>()
51        .join("_")
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn test_slugify() {
60        assert_eq!(slugify("successful transfer"), "successful_transfer");
61        assert_eq!(
62            slugify("frozen account rejected"),
63            "frozen_account_rejected"
64        );
65        assert_eq!(slugify("  spaces  and  gaps  "), "spaces_and_gaps");
66        assert_eq!(slugify("CamelCase Name"), "camelcase_name");
67    }
68
69    #[test]
70    fn test_expected_test_names() {
71        let src = r#"module Test
72
73entity Foo { id: UUID }
74
75action Bar { x: Int }
76
77test "happy path" {
78  given { x = 42 }
79  when Bar { x: x }
80  then { x == 42 }
81}
82
83test "sad path" {
84  given { x = 0 }
85  when Bar { x: x }
86  then fails
87}
88"#;
89        let file = intent_parser::parse_file(src).expect("parse");
90        let names = expected_test_names(&file);
91        assert_eq!(names, vec!["test_happy_path", "test_sad_path"]);
92    }
93
94    #[test]
95    fn test_empty_for_no_tests() {
96        let src = "module Test\n\nentity Foo { id: UUID }\n";
97        let file = intent_parser::parse_file(src).expect("parse");
98        assert!(generate(&file, Language::Rust).is_empty());
99    }
100}