mod assertions;
mod config;
mod helpers;
mod http;
mod json;
mod test_file;
mod test_function;
mod visitors;
use std::path::PathBuf;
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::E2eConfig;
use crate::e2e::escape::sanitize_filename;
use crate::e2e::fixture::{Fixture, FixtureGroup};
use anyhow::Result;
use self::config::{render_conftest, render_pyproject};
use self::helpers::is_skipped;
use self::test_file::render_test_file;
pub struct PythonE2eCodegen;
impl super::E2eCodegen for PythonE2eCodegen {
fn generate(
&self,
groups: &[FixtureGroup],
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
_type_defs: &[crate::core::ir::TypeDef],
_enums: &[crate::core::ir::EnumDef],
) -> Result<Vec<GeneratedFile>> {
let mut files = Vec::new();
let output_base = PathBuf::from(e2e_config.effective_output()).join("python");
let has_http_fixtures = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| f.http.is_some());
if has_http_fixtures && !e2e_config.harness.imports.is_empty() {
files.push(GeneratedFile {
path: output_base.join("app_harness.py"),
content: config::render_app_harness(e2e_config, groups),
generated_header: true,
});
}
files.push(GeneratedFile {
path: output_base.join("conftest.py"),
content: render_conftest(e2e_config, groups),
generated_header: true,
});
files.push(GeneratedFile {
path: output_base.join("tests").join("__init__.py"),
content: "\n".to_string(),
generated_header: false,
});
let python_pkg = e2e_config.resolve_package("python");
let default_pkg_name = e2e_config.call.module.replace('_', "-");
let pkg_name = python_pkg
.as_ref()
.and_then(|p| p.name.as_deref())
.unwrap_or(default_pkg_name.as_str());
let pkg_path = python_pkg
.as_ref()
.and_then(|p| p.path.as_deref())
.unwrap_or("../../packages/python");
let resolved = config.resolved_version();
let owned_version: String = python_pkg
.as_ref()
.and_then(|p| p.version.as_deref())
.map(str::to_owned)
.or_else(|| resolved.as_ref().map(|v| v.to_string()))
.unwrap_or_else(|| "0.1.0".to_string());
files.push(GeneratedFile {
path: output_base.join("pyproject.toml"),
content: render_pyproject(pkg_name, pkg_path, &owned_version, e2e_config.dep_mode),
generated_header: true,
});
for group in groups {
let fixtures: Vec<&Fixture> = group
.fixtures
.iter()
.filter(|fixture| is_python_fixture_runnable(fixture))
.collect();
if fixtures.is_empty() {
continue;
}
let filename = format!("test_{}.py", sanitize_filename(&group.category));
let content = render_test_file(&group.category, &fixtures, e2e_config, config, _type_defs);
files.push(GeneratedFile {
path: output_base.join("tests").join(filename),
content,
generated_header: true,
});
}
Ok(files)
}
fn language_name(&self) -> &'static str {
"python"
}
}
fn is_python_fixture_runnable(fixture: &Fixture) -> bool {
if is_skipped(fixture, "python") {
return false;
}
if let Some(http) = &fixture.http {
return http.expected_response.status_code != 101;
}
!fixture.assertions.is_empty()
}
pub fn emit_test_backend(
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&crate::core::ir::MethodDef],
fixture: &Fixture,
) -> super::TestBackendEmission {
use crate::codegen::defaults::language_defaults;
use crate::e2e::escape::{escape_python, sanitize_ident};
use std::fmt::Write as FmtWrite;
let stub_name = format!("_TestStub_{}", sanitize_ident(&fixture.id));
let backend_name = extract_backend_name_from_input(&fixture.input, &fixture.id);
let defaults = language_defaults("python");
let mut setup = String::new();
let _ = writeln!(setup, "class {stub_name}:");
let mut method_count = 0usize;
if trait_bridge.super_trait.is_some() {
let escaped = escape_python(&backend_name);
let _ = writeln!(setup, " def name(self):");
let _ = writeln!(setup, " return \"{escaped}\"");
method_count += 1;
let _ = writeln!(setup, " def initialize(self):");
let _ = writeln!(setup, " pass");
method_count += 1;
let _ = writeln!(setup, " def shutdown(self):");
let _ = writeln!(setup, " pass");
method_count += 1;
}
for method in methods {
if method.has_default_impl {
continue;
}
if trait_bridge.super_trait.is_some() && method.name == "name" {
continue;
}
emit_python_stub_method(&mut setup, method, &*defaults);
method_count += 1;
}
if method_count == 0 {
let _ = writeln!(setup, " pass");
}
let arg_expr = format!("{stub_name}()");
let indented_setup = indent_block(&setup, 4);
let teardown_block = trait_bridge
.unregister_fn
.as_deref()
.map(|unregister_fn| {
let escaped = escape_python(&backend_name);
format!(" {unregister_fn}(\"{escaped}\")\n")
})
.unwrap_or_default();
super::TestBackendEmission {
setup_block: indented_setup,
arg_expr,
type_imports: Vec::new(),
teardown_block,
}
}
fn indent_block(block: &str, spaces: usize) -> String {
let prefix = " ".repeat(spaces);
block
.lines()
.map(|line| {
if line.trim().is_empty() {
line.to_string()
} else {
format!("{prefix}{line}")
}
})
.collect::<Vec<_>>()
.join("\n")
+ if block.ends_with('\n') { "\n" } else { "" }
}
fn emit_python_stub_method(
out: &mut String,
method: &crate::core::ir::MethodDef,
defaults: &dyn crate::codegen::defaults::LanguageDefaults,
) {
use std::fmt::Write as FmtWrite;
let mut param_parts = vec!["self".to_string()];
for (i, _) in method.params.iter().enumerate() {
param_parts.push(format!("_p{i}"));
}
let params_str = param_parts.join(", ");
let default_val = match &method.return_type {
crate::core::ir::TypeRef::Named(_) => "{}".to_string(),
crate::core::ir::TypeRef::Primitive(crate::core::ir::PrimitiveType::Bool) => "False".to_string(),
crate::core::ir::TypeRef::Primitive(crate::core::ir::PrimitiveType::F32) => "0.0".to_string(),
crate::core::ir::TypeRef::Primitive(crate::core::ir::PrimitiveType::F64) => "0.0".to_string(),
crate::core::ir::TypeRef::Primitive(_) => "1".to_string(), other => defaults.emit_default(other),
};
let async_kw = if method.is_async { "async " } else { "" };
let _ = writeln!(out, " {async_kw}def {name}({params_str}):", name = method.name);
let _ = writeln!(out, " return {default_val}");
}
fn extract_backend_name_from_input(input: &serde_json::Value, fallback: &str) -> String {
if let Some(obj) = input.as_object() {
if let Some(s) = obj.get("name").and_then(|v| v.as_str()) {
return s.to_string();
}
for v in obj.values() {
if let Some(inner) = v.as_object() {
if let Some(s) = inner.get("name").and_then(|v| v.as_str()) {
return s.to_string();
}
}
}
for v in obj.values() {
if let Some(s) = v.as_str() {
return s.to_string();
}
}
}
fallback.to_string()
}
#[cfg(test)]
fn test_method(
name: &str,
return_type: crate::core::ir::TypeRef,
is_async: bool,
has_default_impl: bool,
) -> crate::core::ir::MethodDef {
crate::core::ir::MethodDef {
name: name.to_string(),
params: Vec::new(),
return_type,
is_async,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(crate::core::ir::ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl,
binding_excluded: false,
binding_exclusion_reason: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::e2e::codegen::E2eCodegen;
fn make_fixture(id: &str, input: serde_json::Value) -> crate::e2e::fixture::Fixture {
serde_json::from_value(serde_json::json!({
"id": id,
"description": "test fixture",
"input": input,
"assertions": []
}))
.expect("minimal fixture JSON must parse")
}
#[test]
fn language_name_is_python() {
let codegen = PythonE2eCodegen;
assert_eq!(codegen.language_name(), "python");
}
#[test]
fn generate_empty_groups_produces_config_files_only() {
use crate::core::config::NewAlefConfig;
let cfg: NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["python"]
[[crates]]
name = "my-lib"
sources = ["src/lib.rs"]
[crates.e2e]
fixtures = "fixtures"
output = "e2e"
[crates.e2e.call]
function = "process"
module = "my-lib"
result_var = "result"
"#,
)
.unwrap();
let e2e = cfg.crates[0].e2e.clone().unwrap();
let resolved = cfg.resolve().unwrap().remove(0);
let codegen = PythonE2eCodegen;
let files = codegen.generate(&[], &e2e, &resolved, &[], &[]).unwrap();
assert_eq!(files.len(), 3, "expected 3 config files, got: {}", files.len());
let paths: Vec<_> = files.iter().map(|f| f.path.to_string_lossy().to_string()).collect();
assert!(paths.iter().any(|p| p.ends_with("conftest.py")));
assert!(paths.iter().any(|p| p.ends_with("pyproject.toml")));
assert!(
paths.iter().any(|p| p.ends_with("tests/__init__.py")),
"tests/__init__.py must still be emitted; got: {paths:?}"
);
assert!(
!paths
.iter()
.any(|p| p.ends_with("python/__init__.py") && !p.ends_with("tests/__init__.py")),
"no root-level __init__.py in test_apps/python/ — would shadow the published wheel \
during uv sync's editable install. got: {paths:?}"
);
}
#[test]
fn emit_test_backend_python_generates_class_and_instance_expr() {
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::TypeRef;
let bridge = TraitBridgeConfig {
trait_name: "TestTrait".to_string(),
super_trait: Some("Plugin".to_string()),
..Default::default()
};
let m1 = test_method("do_work", TypeRef::String, false, false);
let m2 = test_method("async_op", TypeRef::Named("WorkResult".to_string()), true, false);
let methods = [&m1, &m2];
let fixture = make_fixture("py_test_fixture", serde_json::json!({ "name": "my-python-backend" }));
let emission = emit_test_backend(&bridge, &methods, &fixture);
assert!(
emission.setup_block.contains("class _TestStub_py_test_fixture"),
"setup_block should define the stub class, got: {}",
emission.setup_block
);
assert!(
!emission.setup_block.contains("OcrBackend"),
"setup_block must not hardcode OcrBackend"
);
assert!(
!emission.setup_block.contains("DocumentExtractor"),
"setup_block must not hardcode DocumentExtractor"
);
assert!(
emission.setup_block.contains("def name("),
"setup_block should emit name() when super_trait is set"
);
assert!(
emission.setup_block.contains("my-python-backend"),
"name() should return the backend name from input"
);
assert!(
emission.setup_block.contains("def do_work("),
"required method do_work should be emitted"
);
assert!(
emission.setup_block.contains("async def async_op("),
"required async method should be emitted"
);
assert_eq!(
emission.arg_expr, "_TestStub_py_test_fixture()",
"arg_expr should be a plain constructor call"
);
assert!(
emission.setup_block.starts_with(" class "),
"setup_block should be 4-space indented, got: {}",
emission.setup_block
);
assert!(
emission.setup_block.contains("return {}"),
"Named return type should emit {{}} not a constructor call, got: {}",
emission.setup_block
);
assert!(
!emission.setup_block.contains("WorkResult()"),
"Named return type must not emit a constructor call, got: {}",
emission.setup_block
);
}
#[test]
fn emit_test_backend_python_skips_default_impl_methods() {
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::TypeRef;
let bridge = TraitBridgeConfig {
trait_name: "TestTrait".to_string(),
..Default::default()
};
let required = test_method("must_implement", TypeRef::String, false, false);
let optional = test_method("may_implement", TypeRef::String, false, true);
let methods = [&required, &optional];
let fixture = make_fixture("py_skip_defaults", serde_json::json!({}));
let emission = emit_test_backend(&bridge, &methods, &fixture);
assert!(
emission.setup_block.contains("def must_implement("),
"required method should be emitted"
);
assert!(
!emission.setup_block.contains("def may_implement("),
"optional method should be skipped"
);
}
#[test]
fn emit_test_backend_python_includes_shutdown_with_super_trait() {
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::{PrimitiveType, TypeRef};
let bridge = TraitBridgeConfig {
trait_name: "EmbeddingBackend".to_string(),
super_trait: Some("Plugin".to_string()),
..Default::default()
};
let dimension_method = test_method("dimensions", TypeRef::Primitive(PrimitiveType::I32), false, false);
let methods = [&dimension_method];
let fixture = make_fixture(
"py_embedding_backend",
serde_json::json!({ "name": "test-embedding-backend" }),
);
let emission = emit_test_backend(&bridge, &methods, &fixture);
assert!(
emission.setup_block.contains("def name(self):"),
"name() should be emitted with super_trait"
);
assert!(
emission.setup_block.contains("def initialize(self):"),
"initialize() should be emitted with super_trait"
);
assert!(
emission.setup_block.contains("def shutdown(self):"),
"shutdown() should be emitted with super_trait, got: {}",
emission.setup_block
);
assert!(
emission.setup_block.contains("def name(self):")
&& emission.setup_block.contains("return \"test-embedding-backend\""),
"name() should return backend name"
);
assert!(
emission.setup_block.contains("def initialize(self):") && emission.setup_block.contains("pass"),
"initialize() should have pass body"
);
assert!(
emission.setup_block.contains("def shutdown(self):") && emission.setup_block.contains("pass"),
"shutdown() should have pass body"
);
}
#[test]
fn emit_test_backend_python_numeric_return_types_return_nonzero() {
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::{PrimitiveType, TypeRef};
let bridge = TraitBridgeConfig {
trait_name: "EmbeddingBackend".to_string(),
super_trait: Some("Plugin".to_string()),
..Default::default()
};
let dimensions_method = test_method("dimensions", TypeRef::Primitive(PrimitiveType::I32), false, false);
let size_method = test_method("embedding_size", TypeRef::Primitive(PrimitiveType::U64), false, false);
let float_method = test_method("similarity_score", TypeRef::Primitive(PrimitiveType::F64), false, false);
let bool_method = test_method("is_valid", TypeRef::Primitive(PrimitiveType::Bool), false, false);
let methods = [&dimensions_method, &size_method, &float_method, &bool_method];
let fixture = make_fixture(
"py_numeric_backend",
serde_json::json!({ "name": "test-numeric-backend" }),
);
let emission = emit_test_backend(&bridge, &methods, &fixture);
assert!(
emission.setup_block.contains("def dimensions(") && emission.setup_block.contains("return 1"),
"I32 should return 1, got: {}",
emission.setup_block
);
assert!(
emission.setup_block.contains("def embedding_size(") && emission.setup_block.contains("return 1"),
"U64 should return 1, got: {}",
emission.setup_block
);
assert!(
emission.setup_block.contains("def similarity_score(") && emission.setup_block.contains("return 0.0"),
"F64 should return 0.0, got: {}",
emission.setup_block
);
assert!(
emission.setup_block.contains("def is_valid(") && emission.setup_block.contains("return False"),
"Bool should return False, got: {}",
emission.setup_block
);
}
#[test]
fn emit_test_backend_python_populates_teardown_when_unregister_fn_is_set() {
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::TypeRef;
let bridge = TraitBridgeConfig {
trait_name: "TestTrait".to_string(),
super_trait: Some("Plugin".to_string()),
register_fn: Some("register_test_backend".to_string()),
unregister_fn: Some("unregister_test_backend".to_string()),
..Default::default()
};
let m = test_method("do_work", TypeRef::String, false, false);
let methods = [&m];
let fixture = make_fixture("teardown_fixture", serde_json::json!({ "name": "my-backend" }));
let emission = emit_test_backend(&bridge, &methods, &fixture);
assert!(
emission
.teardown_block
.contains("unregister_test_backend(\"my-backend\")"),
"teardown_block should call the unregister fn with the backend name, got: {:?}",
emission.teardown_block
);
}
#[test]
fn emit_test_backend_python_omits_teardown_when_unregister_fn_is_unset() {
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::TypeRef;
let bridge = TraitBridgeConfig {
trait_name: "TestTrait".to_string(),
super_trait: Some("Plugin".to_string()),
..Default::default()
};
let m = test_method("do_work", TypeRef::String, false, false);
let methods = [&m];
let fixture = make_fixture("no_teardown_fixture", serde_json::json!({ "name": "x" }));
let emission = emit_test_backend(&bridge, &methods, &fixture);
assert!(
emission.teardown_block.is_empty(),
"teardown_block should be empty when no unregister_fn is configured, got: {:?}",
emission.teardown_block
);
}
}