alef_e2e/codegen/python/
mod.rs1mod 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
28pub struct PythonE2eCodegen;
30
31impl super::E2eCodegen for PythonE2eCodegen {
32 fn generate(
33 &self,
34 groups: &[FixtureGroup],
35 e2e_config: &E2eConfig,
36 config: &ResolvedCrateConfig,
37 _type_defs: &[alef_core::ir::TypeDef],
38 ) -> Result<Vec<GeneratedFile>> {
39 let mut files = Vec::new();
40 let output_base = PathBuf::from(e2e_config.effective_output()).join("python");
41
42 files.push(GeneratedFile {
43 path: output_base.join("conftest.py"),
44 content: render_conftest(e2e_config, groups),
45 generated_header: true,
46 });
47
48 files.push(GeneratedFile {
49 path: output_base.join("__init__.py"),
50 content: "\n".to_string(),
51 generated_header: false,
52 });
53
54 files.push(GeneratedFile {
55 path: output_base.join("tests").join("__init__.py"),
56 content: "\n".to_string(),
57 generated_header: false,
58 });
59
60 let python_pkg = e2e_config.resolve_package("python");
61 let default_pkg_name = e2e_config.call.module.replace('_', "-");
62 let pkg_name = python_pkg
63 .as_ref()
64 .and_then(|p| p.name.as_deref())
65 .unwrap_or(default_pkg_name.as_str());
66 let pkg_path = python_pkg
67 .as_ref()
68 .and_then(|p| p.path.as_deref())
69 .unwrap_or("../../packages/python");
70 let resolved = config.resolved_version();
78 let owned_version: String = python_pkg
79 .as_ref()
80 .and_then(|p| p.version.as_deref())
81 .map(str::to_owned)
82 .or_else(|| resolved.as_ref().map(|v| format!("=={v}")))
83 .unwrap_or_else(|| "==0.1.0".to_string());
84 files.push(GeneratedFile {
85 path: output_base.join("pyproject.toml"),
86 content: render_pyproject(pkg_name, pkg_path, &owned_version, e2e_config.dep_mode),
87 generated_header: true,
88 });
89
90 for group in groups {
91 let fixtures: Vec<&Fixture> = group.fixtures.iter().collect();
92 if fixtures.is_empty() {
93 continue;
94 }
95 if fixtures.iter().all(|f| is_skipped(f, "python")) {
96 continue;
97 }
98
99 let filename = format!("test_{}.py", sanitize_filename(&group.category));
100 let content = render_test_file(&group.category, &fixtures, e2e_config);
101 files.push(GeneratedFile {
102 path: output_base.join("tests").join(filename),
103 content,
104 generated_header: true,
105 });
106 }
107
108 Ok(files)
109 }
110
111 fn language_name(&self) -> &'static str {
112 "python"
113 }
114}
115
116#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::codegen::E2eCodegen;
124
125 #[test]
126 fn language_name_is_python() {
127 let codegen = PythonE2eCodegen;
128 assert_eq!(codegen.language_name(), "python");
129 }
130
131 #[test]
132 fn generate_empty_groups_produces_config_files_only() {
133 use alef_core::config::NewAlefConfig;
134 let cfg: NewAlefConfig = toml::from_str(
135 r#"
136[workspace]
137languages = ["python"]
138
139[[crates]]
140name = "my-lib"
141sources = ["src/lib.rs"]
142
143[crates.e2e]
144fixtures = "fixtures"
145output = "e2e"
146[crates.e2e.call]
147function = "process"
148module = "my-lib"
149result_var = "result"
150"#,
151 )
152 .unwrap();
153 let e2e = cfg.crates[0].e2e.clone().unwrap();
154 let resolved = cfg.resolve().unwrap().remove(0);
155 let codegen = PythonE2eCodegen;
156 let files = codegen.generate(&[], &e2e, &resolved, &[]).unwrap();
157 assert_eq!(files.len(), 4, "expected 4 config files, got: {}", files.len());
159 let paths: Vec<_> = files.iter().map(|f| f.path.to_string_lossy().to_string()).collect();
160 assert!(paths.iter().any(|p| p.ends_with("conftest.py")));
161 assert!(paths.iter().any(|p| p.ends_with("pyproject.toml")));
162 }
163}