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