use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::hash::{self, CommentStyle};
use crate::core::template_versions as tv;
use crate::e2e::config::E2eConfig;
use crate::e2e::escape::{escape_elixir, sanitize_filename, sanitize_ident};
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup, HttpFixture, ValidationErrorExpectation};
use anyhow::Result;
use heck::ToSnakeCase;
use std::collections::HashMap;
use std::fmt::Write as FmtWrite;
use std::path::PathBuf;
use super::E2eCodegen;
use super::client;
pub struct ElixirCodegen;
impl E2eCodegen for ElixirCodegen {
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 raw_module = overrides
.and_then(|o| o.module.as_ref())
.cloned()
.unwrap_or_else(|| call.module.clone());
let module_path = if raw_module.contains('.') || raw_module.chars().next().is_some_and(|c| c.is_uppercase()) {
raw_module.clone()
} else {
elixir_module_name(&raw_module)
};
let base_function_name = overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| call.function.clone());
let function_name =
if call.r#async && !base_function_name.ends_with("_async") && !base_function_name.ends_with("_stream") {
format!("{base_function_name}_async")
} else {
base_function_name
};
let options_type = overrides.and_then(|o| o.options_type.clone());
let options_default_fn = overrides.and_then(|o| o.options_via.clone());
let empty_enum_fields = HashMap::new();
let enum_fields = overrides.map(|o| &o.enum_fields).unwrap_or(&empty_enum_fields);
let handle_struct_type = overrides.and_then(|o| o.handle_struct_type.clone());
let empty_atom_fields = std::collections::HashSet::new();
let handle_atom_list_fields = overrides
.map(|o| &o.handle_atom_list_fields)
.unwrap_or(&empty_atom_fields);
let result_var = &call.result_var;
let has_http_tests = groups.iter().any(|g| g.fixtures.iter().any(|f| f.is_http_test()));
let has_nif_tests = groups.iter().any(|g| g.fixtures.iter().any(|f| !f.is_http_test()));
let has_mock_server_tests = groups.iter().any(|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,
);
let elixir_override = cc
.overrides
.get("elixir")
.or_else(|| e2e_config.call.overrides.get("elixir"));
elixir_override.and_then(|o| o.client_factory.as_deref()).is_some()
})
});
let pkg_ref = e2e_config.resolve_package(lang);
let pkg_dep_ref = if has_nif_tests {
match e2e_config.dep_mode {
crate::e2e::config::DependencyMode::Local => pkg_ref
.as_ref()
.and_then(|p| p.path.as_deref())
.unwrap_or("../../packages/elixir")
.to_string(),
crate::e2e::config::DependencyMode::Registry => pkg_ref
.as_ref()
.and_then(|p| p.version.clone())
.or_else(|| config.resolved_version())
.unwrap_or_else(|| "0.1.0".to_string()),
}
} else {
String::new()
};
let pkg_atom = config.elixir_app_name();
let has_http_fixtures = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| f.http.is_some());
let uses_harness = has_http_fixtures && !e2e_config.harness.imports.is_empty();
files.push(GeneratedFile {
path: output_base.join("mix.exs"),
content: render_mix_exs(
&pkg_atom,
&pkg_dep_ref,
e2e_config.dep_mode,
has_http_tests,
has_mock_server_tests,
has_nif_tests,
uses_harness,
),
generated_header: false,
});
files.push(GeneratedFile {
path: output_base.join("lib").join("e2e_elixir.ex"),
content: "defmodule E2eElixir do\n @moduledoc false\nend\n".to_string(),
generated_header: false,
});
if uses_harness {
files.push(GeneratedFile {
path: output_base.join("app_harness.exs"),
content: render_app_harness(e2e_config, groups),
generated_header: true,
});
}
files.push(GeneratedFile {
path: output_base.join("test").join("test_helper.exs"),
content: render_test_helper(has_http_tests || has_mock_server_tests, uses_harness, e2e_config),
generated_header: false,
});
for group in groups {
let active: Vec<&Fixture> = group
.fixtures
.iter()
.filter(|f| super::should_include_fixture(f, lang, e2e_config))
.collect();
if active.is_empty() {
continue;
}
let filename = format!("{}_test.exs", sanitize_filename(&group.category));
let content = render_test_file(
&group.category,
&active,
e2e_config,
&module_path,
&function_name,
result_var,
&e2e_config.call.args,
options_type.as_deref(),
options_default_fn.as_deref(),
enum_fields,
handle_struct_type.as_deref(),
handle_atom_list_fields,
&config.adapters,
enums,
config,
type_defs,
);
files.push(GeneratedFile {
path: output_base.join("test").join(filename),
content,
generated_header: true,
});
}
Ok(files)
}
fn language_name(&self) -> &'static str {
"elixir"
}
}
pub(super) fn render_app_harness(e2e_config: &E2eConfig, groups: &[FixtureGroup]) -> String {
let mut fixtures_map = serde_json::Map::new();
for group in groups {
for fixture in &group.fixtures {
if fixture.http.is_none() {
continue;
}
let http_data = &fixture.http.as_ref().unwrap();
let fixture_json = serde_json::json!({
"http": {
"handler": {
"route": &http_data.handler.route,
"method": &http_data.handler.method,
"body_schema": http_data.handler.body_schema.clone(),
},
"request": {
"path": &http_data.request.path,
},
"expected_response": {
"status_code": http_data.expected_response.status_code,
"body": &http_data.expected_response.body,
"headers": &http_data.expected_response.headers,
}
}
});
fixtures_map.insert(fixture.id.clone(), fixture_json);
}
}
let fixtures_json_str = serde_json::to_string(&fixtures_map).unwrap_or_default();
let fixtures_json = fixtures_json_str.replace('\\', "\\\\").replace('"', "\\\"");
let fixtures_json = format!("\"{}\"", fixtures_json);
let harness_override = e2e_config.harness.overrides.get("elixir");
let imports_override = harness_override.and_then(|o| o.imports.as_ref());
let imports: &[String] = imports_override.unwrap_or(&e2e_config.harness.imports);
let app_class: Option<&str> = harness_override
.and_then(|o| o.app_class.as_deref())
.or(e2e_config.harness.app_class.as_deref());
let register_route_method: Option<&str> = harness_override
.and_then(|o| o.register_method.as_deref())
.or(e2e_config.harness.register_method.as_deref());
let body_schema_setter: Option<&str> = harness_override
.and_then(|o| o.body_schema_setter.as_deref())
.or(e2e_config.harness.body_schema_setter.as_deref());
let run_method: Option<&str> = harness_override
.and_then(|o| o.run_method.as_deref())
.or(e2e_config.harness.run_method.as_deref());
let host = &e2e_config.harness.host;
let port = e2e_config.harness.port;
let header = hash::header(CommentStyle::Hash);
let binding_path = if e2e_config.dep_mode == crate::e2e::config::DependencyMode::Local {
"../../packages/elixir"
} else {
"."
};
let module_prefix = if !imports.is_empty() {
format!("{}.", elixir_module_name(&imports[0]))
} else {
String::new()
};
let route_builder_class = format!("{}RouteBuilder", module_prefix);
let method_enum_class = format!("{}Method", module_prefix);
let server_config_class = format!("{}ServerConfig", module_prefix);
let ctx = minijinja::context! {
header => header,
app_class => app_class.unwrap_or("App"),
route_builder_class => &route_builder_class,
route_builder_schema_setter => body_schema_setter.unwrap_or("request_schema_json"),
method_enum_class => &method_enum_class,
register_route_method => register_route_method.unwrap_or("route"),
run_method => run_method.unwrap_or("run"),
server_config_class => &server_config_class,
host => host,
port => port,
binding_path => binding_path,
fixtures_json => fixtures_json,
};
crate::e2e::template_env::render("elixir/app_harness.exs.jinja", ctx)
}
fn render_test_helper(has_http_tests: bool, uses_harness: bool, e2e_config: &E2eConfig) -> String {
if uses_harness {
let host = &e2e_config.harness.host;
let port = e2e_config.harness.port;
format!(
r#"# Start a named Finch pool before ExUnit. When tests call Req with
# connect_options: [protocols: [:http1]], they bypass Req's default lazy
# init and require an explicit Finch supervisor to be running.
{{:ok, _}} = Finch.start_link(name: AlefE2EFinch)
ExUnit.start()
# Spawn app_harness subprocess and set SUT_URL
# If SUT_URL is already set, a parent process started a shared harness.
# Use it as-is and do NOT spawn our own.
unless System.get_env("SUT_URL") do
app_harness_bin = Path.expand("../app_harness.exs", __DIR__)
project_root = Path.expand("..", __DIR__)
# Build the list of ebin directories from _build/dev/lib so the harness can access compiled dependencies
build_lib_dir = Path.join(project_root, "_build/dev/lib")
lib_paths = if File.dir?(build_lib_dir) do
File.ls!(build_lib_dir)
|> Enum.map(&Path.join(build_lib_dir, &1))
|> Enum.filter(&File.dir?/1)
|> Enum.flat_map(fn lib_path ->
ebin_path = Path.join(lib_path, "ebin")
if File.dir?(ebin_path), do: ["-pa", ebin_path], else: []
end)
else
[]
end
# Use `elixir` to execute the harness script with proper code paths
port = Port.open({{:spawn_executable, System.find_executable("elixir")}}, [
:binary,
{{:line, 65_536}},
args: lib_paths ++ [app_harness_bin]
])
url = "http://{host}:{port}"
# Poll until the harness accepts TCP connections
deadline = :erlang.monotonic_time(:millisecond) + 15_000
ready = false
{{ready, url}} =
Enum.reduce_while(1..150, {{false, url}}, fn _, {{_, url_acc}} ->
now = :erlang.monotonic_time(:millisecond)
if now > deadline do
{{:halt, {{false, url_acc}}}}
else
case :gen_tcp.connect(String.to_charlist("{host}"), {port}, [], 500) do
{{:ok, socket}} ->
:gen_tcp.close(socket)
{{:halt, {{true, url_acc}}}}
{{:error, _}} ->
Process.sleep(100)
{{:cont, {{false, url_acc}}}}
end
end
end)
unless ready do
Port.close(port)
raise "App harness did not become reachable on {host}:{port} within 15s"
end
System.put_env("SUT_URL", url)
end
"#
)
} else if has_http_tests {
r#"# Start a named Finch pool before ExUnit. When tests call Req with
# connect_options: [protocols: [:http1]], they bypass Req's default lazy
# init and require an explicit Finch supervisor to be running.
{:ok, _} = Finch.start_link(name: AlefE2EFinch)
ExUnit.start()
# Spawn mock-server binary and set MOCK_SERVER_URL for all tests.
#
# Two execution modes:
# 1. External mode (`alef test-apps run` parent): MOCK_SERVER_URL is already set.
# Use it as-is together with any MOCK_SERVERS / MOCK_SERVER_<FIXTURE_ID> vars
# that the parent exported. Do NOT spawn our own server.
# 2. Standalone mode (direct `mix test` / `task elixir:smoke`): Build the
# mock-server binary if it is missing, then spawn it, capture its URL, and
# let it run for the duration of the test suite.
mock_server_bin = Path.expand("../../rust/target/release/mock-server", __DIR__)
fixtures_dir = Path.expand("../../../fixtures", __DIR__)
unless System.get_env("MOCK_SERVER_URL") do
unless File.exists?(mock_server_bin) do
# Build the mock-server from the e2e/rust/ crate that alef generated.
manifest = Path.expand("../../rust/Cargo.toml", __DIR__)
unless File.exists?(manifest) do
raise "mock-server Cargo.toml not found at #{manifest}"
end
{_output, 0} =
System.cmd("cargo", ["build", "--release", "--manifest-path", manifest, "--bin", "mock-server"],
stderr_to_stdout: true)
unless File.exists?(mock_server_bin) do
raise "mock-server binary still missing after build: #{mock_server_bin}"
end
end
port = Port.open({:spawn_executable, mock_server_bin}, [
:binary,
# Use a large line buffer (default 1024 truncates `MOCK_SERVERS={...}` lines for
# fixture sets with many host-root routes, splitting them into `:noeol` chunks
# that the prefix-match clauses below would never see).
{:line, 65_536},
args: [fixtures_dir]
])
# Read startup lines: MOCK_SERVER_URL= then MOCK_SERVERS= (always emitted, possibly `{}`).
# The standalone mock-server prints noisy stderr lines BEFORE the stdout sentinels;
# selective receive ignores anything that doesn't match the two prefix patterns.
# Each iteration only halts after the MOCK_SERVERS= line is processed.
{url, _} =
Enum.reduce_while(1..16, {nil, port}, fn _, {url_acc, p} ->
receive do
{^p, {:data, {:eol, "MOCK_SERVER_URL=" <> u}}} ->
{:cont, {u, p}}
{^p, {:data, {:eol, "MOCK_SERVERS=" <> json_val}}} ->
System.put_env("MOCK_SERVERS", json_val)
case Jason.decode(json_val) do
{:ok, servers} ->
Enum.each(servers, fn {fid, furl} ->
System.put_env("MOCK_SERVER_#{String.upcase(fid)}", furl)
end)
_ ->
:ok
end
{:halt, {url_acc, p}}
after
30_000 ->
raise "mock-server startup timeout"
end
end)
if url != nil do
System.put_env("MOCK_SERVER_URL", url)
end
end
"#
.to_string()
} else {
"ExUnit.start()\n".to_string()
}
}
fn render_mix_exs(
pkg_name: &str,
pkg_path: &str,
dep_mode: crate::e2e::config::DependencyMode,
has_http_tests: bool,
has_mock_server_tests: bool,
has_nif_tests: bool,
uses_harness: bool,
) -> String {
let mut out = String::new();
let _ = writeln!(out, "defmodule E2eElixir.MixProject do");
let _ = writeln!(out, " use Mix.Project");
let _ = writeln!(out);
let _ = writeln!(out, " def project do");
let _ = writeln!(out, " [");
let _ = writeln!(out, " app: :e2e_elixir,");
let _ = writeln!(out, " version: \"0.1.0\",");
let _ = writeln!(out, " elixir: \"~> 1.14\",");
let _ = writeln!(out, " deps: deps()");
let _ = writeln!(out, " ]");
let _ = writeln!(out, " end");
let _ = writeln!(out);
let _ = writeln!(out, " defp deps do");
let _ = writeln!(out, " [");
let mut deps: Vec<String> = Vec::new();
if has_nif_tests && !pkg_path.is_empty() {
let pkg_atom = pkg_name;
let nif_dep = match dep_mode {
crate::e2e::config::DependencyMode::Local => {
format!(" {{:{pkg_atom}, path: \"{pkg_path}\"}}")
}
crate::e2e::config::DependencyMode::Registry => {
format!(" {{:{pkg_atom}, \"{pkg_path}\"}}")
}
};
deps.push(nif_dep);
deps.push(format!(
" {{:rustler_precompiled, \"{rp}\"}}",
rp = tv::hex::RUSTLER_PRECOMPILED
));
deps.push(format!(
" {{:rustler, \"{rustler}\", runtime: false}}",
rustler = tv::hex::RUSTLER
));
}
if has_http_tests || has_mock_server_tests || uses_harness {
deps.push(format!(" {{:finch, \"{finch}\"}}", finch = tv::hex::FINCH));
deps.push(format!(" {{:req, \"{req}\"}}", req = tv::hex::REQ));
deps.push(format!(" {{:jason, \"{jason}\"}}", jason = tv::hex::JASON));
}
let _ = writeln!(out, "{}", deps.join(",\n"));
let _ = writeln!(out, " ]");
let _ = writeln!(out, " end");
let _ = writeln!(out, "end");
out
}
#[allow(clippy::too_many_arguments)]
fn render_test_file(
category: &str,
fixtures: &[&Fixture],
e2e_config: &E2eConfig,
module_path: &str,
function_name: &str,
result_var: &str,
args: &[crate::e2e::config::ArgMapping],
options_type: Option<&str>,
options_default_fn: Option<&str>,
enum_fields: &HashMap<String, String>,
handle_struct_type: Option<&str>,
handle_atom_list_fields: &std::collections::HashSet<String>,
adapters: &[crate::core::config::extras::AdapterConfig],
enums: &[crate::core::ir::EnumDef],
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
) -> String {
let mut out = String::new();
out.push_str(&hash::header(CommentStyle::Hash));
let _ = writeln!(out, "# E2e tests for category: {category}");
let mut trait_bridge_module_defs = Vec::new();
for fixture in fixtures {
let call_config = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let resolved_args = fixture.resolved_args(call_config);
for arg in resolved_args.iter() {
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 mut methods: Vec<&crate::core::ir::MethodDef> = type_defs
.iter()
.find(|t| t.name == *trait_name)
.map(|t| t.methods.iter().collect())
.unwrap_or_default();
if let Some(super_trait) = &trait_bridge.super_trait {
if let Some(super_type) = type_defs.iter().find(|t| &t.name == super_trait) {
for method in &super_type.methods {
if !methods.iter().any(|m| m.name == method.name) {
methods.push(method);
}
}
}
}
let elixir_nif_module = format!("{module_path}.Native");
let emission = emit_test_backend(trait_bridge, &methods, fixture, &elixir_nif_module);
if let Some(pos) = emission.setup_block.find("__TRAIT_BRIDGE_MODULE_DEFS_END__") {
let marker_start = emission.setup_block[..pos].rfind('\n').unwrap_or(0);
let module_defs_str = emission.setup_block[..marker_start].trim_end().to_string();
for line in module_defs_str.lines() {
if !line.is_empty() {
trait_bridge_module_defs.push(line.to_string());
}
}
}
}
}
}
}
}
for module_def_line in &trait_bridge_module_defs {
let _ = writeln!(out, "{}", module_def_line.trim_start());
}
let _ = writeln!(out, "defmodule E2e.{}Test do", elixir_module_name(category));
let has_http = fixtures.iter().any(|f| f.is_http_test());
let async_flag = if has_http { "true" } else { "false" };
let _ = writeln!(out, " use ExUnit.Case, async: {async_flag}");
if has_http {
let _ = writeln!(out);
let _ = writeln!(out, " defp mock_server_url do");
let _ = writeln!(
out,
" System.get_env(\"MOCK_SERVER_URL\") || \"http://localhost:8080\""
);
let _ = writeln!(out, " end");
}
let has_array_contains = fixtures.iter().any(|fixture| {
let cc = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let fr = FieldResolver::new(
e2e_config.effective_fields(cc),
e2e_config.effective_fields_optional(cc),
e2e_config.effective_result_fields(cc),
e2e_config.effective_fields_array(cc),
&std::collections::HashSet::new(),
);
fixture.assertions.iter().any(|a| {
matches!(a.assertion_type.as_str(), "contains" | "contains_all" | "not_contains")
&& a.field
.as_deref()
.is_some_and(|f| !f.is_empty() && fr.is_array(fr.resolve(f)))
})
});
if has_array_contains {
let _ = writeln!(out);
let _ = writeln!(out, " defp alef_e2e_item_texts(item) when is_binary(item), do: [item]");
let _ = writeln!(out, " defp alef_e2e_item_texts(item) do");
let _ = writeln!(out, " [:kind, :name, :signature, :path, :alias, :text, :source]");
let _ = writeln!(out, " |> Enum.filter(&Map.has_key?(item, &1))");
let _ = writeln!(out, " |> Enum.flat_map(fn attr ->");
let _ = writeln!(out, " case Map.get(item, attr) do");
let _ = writeln!(out, " nil -> []");
let _ = writeln!(
out,
" atom when is_atom(atom) -> [atom |> to_string() |> String.capitalize()]"
);
let _ = writeln!(out, " str -> [inspect(str)]");
let _ = writeln!(out, " end");
let _ = writeln!(out, " end)");
let _ = writeln!(out, " end");
}
let has_format_metadata = fixtures.iter().any(|fixture| {
fixture.assertions.iter().any(|a| {
a.field
.as_deref()
.is_some_and(|f| f.contains("format") && f.contains("metadata"))
})
});
if has_format_metadata {
let _ = writeln!(out);
let _ = writeln!(
out,
" defp alef_e2e_format_to_string(value) when is_binary(value), do: value"
);
let _ = writeln!(out, " defp alef_e2e_format_to_string(metadata) do");
let _ = writeln!(out, " case metadata.image do");
let _ = writeln!(out, " %{{format: fmt}} when is_binary(fmt) -> fmt");
let _ = writeln!(out, " _ ->");
let _ = writeln!(out, " case metadata.pdf do");
let _ = writeln!(out, " %{{}} -> \"PDF\"");
let _ = writeln!(out, " _ ->");
let _ = writeln!(out, " case metadata.html do");
let _ = writeln!(out, " %{{}} -> \"HTML\"");
let _ = writeln!(out, " _ -> inspect(metadata)");
let _ = writeln!(out, " end");
let _ = writeln!(out, " end");
let _ = writeln!(out, " end");
let _ = writeln!(out, " end");
}
let _ = writeln!(out);
for (i, fixture) in fixtures.iter().enumerate() {
if let Some(http) = &fixture.http {
render_http_test_case(&mut out, fixture, http);
} else {
render_test_case(
&mut out,
fixture,
e2e_config,
module_path,
function_name,
result_var,
args,
options_type,
options_default_fn,
enum_fields,
handle_struct_type,
handle_atom_list_fields,
adapters,
enums,
config,
type_defs,
);
}
if i + 1 < fixtures.len() {
let _ = writeln!(out);
}
}
let _ = writeln!(out, "end");
out
}
const FINCH_UNSUPPORTED_METHODS: &[&str] = &["TRACE", "CONNECT"];
const REQ_CONVENIENCE_METHODS: &[&str] = &["get", "post", "put", "patch", "delete", "head"];
const REQ_HTTP1_OPT: &str = "connect_options: [protocols: [:http1]]";
struct ElixirTestClientRenderer<'a> {
fixture_id: &'a str,
expected_status: u16,
}
impl<'a> client::TestClientRenderer for ElixirTestClientRenderer<'a> {
fn language_name(&self) -> &'static str {
"elixir"
}
fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
let escaped_description = description.replace('"', "\\\"");
let _ = writeln!(out, " describe \"{fn_name}\" do");
if skip_reason.is_some() {
let _ = writeln!(out, " @tag :skip");
}
let _ = writeln!(out, " test \"{escaped_description}\" do");
}
fn render_test_close(&self, out: &mut String) {
let _ = writeln!(out, " end");
let _ = writeln!(out, " end");
}
fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
let method = ctx.method.to_lowercase();
let mut opts: Vec<String> = vec![REQ_HTTP1_OPT.to_string(), "finch: AlefE2EFinch".to_string()];
if let Some(body) = ctx.body {
let elixir_val = json_to_elixir(body);
opts.push(format!("json: {elixir_val}"));
}
if !ctx.headers.is_empty() {
let header_pairs: Vec<String> = ctx
.headers
.iter()
.map(|(k, v)| format!("{{\"{}\", \"{}\"}}", escape_elixir(k), escape_elixir(v)))
.collect();
opts.push(format!("headers: [{}]", header_pairs.join(", ")));
}
if !ctx.cookies.is_empty() {
let cookie_str = ctx
.cookies
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join("; ");
opts.push(format!("headers: [{{\"cookie\", \"{}\"}}]", escape_elixir(&cookie_str)));
}
if !ctx.query_params.is_empty() {
let pairs: Vec<String> = ctx
.query_params
.iter()
.map(|(k, v)| {
let val_str = match v {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
};
format!("{{\"{}\", \"{}\"}}", escape_elixir(k), escape_elixir(&val_str))
})
.collect();
opts.push(format!("params: [{}]", pairs.join(", ")));
}
if (300..400).contains(&self.expected_status) {
opts.push("redirect: false".to_string());
}
let fixture_id = escape_elixir(self.fixture_id);
let sut_url_expr = "System.get_env(\"SUT_URL\") || mock_server_url()";
let url_expr = format!("({sut_url_expr}) <> \"/fixtures/{fixture_id}\"");
if REQ_CONVENIENCE_METHODS.contains(&method.as_str()) {
let opts_str = opts.join(", ");
let _ = writeln!(
out,
" {{:ok, response}} = Req.{method}(url: {url_expr}, {opts_str})"
);
} else {
opts.insert(0, format!("method: :{method}"));
opts.insert(1, format!("url: {url_expr}"));
let opts_str = opts.join(", ");
let _ = writeln!(out, " {{:ok, response}} = Req.request({opts_str})");
}
}
fn render_assert_status(&self, out: &mut String, response_var: &str, status: u16) {
let _ = writeln!(out, " assert {response_var}.status == {status}");
}
fn render_assert_header(&self, out: &mut String, response_var: &str, name: &str, expected: &str) {
let header_key = name.to_lowercase();
if header_key == "connection" {
return;
}
let key_lit = format!("\"{}\"", escape_elixir(&header_key));
let get_header_expr = format!(
"Enum.find_value({response_var}.headers, fn {{k, v}} -> if String.downcase(k) == {key_lit}, do: List.first(List.wrap(v)) end)"
);
match expected {
"<<present>>" => {
let _ = writeln!(out, " assert {get_header_expr} != nil");
}
"<<absent>>" => {
let _ = writeln!(out, " assert {get_header_expr} == nil");
}
"<<uuid>>" => {
let var = sanitize_ident(&header_key);
let _ = writeln!(out, " header_val_{var} = {get_header_expr}");
let _ = writeln!(
out,
" assert Regex.match?(~r/^[0-9a-f]{{8}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{12}}$/i, to_string(header_val_{var}))"
);
}
literal => {
let val_lit = format!("\"{}\"", escape_elixir(literal));
let _ = writeln!(out, " assert {get_header_expr} == {val_lit}");
}
}
}
fn render_assert_json_body(&self, out: &mut String, response_var: &str, expected: &serde_json::Value) {
let elixir_val = json_to_elixir(expected);
match expected {
serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
let _ = writeln!(
out,
" body_decoded = if is_binary({response_var}.body), do: Jason.decode!({response_var}.body), else: {response_var}.body"
);
let _ = writeln!(out, " assert body_decoded == {elixir_val}");
}
_ => {
let _ = writeln!(out, " assert {response_var}.body == {elixir_val}");
}
}
}
fn render_assert_partial_body(&self, out: &mut String, response_var: &str, expected: &serde_json::Value) {
if let Some(obj) = expected.as_object() {
let _ = writeln!(
out,
" decoded_body = if is_binary({response_var}.body), do: Jason.decode!({response_var}.body), else: {response_var}.body"
);
for (key, val) in obj {
let key_lit = format!("\"{}\"", escape_elixir(key));
let elixir_val = json_to_elixir(val);
let _ = writeln!(out, " assert decoded_body[{key_lit}] == {elixir_val}");
}
}
}
fn render_assert_validation_errors(
&self,
out: &mut String,
response_var: &str,
errors: &[ValidationErrorExpectation],
) {
for err in errors {
let msg_lit = format!("\"{}\"", escape_elixir(&err.msg));
let _ = writeln!(
out,
" assert String.contains?(Jason.encode!({response_var}.body), {msg_lit})"
);
}
}
}
fn render_http_test_case(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
let method = http.request.method.to_uppercase();
if FINCH_UNSUPPORTED_METHODS.contains(&method.as_str()) {
let test_name = sanitize_ident(&fixture.id);
let test_label = fixture.id.replace('"', "\\\"");
let path = &http.request.path;
let _ = writeln!(out, " describe \"{test_name}\" do");
let _ = writeln!(out, " @tag :skip");
let _ = writeln!(out, " test \"{method} {path} - {test_label}\" do");
let _ = writeln!(out, " end");
let _ = writeln!(out, " end");
return;
}
let renderer = ElixirTestClientRenderer {
fixture_id: &fixture.id,
expected_status: http.expected_response.status_code,
};
client::http_call::render_http_test(out, &renderer, fixture);
}
#[allow(clippy::too_many_arguments)]
fn render_test_case(
out: &mut String,
fixture: &Fixture,
e2e_config: &E2eConfig,
_default_module_path: &str,
_default_function_name: &str,
_default_result_var: &str,
_args: &[crate::e2e::config::ArgMapping],
options_type: Option<&str>,
options_default_fn: Option<&str>,
_enum_fields: &HashMap<String, String>,
handle_struct_type: Option<&str>,
_handle_atom_list_fields: &std::collections::HashSet<String>,
adapters: &[crate::core::config::extras::AdapterConfig],
enums: &[crate::core::ir::EnumDef],
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
) {
let test_name = sanitize_ident(&fixture.id);
let test_label = fixture.id.replace('"', "\\\"");
fn extract_trait_bridge_parts(setup_block: &str) -> (String, String) {
if let Some(pos) = setup_block.find("__TRAIT_BRIDGE_MODULE_DEFS_END__") {
let marker_start = setup_block[..pos].rfind('\n').unwrap_or(0);
let marker_end = if let Some(nl) = setup_block[pos + 32..].find('\n') {
pos + 32 + nl + 1
} else {
setup_block.len()
};
let module_defs = setup_block[..marker_start].trim_end().to_string();
let test_setup = setup_block[marker_end..].trim_start().to_string();
(module_defs, test_setup)
} else {
(String::new(), setup_block.to_string())
}
}
if fixture.mock_response.is_none() && !fixture_has_elixir_callable(fixture, e2e_config) {
let _ = writeln!(out, " describe \"{test_name}\" do");
let _ = writeln!(out, " @tag :skip");
let _ = writeln!(out, " test \"{test_label}\" do");
let _ = writeln!(
out,
" # non-HTTP fixture: Elixir binding does not expose a callable for the configured `[e2e.call]` function"
);
let _ = writeln!(out, " :ok");
let _ = writeln!(out, " end");
let _ = writeln!(out, " end");
return;
}
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),
&std::collections::HashSet::new(),
);
let field_resolver = &call_field_resolver;
let lang = "elixir";
let call_overrides = call_config.overrides.get(lang);
let base_fn = call_overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| call_config.function.clone());
if base_fn.starts_with("batch_extract_") {
let _ = writeln!(
out,
" describe \"{test_name}\" do",
test_name = sanitize_ident(&fixture.id)
);
let _ = writeln!(out, " @tag :skip");
let _ = writeln!(
out,
" test \"{test_label}\" do",
test_label = fixture.id.replace('"', "\\\"")
);
let _ = writeln!(
out,
" # batch functions excluded from Elixir binding: unsafe NIF tuple marshalling"
);
let _ = writeln!(out, " :ok");
let _ = writeln!(out, " end");
let _ = writeln!(out, " end");
return;
}
let raw_module = call_overrides
.and_then(|o| o.module.as_ref())
.cloned()
.unwrap_or_else(|| call_config.module.clone());
let module_path = if raw_module.contains('.') || raw_module.chars().next().is_some_and(|c| c.is_uppercase()) {
raw_module
} else {
elixir_module_name(&raw_module)
};
let function_name = if call_config.r#async && !base_fn.ends_with("_async") && !base_fn.ends_with("_stream") {
format!("{base_fn}_async")
} else {
base_fn
};
let result_var = call_config.result_var.clone();
let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
let validation_creation_failure = expects_error && fixture.resolved_category() == "validation";
let co = call_config.overrides.get(lang);
let empty_enum_fields_local: HashMap<String, String> = HashMap::new();
let empty_atom_fields_local: std::collections::HashSet<String> = std::collections::HashSet::new();
let resolved_args = fixture.resolved_args(call_config);
let resolved_options_type = co
.and_then(|o| o.options_type.clone())
.or_else(|| options_type.map(|s| s.to_string()));
let resolved_options_default_fn = co
.and_then(|o| o.options_via.clone())
.or_else(|| options_default_fn.map(|s| s.to_string()));
let resolved_enum_fields_ref = co.map(|o| &o.enum_fields).unwrap_or(&empty_enum_fields_local);
let resolved_handle_struct_type = co
.and_then(|o| o.handle_struct_type.clone())
.or_else(|| handle_struct_type.map(|s| s.to_string()));
let resolved_handle_atom_list_fields_ref = co
.map(|o| &o.handle_atom_list_fields)
.unwrap_or(&empty_atom_fields_local);
let test_documents_path = e2e_config.test_documents_relative_from(0);
let adapter_request_type: Option<String> = adapters
.iter()
.find(|a| a.name == call_config.function.as_str())
.and_then(|a| a.request_type.as_deref())
.map(|rt| rt.rsplit("::").next().unwrap_or(rt).to_string());
let (mut setup_lines, args_str) = build_args_and_setup(
&fixture.input,
resolved_args,
&module_path,
resolved_options_type.as_deref(),
resolved_options_default_fn.as_deref(),
resolved_enum_fields_ref,
fixture,
resolved_handle_struct_type.as_deref(),
resolved_handle_atom_list_fields_ref,
&test_documents_path,
adapter_request_type.as_deref(),
enums,
config,
type_defs,
);
let visitor_var = fixture
.visitor
.as_ref()
.map(|visitor_spec| build_elixir_visitor(&mut setup_lines, visitor_spec));
let final_args = if let Some(ref visitor_var) = visitor_var {
let parts: Vec<&str> = args_str.split(", ").collect();
if parts.len() == 2 && parts[1] == "nil" {
format!("{}, %{{visitor: {}}}", parts[0], visitor_var)
} else if parts.len() == 2 {
setup_lines.push(format!(
"{} = Map.put({}, :visitor, {})",
parts[1], parts[1], visitor_var
));
args_str
} else if parts.len() == 1 {
format!("{}, %{{visitor: {}}}", parts[0], visitor_var)
} else {
args_str
}
} else {
args_str
};
let client_factory = call_overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
e2e_config
.call
.overrides
.get("elixir")
.and_then(|o| o.client_factory.as_deref())
});
let extra_args: Vec<String> = call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
let final_args_with_extras = if extra_args.is_empty() {
final_args
} else if final_args.is_empty() {
extra_args.join(", ")
} else {
format!("{final_args}, {}", extra_args.join(", "))
};
let effective_args = if client_factory.is_some() {
if final_args_with_extras.is_empty() {
"client".to_string()
} else {
format!("client, {final_args_with_extras}")
}
} else {
final_args_with_extras
};
let has_mock = fixture.mock_response.is_some() || fixture.http.is_some();
let api_key_var_opt = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
let needs_api_key_skip = !has_mock && api_key_var_opt.is_some();
let needs_env_fallback = has_mock && api_key_var_opt.is_some();
let mut cleaned_setup_lines = Vec::new();
for line in setup_lines.iter() {
if line.contains("__TRAIT_BRIDGE_MODULE_DEFS_END__") {
let (_module_part, test_part) = extract_trait_bridge_parts(line);
for test_line in test_part.lines() {
if !test_line.is_empty() {
cleaned_setup_lines.push(test_line.to_string());
}
}
} else {
cleaned_setup_lines.push(line.clone());
}
}
let _ = writeln!(out, " describe \"{test_name}\" do");
let _ = writeln!(out, " test \"{test_label}\" do");
if needs_api_key_skip {
let api_key_var = api_key_var_opt.unwrap_or("");
let _ = writeln!(out, " if System.get_env(\"{api_key_var}\") in [nil, \"\"] do");
let _ = writeln!(out, " # {api_key_var} not set — skipping live smoke test");
let _ = writeln!(out, " :ok");
let _ = writeln!(out, " else");
}
if validation_creation_failure {
let mut emitted_error_assertion = false;
for line in &cleaned_setup_lines {
if !emitted_error_assertion && line.starts_with("{:ok,") {
if let Some(rhs) = line.split_once('=').map(|x| x.1) {
let rhs = rhs.trim();
let _ = writeln!(out, " assert {{:error, _}} = {rhs}");
emitted_error_assertion = true;
} else {
let _ = writeln!(out, " {line}");
}
} else {
let _ = writeln!(out, " {line}");
}
}
if !emitted_error_assertion {
let call_invocation = if effective_args.is_empty() {
format!("{module_path}.{function_name}()")
} else {
format!("{module_path}.{function_name}({effective_args})")
};
let _ = writeln!(out, " assert {{:error, _}} = {call_invocation}");
}
if needs_api_key_skip {
let _ = writeln!(out, " end");
}
let _ = writeln!(out, " end");
let _ = writeln!(out, " end");
return;
}
if expects_error {
for line in &cleaned_setup_lines {
let _ = writeln!(out, " {line}");
}
if let Some(factory) = client_factory {
let fixture_id = &fixture.id;
let base_url_expr = if fixture.has_host_root_route() {
let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
format!(
"(System.get_env(\"{env_key}\") || (System.get_env(\"MOCK_SERVER_URL\") || \"\") <> \"/fixtures/{fixture_id}\")"
)
} else {
format!("(System.get_env(\"MOCK_SERVER_URL\") || \"\") <> \"/fixtures/{fixture_id}\"")
};
let _ = writeln!(
out,
" {{:ok, client}} = {module_path}.{factory}(\"test-key\", base_url: {base_url_expr})"
);
}
let call_invocation = if effective_args.is_empty() {
format!("{module_path}.{function_name}()")
} else {
format!("{module_path}.{function_name}({effective_args})")
};
let _ = writeln!(out, " assert {{:error, _}} = {call_invocation}");
if needs_api_key_skip {
let _ = writeln!(out, " end");
}
let _ = writeln!(out, " end");
let _ = writeln!(out, " end");
return;
}
for line in &cleaned_setup_lines {
let _ = writeln!(out, " {line}");
}
if let Some(factory) = client_factory {
let fixture_id = &fixture.id;
if needs_env_fallback {
let api_key_var = api_key_var_opt.unwrap_or("");
let mock_url_expr = if fixture.has_host_root_route() {
let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
format!(
"System.get_env(\"{env_key}\") || (System.get_env(\"MOCK_SERVER_URL\") || \"\") <> \"/fixtures/{fixture_id}\""
)
} else {
format!("(System.get_env(\"MOCK_SERVER_URL\") || \"\") <> \"/fixtures/{fixture_id}\"")
};
let _ = writeln!(out, " api_key_val = System.get_env(\"{api_key_var}\")");
let _ = writeln!(
out,
" {{api_key_val, client_opts}} = if api_key_val && api_key_val != \"\" do"
);
let _ = writeln!(
out,
" IO.puts(\"{fixture_id}: using real API ({api_key_var} is set)\")"
);
let _ = writeln!(out, " {{api_key_val, []}}");
let _ = writeln!(out, " else");
let _ = writeln!(
out,
" IO.puts(\"{fixture_id}: using mock server ({api_key_var} not set)\")"
);
let _ = writeln!(out, " {{\"test-key\", [base_url: {mock_url_expr}]}}");
let _ = writeln!(out, " end");
let _ = writeln!(
out,
" {{:ok, client}} = {module_path}.{factory}(api_key_val, client_opts)"
);
} else {
let base_url_expr = if fixture.has_host_root_route() {
let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
format!(
"(System.get_env(\"{env_key}\") || (System.get_env(\"MOCK_SERVER_URL\") || \"\") <> \"/fixtures/{fixture_id}\")"
)
} else {
format!("(System.get_env(\"MOCK_SERVER_URL\") || \"\") <> \"/fixtures/{fixture_id}\"")
};
let _ = writeln!(
out,
" {{:ok, client}} = {module_path}.{factory}(\"test-key\", base_url: {base_url_expr})"
);
}
}
let returns_result = call_overrides
.and_then(|o| o.returns_result)
.unwrap_or(call_config.returns_result || client_factory.is_some());
let result_is_simple = call_config.result_is_simple || call_overrides.is_some_and(|o| o.result_is_simple);
let is_streaming = crate::e2e::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
let chunks_var = "chunks";
let actual_result_var = if fixture.assertions.is_empty() && !is_streaming {
format!("_{result_var}")
} else {
result_var.to_string()
};
let call_invocation = if effective_args.is_empty() {
format!("{module_path}.{function_name}()")
} else {
format!("{module_path}.{function_name}({effective_args})")
};
if returns_result {
let _ = writeln!(out, " {{:ok, {actual_result_var}}} = {call_invocation}");
} else {
let _ = writeln!(out, " {actual_result_var} = {call_invocation}");
}
if is_streaming {
if let Some(collect) = crate::e2e::codegen::streaming_assertions::StreamingFieldResolver::collect_snippet(
"elixir",
&result_var,
chunks_var,
) {
let _ = writeln!(out, " {collect}");
}
}
for assertion in &fixture.assertions {
render_assertion(
out,
assertion,
if is_streaming { chunks_var } else { &result_var },
field_resolver,
&module_path,
e2e_config.effective_fields_enum(call_config),
resolved_enum_fields_ref,
result_is_simple,
is_streaming,
);
}
if needs_api_key_skip {
let _ = writeln!(out, " end");
}
let _ = writeln!(out, " end");
let _ = writeln!(out, " end");
}
#[allow(clippy::too_many_arguments)]
fn build_args_and_setup(
input: &serde_json::Value,
args: &[crate::e2e::config::ArgMapping],
module_path: &str,
options_type: Option<&str>,
options_default_fn: Option<&str>,
enum_fields: &HashMap<String, String>,
fixture: &crate::e2e::fixture::Fixture,
_handle_struct_type: Option<&str>,
_handle_atom_list_fields: &std::collections::HashSet<String>,
test_documents_path: &str,
adapter_request_type: Option<&str>,
enums: &[crate::core::ir::EnumDef],
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
) -> (Vec<String>, String) {
let fixture_id = &fixture.id;
if args.is_empty() {
let cleaned_input = match input {
serde_json::Value::Object(m) => {
let mut cleaned = m.clone();
cleaned.remove("setup");
if cleaned.is_empty() {
serde_json::Value::Null
} else {
serde_json::Value::Object(cleaned)
}
}
other => other.clone(),
};
let is_empty_input = matches!(cleaned_input, serde_json::Value::Null);
if is_empty_input {
return (Vec::new(), String::new());
}
return (Vec::new(), json_to_elixir(&cleaned_input));
}
let mut setup_lines: Vec<String> = Vec::new();
let mut parts: Vec<String> = Vec::new();
let trailing_keyword_count = args
.iter()
.rev()
.take_while(|a| a.optional)
.filter(|a| {
a.arg_type != "mock_url" && a.arg_type != "mock_url_list" && a.arg_type != "handle"
})
.count();
let use_keyword_form_for_optional_args = trailing_keyword_count >= 2;
for arg in args {
if arg.arg_type == "mock_url" {
if fixture.has_host_root_route() {
let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
setup_lines.push(format!(
"{} = System.get_env(\"{env_key}\") || (System.get_env(\"MOCK_SERVER_URL\") || \"\") <> \"/fixtures/{fixture_id}\"",
arg.name,
));
} else {
setup_lines.push(format!(
"{} = (System.get_env(\"MOCK_SERVER_URL\") || \"\") <> \"/fixtures/{fixture_id}\"",
arg.name,
));
}
if let Some(req_type) = adapter_request_type {
let req_var = format!("{}_req", arg.name);
setup_lines.push(format!("{req_var} = %{module_path}.{req_type}{{url: {}}}", arg.name,));
parts.push(req_var);
} else {
parts.push(arg.name.clone());
}
continue;
}
if arg.arg_type == "mock_url_list" {
let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let val = input.get(field).unwrap_or(&serde_json::Value::Null);
let paths: Vec<String> = if let Some(arr) = val.as_array() {
arr.iter()
.filter_map(|v| v.as_str().map(|s| format!("\"{}\"", escape_elixir(s))))
.collect()
} else {
Vec::new()
};
let paths_literal = paths.join(", ");
let name = &arg.name;
setup_lines.push(format!(
"{name}_base = System.get_env(\"{env_key}\") || ((System.get_env(\"MOCK_SERVER_URL\") || \"\") <> \"/fixtures/{fixture_id}\")"
));
setup_lines.push(format!(
"{name} = Enum.map([{paths_literal}], fn p -> if String.starts_with?(p, \"http\"), do: p, else: {name}_base <> p end)"
));
parts.push(name.clone());
continue;
}
if arg.arg_type == "handle" {
let constructor_name = format!("create_{}", arg.name.to_snake_case());
let config_value = if arg.field == "input" {
input
} else {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
input.get(field).unwrap_or(&serde_json::Value::Null)
};
let name = &arg.name;
if config_value.is_null()
|| config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
{
setup_lines.push(format!("{{:ok, {name}}} = {module_path}.{constructor_name}(nil)"));
} else {
let json_str = serde_json::to_string(config_value).unwrap_or_else(|_| "{}".to_string());
let escaped = escape_elixir(&json_str);
setup_lines.push(format!("{name}_config = \"{escaped}\""));
setup_lines.push(format!(
"{{:ok, {name}}} = {module_path}.{constructor_name}({name}_config)",
));
}
parts.push(arg.name.clone());
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 mut methods: Vec<&crate::core::ir::MethodDef> = type_defs
.iter()
.find(|t| t.name == *trait_name)
.map(|t| t.methods.iter().collect())
.unwrap_or_default();
if let Some(super_trait) = &trait_bridge.super_trait {
if let Some(super_type) = type_defs.iter().find(|t| &t.name == super_trait) {
for method in &super_type.methods {
if !methods.iter().any(|m| m.name == method.name) {
methods.push(method);
}
}
}
}
let elixir_nif_module = format!("{module_path}.Native");
let emission = emit_test_backend(trait_bridge, &methods, fixture, &elixir_nif_module);
if let Some(pos) = emission.setup_block.find("__TRAIT_BRIDGE_MODULE_DEFS_END__") {
let marker_end = emission.setup_block[pos + 32..]
.find('\n')
.map(|i| pos + 32 + i + 1)
.unwrap_or_else(|| emission.setup_block.len());
let test_setup = emission.setup_block[marker_end..].trim_start().to_string();
if !test_setup.is_empty() {
setup_lines.push(test_setup);
}
} else {
setup_lines.push(emission.setup_block);
}
parts.push(emission.arg_expr);
if trait_bridge.register_fn.is_some() {
let backend_name = extract_backend_name_from_input(&fixture.input, &fixture.id);
parts.push(format!("\"{}\"", escape_elixir(&backend_name)));
}
continue;
}
}
let emission = crate::e2e::codegen::TestBackendEmission::unimplemented("elixir");
setup_lines.push(format!("# {}", emission.arg_expr));
parts.push("nil".to_string());
continue;
}
let val = if arg.field == "input" {
Some(input)
} else {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
input.get(field)
};
match val {
None | Some(serde_json::Value::Null) if arg.optional => {
continue;
}
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(),
_ => "nil".to_string(),
};
parts.push(default_val);
}
Some(v) => {
if arg.arg_type == "file_path" {
if let Some(path_str) = v.as_str() {
let full_path = format!("{test_documents_path}/{path_str}");
let formatted = format!("\"{}\"", escape_elixir(&full_path));
if arg.optional {
parts.push(format!("{}: {formatted}", arg.name));
} else {
parts.push(formatted);
}
continue;
}
}
if arg.arg_type == "bytes" {
if let Some(raw) = v.as_str() {
let var_name = &arg.name;
if raw.starts_with('<') || raw.starts_with('{') || raw.starts_with('[') || raw.contains(' ') {
let formatted = format!("\"{}\"", escape_elixir(raw));
if arg.optional {
parts.push(format!("{}: {formatted}", arg.name));
} else {
parts.push(formatted);
}
} else {
let first = raw.chars().next().unwrap_or('\0');
let is_file_path = (first.is_ascii_alphanumeric() || first == '_')
&& raw
.find('/')
.is_some_and(|slash_pos| slash_pos > 0 && raw[slash_pos + 1..].contains('.'));
if is_file_path {
let full_path = format!("{test_documents_path}/{raw}");
let escaped = escape_elixir(&full_path);
setup_lines.push(format!("{var_name} = File.read!(\"{escaped}\")"));
if arg.optional {
parts.push(format!("{}: {var_name}", arg.name));
} else {
parts.push(var_name.to_string());
}
} else {
setup_lines.push(format!(
"{var_name} = Base.decode64!(\"{}\", padding: false)",
escape_elixir(raw)
));
if arg.optional {
parts.push(format!("{}: {var_name}", arg.name));
} else {
parts.push(var_name.to_string());
}
}
}
continue;
}
}
if arg.arg_type == "json_object" && !v.is_null() {
if let (Some(_opts_type), Some(options_fn), Some(obj)) =
(options_type, options_default_fn, v.as_object())
{
let options_var = "options";
setup_lines.push(format!("{options_var} = {module_path}.{options_fn}()"));
for (k, vv) in obj.iter() {
let snake_key = k.to_snake_case();
let elixir_val = if let Some(_enum_type) = enum_fields.get(k) {
if let Some(s) = vv.as_str() {
let snake_val = s.to_snake_case();
format!(":{snake_val}")
} else {
json_to_elixir(vv)
}
} else {
json_to_elixir(vv)
};
setup_lines.push(format!(
"{options_var} = %{{{options_var} | {snake_key}: {elixir_val}}}"
));
}
parts.push(format!("{}: {options_var}", arg.name));
continue;
}
if let (Some(opts_type), None, Some(obj)) = (options_type, options_default_fn, v.as_object()) {
let options_var = "options";
let mut field_strs = Vec::new();
for (k, vv) in obj.iter() {
let snake_key = k.to_snake_case();
let elixir_val = if enum_fields.contains_key(k) {
if let Some(s) = vv.as_str() {
let snake_val = s.to_snake_case();
format!(":{snake_val}")
} else {
json_to_elixir(vv)
}
} else {
json_to_elixir(vv)
};
field_strs.push(format!("{snake_key}: {elixir_val}"));
}
let fields = field_strs.join(", ");
setup_lines.push(format!("{options_var} = %{module_path}.{opts_type}{{{fields}}}"));
if use_keyword_form_for_optional_args && arg.optional {
parts.push(format!("{}: {options_var}", arg.name));
} else {
parts.push(options_var.to_string());
}
continue;
}
if let Some(elem_type) = &arg.element_type {
if v.is_array()
&& let Some(enum_def) = enums.iter().find(|e| &e.name == elem_type && e.serde_tag.is_some())
{
let formatted = emit_tagged_enum_array(v, enum_def, enums);
if arg.optional {
parts.push(format!("{}: {formatted}", arg.name));
} else {
parts.push(formatted);
}
continue;
}
if v.is_array() {
let formatted = json_to_elixir(v);
if arg.optional {
parts.push(format!("{}: {formatted}", arg.name));
} else {
parts.push(formatted);
}
continue;
}
}
if !v.is_null() {
let json_str = serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string());
let escaped = escape_elixir(&json_str);
let formatted = format!("\"{escaped}\"");
if use_keyword_form_for_optional_args && arg.optional {
parts.push(format!("{}: {formatted}", arg.name));
} else {
parts.push(formatted);
}
continue;
}
}
let elixir_val = json_to_elixir(v);
if arg.optional {
parts.push(format!("{}: {elixir_val}", arg.name));
} else {
parts.push(elixir_val);
}
}
}
}
let mut positional_args = Vec::new();
let mut keyword_args = Vec::new();
for part in parts {
let is_keyword = part.contains(": ") && !part.starts_with('"');
if is_keyword {
keyword_args.push(part);
} else {
positional_args.push(part);
}
}
let mut final_args = positional_args;
final_args.extend(keyword_args);
(setup_lines, final_args.join(", "))
}
fn is_numeric_expr(field_expr: &str) -> bool {
field_expr.starts_with("length(")
}
#[allow(clippy::too_many_arguments)]
fn render_assertion(
out: &mut String,
assertion: &Assertion,
result_var: &str,
field_resolver: &FieldResolver,
module_path: &str,
fields_enum: &std::collections::HashSet<String>,
per_call_enum_fields: &HashMap<String, String>,
result_is_simple: bool,
is_streaming: bool,
) {
if let Some(f) = &assertion.field {
match f.as_str() {
"chunks_have_content" => {
let pred =
format!("Enum.all?({result_var}.chunks || [], fn c -> c.content != nil and c.content != \"\" end)");
match assertion.assertion_type.as_str() {
"is_true" => {
let _ = writeln!(out, " assert {pred}");
}
"is_false" => {
let _ = writeln!(out, " refute {pred}");
}
_ => {
let _ = writeln!(
out,
" # skipped: unsupported assertion type on synthetic field '{f}'"
);
}
}
return;
}
"chunks_have_embeddings" => {
let pred = format!(
"Enum.all?({result_var}.chunks || [], fn c -> c.embedding != nil and c.embedding != [] end)"
);
match assertion.assertion_type.as_str() {
"is_true" => {
let _ = writeln!(out, " assert {pred}");
}
"is_false" => {
let _ = writeln!(out, " refute {pred}");
}
_ => {
let _ = writeln!(
out,
" # skipped: unsupported assertion type on synthetic field '{f}'"
);
}
}
return;
}
"chunks_have_heading_context" => {
let pred = format!(
"Enum.all?({result_var}.chunks || [], fn c -> c.metadata != nil and c.metadata.heading_context != nil end)"
);
match assertion.assertion_type.as_str() {
"is_true" => {
let _ = writeln!(out, " assert {pred}");
}
"is_false" => {
let _ = writeln!(out, " refute {pred}");
}
_ => {
let _ = writeln!(
out,
" # skipped: unsupported assertion type on synthetic field '{f}'"
);
}
}
return;
}
"first_chunk_starts_with_heading" => {
let expr = format!(
"case List.first({result_var}.chunks || []) do
c when is_map(c) -> String.trim_leading(c.content || \"\") |> String.starts_with?(\"#\")
_ -> false
end"
);
match assertion.assertion_type.as_str() {
"is_true" => {
let _ = writeln!(out, " assert ({expr})");
}
"is_false" => {
let _ = writeln!(out, " refute ({expr})");
}
_ => {
let _ = writeln!(
out,
" # skipped: unsupported assertion type on synthetic field '{f}'"
);
}
}
return;
}
"embeddings" => {
match assertion.assertion_type.as_str() {
"count_equals" => {
if let Some(val) = &assertion.value {
let ex_val = json_to_elixir(val);
let _ = writeln!(out, " assert length({result_var}) == {ex_val}");
}
}
"count_min" => {
if let Some(val) = &assertion.value {
let ex_val = json_to_elixir(val);
let _ = writeln!(out, " assert length({result_var}) >= {ex_val}");
}
}
"not_empty" => {
let _ = writeln!(out, " assert {result_var} != []");
}
"is_empty" => {
let _ = writeln!(out, " assert {result_var} == []");
}
_ => {
let _ = writeln!(
out,
" # skipped: unsupported assertion type on synthetic field 'embeddings'"
);
}
}
return;
}
"embedding_dimensions" => {
let expr = format!("(if {result_var} == [], do: 0, else: length(hd({result_var})))");
match assertion.assertion_type.as_str() {
"equals" => {
if let Some(val) = &assertion.value {
let ex_val = json_to_elixir(val);
let _ = writeln!(out, " assert {expr} == {ex_val}");
}
}
"greater_than" => {
if let Some(val) = &assertion.value {
let ex_val = json_to_elixir(val);
let _ = writeln!(out, " assert {expr} > {ex_val}");
}
}
_ => {
let _ = writeln!(
out,
" # skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
);
}
}
return;
}
"embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
let pred = match f.as_str() {
"embeddings_valid" => {
format!("Enum.all?({result_var}, fn e -> e != [] end)")
}
"embeddings_finite" => {
format!("Enum.all?({result_var}, fn e -> Enum.all?(e, fn v -> is_float(v) and v == v end) end)")
}
"embeddings_non_zero" => {
format!("Enum.all?({result_var}, fn e -> Enum.any?(e, fn v -> v != 0.0 end) end)")
}
"embeddings_normalized" => {
format!(
"Enum.all?({result_var}, fn e -> n = Enum.reduce(e, 0.0, fn v, acc -> acc + v * v end); abs(n - 1.0) < 1.0e-3 end)"
)
}
_ => unreachable!(),
};
match assertion.assertion_type.as_str() {
"is_true" => {
let _ = writeln!(out, " assert {pred}");
}
"is_false" => {
let _ = writeln!(out, " refute {pred}");
}
_ => {
let _ = writeln!(
out,
" # skipped: unsupported assertion type on synthetic field '{f}'"
);
}
}
return;
}
"keywords" | "keywords_count" => {
let _ = writeln!(
out,
" # skipped: field '{f}' not available on Elixir ExtractionResult"
);
return;
}
_ => {}
}
}
if is_streaming {
if let Some(f) = &assertion.field {
if !f.is_empty() && crate::e2e::codegen::streaming_assertions::is_streaming_virtual_field(f) {
if let Some(expr) =
crate::e2e::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "elixir", result_var)
{
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, " assert length({expr}) >= {n}");
}
}
"count_equals" => {
if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
let _ = writeln!(out, " assert length({expr}) == {n}");
}
}
"equals" => {
if let Some(serde_json::Value::String(s)) = &assertion.value {
let escaped = escape_elixir(s);
let _ = writeln!(out, " assert {expr} == \"{escaped}\"");
} else if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
let _ = writeln!(out, " assert {expr} == {n}");
}
}
"not_empty" => {
let _ = writeln!(out, " assert {expr} != []");
}
"is_empty" => {
let _ = writeln!(out, " assert {expr} == []");
}
"is_true" => {
let _ = writeln!(out, " assert {expr}");
}
"is_false" => {
let _ = writeln!(out, " refute {expr}");
}
"greater_than" => {
if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
let _ = writeln!(out, " assert {expr} > {n}");
}
}
"greater_than_or_equal" => {
if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
let _ = writeln!(out, " assert {expr} >= {n}");
}
}
"contains" => {
if let Some(serde_json::Value::String(s)) = &assertion.value {
let escaped = escape_elixir(s);
let _ = writeln!(out, " assert String.contains?({expr}, \"{escaped}\")");
}
}
_ => {
let _ = writeln!(
out,
" # streaming field '{f}': assertion type '{}' not rendered",
assertion.assertion_type
);
}
}
}
return;
}
}
}
if !result_is_simple {
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_expr = if result_is_simple {
result_var.to_string()
} else {
match &assertion.field {
Some(f) if !f.is_empty() => field_resolver.accessor(f, "elixir", result_var),
_ => result_var.to_string(),
}
};
let is_numeric = is_numeric_expr(&field_expr);
let field_is_enum = assertion.field.as_deref().filter(|f| !f.is_empty()).is_some_and(|f| {
let resolved = field_resolver.resolve(f);
fields_enum.contains(f)
|| fields_enum.contains(resolved)
|| per_call_enum_fields.contains_key(f)
|| per_call_enum_fields.contains_key(resolved)
});
let field_is_format_metadata = assertion
.field
.as_deref()
.filter(|f| !f.is_empty())
.is_some_and(|f| f == "metadata.format" || f.ends_with(".metadata.format"));
let coerced_field_expr = if field_is_format_metadata {
format!("alef_e2e_format_to_string({field_expr})")
} else if field_is_enum {
format!("to_string({field_expr})")
} else {
field_expr.clone()
};
let trimmed_field_expr = if is_numeric {
field_expr.clone()
} else {
format!("String.trim({coerced_field_expr})")
};
let field_is_array = assertion
.field
.as_deref()
.filter(|f| !f.is_empty())
.is_some_and(|f| field_resolver.is_array(field_resolver.resolve(f)));
match assertion.assertion_type.as_str() {
"equals" => {
if let Some(expected) = &assertion.value {
let elixir_val = json_to_elixir(expected);
let is_string_expected = expected.is_string();
if is_string_expected && !is_numeric {
let _ = writeln!(out, " assert {trimmed_field_expr} == {elixir_val}");
} else if field_is_enum {
let _ = writeln!(out, " assert {coerced_field_expr} == {elixir_val}");
} else {
let _ = writeln!(out, " assert {field_expr} == {elixir_val}");
}
}
}
"contains" => {
if let Some(expected) = &assertion.value {
let elixir_val = json_to_elixir(expected);
if field_is_array && expected.is_string() {
let _ = writeln!(
out,
" assert Enum.any?({field_expr}, fn item -> Enum.any?(alef_e2e_item_texts(item), &String.contains?(&1, {elixir_val})) end)"
);
} else {
let _ = writeln!(
out,
" assert String.contains?(to_string({field_expr}), {elixir_val})"
);
}
}
}
"contains_all" => {
if let Some(values) = &assertion.values {
for val in values {
let elixir_val = json_to_elixir(val);
if field_is_array && val.is_string() {
let _ = writeln!(
out,
" assert Enum.any?({field_expr}, fn item -> Enum.any?(alef_e2e_item_texts(item), &String.contains?(&1, {elixir_val})) end)"
);
} else {
let _ = writeln!(
out,
" assert String.contains?(to_string({field_expr}), {elixir_val})"
);
}
}
}
}
"not_contains" => {
if let Some(expected) = &assertion.value {
let elixir_val = json_to_elixir(expected);
if field_is_array && expected.is_string() {
let _ = writeln!(
out,
" refute Enum.any?({field_expr}, fn item -> Enum.any?(alef_e2e_item_texts(item), &String.contains?(&1, {elixir_val})) end)"
);
} else {
let _ = writeln!(
out,
" refute String.contains?(to_string({field_expr}), {elixir_val})"
);
}
}
}
"not_empty" => {
let _ = writeln!(out, " assert {field_expr} != \"\"");
}
"is_empty" => {
if is_numeric {
let _ = writeln!(out, " assert {field_expr} == 0");
} else {
let _ = writeln!(out, " assert is_nil({field_expr}) or {trimmed_field_expr} == \"\"");
}
}
"contains_any" => {
if let Some(values) = &assertion.values {
let items: Vec<String> = values.iter().map(json_to_elixir).collect();
let list_str = items.join(", ");
let _ = writeln!(
out,
" assert Enum.any?([{list_str}], fn v -> String.contains?(to_string({field_expr}), v) end)"
);
}
}
"greater_than" => {
if let Some(val) = &assertion.value {
let elixir_val = json_to_elixir(val);
let _ = writeln!(out, " assert {field_expr} > {elixir_val}");
}
}
"less_than" => {
if let Some(val) = &assertion.value {
let elixir_val = json_to_elixir(val);
let _ = writeln!(out, " assert {field_expr} < {elixir_val}");
}
}
"greater_than_or_equal" => {
if let Some(val) = &assertion.value {
let elixir_val = json_to_elixir(val);
let _ = writeln!(out, " assert {field_expr} >= {elixir_val}");
}
}
"less_than_or_equal" => {
if let Some(val) = &assertion.value {
let elixir_val = json_to_elixir(val);
let _ = writeln!(out, " assert {field_expr} <= {elixir_val}");
}
}
"starts_with" => {
if let Some(expected) = &assertion.value {
let elixir_val = json_to_elixir(expected);
let _ = writeln!(out, " assert String.starts_with?({field_expr}, {elixir_val})");
}
}
"ends_with" => {
if let Some(expected) = &assertion.value {
let elixir_val = json_to_elixir(expected);
let _ = writeln!(out, " assert String.ends_with?({field_expr}, {elixir_val})");
}
}
"min_length" => {
if let Some(val) = &assertion.value {
if let Some(n) = val.as_u64() {
let _ = writeln!(
out,
" assert (is_binary({field_expr}) && byte_size({field_expr}) >= {n}) || (is_list({field_expr}) && length({field_expr}) >= {n}) || (is_binary({field_expr}) == false && is_list({field_expr}) == false && String.length({field_expr}) >= {n})"
);
}
}
}
"max_length" => {
if let Some(val) = &assertion.value {
if let Some(n) = val.as_u64() {
let _ = writeln!(
out,
" assert (is_binary({field_expr}) && byte_size({field_expr}) <= {n}) || (is_list({field_expr}) && length({field_expr}) <= {n}) || (is_binary({field_expr}) == false && is_list({field_expr}) == false && String.length({field_expr}) <= {n})"
);
}
}
}
"count_min" => {
if let Some(val) = &assertion.value {
if let Some(n) = val.as_u64() {
let _ = writeln!(out, " assert length({field_expr}) >= {n}");
}
}
}
"count_equals" => {
if let Some(val) = &assertion.value {
if let Some(n) = val.as_u64() {
let _ = writeln!(out, " assert length({field_expr}) == {n}");
}
}
}
"is_true" => {
let _ = writeln!(out, " assert {field_expr} == true");
}
"is_false" => {
let _ = writeln!(out, " assert {field_expr} == false");
}
"method_result" => {
if let Some(method_name) = &assertion.method {
let call_expr = build_elixir_method_call(result_var, method_name, assertion.args.as_ref(), module_path);
let check = assertion.check.as_deref().unwrap_or("is_true");
match check {
"equals" => {
if let Some(val) = &assertion.value {
let elixir_val = json_to_elixir(val);
let _ = writeln!(out, " assert {call_expr} == {elixir_val}");
}
}
"is_true" => {
let _ = writeln!(out, " assert {call_expr} == true");
}
"is_false" => {
let _ = writeln!(out, " assert {call_expr} == false");
}
"greater_than_or_equal" => {
if let Some(val) = &assertion.value {
let n = val.as_u64().unwrap_or(0);
let _ = writeln!(out, " assert {call_expr} >= {n}");
}
}
"count_min" => {
if let Some(val) = &assertion.value {
let n = val.as_u64().unwrap_or(0);
let _ = writeln!(out, " assert length({call_expr}) >= {n}");
}
}
"contains" => {
if let Some(val) = &assertion.value {
let elixir_val = json_to_elixir(val);
let _ = writeln!(out, " assert String.contains?({call_expr}, {elixir_val})");
}
}
"is_error" => {
let _ = writeln!(out, " assert_raise RuntimeError, fn -> {call_expr} end");
}
other_check => {
panic!("Elixir e2e generator: unsupported method_result check type: {other_check}");
}
}
} else {
panic!("Elixir e2e generator: method_result assertion missing 'method' field");
}
}
"matches_regex" => {
if let Some(expected) = &assertion.value {
let elixir_val = json_to_elixir(expected);
let _ = writeln!(out, " assert Regex.match?(~r/{elixir_val}/, {field_expr})");
}
}
"not_error" => {
}
"error" => {
}
other => {
panic!("Elixir e2e generator: unsupported assertion type: {other}");
}
}
}
fn build_elixir_method_call(
result_var: &str,
method_name: &str,
args: Option<&serde_json::Value>,
module_path: &str,
) -> String {
match method_name {
"root_child_count" => format!("{module_path}.root_child_count({result_var})"),
"has_error_nodes" => format!("{module_path}.tree_has_error_nodes({result_var})"),
"error_count" | "tree_error_count" => format!("{module_path}.tree_error_count({result_var})"),
"tree_to_sexp" => format!("{module_path}.tree_to_sexp({result_var})"),
"contains_node_type" => {
let node_type = args
.and_then(|a| a.get("node_type"))
.and_then(|v| v.as_str())
.unwrap_or("");
format!("{module_path}.tree_contains_node_type({result_var}, \"{node_type}\")")
}
"find_nodes_by_type" => {
let node_type = args
.and_then(|a| a.get("node_type"))
.and_then(|v| v.as_str())
.unwrap_or("");
format!("{module_path}.find_nodes_by_type({result_var}, \"{node_type}\")")
}
"run_query" => {
let query_source = args
.and_then(|a| a.get("query_source"))
.and_then(|v| v.as_str())
.unwrap_or("");
let language = args
.and_then(|a| a.get("language"))
.and_then(|v| v.as_str())
.unwrap_or("");
format!("{module_path}.run_query({result_var}, \"{language}\", \"{query_source}\", source)")
}
_ => format!("{module_path}.{method_name}({result_var})"),
}
}
fn elixir_module_name(category: &str) -> String {
use heck::ToUpperCamelCase;
category.to_upper_camel_case()
}
fn json_to_elixir(value: &serde_json::Value) -> String {
match value {
serde_json::Value::String(s) => format!("\"{}\"", escape_elixir(s)),
serde_json::Value::Bool(true) => "true".to_string(),
serde_json::Value::Bool(false) => "false".to_string(),
serde_json::Value::Number(n) => {
let s = n.to_string().replace("e+", "e");
if s.contains('e') && !s.contains('.') {
s.replacen('e', ".0e", 1)
} else {
s
}
}
serde_json::Value::Null => "nil".to_string(),
serde_json::Value::Array(arr) => {
let items: Vec<String> = arr.iter().map(json_to_elixir).collect();
format!("[{}]", items.join(", "))
}
serde_json::Value::Object(map) => {
let entries: Vec<String> = map
.iter()
.map(|(k, v)| format!("\"{}\" => {}", escape_elixir(k), json_to_elixir(v)))
.collect();
format!("%{{{}}}", entries.join(", "))
}
}
}
fn build_elixir_visitor(setup_lines: &mut Vec<String>, visitor_spec: &crate::e2e::fixture::VisitorSpec) -> String {
use std::fmt::Write as FmtWrite;
let mut visitor_obj = String::new();
let _ = writeln!(visitor_obj, "%{{");
for (method_name, action) in &visitor_spec.callbacks {
emit_elixir_visitor_method(&mut visitor_obj, method_name, action);
}
let _ = writeln!(visitor_obj, " }}");
setup_lines.push(format!("visitor = {visitor_obj}"));
"visitor".to_string()
}
fn emit_elixir_visitor_method(out: &mut String, method_name: &str, action: &CallbackAction) {
use std::fmt::Write as FmtWrite;
let handle_method = format!("handle_{}", &method_name[6..]);
let arg_binding = match action {
CallbackAction::CustomTemplate { .. } => "args",
_ => "_args",
};
let _ = writeln!(out, " :{handle_method} => fn({arg_binding}) ->");
match action {
CallbackAction::Skip => {
let _ = writeln!(out, " :skip");
}
CallbackAction::Continue => {
let _ = writeln!(out, " :continue");
}
CallbackAction::PreserveHtml => {
let _ = writeln!(out, " :preserve_html");
}
CallbackAction::Custom { output } => {
let escaped = escape_elixir(output);
let _ = writeln!(out, " {{:custom, \"{escaped}\"}}");
}
CallbackAction::CustomTemplate { template, .. } => {
let expr = template_to_elixir_concat(template);
let _ = writeln!(out, " {{:custom, {expr}}}");
}
}
let _ = writeln!(out, " end,");
}
fn template_to_elixir_concat(template: &str) -> String {
let mut parts: Vec<String> = Vec::new();
let mut static_buf = String::new();
let mut chars = template.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '{' {
let mut key = String::new();
let mut closed = false;
for kc in chars.by_ref() {
if kc == '}' {
closed = true;
break;
}
key.push(kc);
}
if closed && !key.is_empty() {
if !static_buf.is_empty() {
let escaped = escape_elixir(&static_buf);
parts.push(format!("\"{escaped}\""));
static_buf.clear();
}
let escaped_key = escape_elixir(&key);
parts.push(format!("Map.get(args, \"{escaped_key}\", \"\")"));
} else {
static_buf.push('{');
static_buf.push_str(&key);
if !closed {
}
}
} else {
static_buf.push(ch);
}
}
if !static_buf.is_empty() {
let escaped = escape_elixir(&static_buf);
parts.push(format!("\"{escaped}\""));
}
if parts.is_empty() {
return "\"\"".to_string();
}
parts.join(" <> ")
}
fn fixture_has_elixir_callable(fixture: &Fixture, e2e_config: &E2eConfig) -> bool {
if fixture.is_http_test() {
return false;
}
let call_config = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let elixir_override = call_config
.overrides
.get("elixir")
.or_else(|| e2e_config.call.overrides.get("elixir"));
if elixir_override.and_then(|o| o.client_factory.as_deref()).is_some() {
return true;
}
let function_from_override = elixir_override.and_then(|o| o.function.as_deref());
function_from_override.is_some() || !call_config.function.is_empty()
}
fn apply_rename_all(name: &str, strategy: Option<&str>) -> String {
use heck::{ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToUpperCamelCase};
match strategy {
Some("snake_case") | None => name.to_snake_case(),
Some("camelCase") => name.to_lower_camel_case(),
Some("PascalCase") => name.to_upper_camel_case(),
Some("SCREAMING_SNAKE_CASE") | Some("UPPERCASE") => name.to_shouty_snake_case(),
Some("kebab-case") => name.to_kebab_case(),
Some("SCREAMING-KEBAB-CASE") => name.to_shouty_kebab_case(),
Some("lowercase") => name.to_lowercase(),
Some(_) => name.to_snake_case(),
}
}
fn match_unit_enum_atom(value: &serde_json::Value, enum_def: &crate::core::ir::EnumDef) -> Option<String> {
let s = value.as_str()?;
if enum_def.variants.iter().any(|v| !v.fields.is_empty()) {
return None;
}
for variant in &enum_def.variants {
let wire_tag = variant
.serde_rename
.clone()
.unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
if wire_tag == s {
return Some(format!(":{}", variant.name.to_snake_case()));
}
}
None
}
fn emit_tagged_enum_array(
value: &serde_json::Value,
enum_def: &crate::core::ir::EnumDef,
all_enums: &[crate::core::ir::EnumDef],
) -> String {
let arr = match value.as_array() {
Some(a) => a,
None => return json_to_elixir(value),
};
let tag_key = enum_def.serde_tag.as_deref().unwrap_or("type");
let mut elements: Vec<String> = Vec::with_capacity(arr.len());
for item in arr {
let obj = match item.as_object() {
Some(o) => o,
None => {
elements.push(json_to_elixir(item));
continue;
}
};
let tag_value = obj.get(tag_key).and_then(|v| v.as_str()).unwrap_or("");
let matched = enum_def.variants.iter().find(|variant| {
let wire_tag = variant
.serde_rename
.clone()
.unwrap_or_else(|| apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
wire_tag == tag_value
});
let Some(variant) = matched else {
elements.push(json_to_elixir(item));
continue;
};
let variant_atom = format!(":{}", variant.name.to_snake_case());
if variant.fields.is_empty() {
elements.push(variant_atom);
continue;
}
let mut field_strs: Vec<String> = Vec::with_capacity(variant.fields.len());
for field in &variant.fields {
let wire_field = field.serde_rename.as_deref().unwrap_or(&field.name);
let rust_field_atom = field.name.clone();
let emitted_val = if let Some(field_val) = obj.get(wire_field) {
if let crate::core::ir::TypeRef::Named(type_name) = &field.ty {
all_enums
.iter()
.find(|e| &e.name == type_name && e.serde_tag.is_none())
.and_then(|nested| match_unit_enum_atom(field_val, nested))
.unwrap_or_else(|| json_to_elixir(field_val))
} else {
json_to_elixir(field_val)
}
} else if field.optional {
"nil".to_string()
} else {
continue;
};
field_strs.push(format!("{rust_field_atom}: {emitted_val}"));
}
let map_body = field_strs.join(", ");
elements.push(format!("{{{variant_atom}, %{{{map_body}}}}}"));
}
format!("[{}]", elements.join(", "))
}
fn elixir_stub_default(
return_type: &crate::core::ir::TypeRef,
defaults: &dyn crate::codegen::defaults::LanguageDefaults,
) -> String {
use crate::core::ir::{PrimitiveType, TypeRef};
match return_type {
TypeRef::Primitive(PrimitiveType::Bool | PrimitiveType::F32 | PrimitiveType::F64) => {
defaults.emit_default(return_type)
}
TypeRef::Primitive(_) => "1".to_string(),
_ => defaults.emit_default(return_type),
}
}
pub fn emit_test_backend(
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&crate::core::ir::MethodDef],
fixture: &crate::e2e::fixture::Fixture,
nif_module: &str,
) -> super::TestBackendEmission {
use crate::codegen::defaults::language_defaults;
use heck::ToUpperCamelCase;
use std::fmt::Write as _;
let pascal_id = fixture.id.to_upper_camel_case();
let module_name = format!("TestStub{pascal_id}");
let effective_nif_module = if nif_module.is_empty() { "Native" } else { nif_module };
let plugin_name = fixture
.input
.as_object()
.and_then(|obj| obj.values().next()) .and_then(|arg_obj| arg_obj.get("name"))
.and_then(|v| v.as_str())
.unwrap_or(&fixture.id)
.to_string();
let defaults = language_defaults("elixir");
let qualified_module = format!("E2e.TestStubs.{module_name}");
let genserver_module = format!("{}GenServer", qualified_module);
let mut module_defs = String::new();
let _ = writeln!(module_defs, "unless Code.ensure_loaded?({qualified_module}) do");
let _ = writeln!(module_defs, "defmodule {qualified_module} do");
if trait_bridge.super_trait.is_some() {
let _ = writeln!(module_defs, " def name, do: \"{plugin_name}\"");
let _ = writeln!(module_defs, " def version, do: \"test\"");
let _ = writeln!(module_defs, " def initialize, do: :ok");
let _ = writeln!(module_defs, " def shutdown, do: :ok");
}
for method in methods {
let params: Vec<&str> = method.params.iter().map(|p| p.name.as_str()).collect();
let params_str = params.join(", ");
let default_val = elixir_stub_default(&method.return_type, &*defaults);
let return_expr = if method.error_type.is_some() {
format!("{{:ok, {default_val}}}")
} else {
default_val
};
if params_str.is_empty() {
let _ = writeln!(module_defs, " def {}, do: {return_expr}", method.name);
} else {
let _ = writeln!(module_defs, " def {}({params_str}), do: {return_expr}", method.name);
}
}
let _ = writeln!(module_defs, "end");
let _ = writeln!(module_defs, "end");
let _ = writeln!(module_defs, "unless Code.ensure_loaded?({genserver_module}) do");
let _ = writeln!(module_defs, "defmodule {genserver_module} do");
let _ = writeln!(module_defs, " use GenServer");
let _ = writeln!(module_defs);
let _ = writeln!(module_defs, " def start_link(_opts) do");
let _ = writeln!(module_defs, " GenServer.start_link(__MODULE__, nil)");
let _ = writeln!(module_defs, " end");
let _ = writeln!(module_defs);
let _ = writeln!(module_defs, " @impl true");
let _ = writeln!(module_defs, " def init(_), do: {{:ok, nil}}");
let _ = writeln!(module_defs);
let _ = writeln!(module_defs, " @impl true");
let _ = writeln!(
module_defs,
" def handle_info({{:trait_call, method_atom, args_json, reply_id}}, state) do"
);
let _ = writeln!(module_defs, " args = Jason.decode!(args_json)");
let _ = writeln!(module_defs, " method_name = to_string(method_atom)");
let _ = writeln!(
module_defs,
" ordered_args = __alef_ordered_args__(method_name, args)"
);
let _ = writeln!(
module_defs,
" result = apply({qualified_module}, String.to_existing_atom(method_name), ordered_args)"
);
let _ = writeln!(module_defs, " result_json = Jason.encode!(result)");
let _ = writeln!(
module_defs,
" {effective_nif_module}.complete_trait_call(reply_id, result_json)"
);
let _ = writeln!(module_defs, " {{:noreply, state}}");
let _ = writeln!(module_defs, " end");
let _ = writeln!(module_defs);
for method in methods {
let args = method
.params
.iter()
.map(|p| format!("args[\"{}\"]", p.name))
.collect::<Vec<_>>()
.join(", ");
let _ = writeln!(
module_defs,
" defp __alef_ordered_args__(\"{}\", args), do: [{}]",
method.name, args
);
}
if trait_bridge.super_trait.is_some() {
let _ = writeln!(module_defs, " defp __alef_ordered_args__(\"version\", _args), do: []");
let _ = writeln!(
module_defs,
" defp __alef_ordered_args__(\"initialize\", _args), do: []"
);
let _ = writeln!(module_defs, " defp __alef_ordered_args__(\"shutdown\", _args), do: []");
}
let _ = writeln!(
module_defs,
" defp __alef_ordered_args__(_method, args) when map_size(args) == 0, do: []"
);
let _ = writeln!(module_defs, "end");
let _ = writeln!(module_defs, "end");
let pid_var = format!("{}_pid", pascal_id.to_lowercase());
let mut test_setup = String::new();
let _ = writeln!(test_setup, "{{:ok, {pid_var}}} = {genserver_module}.start_link(nil)");
let mut combined_setup = module_defs;
combined_setup.push_str("\n__TRAIT_BRIDGE_MODULE_DEFS_END__\n");
combined_setup.push_str(&test_setup);
super::TestBackendEmission {
setup_block: combined_setup,
arg_expr: pid_var,
type_imports: Vec::new(),
teardown_block: String::new(),
}
}
fn extract_backend_name_from_input(input: &serde_json::Value, fallback: &str) -> String {
if let Some(obj) = input.as_object() {
if let Some(s) = obj.get("name").and_then(|v| v.as_str()) {
return s.to_string();
}
for v in obj.values() {
if let Some(inner) = v.as_object() {
if let Some(s) = inner.get("name").and_then(|v| v.as_str()) {
return s.to_string();
}
}
}
for v in obj.values() {
if let Some(s) = v.as_str() {
return s.to_string();
}
}
}
fallback.to_string()
}
#[cfg(test)]
mod test_backend_tests {
use super::emit_test_backend;
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::{MethodDef, PrimitiveType, TypeRef};
use crate::e2e::fixture::Fixture;
fn make_trait_bridge(trait_name: &str) -> TraitBridgeConfig {
TraitBridgeConfig {
trait_name: trait_name.to_string(),
super_trait: Some("Plugin".to_string()),
register_fn: Some(format!("register_{}", trait_name.to_lowercase())),
..Default::default()
}
}
fn make_method(name: &str, required: bool) -> MethodDef {
MethodDef {
name: name.to_string(),
params: vec![],
return_type: TypeRef::Primitive(PrimitiveType::Bool),
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(crate::core::ir::ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: !required,
binding_excluded: false,
binding_exclusion_reason: None,
}
}
fn make_fixture(id: &str) -> Fixture {
Fixture {
id: id.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![],
}
}
#[test]
fn elixir_stub_contains_no_sample_crate_domain_names() {
let bridge = make_trait_bridge("TestTrait");
let required_method = make_method("process", true);
let methods = [&required_method];
let fixture = make_fixture("my_test_fixture");
let emission = emit_test_backend(&bridge, &methods, &fixture, "");
let output = format!("{}\n{}", emission.setup_block, emission.arg_expr);
assert!(
!output.contains("SampleCrate"),
"must not contain literal 'SampleCrate', got:\n{output}"
);
assert!(
!output.contains("sample_crate::"),
"must not contain 'sample_crate::', got:\n{output}"
);
assert!(
!output.contains("SampleCrateBridge"),
"must not contain 'SampleCrateBridge', got:\n{output}"
);
assert!(
output.contains("TestStubMyTestFixture"),
"module name must be derived from fixture id, got:\n{output}"
);
assert!(
output.contains("def process"),
"required method 'process' must be emitted, got:\n{output}"
);
}
#[test]
fn elixir_stub_defmodule_guarded_against_redefinition() {
let bridge = make_trait_bridge("TestTrait");
let required_method = make_method("process", true);
let methods = [&required_method];
let fixture = make_fixture("my_test_fixture");
let emission = emit_test_backend(&bridge, &methods, &fixture, "");
let output = format!("{}\n{}", emission.setup_block, emission.arg_expr);
assert!(
output.contains("unless Code.ensure_loaded?"),
"defmodule must be guarded with `unless Code.ensure_loaded?` to prevent redefine warnings, got:\n{output}"
);
assert!(
emission.setup_block.contains(&emission.arg_expr),
"setup_block must reference the same module atom as arg_expr, got:\narg_expr={}\nsetup_block={}",
emission.arg_expr,
emission.setup_block
);
}
#[test]
fn elixir_stub_uses_fixture_input_name_for_plugin_name() {
let bridge = make_trait_bridge("TestTrait");
let required_method = make_method("process", true);
let methods = [&required_method];
let mut fixture = make_fixture("my_fixture_id");
fixture.input = serde_json::json!({ "backend": { "name": "my-backend-name" } });
let emission = emit_test_backend(&bridge, &methods, &fixture, "");
let output = format!("{}\n{}", emission.setup_block, emission.arg_expr);
assert!(
output.contains("\"my-backend-name\""),
"plugin name must come from fixture.input.<arg>.name, got:\n{output}"
);
}
#[test]
fn elixir_stub_uses_scoped_namespace() {
let bridge = make_trait_bridge("TestTrait");
let required_method = make_method("process", true);
let methods = [&required_method];
let fixture = make_fixture("my_test_fixture");
let emission = emit_test_backend(&bridge, &methods, &fixture, "");
assert!(
emission.setup_block.contains("E2e.TestStubs."),
"setup_block must reference E2e.TestStubs namespace, got:\n{}",
emission.setup_block
);
}
#[test]
fn elixir_stub_emits_genserver_wrapper() {
let bridge = make_trait_bridge("TestTrait");
let required_method = make_method("process", true);
let methods = [&required_method];
let fixture = make_fixture("my_test_fixture");
let emission = emit_test_backend(&bridge, &methods, &fixture, "");
assert!(
emission.setup_block.contains("defmodule") && emission.setup_block.contains("GenServer"),
"setup_block must define a GenServer module, got:\n{}",
emission.setup_block
);
assert!(
emission.setup_block.contains("handle_info"),
"GenServer must implement handle_info for trait_call messages, got:\n{}",
emission.setup_block
);
assert!(
emission.setup_block.contains("complete_trait_call"),
"GenServer must reply via the NIF complete_trait_call/2, got:\n{}",
emission.setup_block
);
assert!(
emission
.setup_block
.contains("ordered_args = __alef_ordered_args__(method_name, args)")
&& emission.setup_block.contains(
"apply(E2e.TestStubs.TestStubMyTestFixture, String.to_existing_atom(method_name), ordered_args)"
),
"GenServer must convert decoded JSON objects into ordered apply/3 args, got:\n{}",
emission.setup_block
);
}
#[test]
fn elixir_stub_orders_callback_args_by_method_signature() {
let bridge = make_trait_bridge("TestTrait");
let mut required_method = make_method("process", true);
required_method.params = vec![
crate::core::ir::ParamDef {
name: "first".to_string(),
ty: crate::core::ir::TypeRef::String,
..Default::default()
},
crate::core::ir::ParamDef {
name: "second".to_string(),
ty: crate::core::ir::TypeRef::String,
..Default::default()
},
];
let methods = [&required_method];
let fixture = make_fixture("my_test_fixture");
let emission = emit_test_backend(&bridge, &methods, &fixture, "");
assert!(
emission
.setup_block
.contains("defp __alef_ordered_args__(\"process\", args), do: [args[\"first\"], args[\"second\"]]"),
"GenServer must emit method-specific ordered args, got:\n{}",
emission.setup_block
);
}
#[test]
fn elixir_stub_arg_expr_is_pid_variable() {
let bridge = make_trait_bridge("TestTrait");
let required_method = make_method("process", true);
let methods = [&required_method];
let fixture = make_fixture("my_test_fixture");
let emission = emit_test_backend(&bridge, &methods, &fixture, "");
assert!(
!emission.arg_expr.contains("."),
"arg_expr must be a PID variable (not a module atom), got:\n{}",
emission.arg_expr
);
assert!(
emission.arg_expr.ends_with("_pid"),
"arg_expr must end with _pid to indicate it is a process identifier, got:\n{}",
emission.arg_expr
);
assert!(
emission
.setup_block
.contains(&format!("{{:ok, {}}}", emission.arg_expr)),
"setup_block must start GenServer and assign its PID to the arg_expr variable, got:\n{}",
emission.setup_block
);
}
}
#[cfg(test)]
mod mix_exs_tests {
use super::render_mix_exs;
use crate::e2e::config::DependencyMode;
#[test]
fn mix_exs_includes_finch_when_mock_server_tests_are_present() {
let output = render_mix_exs(
"liter_llm",
"1.4.0-rc.55",
DependencyMode::Registry,
false, true, true, false, );
assert!(
output.contains(":finch,"),
"mock-server-only project must declare :finch dep, got:\n{output}"
);
assert!(
output.contains(":req,"),
"mock-server-only project must declare :req dep, got:\n{output}"
);
assert!(
output.contains(":jason,"),
"mock-server-only project must declare :jason dep, got:\n{output}"
);
}
#[test]
fn mix_exs_omits_finch_when_no_http_or_mock_server_tests() {
let output = render_mix_exs(
"liter_llm",
"../../packages/elixir",
DependencyMode::Local,
false, false, true, false, );
assert!(
!output.contains(":finch,"),
"pure-NIF project must not declare :finch dep, got:\n{output}"
);
}
}
#[cfg(test)]
mod test_helper_tests {
use super::render_test_helper;
use crate::e2e::config::E2eConfig;
fn make_e2e_config() -> E2eConfig {
E2eConfig::default()
}
#[test]
fn test_helper_harness_path_includes_named_finch_supervisor() {
let config = make_e2e_config();
let output = render_test_helper(false, true, &config);
assert!(
output.contains("Finch.start_link(name: AlefE2EFinch)"),
"uses_harness path must start named Finch pool, got:\n{output}"
);
assert!(
output.contains("ExUnit.start()"),
"uses_harness path must call ExUnit.start(), got:\n{output}"
);
}
#[test]
fn test_helper_http_path_includes_named_finch_supervisor() {
let config = make_e2e_config();
let output = render_test_helper(true, false, &config);
assert!(
output.contains("Finch.start_link(name: AlefE2EFinch)"),
"has_http_tests path must start named Finch pool, got:\n{output}"
);
assert!(
output.contains("ExUnit.start()"),
"has_http_tests path must call ExUnit.start(), got:\n{output}"
);
}
}