use std::collections::BTreeSet;
use std::fmt::Write as FmtWrite;
use heck::ToSnakeCase;
use crate::core::hash::{self, CommentStyle};
use crate::e2e::codegen::resolve_field;
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::Fixture;
use super::helpers::{
self, BytesKind, classify_bytes_value, python_method_helper_import, resolve_client_factory, resolve_enum_fields,
resolve_function_name, resolve_function_name_for_call, resolve_handle_dict_types, resolve_handle_nested_types,
resolve_module, resolve_options_type, resolve_options_via,
};
use super::http::render_http_test_function;
use super::test_function::{render_test_function, resolve_field_enum_type};
#[allow(clippy::too_many_arguments)]
pub(super) fn render_test_file(
category: &str,
fixtures: &[&Fixture],
e2e_config: &E2eConfig,
config: &crate::core::config::ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
enums: &[crate::core::ir::EnumDef],
force_bind_result: bool,
) -> String {
let module = resolve_module(e2e_config);
let function_name = resolve_function_name(e2e_config);
let options_type = resolve_options_type(e2e_config);
let options_via = resolve_options_via(e2e_config);
let effective_options_type: Option<String> = options_type.clone().or_else(|| {
fixtures.iter().find_map(|f| {
let cc = e2e_config.resolve_call_for_fixture(
f.call.as_deref(),
&f.id,
&f.resolved_category(),
&f.tags,
&f.input,
);
cc.overrides
.get("python")
.and_then(|o| o.options_type.clone())
.or_else(|| cc.options_type.clone())
})
});
let effective_options_via: &str = if options_via != "kwargs" {
options_via
} else {
fixtures
.iter()
.find_map(|f| {
let cc = e2e_config.resolve_call_for_fixture(
f.call.as_deref(),
&f.id,
&f.resolved_category(),
&f.tags,
&f.input,
);
cc.overrides.get("python").and_then(|o| o.options_via.as_deref())
})
.unwrap_or(options_via)
};
let convertible_types = helpers::core_to_binding_convertible_types(type_defs, enums);
let crate_has_serde = crate::backends::pyo3::gen_bindings::crate_has_serde(config);
let effective_options_via = helpers::effective_options_via_for_type(
effective_options_via,
effective_options_type.as_deref(),
type_defs,
&convertible_types,
crate_has_serde,
);
let enum_fields = resolve_enum_fields(e2e_config);
let handle_nested_types = resolve_handle_nested_types(e2e_config);
let handle_dict_types = resolve_handle_dict_types(e2e_config);
let has_error_test = fixtures
.iter()
.any(|f| f.assertions.iter().any(|a| a.assertion_type == "error"));
let has_http_tests = fixtures.iter().any(|f| f.is_http_test());
let global_python_async_override = e2e_config.call.overrides.get("python").and_then(|o| o.r#async);
let is_async = global_python_async_override.unwrap_or_else(|| {
fixtures.iter().any(|f| {
let cc = e2e_config.resolve_call_for_fixture(
f.call.as_deref(),
&f.id,
&f.resolved_category(),
&f.tags,
&f.input,
);
let per_fixture_override = cc.overrides.get("python").and_then(|o| o.r#async);
per_fixture_override.unwrap_or(cc.r#async)
|| crate::e2e::codegen::streaming_assertions::resolve_is_streaming(f, cc.streaming_enabled())
}) || e2e_config.call.r#async
});
let has_env_api_key = fixtures
.iter()
.any(|f| f.env.as_ref().and_then(|e| e.api_key_var.as_ref()).is_some());
let needs_pytest = has_error_test || is_async || has_env_api_key;
let has_mock_url_placeholder = fixtures.iter().any(|f| {
let cc =
e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
cc.args.iter().any(|arg| {
arg.arg_type == "json_object"
&& crate::e2e::codegen::value_contains_mock_url_placeholder(resolve_field(&f.input, &arg.field))
})
});
let needs_json_import = has_mock_url_placeholder
|| effective_options_via == "json"
&& fixtures.iter().any(|f| {
e2e_config
.call
.args
.iter()
.any(|arg| arg.arg_type == "json_object" && !resolve_field(&f.input, &arg.field).is_null())
});
let client_factory = resolve_client_factory(e2e_config);
let needs_os_import = client_factory.is_some()
|| has_mock_url_placeholder
|| e2e_config
.call
.args
.iter()
.any(|arg| arg.arg_type == "mock_url" || arg.arg_type == "mock_url_list");
let from_json_module: Option<String> = e2e_config
.call
.overrides
.get("python")
.and_then(|o| o.from_json_module.clone())
.or_else(|| {
fixtures.iter().find_map(|f| {
let cc = e2e_config.resolve_call_for_fixture(
f.call.as_deref(),
&f.id,
&f.resolved_category(),
&f.tags,
&f.input,
);
cc.overrides.get("python").and_then(|o| o.from_json_module.clone())
})
});
let needs_path_import = fixtures.iter().any(|f| {
if f.docs
.as_ref()
.and_then(|docs| docs.presentation.as_ref())
.is_some_and(|presentation| !presentation.files.is_empty())
{
return true;
}
let cc =
e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
cc.args.iter().any(|arg| {
if arg.arg_type != "bytes" {
return false;
}
let val = resolve_field(&f.input, &arg.field);
val.as_str()
.is_some_and(|s| matches!(classify_bytes_value(s), BytesKind::FilePath))
})
});
let needs_base64_import = fixtures.iter().any(|f| {
let cc =
e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
cc.args.iter().any(|arg| {
if arg.arg_type != "bytes" {
return false;
}
let val = resolve_field(&f.input, &arg.field);
val.as_str()
.is_some_and(|s| matches!(classify_bytes_value(s), BytesKind::Base64))
})
});
let _ = has_http_tests;
let needs_options_type = (effective_options_via == "kwargs" || effective_options_via == "from_json")
&& effective_options_type.is_some()
&& fixtures.iter().any(|f| {
e2e_config
.call
.args
.iter()
.any(|arg| arg.arg_type == "json_object" && !resolve_field(&f.input, &arg.field).is_null())
});
let mut used_enum_types: BTreeSet<String> = BTreeSet::new();
let mut used_config_types: BTreeSet<String> = BTreeSet::new();
for fixture in fixtures.iter() {
let cc = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let fixture_opts_type: Option<String> = cc
.overrides
.get("python")
.and_then(|o| o.options_type.clone())
.or_else(|| cc.options_type.clone())
.or_else(|| effective_options_type.clone());
for arg in &cc.args {
let value = resolve_field(&fixture.input, &arg.field);
if arg.arg_type == "json_object" && !value.is_null() {
let constructor_type =
crate::e2e::codegen::recipe::json_object_constructor_type(arg, fixture_opts_type.as_deref(), value);
if let Some(obj) = value.as_object() {
for key in obj.keys() {
if let Some(enum_type) = enum_fields.get(key) {
used_enum_types.insert(enum_type.clone());
} else if let Some(auto_enum_type) =
resolve_field_enum_type(key, constructor_type, type_defs, enums)
{
used_enum_types.insert(auto_enum_type);
}
}
}
if let Some(opts_type) = constructor_type
&& !value.is_null()
&& value.is_object()
{
used_config_types.insert(opts_type.to_string());
}
}
if arg.arg_type == "handle"
&& let Some(elem_type) = &arg.element_type
{
let is_primitive = matches!(
elem_type.as_str(),
"str"
| "int"
| "float"
| "bool"
| "bytes"
| "list"
| "dict"
| "tuple"
| "Any"
| "String"
| "&str"
| "char"
| "u8"
| "u16"
| "u32"
| "u64"
| "u128"
| "usize"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "isize"
| "f32"
| "f64"
);
if !is_primitive {
used_config_types.insert(elem_type.clone());
}
}
}
}
let mut stdlib_imports: Vec<String> = Vec::new();
let mut thirdparty_bare: Vec<String> = Vec::new();
let mut thirdparty_from: Vec<String> = Vec::new();
if needs_base64_import {
stdlib_imports.push("import base64".to_string());
}
if needs_json_import {
stdlib_imports.push("import json".to_string());
}
if needs_os_import {
stdlib_imports.push("import os".to_string());
}
if needs_path_import {
stdlib_imports.push("from pathlib import Path".to_string());
}
if needs_pytest {
thirdparty_bare.push("import pytest # noqa: F401".to_string());
}
let has_non_http_fixtures = fixtures.iter().any(|f| !f.is_http_test());
if has_non_http_fixtures {
build_thirdparty_imports(
fixtures,
e2e_config,
config,
&module,
&function_name,
client_factory.as_deref(),
&effective_options_type,
effective_options_via,
from_json_module.as_deref(),
needs_options_type,
enum_fields,
handle_nested_types,
&used_enum_types,
&used_config_types,
type_defs,
&convertible_types,
crate_has_serde,
&mut thirdparty_from,
);
}
stdlib_imports.sort();
thirdparty_bare.sort();
thirdparty_from.sort();
let mut fixtures_body = String::new();
for fixture in fixtures {
if fixture.is_http_test() {
render_http_test_function(&mut fixtures_body, fixture);
} else {
render_test_function(
&mut fixtures_body,
fixture,
e2e_config,
config,
type_defs,
enums,
effective_options_type.as_deref(),
effective_options_via,
enum_fields,
handle_nested_types,
handle_dict_types,
force_bind_result,
&convertible_types,
crate_has_serde,
);
}
let _ = writeln!(fixtures_body);
}
let mut item_texts_helper = String::new();
if references_identifier(&fixtures_body, "_alef_e2e_item_texts") {
render_item_texts_helper(&mut item_texts_helper);
}
let mut helper_functions = String::new();
if references_identifier(&fixtures_body, "_alef_e2e_text")
|| references_identifier(&item_texts_helper, "_alef_e2e_text")
{
render_text_helper(&mut helper_functions);
}
helper_functions.push_str(&item_texts_helper);
prune_unreferenced_from_imports(
&mut thirdparty_from,
&[helper_functions.as_str(), fixtures_body.as_str()],
);
let ctx = minijinja::context! {
header => hash::header(CommentStyle::Hash),
docstring => format!("E2e tests for category: {category}."),
stdlib_imports => stdlib_imports,
thirdparty_bare => thirdparty_bare,
thirdparty_from => thirdparty_from,
helper_functions => helper_functions,
fixtures_body => fixtures_body,
};
crate::e2e::template_env::render("python/test_file.jinja", ctx)
}
fn references_identifier(source: &str, name: &str) -> bool {
if name.is_empty() {
return false;
}
let is_ident_char = |c: char| c.is_alphanumeric() || c == '_';
let mut offset = 0;
while let Some(found) = source[offset..].find(name) {
let start = offset + found;
let end = start + name.len();
let before_ok = source[..start].chars().next_back().is_none_or(|c| !is_ident_char(c));
let after_ok = source[end..].chars().next().is_none_or(|c| !is_ident_char(c));
if before_ok && after_ok {
return true;
}
offset = start + name.len().max(1);
}
false
}
fn prune_unreferenced_from_imports(imports: &mut Vec<String>, emitted: &[&str]) {
let pruned: Vec<String> = imports
.iter()
.filter_map(|line| {
let Some((prefix, names)) = line.split_once(" import ") else {
return Some(line.clone());
};
let kept: Vec<&str> = names
.split(", ")
.map(str::trim)
.filter(|name| emitted.iter().any(|source| references_identifier(source, name)))
.collect();
if kept.is_empty() {
return None;
}
Some(format!("{prefix} import {}", kept.join(", ")))
})
.collect();
*imports = pruned;
}
fn render_text_helper(out: &mut String) {
let _ = writeln!(out, "def _alef_e2e_text(value: object) -> str:");
let _ = writeln!(out, " return \"\" if value is None else str(value)");
let _ = writeln!(out);
let _ = writeln!(out);
}
fn render_item_texts_helper(out: &mut String) {
let _ = writeln!(out, "def _alef_e2e_item_texts(item: object) -> tuple[str, ...]:");
let _ = writeln!(out, " raw_items = getattr(item, \"items\", None)");
let _ = writeln!(
out,
" items_text = \" \".join(str(value) for value in raw_items) if isinstance(raw_items, list) else \"\""
);
let _ = writeln!(out, " return (");
let _ = writeln!(out, " _alef_e2e_text(item),");
let _ = writeln!(out, " _alef_e2e_text(getattr(item, \"kind\", None)),");
let _ = writeln!(out, " _alef_e2e_text(getattr(item, \"name\", None)),");
let _ = writeln!(out, " _alef_e2e_text(getattr(item, \"source\", None)),");
let _ = writeln!(out, " _alef_e2e_text(getattr(item, \"alias\", None)),");
let _ = writeln!(out, " _alef_e2e_text(getattr(item, \"text\", None)),");
let _ = writeln!(out, " _alef_e2e_text(getattr(item, \"signature\", None)),");
let _ = writeln!(out, " items_text,");
let _ = writeln!(out, " )");
let _ = writeln!(out);
let _ = writeln!(out);
}
#[allow(clippy::too_many_arguments)]
fn build_thirdparty_imports(
fixtures: &[&Fixture],
e2e_config: &E2eConfig,
config: &crate::core::config::ResolvedCrateConfig,
module: &str,
function_name: &str,
client_factory: Option<&str>,
options_type: &Option<String>,
options_via: &str,
from_json_module: Option<&str>,
needs_options_type: bool,
enum_fields: &std::collections::HashMap<String, String>,
handle_nested_types: &std::collections::HashMap<String, String>,
used_enum_types: &BTreeSet<String>,
used_config_types: &BTreeSet<String>,
type_defs: &[crate::core::ir::TypeDef],
convertible_types: &ahash::AHashSet<String>,
crate_has_serde: bool,
thirdparty_from: &mut Vec<String>,
) {
let handle_constructors: Vec<String> = e2e_config
.call
.args
.iter()
.filter(|arg| arg.arg_type == "handle")
.map(|arg| format!("create_{}", arg.name.to_snake_case()))
.collect();
let mut import_names: Vec<String> = Vec::new();
if let Some(factory) = client_factory {
import_names.push(factory.to_string());
} else {
for fixture in fixtures.iter() {
let cc = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let fn_name = resolve_function_name_for_call(cc);
if !import_names.contains(&fn_name) {
import_names.push(fn_name);
}
}
if import_names.is_empty() {
import_names.push(function_name.to_string());
}
}
for ctor in &handle_constructors {
if !import_names.contains(ctor) {
import_names.push(ctor.clone());
}
}
for fixture in fixtures.iter() {
let cc = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
for arg in fixture.resolved_args(cc) {
if arg.arg_type != "test_backend" {
continue;
}
let Some(trait_name) = arg.trait_name.as_deref() else {
continue;
};
if let Some(bridge) = config.trait_bridges.iter().find(|tb| tb.trait_name == trait_name)
&& let Some(unregister_fn) = bridge.unregister_fn.as_deref()
{
let unregister_str = unregister_fn.to_string();
if !import_names.contains(&unregister_str) {
import_names.push(unregister_str);
}
}
}
}
for fixture in fixtures.iter() {
let cc = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
for arg in &cc.args {
if let Some(elem_type) = &arg.element_type {
let is_primitive = matches!(
elem_type.as_str(),
"str" | "int" | "float" | "bool" | "bytes" | "list" | "dict" | "tuple" | "Any"
| "String" | "&str" | "char"
| "u8" | "u16" | "u32" | "u64" | "u128" | "usize"
| "i8" | "i16" | "i32" | "i64" | "i128" | "isize"
| "f32" | "f64"
);
if !is_primitive && !import_names.contains(elem_type) {
import_names.push(elem_type.clone());
}
}
}
}
let needs_config_import = e2e_config.call.args.iter().any(|arg| {
arg.arg_type == "handle"
&& fixtures.iter().any(|f| {
let val = resolve_field(&f.input, &arg.field);
!val.is_null() && val.as_object().is_some_and(|o| !o.is_empty())
})
});
if needs_config_import {
let config_class = options_type.as_deref().unwrap_or_else(|| {
panic!(
"python e2e: handle arg present but no `options_type` configured on the call (set `[e2e.call] options_type = \"...\"` to the Python class name of the handle's config struct)"
)
});
if !import_names.contains(&config_class.to_string()) {
import_names.push(config_class.to_string());
}
}
if !handle_nested_types.is_empty() {
let mut used_nested_types: BTreeSet<String> = BTreeSet::new();
for fixture in fixtures.iter() {
for arg in &e2e_config.call.args {
if arg.arg_type == "handle" {
let config_value = resolve_field(&fixture.input, &arg.field);
if let Some(obj) = config_value.as_object() {
for key in obj.keys() {
if let Some(type_name) = handle_nested_types.get(key)
&& obj[key].is_object()
{
used_nested_types.insert(type_name.clone());
}
}
}
}
}
}
for type_name in used_nested_types {
if !import_names.contains(&type_name) {
import_names.push(type_name);
}
}
}
for fixture in fixtures.iter() {
for assertion in &fixture.assertions {
if assertion.assertion_type == "method_result"
&& let Some(method_name) = &assertion.method
&& let Some(name) = python_method_helper_import(method_name)
&& !import_names.contains(&name)
{
import_names.push(name);
}
}
}
for config_type in used_config_types {
if !import_names.contains(config_type) {
import_names.push(config_type.clone());
}
}
for enum_type in used_enum_types {
if !import_names.contains(enum_type) {
import_names.push(enum_type.clone());
}
}
let mut extra_from_json_imports: BTreeSet<(String, String)> = BTreeSet::new();
for fixture in fixtures.iter() {
let cc = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
if let Some(python_override) = cc.overrides.get("python")
&& python_override.options_via.as_deref() == Some("from_json")
&& let Some(options_type) = &python_override.options_type
&& helpers::effective_options_via_for_type(
"from_json",
Some(options_type.as_str()),
type_defs,
convertible_types,
crate_has_serde,
) == "from_json"
{
let native_module = python_override.from_json_module.as_deref().unwrap_or(module);
extra_from_json_imports.insert((native_module.to_string(), options_type.clone()));
}
}
let filtered_public_imports = public_import_names(&import_names, &extra_from_json_imports);
if let (true, Some(opts_type)) = (
needs_options_type && (options_via == "kwargs" || options_via == "from_json"),
options_type,
) {
if options_via == "from_json" {
let public_names: Vec<&str> = filtered_public_imports
.iter()
.copied()
.filter(|name| *name != opts_type)
.collect();
if !public_names.is_empty() {
thirdparty_from.push(format!("from {module} import {}", public_names.join(", ")));
}
let native_mod = from_json_module.unwrap_or(module);
thirdparty_from.push(format!("from {native_mod} import {opts_type}"));
} else {
if !import_names.contains(opts_type) {
import_names.push(opts_type.clone());
}
let public_names = public_import_names(&import_names, &extra_from_json_imports);
if !public_names.is_empty() {
thirdparty_from.push(format!("from {module} import {}", public_names.join(", ")));
}
}
} else if !filtered_public_imports.is_empty() {
thirdparty_from.push(format!("from {module} import {}", filtered_public_imports.join(", ")));
}
for (native_module, options_type) in extra_from_json_imports {
let imp = format!("from {native_module} import {options_type}");
if !thirdparty_from.contains(&imp) {
thirdparty_from.push(imp);
}
}
let _ = enum_fields;
}
fn public_import_names<'a>(import_names: &'a [String], native_imports: &BTreeSet<(String, String)>) -> Vec<&'a str> {
import_names
.iter()
.filter(|name| !native_imports.iter().any(|(_, native_type)| native_type == *name))
.map(String::as_str)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::e2e::escape::sanitize_filename;
use crate::e2e::fixture::FixtureGroup;
fn test_filenames(groups: &[FixtureGroup]) -> Vec<String> {
groups
.iter()
.map(|g| format!("test_{}.py", sanitize_filename(&g.category)))
.collect()
}
#[test]
fn test_filenames_produces_snake_case_names() {
let groups = vec![
FixtureGroup {
category: "MyCategory".to_string(),
fixtures: Vec::new(),
},
FixtureGroup {
category: "another-thing".to_string(),
fixtures: Vec::new(),
},
];
let names = test_filenames(&groups);
assert_eq!(names[0], "test_mycategory.py");
assert_eq!(names[1], "test_another_thing.py");
}
#[test]
fn render_test_file_no_fixtures_produces_header_only() {
let fixtures: Vec<&crate::e2e::fixture::Fixture> = Vec::new();
let e2e_config = crate::e2e::config::E2eConfig::default();
let config = crate::core::config::ResolvedCrateConfig::default();
let type_defs: Vec<crate::core::ir::TypeDef> = Vec::new();
let enums: Vec<crate::core::ir::EnumDef> = Vec::new();
let out = render_test_file("basic", &fixtures, &e2e_config, &config, &type_defs, &enums, false);
assert!(out.contains("E2e tests for category: basic"), "got: {out}");
}
#[test]
fn per_call_native_types_are_excluded_from_public_imports() {
let import_names = vec!["create_client".to_string(), "WidgetRequest".to_string()];
let native_imports = [("my_lib._internal_bindings".to_string(), "WidgetRequest".to_string())]
.into_iter()
.collect();
assert_eq!(
public_import_names(&import_names, &native_imports),
vec!["create_client"]
);
}
#[test]
fn build_thirdparty_imports_does_not_duplicate_the_from_json_type_across_modules() {
let fixtures: Vec<&crate::e2e::fixture::Fixture> = Vec::new();
let e2e_config = crate::e2e::config::E2eConfig::default();
let config = crate::core::config::ResolvedCrateConfig::default();
let options_type = Some("WidgetRequest".to_string());
let used_config_types: BTreeSet<String> = ["WidgetRequest".to_string()].into_iter().collect();
let mut thirdparty_from: Vec<String> = Vec::new();
build_thirdparty_imports(
&fixtures,
&e2e_config,
&config,
"my_lib",
"create_widget",
Some("create_client"),
&options_type,
"from_json",
Some("my_lib._internal_bindings"),
true,
&std::collections::HashMap::new(),
&std::collections::HashMap::new(),
&BTreeSet::new(),
&used_config_types,
&[],
&ahash::AHashSet::new(),
false,
&mut thirdparty_from,
);
let import_lines_with_type: Vec<&String> = thirdparty_from
.iter()
.filter(|line| line.starts_with("from ") && line.contains("WidgetRequest"))
.collect();
assert_eq!(
import_lines_with_type,
vec!["from my_lib._internal_bindings import WidgetRequest"],
"WidgetRequest must be imported from exactly one module, got: {thirdparty_from:?}"
);
assert!(
thirdparty_from.contains(&"from my_lib import create_client".to_string()),
"the client factory must still be imported from the public module, got: {thirdparty_from:?}"
);
}
fn minimal_fixture(id: &str, assertions: Vec<crate::e2e::fixture::Assertion>) -> crate::e2e::fixture::Fixture {
crate::e2e::fixture::Fixture {
docs: None,
requirements: Vec::new(),
id: id.to_string(),
description: "Smoke test".to_string(),
input: serde_json::Value::Null,
http: None,
asyncapi: None,
websocket: None,
preserve_input_urls: false,
assertions,
call: None,
skip: None,
env: None,
setup: Vec::new(),
visitor: None,
args: vec![],
assertion_recipes: vec![],
mock_response: None,
source: String::new(),
category: None,
tags: Vec::new(),
}
}
#[test]
fn render_test_file_without_array_assertions_omits_the_dead_item_text_helper() {
let fixture = minimal_fixture(
"widget_smoke",
vec![crate::e2e::fixture::Assertion {
assertion_type: "not_error".to_string(),
..Default::default()
}],
);
let fixtures: Vec<&crate::e2e::fixture::Fixture> = vec![&fixture];
let e2e_config = crate::e2e::config::E2eConfig::default();
let config = crate::core::config::ResolvedCrateConfig::default();
let type_defs: Vec<crate::core::ir::TypeDef> = Vec::new();
let enums: Vec<crate::core::ir::EnumDef> = Vec::new();
let out = render_test_file("smoke", &fixtures, &e2e_config, &config, &type_defs, &enums, false);
assert!(
!out.contains("_alef_e2e_item_texts"),
"a file with no array-contains assertion must not define the unused helper, got: {out}"
);
assert!(!out.contains("_alef_e2e_text"), "got: {out}");
}
#[test]
fn references_identifier_finds_the_item_texts_helper_call_site() {
let emitted_assertion = " assert any(\"Function\" in text for item in result.structure for text in _alef_e2e_item_texts(item)) # noqa: S101\n";
assert!(references_identifier(emitted_assertion, "_alef_e2e_item_texts"));
let emitted_without_helper = " assert result.content == \"hello\" # noqa: S101\n";
assert!(!references_identifier(emitted_without_helper, "_alef_e2e_item_texts"));
}
fn helpers_defined_in(source: &str) -> BTreeSet<String> {
source
.lines()
.filter_map(|line| line.strip_prefix("def "))
.filter(|rest| rest.starts_with("_alef_e2e_"))
.filter_map(|rest| rest.split('(').next())
.map(str::to_string)
.collect()
}
fn helpers_called_in(source: &str) -> BTreeSet<String> {
const PREFIX: &str = "_alef_e2e_";
let mut called = BTreeSet::new();
for line in source.lines().filter(|line| !line.starts_with("def ")) {
let mut rest = line;
while let Some(at) = rest.find(PREFIX) {
let tail = &rest[at..];
let name: String = tail
.chars()
.take_while(|character| character.is_alphanumeric() || *character == '_')
.collect();
if tail[name.len()..].starts_with('(') {
called.insert(name);
}
rest = &tail[PREFIX.len()..];
}
}
called
}
#[test]
fn every_generated_python_file_defines_the_helpers_it_calls() {
let enum_equals = minimal_fixture(
"enum_equals",
vec![crate::e2e::fixture::Assertion {
assertion_type: "equals".to_string(),
field: Some("structure[0].kind".to_string()),
value: Some(serde_json::json!("Function")),
..Default::default()
}],
);
let array_contains = minimal_fixture(
"array_contains",
vec![crate::e2e::fixture::Assertion {
assertion_type: "contains".to_string(),
field: Some("structure".to_string()),
value: Some(serde_json::json!("Function")),
..Default::default()
}],
);
let no_helpers = minimal_fixture(
"no_helpers",
vec![crate::e2e::fixture::Assertion {
assertion_type: "not_error".to_string(),
..Default::default()
}],
);
let mut e2e_config = crate::e2e::config::E2eConfig::default();
e2e_config.fields_array.insert("structure".to_string());
let config = crate::core::config::ResolvedCrateConfig::default();
let suite: Vec<(&str, String)> = [
("enum_equals", &enum_equals),
("array_contains", &array_contains),
("no_helpers", &no_helpers),
]
.into_iter()
.map(|(category, fixture)| {
let fixtures: Vec<&crate::e2e::fixture::Fixture> = vec![fixture];
let out = render_test_file(category, &fixtures, &e2e_config, &config, &[], &[], false);
(category, out)
})
.collect();
let enum_file = &suite[0].1;
let array_file = &suite[1].1;
assert!(
helpers_called_in(enum_file).contains("_alef_e2e_text"),
"the enum `equals` assertion must call `_alef_e2e_text`, got:\n{enum_file}"
);
assert!(
helpers_called_in(array_file).contains("_alef_e2e_item_texts"),
"the array `contains` assertion must call `_alef_e2e_item_texts`, got:\n{array_file}"
);
for (category, out) in &suite {
let defined = helpers_defined_in(out);
let called = helpers_called_in(out);
let undefined: Vec<&String> = called.difference(&defined).collect();
assert!(
undefined.is_empty(),
"test_{category}.py calls undefined helpers {undefined:?}, got:\n{out}"
);
let unused: Vec<&String> = defined.difference(&called).collect();
assert!(
unused.is_empty(),
"test_{category}.py defines unused helpers {unused:?}, got:\n{out}"
);
}
assert_eq!(
helpers_defined_in(&suite[2].1),
BTreeSet::new(),
"a file with no helper call must define no helpers, got:\n{}",
suite[2].1
);
}
}