use super::*;
use crate::e2e::codegen::TestBackendEmission;
fn zig_type_for_stub(ty: &crate::core::ir::TypeRef, _excluded_types: &std::collections::HashSet<&str>) -> String {
use crate::core::ir::{PrimitiveType, TypeRef};
match ty {
TypeRef::Primitive(p) => match p {
PrimitiveType::Bool => "i32".to_string(),
PrimitiveType::U8 => "u8".to_string(),
PrimitiveType::U16 => "u16".to_string(),
PrimitiveType::U32 => "u32".to_string(),
PrimitiveType::U64 | PrimitiveType::Usize => "u64".to_string(),
PrimitiveType::I8 => "i8".to_string(),
PrimitiveType::I16 => "i16".to_string(),
PrimitiveType::I32 => "i32".to_string(),
PrimitiveType::I64 | PrimitiveType::Isize => "i64".to_string(),
PrimitiveType::F32 => "f32".to_string(),
PrimitiveType::F64 => "f64".to_string(),
},
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json | TypeRef::Bytes => "[*c]const u8".to_string(),
TypeRef::Unit => "void".to_string(),
TypeRef::Optional(inner) => {
match inner.as_ref() {
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json | TypeRef::Bytes => {
"?[*c]const u8".to_string()
}
_ => format!("?{}", zig_type_for_stub(inner, _excluded_types)),
}
}
TypeRef::Vec(_inner) => {
"[*c]const u8".to_string()
}
TypeRef::Map(_, _v) => "[*c]const u8".to_string(),
TypeRef::Named(_) => "[*c]const u8".to_string(),
TypeRef::Duration => "i64".to_string(),
}
}
fn zig_stub_default_value(stub_type: &str) -> String {
match stub_type {
"[*c]const u8" => "\"\"".to_string(),
"?[*c]const u8" => "null".to_string(),
"void" => "".to_string(),
"i32" | "i16" | "i8" => "0".to_string(),
"i64" => "0".to_string(),
"u8" | "u16" | "u32" | "u64" => "0".to_string(),
"f32" | "f64" => "0.0".to_string(),
_ => "undefined".to_string(),
}
}
fn method_needs_json_default(method: &crate::core::ir::MethodDef) -> bool {
if method.error_type.is_some() {
return false;
}
use crate::core::ir::TypeRef;
match &method.return_type {
TypeRef::Unit => false,
TypeRef::Primitive(_) => false,
_ => true, }
}
fn zig_json_default_for_type(return_type: &crate::core::ir::TypeRef) -> String {
use crate::core::ir::TypeRef;
match return_type {
TypeRef::Vec(_) => "\"[]\"".to_string(), TypeRef::Map(_, _) => "\"{}\"".to_string(), TypeRef::String => "\"\"".to_string(), TypeRef::Named(_) => "\"{}\"".to_string(), _ => "\"{}\"".to_string(), }
}
pub(super) fn emit_test_backend_with_excluded(
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&crate::core::ir::MethodDef],
fixture: &crate::e2e::fixture::Fixture,
excluded_types: &std::collections::HashSet<&str>,
) -> TestBackendEmission {
emit_test_backend_inner(trait_bridge, methods, fixture, excluded_types)
}
pub fn emit_test_backend(
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&crate::core::ir::MethodDef],
fixture: &crate::e2e::fixture::Fixture,
) -> TestBackendEmission {
let excluded_types = std::collections::HashSet::new();
emit_test_backend_inner(trait_bridge, methods, fixture, &excluded_types)
}
fn emit_test_backend_inner(
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&crate::core::ir::MethodDef],
fixture: &crate::e2e::fixture::Fixture,
excluded_types: &std::collections::HashSet<&str>,
) -> TestBackendEmission {
use crate::codegen::defaults::language_defaults;
use crate::core::ir::TypeRef;
let _defaults = language_defaults("zig");
let id_snake = crate::e2e::escape::sanitize_ident(&fixture.id.to_snake_case());
let struct_name = format!("TestStub_{id_snake}");
let var_name = format!("stub_{id_snake}");
let vtable_var = format!("vtable_{id_snake}");
let trait_snake = trait_bridge.trait_name.to_snake_case();
let mut setup = String::new();
let _ = writeln!(setup, "const {struct_name} = struct {{");
let _defaults = language_defaults("zig");
if let Some(super_trait) = trait_bridge.super_trait.as_deref() {
for method in methods
.iter()
.filter(|m| m.trait_source.as_deref() == Some(super_trait))
{
let method_snake = method.name.to_snake_case();
if method.name == "name" {
let _ = writeln!(
setup,
" pub fn {method_snake}() ?[*:0]const u8 {{ return \"test\"; }}"
);
} else if method.name == "version" {
let _ = writeln!(
setup,
" pub fn {method_snake}() ?[*:0]const u8 {{ return \"0.0.1\"; }}"
);
} else {
let _ = writeln!(setup, " pub fn {method_snake}(_: *@This()) !void {{}}");
}
}
}
for method in methods.iter() {
if trait_bridge
.super_trait
.as_deref()
.is_some_and(|st| method.trait_source.as_deref() == Some(st))
{
continue;
}
let method_snake = method.name.to_snake_case();
let ret_ty = zig_type_for_stub(&method.return_type, excluded_types);
let default_val = if method_needs_json_default(method) {
zig_json_default_for_type(&method.return_type)
} else {
zig_stub_default_value(&ret_ty)
};
let _ = _defaults;
let mut params = vec!["_: *@This()".to_string()];
for p in &method.params {
let p_ty = zig_type_for_stub(&p.ty, excluded_types);
params.push(format!("_: {}", p_ty)); }
let param_list = params.join(", ");
let ret_sig = if method.error_type.is_some() {
if matches!(method.return_type, TypeRef::Unit) {
"!void".to_string()
} else {
format!("!{}", ret_ty)
}
} else {
if matches!(method.return_type, TypeRef::Unit) {
"void".to_string()
} else {
ret_ty.clone()
}
};
if matches!(method.return_type, TypeRef::Unit) {
let _ = writeln!(setup, " pub fn {method_snake}({param_list}) {ret_sig} {{}}");
} else {
let _ = writeln!(
setup,
" pub fn {method_snake}({param_list}) {ret_sig} {{ return {default_val}; }}"
);
}
}
let _ = writeln!(setup, "}};");
let _ = writeln!(setup, "var {var_name} = {struct_name}{{}};");
let _ = writeln!(
setup,
"const {vtable_var} = lib.make_{trait_snake}_vtable({struct_name}, &{var_name});"
);
let out_err_var = format!("out_err_{id_snake}");
let _ = writeln!(setup, "var {out_err_var}: ?[*c]u8 = null;");
let arg_expr = format!("\"test\", {vtable_var}, &{var_name}, @ptrCast(&{out_err_var})");
TestBackendEmission {
setup_block: setup,
arg_expr,
type_imports: Vec::new(),
teardown_block: String::new(),
}
}
mod tests_trait_bridge {
#[test]
fn test_emit_test_backend_is_generic_no_domain_names() {
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::{MethodDef, ParamDef, ReceiverKind, TypeRef};
use crate::e2e::fixture::Fixture;
let method = MethodDef {
name: "do_work".to_string(),
params: vec![ParamDef {
name: "payload".to_string(),
ty: TypeRef::String,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: false,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: crate::core::ir::CoreWrapper::None,
}],
return_type: TypeRef::String,
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
};
let bridge = TraitBridgeConfig {
trait_name: "TestTrait".to_string(),
super_trait: Some("Plugin".to_string()),
register_fn: Some("register_test_trait".to_string()),
..Default::default()
};
let fixture = Fixture {
id: "my_fixture".to_string(),
category: None,
description: "test".to_string(),
tags: vec![],
skip: None,
env: None,
setup: Vec::new(),
call: None,
input: serde_json::Value::Null,
mock_response: None,
source: String::new(),
http: None,
assertions: vec![],
visitor: None,
args: vec![],
assertion_recipes: vec![],
};
let methods = vec![&method];
let emission = super::emit_test_backend(&bridge, &methods, &fixture);
assert!(
emission.setup_block.contains("do_work"),
"setup_block should contain method 'do_work', got:\n{}",
emission.setup_block
);
assert!(
emission.setup_block.contains("make_test_trait_vtable"),
"setup_block should invoke make_test_trait_vtable, got:\n{}",
emission.setup_block
);
assert!(
emission.arg_expr.contains("vtable_my_fixture"),
"arg_expr should reference vtable_my_fixture, got:\n{}",
emission.arg_expr
);
assert!(
emission.arg_expr.contains("@ptrCast"),
"arg_expr should contain @ptrCast for out_err, got:\n{}",
emission.arg_expr
);
for name in &[
"ImageBackend",
"RecordProvider",
"processImage",
"process_image_fn",
"sample_lib",
] {
assert!(
!emission.setup_block.contains(name),
"setup_block must not contain domain name '{name}', got:\n{}",
emission.setup_block
);
}
}
}