use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::hash::{self, CommentStyle};
use crate::core::template_versions::toolchain;
use crate::e2e::config::E2eConfig;
use crate::e2e::escape::{escape_zig, sanitize_filename};
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{Assertion, Fixture, FixtureGroup};
use anyhow::Result;
use heck::{ToShoutySnakeCase, ToSnakeCase};
use std::collections::HashSet;
use std::fmt::Write as FmtWrite;
use std::path::PathBuf;
use super::E2eCodegen;
use super::client;
use super::streaming_assertions::{StreamingFieldResolver, is_streaming_virtual_field};
pub struct ZigE2eCodegen;
impl E2eCodegen for ZigE2eCodegen {
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 lang = self.language_name();
let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
let mut files = Vec::new();
let call = &e2e_config.call;
let overrides = call.overrides.get(lang);
let _module_path = overrides
.and_then(|o| o.module.as_ref())
.cloned()
.unwrap_or_else(|| call.module.clone());
let function_name = overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| call.function.clone());
let result_var = &call.result_var;
let zig_pkg = e2e_config.resolve_package("zig");
let pkg_path = zig_pkg
.as_ref()
.and_then(|p| p.path.as_ref())
.cloned()
.unwrap_or_else(|| "../../packages/zig".to_string());
let pkg_name = zig_pkg
.as_ref()
.and_then(|p| p.name.as_ref())
.cloned()
.unwrap_or_else(|| config.name.to_snake_case());
let pkg_version = zig_pkg
.as_ref()
.and_then(|p| p.version.as_ref())
.cloned()
.or_else(|| config.resolved_version())
.unwrap_or_else(|| "0.1.0".to_string());
let explicit_hash = zig_pkg.as_ref().and_then(|p| p.hash.clone());
let crate_name = &config.name;
let github_repo_owned = e2e_config
.registry
.github_repo
.clone()
.unwrap_or_else(|| config.github_repo());
let github_repo = github_repo_owned.trim_end_matches('/');
let pkg_hash = if e2e_config.dep_mode == crate::e2e::config::DependencyMode::Registry {
let url = format!("{github_repo}/releases/download/v{pkg_version}/{crate_name}-zig-v{pkg_version}.tar.gz");
resolve_zig_hash(explicit_hash.as_deref(), &url)
} else {
explicit_hash
};
files.push(GeneratedFile {
path: output_base.join("build.zig.zon"),
content: render_build_zig_zon(
&pkg_name,
&pkg_path,
e2e_config.dep_mode,
&pkg_version,
pkg_hash.as_deref(),
crate_name,
github_repo,
),
generated_header: false,
});
let module_name = config.zig_module_name();
let ffi_prefix = config.ffi_prefix();
let has_file_fixtures = groups.iter().flat_map(|g| g.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(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
});
let needs_mock_server = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
if f.needs_mock_server() {
return true;
}
let cc = e2e_config.resolve_call_for_fixture(
f.call.as_deref(),
&f.id,
&f.resolved_category(),
&f.tags,
&f.input,
);
if cc
.args
.iter()
.any(|a| a.arg_type == "mock_url" || a.arg_type == "mock_url_list")
{
return true;
}
cc.overrides
.get("zig")
.or_else(|| e2e_config.call.overrides.get("zig"))
.and_then(|o| o.client_factory.as_deref())
.is_some()
});
let zig_languages = config.zig.as_ref().and_then(|z| {
if z.languages.is_empty() {
None
} else {
Some(z.languages.clone())
}
});
let mut test_filenames: Vec<String> = Vec::new();
for group in groups {
let active: Vec<&Fixture> = group
.fixtures
.iter()
.filter(|f| super::should_include_fixture(f, lang, e2e_config))
.filter(|f| {
if let Some(ref zig_langs) = zig_languages {
let fix_lang = f.input.get("language").and_then(|v| v.as_str()).or_else(|| {
f.input
.get("config")
.and_then(|c| c.get("language"))
.and_then(|v| v.as_str())
});
if let Some(fix_lang) = fix_lang
&& !zig_langs.iter().any(|l| l == fix_lang)
{
return false;
}
}
true
})
.filter(|f| {
let cc = e2e_config.resolve_call_for_fixture(
f.call.as_deref(),
&f.id,
&f.resolved_category(),
&f.tags,
&f.input,
);
cc.streaming != Some(true)
})
.collect();
if active.is_empty() {
continue;
}
let filename = format!("{}_test.zig", sanitize_filename(&group.category));
test_filenames.push(filename.clone());
let content = render_test_file(
&group.category,
&active,
e2e_config,
&function_name,
result_var,
&e2e_config.call.args,
&module_name,
&ffi_prefix,
config,
type_defs,
);
files.push(GeneratedFile {
path: output_base.join("src").join(filename),
content,
generated_header: true,
});
}
files.insert(
files
.iter()
.position(|f| f.path.file_name().is_some_and(|n| n == "build.zig.zon"))
.unwrap_or(1),
GeneratedFile {
path: output_base.join("build.zig"),
content: render_build_zig(
&test_filenames,
&pkg_name,
&module_name,
&config.ffi_lib_name(),
&config.ffi_crate_path(),
ZigBuildFlags {
has_file_fixtures,
needs_mock_server,
},
&e2e_config.test_documents_relative_from(0),
),
generated_header: false,
},
);
Ok(files)
}
fn language_name(&self) -> &'static str {
"zig"
}
}
fn zig_hash_cache_path() -> Option<std::path::PathBuf> {
if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
if !xdg.is_empty() {
return Some(std::path::PathBuf::from(xdg).join("alef").join("zig-hashes.json"));
}
}
if let Ok(home) = std::env::var("HOME") {
if !home.is_empty() {
return Some(
std::path::PathBuf::from(home)
.join(".cache")
.join("alef")
.join("zig-hashes.json"),
);
}
}
if let Ok(local_app) = std::env::var("LOCALAPPDATA") {
if !local_app.is_empty() {
return Some(std::path::PathBuf::from(local_app).join("alef").join("zig-hashes.json"));
}
}
None
}
fn read_zig_hash_cache() -> std::collections::HashMap<String, String> {
let Some(path) = zig_hash_cache_path() else {
return std::collections::HashMap::new();
};
let Ok(bytes) = std::fs::read(&path) else {
return std::collections::HashMap::new();
};
serde_json::from_slice(&bytes).unwrap_or_default()
}
fn write_zig_hash_cache_entry(url: &str, hash: &str) {
let Some(path) = zig_hash_cache_path() else {
return;
};
let mut map = read_zig_hash_cache();
map.insert(url.to_string(), hash.to_string());
let Ok(json) = serde_json::to_string_pretty(&map) else {
return;
};
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).ok();
}
std::fs::write(&path, json).ok();
}
fn fetch_zig_hash_from_network(url: &str) -> Option<String> {
let tmp = tempfile::tempdir().ok()?;
let stub = r#".{
.name = .zig_hash_fetch_stub,
.version = "0.0.0",
.fingerprint = 0x0000000000000001,
.dependencies = .{},
.paths = .{"build.zig.zon"},
}
"#;
std::fs::write(tmp.path().join("build.zig.zon"), stub).ok()?;
std::fs::write(
tmp.path().join("build.zig"),
"pub fn build(b: *@import(\"std\").Build) void {\n _ = b;\n}\n",
)
.ok()?;
let output = std::process::Command::new("zig")
.arg("fetch")
.arg(url)
.current_dir(tmp.path())
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
stdout
.lines()
.map(|l| l.trim())
.find(|l| !l.is_empty())
.map(|s| s.to_string())
}
fn resolve_zig_hash(explicit: Option<&str>, url: &str) -> Option<String> {
if let Some(h) = explicit {
return Some(h.to_string());
}
let cache = read_zig_hash_cache();
if let Some(h) = cache.get(url) {
return Some(h.clone());
}
match fetch_zig_hash_from_network(url) {
Some(h) => {
write_zig_hash_cache_entry(url, &h);
Some(h)
}
None => {
tracing::warn!(
"zig hash skipped — asset {} not yet published; regen after release",
url
);
None
}
}
}
fn render_build_zig_zon(
pkg_name: &str,
pkg_path: &str,
dep_mode: crate::e2e::config::DependencyMode,
version: &str,
hash: Option<&str>,
crate_name: &str,
github_repo: &str,
) -> String {
let dep_block = match dep_mode {
crate::e2e::config::DependencyMode::Registry => {
let url = format!("{github_repo}/releases/download/v{version}/{crate_name}-zig-v{version}.tar.gz");
match hash {
Some(h) => {
format!(
r#".{{
.url = "{url}",
.hash = "{h}",
}}"#
)
}
None => {
format!(
r#".{{
.url = "{url}",
// alef: hash not pinned — run `zig fetch --save {url}` once to populate,
// then set `hash` under [crates.e2e.registry.packages.zig] in alef.toml.
.hash = "TODO",
}}"#
)
}
}
}
crate::e2e::config::DependencyMode::Local => {
format!(r#".{{ .path = "{pkg_path}" }}"#)
}
};
let min_zig = toolchain::MIN_ZIG_VERSION;
let name_bytes: &[u8] = b"e2e_zig";
let mut crc: u32 = 0xffff_ffff;
for byte in name_bytes {
crc ^= *byte as u32;
for _ in 0..8 {
let mask = (crc & 1).wrapping_neg();
crc = (crc >> 1) ^ (0xedb8_8320 & mask);
}
}
let name_crc: u32 = !crc;
let mut id: u32 = 0x811c_9dc5;
for byte in name_bytes {
id ^= *byte as u32;
id = id.wrapping_mul(0x0100_0193);
}
if id == 0 || id == 0xffff_ffff {
id = 0x1;
}
let fingerprint: u64 = ((name_crc as u64) << 32) | (id as u64);
format!(
r#".{{
.name = .e2e_zig,
.version = "0.1.0",
.fingerprint = 0x{fingerprint:016x},
.minimum_zig_version = "{min_zig}",
.dependencies = .{{
.{pkg_name} = {dep_block},
}},
.paths = .{{
"build.zig",
"build.zig.zon",
"src",
}},
}}
"#
)
}
#[derive(Debug, Clone, Copy)]
struct ZigBuildFlags {
has_file_fixtures: bool,
needs_mock_server: bool,
}
fn render_build_zig(
test_filenames: &[String],
_pkg_name: &str,
module_name: &str,
ffi_lib_name: &str,
ffi_crate_path: &str,
flags: ZigBuildFlags,
test_documents_path: &str,
) -> String {
let ZigBuildFlags {
has_file_fixtures,
needs_mock_server,
} = flags;
if test_filenames.is_empty() {
return r#"const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const test_step = b.step("test", "Run tests");
}
"#
.to_string();
}
let mut content = String::from("const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n");
content.push_str(" const target = b.standardTargetOptions(.{});\n");
content.push_str(" const optimize = b.standardOptimizeOption(.{});\n");
content.push_str(" const test_step = b.step(\"test\", \"Run tests\");\n");
let _ = writeln!(
content,
" const ffi_path = b.option([]const u8, \"ffi_path\", \"Path to directory containing lib{ffi_lib_name}\") orelse \"../../target/release\";"
);
let _ = writeln!(
content,
" const ffi_include = b.option([]const u8, \"ffi_include_path\", \"Path to directory containing FFI header\") orelse \"{ffi_crate_path}/include\";"
);
let _ = writeln!(content);
let _ = writeln!(
content,
" const {module_name}_module = b.addModule(\"{module_name}\", .{{"
);
let _ = writeln!(
content,
" .root_source_file = b.path(\"../../packages/zig/src/{module_name}.zig\"),"
);
content.push_str(" .target = target,\n");
content.push_str(" .optimize = optimize,\n");
content.push_str(" .link_libc = true,\n");
content.push_str(" });\n");
let _ = writeln!(
content,
" {module_name}_module.addLibraryPath(.{{ .cwd_relative = ffi_path }});"
);
let _ = writeln!(
content,
" {module_name}_module.addIncludePath(.{{ .cwd_relative = ffi_include }});"
);
let _ = writeln!(
content,
" {module_name}_module.linkSystemLibrary(\"{ffi_lib_name}\", .{{}});"
);
let _ = writeln!(content);
if needs_mock_server {
content.push_str(render_zig_mock_server_spawn());
let _ = writeln!(content);
}
for filename in test_filenames {
let test_name = filename.trim_end_matches("_test.zig");
content.push_str(&format!(" const {test_name}_module = b.createModule(.{{\n"));
content.push_str(&format!(" .root_source_file = b.path(\"src/{filename}\"),\n"));
content.push_str(" .target = target,\n");
content.push_str(" .optimize = optimize,\n");
// Each test module also needs libc linking because it imports the binding
// module (which references C stdlib symbols) and may directly call helpers
// like `std.c.getenv` for env-var-driven mock-server URLs.
content.push_str(" .link_libc = true,\n");
content.push_str(" });\n");
content.push_str(&format!(
" {test_name}_module.addImport(\"{module_name}\", {module_name}_module);\n"
));
content.push_str(&format!(" const {test_name}_tests = b.addTest(.{{\n"));
content.push_str(&format!(" .name = \"{test_name}_test\",\n"));
content.push_str(&format!(" .root_module = {test_name}_module,\n"));
content.push_str(" .use_llvm = true,\n");
content.push_str(" });\n");
content.push_str(&format!(
" const {test_name}_run = b.addRunArtifact({test_name}_tests);\n"
));
if has_file_fixtures {
content.push_str(&format!(
" {test_name}_run.setCwd(b.path(\"{test_documents_path}\"));\n"
));
}
if needs_mock_server {
content.push_str(" if (mock_server_url) |_url| {\n");
content.push_str(&format!(
" {test_name}_run.setEnvironmentVariable(\"MOCK_SERVER_URL\", _url);\n"
));
content.push_str(" }\n");
content.push_str(" if (mock_servers_json) |_json| {\n");
content.push_str(&format!(
" {test_name}_run.setEnvironmentVariable(\"MOCK_SERVERS\", _json);\n"
));
content.push_str(" }\n");
content.push_str(" {\n");
content.push_str(" var _it = mock_servers_map.iterator();\n");
content.push_str(" while (_it.next()) |_entry| {\n");
content.push_str(&format!(
" {test_name}_run.setEnvironmentVariable(_entry.key_ptr.*, _entry.value_ptr.*);\n"
));
content.push_str(" }\n");
content.push_str(" }\n");
}
content.push_str(&format!(" test_step.dependOn(&{test_name}_run.step);\n\n"));
}
content.push_str("}\n");
content
}
fn render_zig_mock_server_spawn() -> &'static str {
r#" const _alloc = b.allocator;
var mock_server_url: ?[]const u8 = b.graph.environ_map.get("MOCK_SERVER_URL");
var mock_servers_json: ?[]const u8 = null;
var mock_servers_map = std.StringHashMap([]const u8).init(_alloc);
if (mock_server_url == null) {
const _bin = b.pathFromRoot("../rust/target/release/mock-server");
const _fixtures = b.pathFromRoot("../../fixtures");
var _threaded = std.Io.Threaded.init(_alloc, .{});
const _io = _threaded.io();
const _spawned = std.process.spawn(_io, .{
.argv = &.{ _bin, _fixtures },
.stdin = .pipe,
.stdout = .pipe,
.stderr = .inherit,
});
if (_spawned) |_child| {
// The child is intentionally not awaited: it lives for the duration
// of the `zig build` process, which spans test execution.
const _stdout = _child.stdout.?;
var _buf: [65536]u8 = undefined;
var _file_reader = _stdout.readerStreaming(_io, &_buf);
const _r = &_file_reader.interface;
// Read startup lines: MOCK_SERVER_URL= then MOCK_SERVERS= (always
// emitted, possibly `{}`). Cap the loop so a misbehaving server
// cannot block the build indefinitely.
var _saw_url = false;
var _i: usize = 0;
while (_i < 64) : (_i += 1) {
const _line_raw = _r.takeDelimiterExclusive('\n') catch break;
const _line = std.mem.trim(u8, _line_raw, " \r\t");
if (std.mem.startsWith(u8, _line, "MOCK_SERVER_URL=")) {
mock_server_url = _alloc.dupe(u8, _line["MOCK_SERVER_URL=".len..]) catch null;
_saw_url = true;
} else if (std.mem.startsWith(u8, _line, "MOCK_SERVERS=")) {
const _json = _line["MOCK_SERVERS=".len..];
mock_servers_json = _alloc.dupe(u8, _json) catch null;
if (std.json.parseFromSlice(std.json.Value, _alloc, _json, .{})) |_parsed| {
if (_parsed.value == .object) {
var _entries = _parsed.value.object.iterator();
while (_entries.next()) |_entry| {
if (_entry.value_ptr.* == .string) {
const _key = std.fmt.allocPrint(_alloc, "MOCK_SERVER_{s}", .{_entry.key_ptr.*}) catch continue;
for (_key) |*_c| _c.* = std.ascii.toUpper(_c.*);
const _val = _alloc.dupe(u8, _entry.value_ptr.*.string) catch continue;
mock_servers_map.put(_key, _val) catch {};
}
}
}
} else |_| {}
break;
} else if (_saw_url) {
break;
}
}
} else |_| {
// Binary not built — leave mock_server_url null so tests surface a
// clear connection error rather than a build failure.
}
}
"#
}
struct ZigTestClientRenderer;
impl client::TestClientRenderer for ZigTestClientRenderer {
fn language_name(&self) -> &'static str {
"zig"
}
fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
if let Some(reason) = skip_reason {
let _ = writeln!(out, "test \"{fn_name}\" {{");
let _ = writeln!(out, " // {description}");
let _ = writeln!(out, " // skipped: {reason}");
let _ = writeln!(out, " return error.SkipZigTest;");
} else {
let _ = writeln!(out, "test \"{fn_name}\" {{");
let _ = writeln!(out, " // {description}");
}
}
fn render_test_close(&self, out: &mut String) {
let _ = writeln!(out, "}}");
}
fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
let method = ctx.method.to_uppercase();
let fixture_id = ctx.path.trim_start_matches("/fixtures/");
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, " var url_buf: [512]u8 = undefined;");
let _ = writeln!(
out,
" const url = try std.fmt.bufPrint(&url_buf, \"{{s}}/fixtures/{fixture_id}\", .{{if (std.c.getenv(\"MOCK_SERVER_URL\")) |v| std.mem.span(v) else \"http://localhost:8080\"}});"
);
if !ctx.headers.is_empty() {
let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
header_pairs.sort_by_key(|(k, _)| k.as_str());
let _ = writeln!(out, " const headers = [_]std.http.Header{{");
for (k, v) in &header_pairs {
let ek = escape_zig(k);
let ev = escape_zig(v);
let _ = writeln!(out, " .{{ .name = \"{ek}\", .value = \"{ev}\" }},");
}
let _ = writeln!(out, " }};");
}
let headers_arg = if ctx.headers.is_empty() { "&.{}" } else { "&headers" };
let has_body = ctx.body.is_some();
let method_requires_body = matches!(method.as_str(), "POST" | "PUT" | "PATCH");
let emit_payload = has_body || method_requires_body;
if let Some(body) = ctx.body {
let json_str = serde_json::to_string(body).unwrap_or_default();
let escaped = escape_zig(&json_str);
let _ = writeln!(out, " const body_bytes: []const u8 = \"{escaped}\";");
} else if emit_payload {
let _ = writeln!(out, " const body_bytes: []const u8 = \"\";");
}
let _ = writeln!(out, " var threaded = std.Io.Threaded.init(allocator, .{{}});");
let _ = writeln!(out, " defer threaded.deinit();");
let _ = writeln!(out, " const io = threaded.io();");
let _ = writeln!(
out,
" var http_client = std.http.Client{{ .allocator = allocator, .io = io }};"
);
let _ = writeln!(out, " defer http_client.deinit();");
let _ = writeln!(out, " var response_body = std.Io.Writer.Allocating.init(allocator);");
let _ = writeln!(out, " defer response_body.deinit();");
let method_zig = match method.as_str() {
"GET" => ".GET",
"POST" => ".POST",
"PUT" => ".PUT",
"DELETE" => ".DELETE",
"PATCH" => ".PATCH",
"HEAD" => ".HEAD",
"OPTIONS" => ".OPTIONS",
_ => ".GET",
};
let payload_field = if emit_payload { ", .payload = body_bytes" } else { "" };
let _ = writeln!(
out,
" const {rv} = try http_client.fetch(.{{ .location = .{{ .url = url }}, .method = {method_zig}, .extra_headers = {headers_arg}{payload_field}, .keep_alive = false, .redirect_behavior = .unhandled, .response_writer = &response_body.writer }});",
rv = ctx.response_var,
);
}
fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
let _ = writeln!(
out,
" try testing.expectEqual(@as(u10, {status}), @intFromEnum({response_var}.status));"
);
}
fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
let ename = escape_zig(&name.to_lowercase());
match expected {
"<<present>>" => {
let _ = writeln!(
out,
" // assert header '{ename}' is present (header inspection not yet implemented)"
);
}
"<<absent>>" => {
let _ = writeln!(
out,
" // assert header '{ename}' is absent (header inspection not yet implemented)"
);
}
"<<uuid>>" => {
let _ = writeln!(
out,
" // assert header '{ename}' matches UUID pattern (header inspection not yet implemented)"
);
}
exact => {
let evalue = escape_zig(exact);
let _ = writeln!(
out,
" // assert header '{ename}' == \"{evalue}\" (header inspection not yet implemented)"
);
}
}
}
fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
let escaped = match expected {
serde_json::Value::String(s) => escape_zig(s),
other => escape_zig(&serde_json::to_string(other).unwrap_or_default()),
};
let _ = writeln!(
out,
" try testing.expectEqualStrings(\"{escaped}\", response_body.written());"
);
}
fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
if let Some(obj) = expected.as_object() {
for (key, val) in obj {
let ekey = escape_zig(key);
let eval = escape_zig(&serde_json::to_string(val).unwrap_or_default());
let _ = writeln!(
out,
" // assert body contains field \"{ekey}\" = \"{eval}\" (partial JSON not yet implemented)"
);
}
}
}
fn render_assert_validation_errors(
&self,
out: &mut String,
_response_var: &str,
errors: &[crate::e2e::fixture::ValidationErrorExpectation],
) {
for ve in errors {
let loc = ve.loc.join(".");
let escaped_loc = escape_zig(&loc);
let escaped_msg = escape_zig(&ve.msg);
let _ = writeln!(
out,
" // assert validation error at \"{escaped_loc}\": \"{escaped_msg}\" (not yet implemented)"
);
}
}
}
fn render_http_test_case(out: &mut String, fixture: &Fixture) {
client::http_call::render_http_test(out, &ZigTestClientRenderer, fixture);
}
#[allow(clippy::too_many_arguments)]
fn render_test_file(
category: &str,
fixtures: &[&Fixture],
e2e_config: &E2eConfig,
function_name: &str,
result_var: &str,
args: &[crate::e2e::config::ArgMapping],
module_name: &str,
ffi_prefix: &str,
config: &crate::core::config::ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
) -> String {
let mut out = String::new();
out.push_str(&hash::header(CommentStyle::DoubleSlash));
let _ = writeln!(out, "const std = @import(\"std\");");
let _ = writeln!(out, "const testing = std.testing;");
let _ = writeln!(out, "const {module_name} = @import(\"{module_name}\");");
let _ = writeln!(out);
let _ = writeln!(
out,
"// Suppress C++ global destructor aborts that break zig's --listen=- IPC"
);
let _ = writeln!(out, "extern \"c\" fn signal(sig: i32, handler: usize) usize;");
let _ = writeln!(out, "var _abort_handler_installed: bool = false;");
let _ = writeln!(out, "fn suppress_abort() void {{");
let _ = writeln!(out, " if (!_abort_handler_installed) {{");
let _ = writeln!(out, " // SIGABRT = 6 on POSIX; SIG_IGN = 1");
let _ = writeln!(out, " _ = signal(6, 1);");
let _ = writeln!(out, " _abort_handler_installed = true;");
let _ = writeln!(out, " }}");
let _ = writeln!(out, "}}");
let _ = writeln!(out);
let _ = writeln!(out, "// E2e tests for category: {category}");
let _ = writeln!(out);
for fixture in fixtures {
if fixture.http.is_some() {
render_http_test_case(&mut out, fixture);
} else {
render_test_fn(
&mut out,
fixture,
e2e_config,
function_name,
result_var,
args,
module_name,
ffi_prefix,
config,
type_defs,
);
}
let _ = writeln!(out);
}
out
}
#[allow(clippy::too_many_arguments)]
fn render_test_fn(
out: &mut String,
fixture: &Fixture,
e2e_config: &E2eConfig,
_function_name: &str,
_result_var: &str,
_args: &[crate::e2e::config::ArgMapping],
module_name: &str,
ffi_prefix: &str,
config: &crate::core::config::ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
) {
let call_config = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let call_field_resolver = FieldResolver::new(
e2e_config.effective_fields(call_config),
e2e_config.effective_fields_optional(call_config),
e2e_config.effective_result_fields(call_config),
e2e_config.effective_fields_array(call_config),
e2e_config.effective_fields_method_calls(call_config),
);
let field_resolver = &call_field_resolver;
let enum_fields = e2e_config.effective_fields_enum(call_config);
let lang = "zig";
let call_overrides = call_config.overrides.get(lang);
let function_name = call_overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| call_config.function.clone());
let result_var = &call_config.result_var;
let args = fixture.resolved_args(call_config);
let client_factory = call_overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
e2e_config
.call
.overrides
.get(lang)
.and_then(|o| o.client_factory.as_deref())
});
let call_result_is_bytes = call_config.result_is_bytes || call_config.overrides.values().any(|o| o.result_is_bytes);
let result_is_json_struct =
!call_result_is_bytes && (call_overrides.is_some_and(|o| o.result_is_json_struct) || client_factory.is_some());
let result_is_option = call_overrides.is_some_and(|o| o.result_is_option) || call_config.result_is_option;
let call_returns_error_union = call_overrides.and_then(|o| o.returns_result) != Some(false);
let test_name = fixture.id.to_snake_case();
let description = &fixture.description;
let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
let (setup_lines, args_str, setup_needs_gpa) = build_args_and_setup(
&fixture.input,
args,
&fixture.id,
module_name,
config,
type_defs,
fixture,
);
let extra_args: Vec<String> = call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
let args_str = if extra_args.is_empty() {
args_str
} else if args_str.is_empty() {
extra_args.join(", ")
} else {
format!("{args_str}, {}", extra_args.join(", "))
};
let any_happy_emits_code = fixture
.assertions
.iter()
.any(|a| assertion_emits_code(a, field_resolver));
let any_non_error_emits_code = fixture
.assertions
.iter()
.filter(|a| a.assertion_type != "error")
.any(|a| assertion_emits_code(a, field_resolver));
let has_streaming_virtual_assertions = fixture.assertions.iter().any(|a| {
a.field
.as_ref()
.is_some_and(|f| !f.is_empty() && is_streaming_virtual_field(f))
});
let is_stream_fn = function_name.contains("stream");
let uses_streaming_virtual_path =
result_is_json_struct && has_streaming_virtual_assertions && is_stream_fn && client_factory.is_some();
let streaming_path_has_non_streaming = uses_streaming_virtual_path
&& fixture.assertions.iter().any(|a| {
!a.field
.as_ref()
.is_some_and(|f| !f.is_empty() && is_streaming_virtual_field(f))
&& !matches!(a.assertion_type.as_str(), "not_error" | "error")
&& a.field
.as_ref()
.is_some_and(|f| !f.is_empty() && field_resolver.is_valid_for_result(f))
});
let _ = writeln!(out, "test \"{test_name}\" {{");
let _ = writeln!(out, " // {description}");
let _ = writeln!(out, " suppress_abort();");
if let Some(visitor_spec) = &fixture.visitor {
let html = fixture.input.get("html").and_then(|v| v.as_str()).unwrap_or_default();
let options_value = fixture.input.get("options").cloned();
emit_visitor_test_body(
out,
&fixture.id,
html,
options_value.as_ref(),
visitor_spec,
module_name,
&fixture.assertions,
expects_error,
field_resolver,
);
let _ = writeln!(out, "}}");
let _ = writeln!(out);
return;
}
let needs_gpa = setup_needs_gpa
|| streaming_path_has_non_streaming
|| (!uses_streaming_virtual_path && result_is_json_struct && !expects_error && any_happy_emits_code)
|| (!uses_streaming_virtual_path && result_is_json_struct && expects_error && any_non_error_emits_code);
if needs_gpa {
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);
}
for line in &setup_lines {
let _ = writeln!(out, " {line}");
}
let call_prefix = if let Some(factory) = client_factory {
let fixture_id = &fixture.id;
let _ = writeln!(
out,
" const _mock_url = try std.fmt.allocPrintSentinel(std.heap.c_allocator, \"{{s}}/fixtures/{fixture_id}\", .{{if (std.c.getenv(\"MOCK_SERVER_URL\")) |v| std.mem.span(v) else \"http://localhost:8080\"}}, 0);"
);
let _ = writeln!(out, " defer std.heap.c_allocator.free(_mock_url);");
let _ = writeln!(
out,
" var _client = try {module_name}.{factory}(\"test-key\", _mock_url, null, null, null);"
);
let _ = writeln!(out, " defer _client.free();");
"_client".to_string()
} else {
module_name.to_string()
};
if expects_error {
if result_is_json_struct {
let _ = writeln!(
out,
" const _result_json = {call_prefix}.{function_name}({args_str}) catch {{"
);
} else {
let _ = writeln!(
out,
" const result = {call_prefix}.{function_name}({args_str}) catch {{"
);
}
let _ = writeln!(out, " try testing.expect(true); // Error occurred as expected");
let _ = writeln!(out, " return;");
let _ = writeln!(out, " }};");
let any_emits_code = fixture
.assertions
.iter()
.filter(|a| a.assertion_type != "error")
.any(|a| assertion_emits_code(a, field_resolver));
if result_is_json_struct && any_emits_code {
let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
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_var} = &_parsed.value;");
let _ = writeln!(out, " // Perform success assertions if any");
for assertion in &fixture.assertions {
if assertion.assertion_type != "error" {
render_json_assertion(out, assertion, result_var, field_resolver, false);
}
}
} else if result_is_json_struct {
let _ = writeln!(out, " _ = _result_json;");
} else if any_emits_code {
let _ = writeln!(out, " // Perform success assertions if any");
for assertion in &fixture.assertions {
if assertion.assertion_type != "error" {
render_assertion(
out,
assertion,
result_var,
field_resolver,
enum_fields,
result_is_option,
);
}
}
} else {
let _ = writeln!(out, " _ = result;");
}
} else if fixture.assertions.is_empty() {
if result_is_json_struct {
let _ = writeln!(
out,
" const _result_json = try {call_prefix}.{function_name}({args_str});"
);
let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
} else if call_returns_error_union {
let _ = writeln!(out, " _ = try {call_prefix}.{function_name}({args_str});");
} else {
let _ = writeln!(out, " _ = {call_prefix}.{function_name}({args_str});");
}
} else {
let any_emits_code = fixture
.assertions
.iter()
.any(|a| assertion_emits_code(a, field_resolver));
if call_result_is_bytes && client_factory.is_some() {
let _ = writeln!(
out,
" const _result_json = try {call_prefix}.{function_name}({args_str});"
);
let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
let has_bytes_assertions = fixture
.assertions
.iter()
.any(|a| matches!(a.assertion_type.as_str(), "not_empty" | "is_empty"));
if has_bytes_assertions {
for assertion in &fixture.assertions {
match assertion.assertion_type.as_str() {
"not_empty" => {
let _ = writeln!(out, " try testing.expect(_result_json.len > 0);");
}
"is_empty" => {
let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _result_json.len);");
}
"not_error" | "error" => {}
_ => {
let atype = &assertion.assertion_type;
let _ = writeln!(
out,
" // bytes result: assertion '{atype}' not implemented for zig bytes"
);
}
}
}
}
} else if result_is_json_struct {
if uses_streaming_virtual_path {
let request_from_json = format!("{ffi_prefix}_chat_completion_request_from_json");
let request_free = format!("{ffi_prefix}_chat_completion_request_free");
let stream_start = format!("{ffi_prefix}_default_client_chat_stream_start");
let stream_free = format!("{ffi_prefix}_default_client_chat_stream_free");
let client_c_type = format!("{}DefaultClient", ffi_prefix.to_shouty_snake_case());
let _ = writeln!(
out,
" const _req_z = try std.heap.c_allocator.dupeZ(u8, {args_str});"
);
let _ = writeln!(out, " defer std.heap.c_allocator.free(_req_z);");
let _ = writeln!(
out,
" const _req_handle = {module_name}.c.{request_from_json}(_req_z.ptr);"
);
let _ = writeln!(out, " defer {module_name}.c.{request_free}(_req_handle);");
let _ = writeln!(
out,
" const _stream_handle = {module_name}.c.{stream_start}(@as(*{module_name}.c.{client_c_type}, @ptrCast(_client._handle)), _req_handle);"
);
let _ = writeln!(out, " if (_stream_handle == null) return error.StreamStartFailed;");
let _ = writeln!(out, " defer {module_name}.c.{stream_free}(_stream_handle);");
let snip =
StreamingFieldResolver::collect_snippet_zig("_stream_handle", "chunks", module_name, ffi_prefix);
out.push_str(" ");
out.push_str(&snip);
out.push('\n');
if streaming_path_has_non_streaming {
let _ = writeln!(
out,
" const _result_json = if (chunks.items.len > 0) chunks.items[chunks.items.len - 1] else &[_]u8{{}};"
);
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_var} = &_parsed.value;");
}
for assertion in &fixture.assertions {
render_json_assertion(out, assertion, result_var, field_resolver, true);
}
} else {
let _ = writeln!(
out,
" const _result_json = try {call_prefix}.{function_name}({args_str});"
);
let _ = writeln!(out, " defer std.heap.c_allocator.free(_result_json);");
if any_emits_code {
let wrap_field = match function_name.as_str() {
"interact" => Some("interaction"),
_ => None,
};
let parse_json_var = if let Some(field) = wrap_field {
let _ = writeln!(
out,
" const _wrapped_json = try std.fmt.allocPrint(allocator, \"{{{{\\\"{}\\\":{{s}}}}}}\", .{{_result_json}});",
field
);
let _ = writeln!(out, " defer allocator.free(_wrapped_json);");
"_wrapped_json".to_string()
} else {
"_result_json".to_string()
};
let _ = writeln!(
out,
" var _parsed = try std.json.parseFromSlice(std.json.Value, allocator, {parse_json_var}, .{{}});"
);
let _ = writeln!(out, " defer _parsed.deinit();");
let _ = writeln!(out, " const {result_var} = &_parsed.value;");
for assertion in &fixture.assertions {
render_json_assertion(out, assertion, result_var, field_resolver, false);
}
}
}
} else if any_emits_code {
let try_kw = if call_returns_error_union { "try " } else { "" };
let _ = writeln!(
out,
" const {result_var} = {try_kw}{call_prefix}.{function_name}({args_str});"
);
for assertion in &fixture.assertions {
render_assertion(
out,
assertion,
result_var,
field_resolver,
enum_fields,
result_is_option,
);
}
} else if call_returns_error_union {
let _ = writeln!(out, " _ = try {call_prefix}.{function_name}({args_str});");
} else {
let _ = writeln!(out, " _ = {call_prefix}.{function_name}({args_str});");
}
}
let _ = writeln!(out, "}}");
}
#[allow(clippy::too_many_arguments)]
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,
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 visitor_block = super::zig_visitors::build_zig_visitor(fixture_id, module_name, visitor_spec);
out.push_str(&visitor_block);
let _ = writeln!(
out,
" const _visitor = {module_name}.c.htm_visitor_create(&_callbacks);"
);
let _ = writeln!(out, " defer {module_name}.c.htm_visitor_free(_visitor);");
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.htm_conversion_options_from_json(_options_z.ptr);"
);
let _ = writeln!(out, " defer {module_name}.c.htm_conversion_options_free(_options);");
let _ = writeln!(
out,
" {module_name}.c.htm_options_set_visitor_handle(_options, _visitor);"
);
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.htm_convert(_html_z.ptr, _options);"
);
if expects_error {
let _ = writeln!(
out,
" try testing.expect(_result == null or {module_name}.c.htm_last_error_code() != 0);"
);
let _ = writeln!(
out,
" if (_result) |r| {module_name}.c.htm_conversion_result_free(r);"
);
return;
}
let _ = writeln!(out, " try testing.expect(_result != null);");
let _ = writeln!(out, " defer {module_name}.c.htm_conversion_result_free(_result.?);");
let _ = writeln!(
out,
" const _json_ptr = {module_name}.c.htm_conversion_result_to_json(_result.?);"
);
let _ = writeln!(out, " defer {module_name}.c.htm_free_string(_json_ptr);");
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);
}
}
}
const FORMAT_METADATA_VARIANTS: &[&str] = &[
"pdf",
"docx",
"excel",
"email",
"pptx",
"archive",
"image",
"xml",
"text",
"html",
"ocr",
"csv",
"bibtex",
"citation",
"fiction_book",
"dbf",
"jats",
"epub",
"pst",
"code",
];
fn json_path_expr(result_var: &str, field_path: &str) -> String {
let segments: Vec<&str> = field_path.split('.').collect();
let mut expr = result_var.to_string();
let mut prev_seg: Option<&str> = None;
for seg in &segments {
if prev_seg == Some("format") && FORMAT_METADATA_VARIANTS.contains(seg) {
prev_seg = Some(seg);
continue;
}
if let Some(key) = seg.strip_suffix("[]") {
expr = format!("{expr}.object.get(\"{key}\").?.array.items[0]");
} else if let Some(bracket_pos) = seg.find('[') {
if let Some(end_pos) = seg.find(']') {
if end_pos > bracket_pos + 1 && end_pos == seg.len() - 1 {
let key = &seg[..bracket_pos];
let idx = &seg[bracket_pos + 1..end_pos];
if idx.chars().all(|c| c.is_ascii_digit()) {
expr = format!("{expr}.object.get(\"{key}\").?.array.items[{idx}]");
prev_seg = Some(seg);
continue;
}
expr = format!("{expr}.object.get(\"{key}\").?.object.get(\"{idx}\").?");
prev_seg = Some(seg);
continue;
}
}
expr = format!("{expr}.object.get(\"{seg}\").?");
} else {
expr = format!("{expr}.object.get(\"{seg}\").?");
}
prev_seg = Some(seg);
}
expr
}
fn emit_zig_chunks_predicate(
out: &mut String,
result_var: &str,
assertion_type: &str,
chunk_field_accessor: &str,
field_name: &str,
require_non_empty_string: bool,
) {
let _ = writeln!(out, " {{");
let _ = writeln!(out, " const _chunks_opt = {result_var}.object.get(\"chunks\");");
let _ = writeln!(out, " var _all: bool = true;");
let _ = writeln!(out, " if (_chunks_opt) |_chunks_val| {{");
let _ = writeln!(out, " if (_chunks_val == .array) {{");
let _ = writeln!(
out,
" if (_chunks_val.array.items.len == 0) _all = false;"
);
let _ = writeln!(out, " for (_chunks_val.array.items) |c| {{");
let _ = writeln!(out, " if (c != .object) {{ _all = false; break; }}");
let _ = writeln!(out, " const _v = {chunk_field_accessor};");
if require_non_empty_string {
let _ = writeln!(
out,
" if (_v == null or _v.? != .string or _v.?.string.len == 0) {{ _all = false; break; }}"
);
} else {
let _ = writeln!(
out,
" if (_v == null or _v.? == .null) {{ _all = false; break; }}"
);
}
let _ = writeln!(out, " }}");
let _ = writeln!(out, " }} else {{ _all = false; }}");
let _ = writeln!(out, " }} else {{ _all = false; }}");
match assertion_type {
"is_true" => {
let _ = writeln!(out, " try testing.expect(_all);");
}
"is_false" => {
let _ = writeln!(out, " try testing.expect(!_all);");
}
_ => {
let _ = writeln!(
out,
" // skipped: unsupported assertion type on synthetic field '{field_name}'"
);
}
}
let _ = writeln!(out, " }}");
}
fn render_json_assertion(
out: &mut String,
assertion: &Assertion,
result_var: &str,
field_resolver: &FieldResolver,
uses_streaming: bool,
) {
if let Some(f) = &assertion.field {
if uses_streaming && !f.is_empty() && is_streaming_virtual_field(f) {
if let Some(expr) = StreamingFieldResolver::accessor(f, "zig", "chunks") {
match assertion.assertion_type.as_str() {
"count_min" => {
if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
let _ = writeln!(out, " try testing.expect({expr}.len >= {n});");
}
}
"count_equals" => {
if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
let _ = writeln!(out, " try testing.expectEqual(@as(usize, {n}), {expr}.len);");
}
}
"equals" => {
if let Some(serde_json::Value::String(s)) = &assertion.value {
let escaped = escape_zig(s);
let _ = writeln!(out, " try testing.expectEqualStrings(\"{escaped}\", {expr});");
} else if let Some(v) = &assertion.value {
let zig_val = json_to_zig(v);
let _ = writeln!(out, " try testing.expectEqual({zig_val}, {expr});");
}
}
"not_empty" => {
let _ = writeln!(out, " try testing.expect({expr}.len > 0);");
}
"is_true" => {
let _ = writeln!(out, " try testing.expect({expr});");
}
"is_false" => {
let _ = writeln!(out, " try testing.expect(!{expr});");
}
_ => {
let atype = &assertion.assertion_type;
let _ = writeln!(
out,
" // streaming virtual field '{f}' assertion '{atype}' not implemented for zig"
);
}
}
}
return;
}
}
if let Some(f) = &assertion.field {
if f == "embeddings" && !field_resolver.has_explicit_field("embeddings") {
match assertion.assertion_type.as_str() {
"count_min" => {
if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
let _ = writeln!(out, " try testing.expect({result_var}.array.items.len >= {n});");
}
return;
}
"count_equals" => {
if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
let _ = writeln!(
out,
" try testing.expectEqual(@as(usize, {n}), {result_var}.array.items.len);"
);
}
return;
}
"not_empty" => {
let _ = writeln!(out, " try testing.expect({result_var}.array.items.len > 0);");
return;
}
"is_empty" => {
let _ = writeln!(
out,
" try testing.expectEqual(@as(usize, 0), {result_var}.array.items.len);"
);
return;
}
_ => {}
}
}
}
if let Some(f) = &assertion.field {
match f.as_str() {
"chunks_have_content" => {
emit_zig_chunks_predicate(
out,
result_var,
assertion.assertion_type.as_str(),
"c.object.get(\"content\")",
"chunks_have_content",
true,
);
return;
}
"chunks_have_heading_context" => {
let _ = writeln!(
out,
" // skipped: synthetic field 'chunks_have_heading_context' not derivable from JSON value alone"
);
return;
}
"first_chunk_starts_with_heading" => {
let _ = writeln!(
out,
" // skipped: synthetic field 'first_chunk_starts_with_heading' not derivable from JSON value alone"
);
return;
}
"chunks_have_embeddings" => {
emit_zig_chunks_predicate(
out,
result_var,
assertion.assertion_type.as_str(),
"c.object.get(\"embedding\")",
"chunks_have_embeddings",
false,
);
return;
}
"keywords" | "keywords_count" => {
let _ = writeln!(
out,
" // skipped: field '{f}' not available on JSON-struct ExtractionResult"
);
return;
}
_ => {}
}
}
if let Some(f) = &assertion.field {
if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
return;
}
}
if matches!(assertion.assertion_type.as_str(), "not_error" | "error") {
return;
}
let raw_field_path = assertion.field.as_deref().unwrap_or("").trim();
let field_path = if raw_field_path.is_empty() {
raw_field_path.to_string()
} else {
field_resolver.resolve(raw_field_path).to_string()
};
let field_path = field_path.trim();
let (field_path_for_expr, is_length_access) = if let Some(parent) = field_path.strip_suffix(".length") {
(parent, true)
} else {
(field_path, false)
};
let field_expr = if field_path_for_expr.is_empty() {
result_var.to_string()
} else {
json_path_expr(result_var, field_path_for_expr)
};
if field_path_for_expr == "metadata.format"
&& matches!(
assertion.assertion_type.as_str(),
"equals" | "contains" | "not_empty" | "is_empty" | "starts_with" | "ends_with"
)
{
let base = json_path_expr(result_var, field_path_for_expr);
let _ = writeln!(out, " {{");
let _ = writeln!(out, " const _fmt_obj = {base}.object;");
let _ = writeln!(out, " const _fmt_type = _fmt_obj.get(\"format_type\").?.string;");
let _ = writeln!(
out,
" const _fmt_display: []const u8 = if (std.mem.eql(u8, _fmt_type, \"image\")) _fmt_obj.get(\"format\").?.string else _fmt_type;"
);
match assertion.assertion_type.as_str() {
"equals" => {
if let Some(serde_json::Value::String(s)) = &assertion.value {
let escaped = escape_zig(s);
let _ = writeln!(
out,
" try testing.expectEqualStrings(\"{escaped}\", std.mem.trim(u8, _fmt_display, \" \\n\\r\\t\"));"
);
}
}
"contains" => {
if let Some(serde_json::Value::String(s)) = &assertion.value {
let escaped = escape_zig(s);
let _ = writeln!(
out,
" try testing.expect(std.mem.indexOf(u8, _fmt_display, \"{escaped}\") != null);"
);
}
}
"starts_with" => {
if let Some(serde_json::Value::String(s)) = &assertion.value {
let escaped = escape_zig(s);
let _ = writeln!(
out,
" try testing.expect(std.mem.startsWith(u8, _fmt_display, \"{escaped}\"));"
);
}
}
"ends_with" => {
if let Some(serde_json::Value::String(s)) = &assertion.value {
let escaped = escape_zig(s);
let _ = writeln!(
out,
" try testing.expect(std.mem.endsWith(u8, _fmt_display, \"{escaped}\"));"
);
}
}
"not_empty" => {
let _ = writeln!(out, " try testing.expect(_fmt_display.len > 0);");
}
"is_empty" => {
let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _fmt_display.len);");
}
_ => {}
}
let _ = writeln!(out, " }}");
return;
}
let zig_val = match &assertion.value {
Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_zig(s)),
_ => String::new(),
};
let is_string_val = matches!(&assertion.value, Some(serde_json::Value::String(_)));
let is_bool_val = matches!(&assertion.value, Some(serde_json::Value::Bool(_)));
let bool_val = match &assertion.value {
Some(serde_json::Value::Bool(b)) if *b => "true",
_ => "false",
};
let is_null_val = matches!(&assertion.value, Some(serde_json::Value::Null));
let n = assertion.value.as_ref().map(json_to_zig).unwrap_or_default();
let has_n = assertion.value.as_ref().is_some_and(|v| v.is_number() || v.is_u64());
let is_float_val = matches!(&assertion.value, Some(serde_json::Value::Number(n)) if !n.is_i64() && !n.is_u64());
let values_list: Vec<String> = assertion
.values
.as_deref()
.unwrap_or_default()
.iter()
.filter_map(|v| {
if let serde_json::Value::String(s) = v {
Some(format!("\"{}\"", escape_zig(s)))
} else {
None
}
})
.collect();
let rendered = crate::e2e::template_env::render(
"zig/json_assertion.jinja",
minijinja::context! {
assertion_type => assertion.assertion_type.as_str(),
field_expr => field_expr,
is_length_access => is_length_access,
zig_val => zig_val,
is_string_val => is_string_val,
is_bool_val => is_bool_val,
bool_val => bool_val,
is_null_val => is_null_val,
n => n,
has_n => has_n,
is_float_val => is_float_val,
values_list => values_list,
},
);
out.push_str(&rendered);
}
fn assertion_emits_code(assertion: &Assertion, field_resolver: &FieldResolver) -> bool {
if let Some(f) = &assertion.field {
if !f.is_empty() && is_streaming_virtual_field(f) {
} else if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
return false;
}
}
matches!(
assertion.assertion_type.as_str(),
"equals"
| "contains"
| "contains_all"
| "not_contains"
| "not_empty"
| "is_empty"
| "starts_with"
| "ends_with"
| "min_length"
| "max_length"
| "count_min"
| "count_equals"
| "is_true"
| "is_false"
| "greater_than"
| "less_than"
| "greater_than_or_equal"
| "less_than_or_equal"
| "contains_any"
)
}
fn build_args_and_setup(
input: &serde_json::Value,
args: &[crate::e2e::config::ArgMapping],
fixture_id: &str,
_module_name: &str,
config: &crate::core::config::ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
fixture: &Fixture,
) -> (Vec<String>, String, bool) {
if args.is_empty() {
return (Vec::new(), String::new(), false);
}
let mut setup_lines: Vec<String> = Vec::new();
let mut parts: Vec<String> = Vec::new();
let mut setup_needs_gpa = false;
for arg in args {
if arg.arg_type == "mock_url" {
let name = arg.name.clone();
let id_upper = fixture_id.to_uppercase();
setup_lines.push(format!(
"const {name} = if (std.c.getenv(\"MOCK_SERVER_{id_upper}\")) |_pf| try std.fmt.allocPrint(allocator, \"{{s}}\", .{{std.mem.span(_pf)}}) else try std.fmt.allocPrint(allocator, \"{{s}}/fixtures/{fixture_id}\", .{{if (std.c.getenv(\"MOCK_SERVER_URL\")) |v| std.mem.span(v) else \"http://localhost:8080\"}});"
));
setup_lines.push(format!("defer allocator.free({name});"));
parts.push(name);
setup_needs_gpa = true;
continue;
}
if arg.arg_type == "handle" {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let json_str = match input.get(field) {
Some(serde_json::Value::Null) | None => "null".to_string(),
Some(v) => format!("\"{}\"", escape_zig(&serde_json::to_string(v).unwrap_or_default())),
};
parts.push(json_str);
continue;
}
if arg.arg_type == "test_backend" {
if let Some(trait_name) = &arg.trait_name {
if let Some(trait_bridge) = config.trait_bridges.iter().find(|tb| tb.trait_name == *trait_name) {
let methods: Vec<&crate::core::ir::MethodDef> = type_defs
.iter()
.find(|t| t.name == *trait_name)
.map(|t| t.methods.iter().collect())
.unwrap_or_default();
let emission = super::emit_test_backend("zig", trait_bridge, &methods, fixture);
let setup_block = emission.setup_block.replace("lib.", &format!("{_module_name}."));
let arg_expr = emission.arg_expr.replace("lib.", &format!("{_module_name}."));
for line in setup_block.lines() {
setup_lines.push(line.to_string());
}
parts.push(arg_expr);
continue;
}
}
let emission = crate::e2e::codegen::TestBackendEmission::unimplemented("zig");
setup_lines.push(format!("// {}", emission.arg_expr));
parts.push("null".to_string());
continue;
}
if arg.name == "config" && arg.arg_type == "json_object" {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let json_str = match input.get(field) {
Some(serde_json::Value::Null) | None => "{}".to_string(),
Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
};
parts.push(format!("\"{}\"", escape_zig(&json_str)));
continue;
}
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let val = if field.is_empty() || field == "input" {
Some(input)
} else {
input.get(field)
};
match val {
None | Some(serde_json::Value::Null) if arg.optional => {
parts.push("null".to_string());
}
None | Some(serde_json::Value::Null) => {
let default_val = match arg.arg_type.as_str() {
"string" => "\"\"".to_string(),
"int" | "integer" => "0".to_string(),
"float" | "number" => "0.0".to_string(),
"bool" | "boolean" => "false".to_string(),
"json_object" => "\"{}\"".to_string(),
_ => "null".to_string(),
};
parts.push(default_val);
}
Some(v) => {
if arg.arg_type == "json_object" {
let json_str = serde_json::to_string(v).unwrap_or_default();
parts.push(format!("\"{}\"", escape_zig(&json_str)));
} else if arg.arg_type == "bytes" {
if let serde_json::Value::String(path) = v {
let var_name = format!("{}_bytes", arg.name);
let epath = escape_zig(path);
setup_lines.push(format!(
"const {var_name} = try std.Io.Dir.cwd().readFileAlloc(std.testing.io, \"{epath}\", std.heap.c_allocator, .unlimited);"
));
setup_lines.push(format!("defer std.heap.c_allocator.free({var_name});"));
parts.push(var_name);
} else {
parts.push(json_to_zig(v));
}
} else {
parts.push(json_to_zig(v));
}
}
}
}
(setup_lines, parts.join(", "), setup_needs_gpa)
}
fn render_assertion(
out: &mut String,
assertion: &Assertion,
result_var: &str,
field_resolver: &FieldResolver,
enum_fields: &HashSet<String>,
result_is_option: bool,
) {
let bare_result_is_option = result_is_option && assertion.field.as_deref().filter(|f| !f.is_empty()).is_none();
if bare_result_is_option {
match assertion.assertion_type.as_str() {
"is_empty" => {
let _ = writeln!(out, " try testing.expect({result_var} == null);");
return;
}
"not_empty" => {
let _ = writeln!(out, " try testing.expect({result_var} != null);");
return;
}
"not_error" => {
let _ = writeln!(out, " // not_error: covered by try propagation");
return;
}
"equals" => {
if let Some(expected) = &assertion.value {
let zig_val = json_to_zig(expected);
let _ = writeln!(out, " try testing.expectEqualStrings({zig_val}, {result_var}.?);");
return;
}
}
_ => {}
}
}
if let Some(f) = &assertion.field {
if f == "embeddings" && !field_resolver.has_explicit_field(f) {
match assertion.assertion_type.as_str() {
"count_min" | "count_equals" | "not_empty" | "is_empty" => {
let _ = writeln!(out, " {{");
let _ = writeln!(
out,
" var _eparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {result_var}, .{{}});"
);
let _ = writeln!(out, " defer _eparse.deinit();");
let _ = writeln!(out, " const _embeddings_len = _eparse.value.array.items.len;");
match assertion.assertion_type.as_str() {
"count_min" => {
if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
let _ = writeln!(out, " try testing.expect(_embeddings_len >= {n});");
}
}
"count_equals" => {
if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
let _ = writeln!(
out,
" try testing.expectEqual(@as(usize, {n}), _embeddings_len);"
);
}
}
"not_empty" => {
let _ = writeln!(out, " try testing.expect(_embeddings_len > 0);");
}
"is_empty" => {
let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), _embeddings_len);");
}
_ => {}
}
let _ = writeln!(out, " }}");
return;
}
_ => {}
}
}
}
if let Some(f) = &assertion.field {
if f == "result" && !field_resolver.has_explicit_field(f) {
match assertion.assertion_type.as_str() {
"contains" => {
if let Some(expected) = &assertion.value {
let zig_val = json_to_zig(expected);
let _ = writeln!(
out,
" try testing.expect(std.mem.indexOf(u8, {result_var}, {zig_val}) != null);"
);
return;
}
}
"not_contains" => {
if let Some(expected) = &assertion.value {
let zig_val = json_to_zig(expected);
let _ = writeln!(
out,
" try testing.expect(std.mem.indexOf(u8, {result_var}, {zig_val}) == null);"
);
return;
}
}
"equals" => {
if let Some(expected) = &assertion.value {
let zig_val = json_to_zig(expected);
let _ = writeln!(out, " try testing.expectEqualStrings({zig_val}, {result_var});");
return;
}
}
"not_empty" => {
let _ = writeln!(out, " try testing.expect({result_var}.len > 0);");
return;
}
"is_empty" => {
let _ = writeln!(out, " try testing.expectEqual(@as(usize, 0), {result_var}.len);");
return;
}
_ => {}
}
}
}
if let Some(f) = &assertion.field {
if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
let _ = writeln!(out, " // skipped: field '{{f}}' not available on result type");
return;
}
}
let _field_is_enum = assertion
.field
.as_deref()
.is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
let field_expr = match &assertion.field {
Some(f) if !f.is_empty() => field_resolver.accessor(f, "zig", result_var),
_ => result_var.to_string(),
};
match assertion.assertion_type.as_str() {
"equals" => {
if let Some(expected) = &assertion.value {
let zig_val = json_to_zig(expected);
let _ = writeln!(out, " try testing.expectEqual({zig_val}, {field_expr});");
}
}
"contains" => {
if let Some(expected) = &assertion.value {
let zig_val = json_to_zig(expected);
let _ = writeln!(
out,
" try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
);
}
}
"contains_all" => {
if let Some(values) = &assertion.values {
for val in values {
let zig_val = json_to_zig(val);
let _ = writeln!(
out,
" try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) != null);"
);
}
}
}
"not_contains" => {
if let Some(expected) = &assertion.value {
let zig_val = json_to_zig(expected);
let _ = writeln!(
out,
" try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
);
} else if let Some(values) = &assertion.values {
for val in values {
let zig_val = json_to_zig(val);
let _ = writeln!(
out,
" try testing.expect(std.mem.indexOf(u8, {field_expr}, {zig_val}) == null);"
);
}
}
}
"not_empty" => {
let _ = writeln!(out, " try testing.expect({field_expr}.len > 0);");
}
"is_empty" => {
let _ = writeln!(out, " try testing.expect({field_expr}.len == 0);");
}
"starts_with" => {
if let Some(expected) = &assertion.value {
let zig_val = json_to_zig(expected);
let _ = writeln!(
out,
" try testing.expect(std.mem.startsWith(u8, {field_expr}, {zig_val}));"
);
}
}
"ends_with" => {
if let Some(expected) = &assertion.value {
let zig_val = json_to_zig(expected);
let _ = writeln!(
out,
" try testing.expect(std.mem.endsWith(u8, {field_expr}, {zig_val}));"
);
}
}
"min_length" => {
if let Some(val) = &assertion.value {
if let Some(n) = val.as_u64() {
let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
}
}
}
"max_length" => {
if let Some(val) = &assertion.value {
if let Some(n) = val.as_u64() {
let _ = writeln!(out, " try testing.expect({field_expr}.len <= {n});");
}
}
}
"count_min" => {
if let Some(val) = &assertion.value {
if let Some(n) = val.as_u64() {
let _ = writeln!(out, " try testing.expect({field_expr}.len >= {n});");
}
}
}
"count_equals" => {
if let Some(val) = &assertion.value {
if let Some(n) = val.as_u64() {
let has_field = assertion.field.as_deref().is_some_and(|f| !f.is_empty());
if has_field {
let _ = writeln!(out, " try testing.expectEqual({n}, {field_expr}.len);");
} else {
let _ = writeln!(out, " {{");
let _ = writeln!(
out,
" var _cparse = try std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {field_expr}, .{{}});"
);
let _ = writeln!(out, " defer _cparse.deinit();");
let _ = writeln!(
out,
" try testing.expectEqual({n}, _cparse.value.array.items.len);"
);
let _ = writeln!(out, " }}");
}
}
}
}
"is_true" => {
let _ = writeln!(out, " try testing.expect({field_expr});");
}
"is_false" => {
let _ = writeln!(out, " try testing.expect(!{field_expr});");
}
"not_error" => {
}
"error" => {
}
"greater_than" => {
if let Some(val) = &assertion.value {
let zig_val = json_to_zig(val);
let _ = writeln!(out, " try testing.expect({field_expr} > {zig_val});");
}
}
"less_than" => {
if let Some(val) = &assertion.value {
let zig_val = json_to_zig(val);
let _ = writeln!(out, " try testing.expect({field_expr} < {zig_val});");
}
}
"greater_than_or_equal" => {
if let Some(val) = &assertion.value {
let zig_val = json_to_zig(val);
let _ = writeln!(out, " try testing.expect({field_expr} >= {zig_val});");
}
}
"less_than_or_equal" => {
if let Some(val) = &assertion.value {
let zig_val = json_to_zig(val);
let _ = writeln!(out, " try testing.expect({field_expr} <= {zig_val});");
}
}
"contains_any" => {
if let Some(values) = &assertion.values {
let string_values: Vec<String> = values
.iter()
.filter_map(|v| {
if let serde_json::Value::String(s) = v {
Some(format!(
"std.mem.indexOf(u8, {field_expr}, \"{}\") != null",
escape_zig(s)
))
} else {
None
}
})
.collect();
if !string_values.is_empty() {
let condition = string_values.join(" or\n ");
let _ = writeln!(out, " try testing.expect(\n {condition}\n );");
}
}
}
"matches_regex" => {
let _ = writeln!(out, " // regex match not yet implemented for Zig");
}
"method_result" => {
let _ = writeln!(out, " // method_result assertions not yet implemented for Zig");
}
other => {
panic!("Zig e2e generator: unsupported assertion type: {other}");
}
}
}
fn json_to_zig(value: &serde_json::Value) -> String {
match value {
serde_json::Value::String(s) => format!("\"{}\"", escape_zig(s)),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Null => "null".to_string(),
serde_json::Value::Array(arr) => {
let items: Vec<String> = arr.iter().map(json_to_zig).collect();
format!("&.{{{}}}", items.join(", "))
}
serde_json::Value::Object(_) => {
let json_str = serde_json::to_string(value).unwrap_or_default();
format!("\"{}\"", escape_zig(&json_str))
}
}
}
fn zig_type_for_stub(ty: &crate::core::ir::TypeRef) -> String {
use crate::core::ir::{PrimitiveType, TypeRef};
match ty {
TypeRef::Primitive(p) => match p {
PrimitiveType::Bool => "bool".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 => "[]const u8".to_string(),
TypeRef::Unit => "void".to_string(),
TypeRef::Optional(inner) => format!("?{}", zig_type_for_stub(inner)),
TypeRef::Vec(inner) => format!("[]const {}", zig_type_for_stub(inner)),
TypeRef::Map(_, v) => format!("std.StringHashMap({})", zig_type_for_stub(v)),
TypeRef::Named(name) => name.clone(),
TypeRef::Duration => "i64".to_string(),
}
}
pub fn emit_test_backend(
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&crate::core::ir::MethodDef],
fixture: &crate::e2e::fixture::Fixture,
) -> super::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 {{");
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().filter(|m| !m.has_default_impl) {
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);
let default_val = defaults.emit_default(&method.return_type);
let mut params = vec!["_: *@This()".to_string()];
for p in &method.params {
let p_ty = zig_type_for_stub(&p.ty);
params.push(format!("{}: {}", p.name, p_ty));
}
let param_list = params.join(", ");
let is_fallible = method.error_type.is_some();
let ret_sig = if is_fallible {
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}: ?[*:0]u8 = null;");
let arg_expr = format!("\"test\", {vtable_var}, &{var_name}, @ptrCast(&{out_err_var})");
super::TestBackendEmission {
setup_block: setup,
arg_expr,
type_imports: Vec::new(),
}
}
#[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,
}],
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,
};
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,
call: None,
input: serde_json::Value::Null,
mock_response: None,
source: String::new(),
http: None,
assertions: vec![],
visitor: None,
args: 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 &[
"OcrBackend",
"DocumentExtractor",
"processImage",
"process_image_fn",
"kreuzberg",
] {
assert!(
!emission.setup_block.contains(name),
"setup_block must not contain domain name '{name}', got:\n{}",
emission.setup_block
);
}
}
}
#[cfg(test)]
mod zig_hash_tests {
use super::{render_build_zig_zon, resolve_zig_hash};
use crate::e2e::config::DependencyMode;
#[test]
fn explicit_hash_override_is_used_verbatim() {
let url = "https://github.com/kreuzberg-dev/liter-llm/releases/download/v1.4.0/liter-llm-zig-v1.4.0.tar.gz";
let pinned = "1220abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab";
let result = resolve_zig_hash(Some(pinned), url);
assert_eq!(
result.as_deref(),
Some(pinned),
"explicit hash must be returned unchanged; got: {result:?}"
);
}
#[test]
fn build_zig_zon_emits_explicit_hash() {
let hash = "12208badf00d";
let content = render_build_zig_zon(
"liter_llm",
"../../packages/zig",
DependencyMode::Registry,
"1.4.0-rc.32",
Some(hash),
"liter-llm",
"https://github.com/kreuzberg-dev/liter-llm",
);
assert!(
content.contains(&format!(".hash = \"{hash}\"")),
"build.zig.zon must embed the explicit hash, got:\n{content}"
);
assert!(
!content.contains(".hash = \"TODO\""),
"build.zig.zon must not emit TODO when hash is provided, got:\n{content}"
);
}
#[test]
fn build_zig_zon_falls_back_to_todo_when_no_hash() {
let content = render_build_zig_zon(
"liter_llm",
"../../packages/zig",
DependencyMode::Registry,
"1.4.0-rc.32",
None,
"liter-llm",
"https://github.com/kreuzberg-dev/liter-llm",
);
assert!(
content.contains(".hash = \"TODO\""),
"build.zig.zon must fall back to TODO when no hash is available, got:\n{content}"
);
}
#[test]
fn build_zig_zon_emits_full_release_url_with_repo_segment() {
let content = render_build_zig_zon(
"html_to_markdown",
"../../packages/zig",
DependencyMode::Registry,
"3.5.1",
None,
"html-to-markdown-rs",
"https://github.com/kreuzberg-dev/html-to-markdown",
);
let expected_url = "https://github.com/kreuzberg-dev/html-to-markdown/releases/download/v3.5.1/html-to-markdown-rs-zig-v3.5.1.tar.gz";
assert!(
content.contains(expected_url),
"build.zig.zon must emit the full release URL with repo segment; got:\n{content}"
);
}
}