Skip to main content

alef_e2e/codegen/python/
mod.rs

1//! Python e2e test code generator.
2//!
3//! Generates `e2e/python/conftest.py` and `tests/test_{category}.py` files from
4//! JSON fixtures, driven entirely by `E2eConfig` and `CallConfig`.
5
6mod assertions;
7mod config;
8mod helpers;
9mod http;
10mod json;
11mod test_file;
12mod test_function;
13mod visitors;
14
15use std::path::PathBuf;
16
17use crate::config::E2eConfig;
18use crate::escape::sanitize_filename;
19use crate::fixture::{Fixture, FixtureGroup};
20use alef_core::backend::GeneratedFile;
21use alef_core::config::ResolvedCrateConfig;
22use anyhow::Result;
23
24use self::config::{render_conftest, render_pyproject};
25use self::helpers::is_skipped;
26use self::test_file::render_test_file;
27
28/// Python e2e test code generator.
29pub struct PythonE2eCodegen;
30
31impl super::E2eCodegen for PythonE2eCodegen {
32    fn generate(
33        &self,
34        groups: &[FixtureGroup],
35        e2e_config: &E2eConfig,
36        _config: &ResolvedCrateConfig,
37    ) -> Result<Vec<GeneratedFile>> {
38        let mut files = Vec::new();
39        let output_base = PathBuf::from(e2e_config.effective_output()).join("python");
40
41        files.push(GeneratedFile {
42            path: output_base.join("conftest.py"),
43            content: render_conftest(e2e_config, groups),
44            generated_header: true,
45        });
46
47        files.push(GeneratedFile {
48            path: output_base.join("__init__.py"),
49            content: "\n".to_string(),
50            generated_header: false,
51        });
52
53        files.push(GeneratedFile {
54            path: output_base.join("tests").join("__init__.py"),
55            content: "\n".to_string(),
56            generated_header: false,
57        });
58
59        let python_pkg = e2e_config.resolve_package("python");
60        let default_pkg_name = e2e_config.call.module.replace('_', "-");
61        let pkg_name = python_pkg
62            .as_ref()
63            .and_then(|p| p.name.as_deref())
64            .unwrap_or(default_pkg_name.as_str());
65        let pkg_path = python_pkg
66            .as_ref()
67            .and_then(|p| p.path.as_deref())
68            .unwrap_or("../../packages/python");
69        let pkg_version = python_pkg
70            .as_ref()
71            .and_then(|p| p.version.as_deref())
72            .unwrap_or("0.1.0");
73        files.push(GeneratedFile {
74            path: output_base.join("pyproject.toml"),
75            content: render_pyproject(pkg_name, pkg_path, pkg_version, e2e_config.dep_mode),
76            generated_header: true,
77        });
78
79        for group in groups {
80            let fixtures: Vec<&Fixture> = group.fixtures.iter().collect();
81            if fixtures.is_empty() {
82                continue;
83            }
84            if fixtures.iter().all(|f| is_skipped(f, "python")) {
85                continue;
86            }
87
88            let filename = format!("test_{}.py", sanitize_filename(&group.category));
89            let content = render_test_file(&group.category, &fixtures, e2e_config);
90            files.push(GeneratedFile {
91                path: output_base.join("tests").join(filename),
92                content,
93                generated_header: true,
94            });
95        }
96
97        Ok(files)
98    }
99
100    fn language_name(&self) -> &'static str {
101        "python"
102    }
103}
104
105// ---------------------------------------------------------------------------
106// Tests
107// ---------------------------------------------------------------------------
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::codegen::E2eCodegen;
113
114    #[test]
115    fn language_name_is_python() {
116        let codegen = PythonE2eCodegen;
117        assert_eq!(codegen.language_name(), "python");
118    }
119
120    #[test]
121    fn generate_empty_groups_produces_config_files_only() {
122        use alef_core::config::NewAlefConfig;
123        let cfg: NewAlefConfig = toml::from_str(
124            r#"
125[workspace]
126languages = ["python"]
127
128[[crates]]
129name = "my-lib"
130sources = ["src/lib.rs"]
131
132[crates.e2e]
133fixtures = "fixtures"
134output = "e2e"
135[crates.e2e.call]
136function = "process"
137module = "my-lib"
138result_var = "result"
139"#,
140        )
141        .unwrap();
142        let e2e = cfg.crates[0].e2e.clone().unwrap();
143        let resolved = cfg.resolve().unwrap().remove(0);
144        let codegen = PythonE2eCodegen;
145        let files = codegen.generate(&[], &e2e, &resolved).unwrap();
146        // conftest.py, __init__.py (root), tests/__init__.py, pyproject.toml
147        assert_eq!(files.len(), 4, "expected 4 config files, got: {}", files.len());
148        let paths: Vec<_> = files.iter().map(|f| f.path.to_string_lossy().to_string()).collect();
149        assert!(paths.iter().any(|p| p.ends_with("conftest.py")));
150        assert!(paths.iter().any(|p| p.ends_with("pyproject.toml")));
151    }
152}