use crate::core::backend::Backend;
use crate::core::config::ResolvedCrateConfig;
use crate::core::config::new_config::NewAlefConfig;
use crate::core::ir::{ApiSurface, FieldDef, FunctionDef, ParamDef, TypeDef, TypeRef};
const FUNCTION_NAME: &str = "render_widget";
fn python_config() -> ResolvedCrateConfig {
let cfg: NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["python"]
[[crates]]
name = "test-lib"
sources = ["src/lib.rs"]
[crates.python]
module_name = "_test_lib"
[crates.python.stubs]
output = "packages/python/test_lib"
"#,
)
.expect("fixture alef.toml parses");
cfg.resolve().expect("fixture alef.toml resolves").remove(0)
}
fn widget_options() -> TypeDef {
TypeDef {
name: "WidgetOptions".to_string(),
rust_path: "test_lib::WidgetOptions".to_string(),
has_serde: true,
has_default: true,
fields: vec![FieldDef {
name: "label".to_string(),
ty: TypeRef::String,
optional: true,
..Default::default()
}],
..Default::default()
}
}
fn param(name: &str, ty: TypeRef, optional: bool) -> ParamDef {
ParamDef {
name: name.to_string(),
ty,
optional,
..Default::default()
}
}
fn surface(params: Vec<ParamDef>) -> ApiSurface {
ApiSurface {
crate_name: "test-lib".to_string(),
version: "0.1.0".to_string(),
types: vec![widget_options()],
functions: vec![FunctionDef {
name: FUNCTION_NAME.to_string(),
rust_path: format!("test_lib::{FUNCTION_NAME}"),
params,
return_type: TypeRef::String,
..Default::default()
}],
..Default::default()
}
}
#[derive(Debug, PartialEq, Eq)]
struct RenderedParam {
name: String,
default: Option<String>,
}
fn rendered_params(source: &str, function_name: &str) -> Vec<RenderedParam> {
let marker = format!("\ndef {function_name}(");
let start = source
.find(&marker)
.map(|idx| idx + marker.len())
.unwrap_or_else(|| panic!("`def {function_name}(` is missing from:\n{source}"));
let mut depth = 1usize;
let mut inner = String::new();
for ch in source[start..].chars() {
match ch {
'(' | '[' => depth += 1,
')' | ']' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
inner.push(ch);
}
assert_eq!(depth, 0, "unbalanced signature parens in:\n{source}");
let mut params = Vec::new();
let mut depth = 0usize;
let mut current = String::new();
for ch in inner.chars() {
match ch {
'(' | '[' => depth += 1,
')' | ']' => depth -= 1,
',' if depth == 0 => {
push_param(&mut params, ¤t);
current.clear();
continue;
}
_ => {}
}
current.push(ch);
}
push_param(&mut params, ¤t);
params
}
fn push_param(params: &mut Vec<RenderedParam>, raw: &str) {
let trimmed = raw.trim();
if trimmed.is_empty() {
return;
}
let (name, rest) = trimmed.split_once(':').unwrap_or((trimmed, ""));
let default = rest.split_once('=').map(|(_, value)| {
value
.split_once('#')
.map_or(value, |(before, _)| before)
.trim()
.to_string()
});
params.push(RenderedParam {
name: name.trim().to_string(),
default,
});
}
fn render_facade_and_stub(api: &ApiSurface) -> (String, String) {
let backend = crate::backends::pyo3::Pyo3Backend;
let config = python_config();
let stub = backend
.generate_type_stubs(api, &config)
.expect("stub generation succeeds")
.into_iter()
.find(|file| file.path.extension().is_some_and(|ext| ext == "pyi"))
.expect("a .pyi stub is generated")
.content;
let facade = backend
.generate_public_api(api, &config)
.expect("public API generation succeeds")
.into_iter()
.find(|file| file.path.ends_with("api.py"))
.expect("api.py is generated")
.content;
(facade, stub)
}
fn assert_same_parameter_order(
params: Vec<ParamDef>,
expected_order: &[&str],
) -> (Vec<RenderedParam>, Vec<RenderedParam>) {
let api = surface(params);
let (facade, stub) = render_facade_and_stub(&api);
let facade_params = rendered_params(&facade, FUNCTION_NAME);
let stub_params = rendered_params(&stub, FUNCTION_NAME);
let facade_names: Vec<&str> = facade_params.iter().map(|p| p.name.as_str()).collect();
let stub_names: Vec<&str> = stub_params.iter().map(|p| p.name.as_str()).collect();
assert_eq!(
stub_names, expected_order,
"the .pyi stub must keep the declared parameter order:\n{stub}"
);
assert_eq!(
facade_names, expected_order,
"api.py must declare the same parameters in the same order as the .pyi stub — a reordered \
facade silently swaps every positional call:\n{facade}"
);
(facade_params, stub_params)
}
#[test]
fn facade_does_not_reorder_a_defaultable_param_behind_a_required_one() {
let (facade_params, stub_params) = assert_same_parameter_order(
vec![
param("options", TypeRef::Named("WidgetOptions".to_string()), false),
param("source", TypeRef::String, false),
],
&["options", "source"],
);
for (facade, stub) in facade_params.iter().zip(&stub_params) {
assert_eq!(
facade.default, None,
"`{}` cannot carry a default without displacing the required parameter after it",
facade.name
);
assert_eq!(stub.default, None, "`{}` is required in the native module", stub.name);
}
}
#[test]
fn facade_may_default_a_trailing_defaultable_param_without_reordering() {
let (facade_params, stub_params) = assert_same_parameter_order(
vec![
param("source", TypeRef::String, false),
param("options", TypeRef::Named("WidgetOptions".to_string()), false),
],
&["source", "options"],
);
assert_eq!(facade_params[0].default, None, "`source` is required in the facade");
assert_eq!(
facade_params[1].default.as_deref(),
Some("None"),
"a trailing Default-able param stays omissible in the facade"
);
assert_eq!(
stub_params.iter().filter(|p| p.default.is_some()).count(),
0,
"the native module requires both parameters"
);
}
#[test]
fn a_genuinely_optional_param_is_defaulted_in_both_artifacts() {
let (facade_params, stub_params) = assert_same_parameter_order(
vec![
param("source", TypeRef::Optional(Box::new(TypeRef::String)), true),
param("limit", TypeRef::Primitive(crate::core::ir::PrimitiveType::U32), false),
],
&["source", "limit"],
);
for params in [&facade_params, &stub_params] {
for rendered in params {
assert_eq!(
rendered.default.as_deref(),
Some("None"),
"`{}` must be defaulted in both artifacts, got {:?}",
rendered.name,
rendered.default
);
}
}
}
#[test]
fn a_required_param_stays_required_in_both_artifacts() {
let (facade_params, stub_params) = assert_same_parameter_order(
vec![
param("source", TypeRef::String, false),
param("limit", TypeRef::Primitive(crate::core::ir::PrimitiveType::U32), false),
],
&["source", "limit"],
);
for params in [&facade_params, &stub_params] {
for rendered in params {
assert_eq!(rendered.default, None, "`{}` must stay required", rendered.name);
}
}
}