use super::assertions::render_json_assertion;
use super::*;
use crate::e2e::codegen::zig_visitors;
pub(super) struct ZigVisitorCallSymbols {
visitor_prefix: String,
visitor_create: String,
visitor_free: String,
options_from_json: String,
options_free: String,
options_set_visitor_handle: String,
function_name: String,
result_free: String,
result_to_json: String,
free_string: String,
last_error_code: String,
}
pub(super) fn resolve_zig_visitor_call_symbols(
call_config: &crate::core::config::e2e::CallConfig,
recipe: &crate::e2e::codegen::recipe::ResolvedE2eCallRecipe<'_>,
ffi_prefix: &str,
) -> ZigVisitorCallSymbols {
let c_override = call_config.overrides.get("c");
let function_name = c_override
.and_then(|override_config| override_config.function.as_ref())
.cloned()
.or_else(|| {
recipe
.override_config
.and_then(|override_config| override_config.function.as_ref())
.cloned()
})
.unwrap_or_else(|| call_config.function.clone());
let options_type_name = c_override
.and_then(|override_config| override_config.options_type.as_deref())
.or(recipe.options_type)
.unwrap_or_default()
.to_string();
let options_type_snake = options_type_name.to_snake_case();
let result_type_name = c_override
.and_then(|override_config| override_config.result_type.as_ref())
.cloned()
.or_else(|| {
recipe
.override_config
.and_then(|override_config| override_config.result_type.as_ref())
.cloned()
})
.unwrap_or_else(|| call_config.function.to_pascal_case());
let result_type_snake = result_type_name.to_snake_case();
ZigVisitorCallSymbols {
visitor_prefix: ffi_prefix.to_string(),
visitor_create: format!("{ffi_prefix}_visitor_create"),
visitor_free: format!("{ffi_prefix}_visitor_free"),
options_from_json: format!("{ffi_prefix}_{options_type_snake}_from_json"),
options_free: format!("{ffi_prefix}_{options_type_snake}_free"),
options_set_visitor_handle: format!("{ffi_prefix}_options_set_visitor"),
function_name,
result_free: format!("{ffi_prefix}_{result_type_snake}_free"),
result_to_json: format!("{ffi_prefix}_{result_type_snake}_to_json"),
free_string: format!("{ffi_prefix}_free_string"),
last_error_code: format!("{ffi_prefix}_last_error_code"),
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn emit_visitor_test_body(
out: &mut String,
fixture_id: &str,
html: &str,
options_value: Option<&serde_json::Value>,
visitor_spec: &crate::e2e::fixture::VisitorSpec,
module_name: &str,
symbols: &ZigVisitorCallSymbols,
assertions: &[Assertion],
expects_error: bool,
field_resolver: &FieldResolver,
) {
let _ = writeln!(out, " var gpa: std.heap.DebugAllocator(.{{}}) = .init;");
let _ = writeln!(out, " defer _ = gpa.deinit();");
let _ = writeln!(out, " const allocator = gpa.allocator();");
let _ = writeln!(out);
let c_prefix = symbols.visitor_prefix.to_uppercase();
let visitor_type_stem = symbols.visitor_prefix.to_pascal_case();
let c_types = zig_visitors::ZigVisitorCTypes {
context_type: format!("{c_prefix}{visitor_type_stem}Context"),
callbacks_type: format!("{c_prefix}{visitor_type_stem}VisitorCallbacks"),
};
let visitor_block = zig_visitors::build_zig_visitor(fixture_id, module_name, visitor_spec, &c_types);
out.push_str(&visitor_block);
let _ = writeln!(
out,
" const _visitor = {module_name}.c.{visitor_create}(&_callbacks);",
visitor_create = symbols.visitor_create
);
let _ = writeln!(
out,
" defer {module_name}.c.{visitor_free}(_visitor);",
visitor_free = symbols.visitor_free
);
let options_json = match options_value {
Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
None => "{}".to_string(),
};
let escaped_options = escape_zig(&options_json);
let _ = writeln!(
out,
" const _options_z = try std.heap.c_allocator.dupeZ(u8, \"{escaped_options}\");"
);
let _ = writeln!(out, " defer std.heap.c_allocator.free(_options_z);");
let _ = writeln!(
out,
" const _options = {module_name}.c.{options_from_json}(_options_z.ptr);",
options_from_json = symbols.options_from_json
);
let _ = writeln!(
out,
" defer {module_name}.c.{options_free}(_options);",
options_free = symbols.options_free
);
let _ = writeln!(
out,
" {module_name}.c.{options_set_visitor_handle}(_options, _visitor);",
options_set_visitor_handle = symbols.options_set_visitor_handle
);
let escaped_html = escape_zig(html);
let _ = writeln!(
out,
" const _html_z = try std.heap.c_allocator.dupeZ(u8, \"{escaped_html}\");"
);
let _ = writeln!(out, " defer std.heap.c_allocator.free(_html_z);");
let _ = writeln!(
out,
" const _result = {module_name}.c.{function_name}(_html_z.ptr, _options);",
function_name = symbols.function_name
);
if expects_error {
let _ = writeln!(
out,
" try testing.expect(_result == null or {module_name}.c.{last_error_code}() != 0);",
last_error_code = symbols.last_error_code
);
let _ = writeln!(
out,
" if (_result) |r| {module_name}.c.{result_free}(r);",
result_free = symbols.result_free
);
return;
}
let _ = writeln!(out, " try testing.expect(_result != null);");
let _ = writeln!(
out,
" defer {module_name}.c.{result_free}(_result.?);",
result_free = symbols.result_free
);
let _ = writeln!(
out,
" const _json_ptr = {module_name}.c.{result_to_json}(_result.?);",
result_to_json = symbols.result_to_json
);
let _ = writeln!(
out,
" defer {module_name}.c.{free_string}(_json_ptr);",
free_string = symbols.free_string
);
let _ = writeln!(out, " const _result_json = std.mem.sliceTo(_json_ptr, 0);");
let _ = writeln!(
out,
" var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, _result_json, .{{}});"
);
let _ = writeln!(out, " defer _parsed.deinit();");
let _ = writeln!(out, " const result = &_parsed.value;");
for assertion in assertions {
if assertion.assertion_type != "error" {
render_json_assertion(out, assertion, "result", field_resolver, false);
}
}
}
#[cfg(test)]
mod zig_visitor_tests {
use super::{emit_visitor_test_body, resolve_zig_visitor_call_symbols};
use crate::core::config::e2e::{CallConfig, CallOverride};
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{CallbackAction, VisitorSpec};
use std::collections::{BTreeMap, HashMap, HashSet};
#[test]
fn visitor_body_uses_configured_ffi_call_symbols() {
let c_override = CallOverride {
function: Some("abc_render_document".to_string()),
options_type: Some("RenderOptions".to_string()),
result_type: Some("RenderResult".to_string()),
..Default::default()
};
let zig_override = CallOverride {
function: Some("renderDocument".to_string()),
options_type: Some("WrapperOptions".to_string()),
result_type: Some("WrapperResult".to_string()),
..Default::default()
};
let call = CallConfig {
function: "render".to_string(),
overrides: [("c".to_string(), c_override), ("zig".to_string(), zig_override)].into(),
..Default::default()
};
let fixture = crate::e2e::fixture::Fixture {
id: "configured_symbols".to_string(),
category: None,
description: "configured symbols".to_string(),
tags: vec![],
skip: None,
env: None,
setup: Vec::new(),
call: None,
input: serde_json::json!({ "html": "<p>Hello</p>", "options": { "trim": true } }),
mock_response: None,
visitor: None,
args: vec![],
assertion_recipes: vec![],
assertions: vec![],
source: String::new(),
http: None,
};
let recipe = crate::e2e::codegen::recipe::ResolvedE2eCallRecipe::resolve("zig", &fixture, &call, &[]);
let symbols = resolve_zig_visitor_call_symbols(&call, &recipe, "abc");
let mut callbacks = BTreeMap::new();
callbacks.insert("visit_text".to_string(), CallbackAction::Continue);
let visitor_spec = VisitorSpec { callbacks };
let resolver = FieldResolver::new(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
);
let mut content = String::new();
emit_visitor_test_body(
&mut content,
"configured_symbols",
"<p>Hello</p>",
fixture.input.get("options"),
&visitor_spec,
"sample",
&symbols,
&[],
false,
&resolver,
);
assert!(content.contains("sample.c.abc_render_options_from_json"));
assert!(content.contains("sample.c.abc_options_set_visitor"));
assert!(content.contains("sample.c.abc_render_document(_html_z.ptr, _options)"));
assert!(content.contains("sample.c.abc_render_result_to_json"));
assert!(content.contains("sample.c.abc_render_result_free"));
for hardcoded in [
"htm_conversion_options_from_json",
"htm_options_set_visitor_handle",
"htm_convert",
"htm_conversion_result_to_json",
"htm_conversion_result_free",
"WrapperOptions",
"WrapperResult",
"renderDocument",
] {
assert!(
!content.contains(hardcoded),
"visitor Zig output leaked `{hardcoded}`:\n{content}"
);
}
}
}
#[cfg(test)]
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
);
}
}
}