use crate::core::config::ResolvedCrateConfig;
use crate::e2e::codegen::transform_json_keys_for_language;
use crate::e2e::escape::{escape_c, sanitize_ident};
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::Fixture;
use heck::ToSnakeCase;
use std::collections::{HashMap, HashSet};
use std::fmt::Write as FmtWrite;
use super::assertions::TargetParams;
use super::docs_input::render_c_docs_json;
use super::{
FieldConfigSources, LeafFieldCheck, build_args_string_c, c_optional_sentinel, emit_nested_accessor,
ensure_leaf_field_exists, infer_opaque_handle_type, is_primitive_c_type, is_skipped_c_field, json_to_c,
render_assertion, render_bytes_test_function, render_c_diagnostic_skip, render_engine_factory_test_function,
render_streaming_test_function, resolve_c_client_owner_type, resolve_c_streaming_adapter, try_emit_enum_accessor,
validate_c_snippet_metadata,
};
pub(super) fn emit_c_error_epilogue(
out: &mut String,
prefix: &str,
fixture: &Fixture,
errors: &[crate::core::ir::ErrorDef],
documentation_snippet: bool,
) {
if documentation_snippet {
return;
}
use crate::e2e::codegen::declared_error_variant::{DeclaredErrorAssertion, classify, skip_line};
match classify("c", fixture, errors) {
DeclaredErrorAssertion::Undeclared => {}
DeclaredErrorAssertion::Assert(declared) => {
let expected = escape_c(declared);
let _ = writeln!(out, " {{");
let _ = writeln!(out, " const char* _err_message = {prefix}_last_error_context();");
let _ = writeln!(
out,
" assert(_err_message != NULL && \"expected an error message\");"
);
let _ = writeln!(
out,
" assert(strstr(_err_message, \"{expected}\") != NULL && \"error message mismatch\");"
);
let _ = writeln!(out, " }}");
}
DeclaredErrorAssertion::Unsubstantiable(variant) => {
let _ = writeln!(out, "{}", skip_line(" ", "//", variant, &fixture.id, "c"));
}
}
crate::e2e::codegen::error_path_assertions::emit(out, fixture, " // ", "c");
}
const SNIPPET_TEST_SKIP_MACRO: &str = concat!(
"#ifndef ALEF_TEST_SKIP\n",
"#define ALEF_TEST_SKIP(reason) do { \\\n",
" fprintf(stderr, \"skipped: %s\\n\", (reason)); \\\n",
" return EXIT_SUCCESS; \\\n",
"} while (0)\n",
"#endif",
);
const SNIPPET_TEST_PASS_MACRO: &str = concat!(
"#ifndef ALEF_TEST_PASS\n",
"#define ALEF_TEST_PASS() do { return EXIT_SUCCESS; } while (0)\n",
"#endif",
);
fn snippet_declarations(body: &str) -> String {
let mut declarations = Vec::new();
if body.contains("ALEF_TEST_SKIP(") {
declarations.push(SNIPPET_TEST_SKIP_MACRO);
}
if body.contains("ALEF_TEST_PASS()") {
declarations.push(SNIPPET_TEST_PASS_MACRO);
}
declarations.join("\n")
}
fn is_expected_result_assertion(line: &str, result_var: &str) -> bool {
line.trim_start().starts_with("assert(") && line.contains(result_var) && line.contains("expected call to fail")
}
fn declared_variable(line: &str) -> Option<&str> {
let (declarator, _) = line.split_once('=')?;
let declarator = declarator.trim();
if declarator.contains(['(', ')', '{', '}', '!', '<', '>', ',']) || declarator.ends_with(['*', '=']) {
return None;
}
if declarator.split_whitespace().count() < 2 {
return None;
}
declarator
.rsplit(|character: char| character.is_whitespace() || character == '*')
.next()
}
fn assemble_snippet_body(
function: &str,
result_var: &str,
expects_error: bool,
fixture_id: &str,
unexpected_success_condition: &str,
) -> anyhow::Result<String> {
let failure_check = format!("if ({unexpected_success_condition}) {{ return EXIT_FAILURE; }}");
let body_line_count = function.lines().count().saturating_sub(3);
let mut result_declared = false;
let mut lines: Vec<String> = Vec::new();
for raw_line in function.lines().skip(2).take(body_line_count) {
if !expects_error && raw_line.trim_start().starts_with("assert(") {
continue;
}
let line = raw_line.strip_prefix(" ").unwrap_or(raw_line);
if declared_variable(line) == Some(result_var) {
result_declared = true;
} else if expects_error && is_expected_result_assertion(line, result_var) {
if !result_declared {
anyhow::bail!(
"C e2e generator: fixture `{fixture_id}` would emit the `{result_var}` failure guard before \
`{result_var}` is declared; the call producing `{result_var}` must be emitted first"
);
}
lines.push(failure_check.clone());
continue;
}
lines.push(line.to_string());
}
Ok(lines.join("\n"))
}
pub(super) struct SnippetContext<'a> {
pub fixture: &'a Fixture,
pub e2e_config: &'a crate::e2e::config::E2eConfig,
pub header: &'a str,
pub prefix: &'a str,
pub info: &'a super::ResolvedCallInfo,
pub field_resolver: &'a FieldResolver,
pub config: &'a ResolvedCrateConfig,
pub type_defs: &'a [crate::core::ir::TypeDef],
pub ir: super::CallIr<'a>,
}
pub(super) fn render_snippet_body(context: SnippetContext<'_>) -> anyhow::Result<String> {
let SnippetContext {
fixture,
e2e_config,
header,
prefix,
info,
field_resolver,
config,
type_defs,
ir,
} = context;
let call = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
if fixture.visitor.is_some() {
return super::visitor::render_visitor_snippet(fixture, header, prefix, e2e_config, config, ir);
}
if fixture
.resolved_args(call)
.iter()
.any(|argument| argument.arg_type == "test_backend")
{
return super::trait_bridge_snippet::render(fixture, header, prefix, config, type_defs);
}
let target_params = if crate::e2e::codegen::recipe::trait_bridge_derived_c_identity(config, fixture).is_some() {
TargetParams::Known(&[])
} else {
TargetParams::resolve(call, "c", ir)
};
if info.returns_void {
let prefix_upper = crate::codegen::c_consumer::export_type_prefix(prefix);
let mut setup = String::new();
let (typed_arg_handles, typed_arg_cleanup) = build_json_object_arg_handles(
&mut setup,
fixture,
prefix,
&prefix_upper,
&info.args,
&info.options_type_name,
true,
);
let mut arg_parts: Vec<String> = if info.args.is_empty() {
Vec::new()
} else {
vec![build_args_string_c(
&fixture.input,
&info.args,
&typed_arg_handles,
config,
type_defs,
fixture,
&info.function_name,
target_params,
)?]
};
arg_parts.extend(info.extra_args.iter().cloned());
let args = arg_parts.join(", ");
let call_line = crate::e2e::template_env::render(
"c/snippet_void_call.jinja",
minijinja::context! { function_name => info.function_name, args => args },
);
let mut cleanup = String::new();
render_typed_arg_cleanup(&mut cleanup, prefix, &typed_arg_cleanup);
let dedent = |text: &str| -> String {
text.lines()
.map(|line| line.strip_prefix(" ").unwrap_or(line))
.collect::<Vec<_>>()
.join("\n")
};
let mut body = dedent(&setup);
if !body.is_empty() {
body.push('\n');
}
body.push_str(call_line.trim_end());
let cleanup = dedent(&cleanup);
if !cleanup.is_empty() {
body.push('\n');
body.push_str(&cleanup);
}
return Ok(crate::e2e::template_env::render(
"c/snippet_body.jinja",
minijinja::context! { header => header, declarations => "", body => body.trim_end() },
));
}
validate_c_snippet_metadata(
config,
type_defs,
fixture,
&info.function_name,
info.client_factory.as_deref(),
info.c_engine_factory.as_deref(),
info.streaming,
)?;
let expects_error = fixture
.assertions
.iter()
.any(|assertion| assertion.assertion_type == "error");
let result_var = call.effective_result_var();
let mut call_fixture = fixture.clone();
if !expects_error {
call_fixture.assertions.clear();
}
let config_sources = FieldConfigSources::resolve(e2e_config, call);
let mut function = String::new();
render_test_function_impl(
&mut function,
&call_fixture,
prefix,
&info.function_name,
result_var,
&info.args,
field_resolver,
e2e_config.effective_fields_c_types(call),
e2e_config.effective_fields_enum(call),
&info.result_type_name,
&info.options_type_name,
info.client_factory.as_deref(),
info.raw_c_result_type.as_deref(),
info.c_free_fn.as_deref(),
info.c_engine_factory.as_deref(),
info.result_is_option,
info.result_is_bytes,
info.streaming,
&info.extra_args,
config,
type_defs,
&[],
true,
&config_sources,
target_params,
)?;
let unexpected_success_condition = if info.result_type_name.returns_status_code() {
format!("{result_var} == 0")
} else {
format!("{result_var} != 0")
};
let body = assemble_snippet_body(
&function,
result_var,
expects_error,
&fixture.id,
&unexpected_success_condition,
)?;
Ok(crate::e2e::template_env::render(
"c/snippet_body.jinja",
minijinja::context! { header => header, declarations => snippet_declarations(&body), body => body },
))
}
#[allow(clippy::too_many_arguments)]
pub(super) fn render_test_function(
out: &mut String,
fixture: &Fixture,
prefix: &str,
function_name: &str,
result_var: &str,
args: &[crate::e2e::config::ArgMapping],
field_resolver: &FieldResolver,
fields_c_types: &HashMap<String, String>,
fields_enum: &HashSet<String>,
result_type_name: &super::ResultTypeName,
options_type_name: &str,
client_factory: Option<&str>,
raw_c_result_type: Option<&str>,
c_free_fn: Option<&str>,
c_engine_factory: Option<&str>,
result_is_option: bool,
result_is_bytes: bool,
streaming: Option<bool>,
extra_args: &[String],
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
errors: &[crate::core::ir::ErrorDef],
documentation_snippet: bool,
config_sources: &FieldConfigSources,
) -> anyhow::Result<()> {
render_test_function_impl(
out,
fixture,
prefix,
function_name,
result_var,
args,
field_resolver,
fields_c_types,
fields_enum,
result_type_name,
options_type_name,
client_factory,
raw_c_result_type,
c_free_fn,
c_engine_factory,
result_is_option,
result_is_bytes,
streaming,
extra_args,
config,
type_defs,
errors,
documentation_snippet,
config_sources,
TargetParams::IrAbsent,
)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn render_test_function_impl(
out: &mut String,
fixture: &Fixture,
prefix: &str,
function_name: &str,
result_var: &str,
args: &[crate::e2e::config::ArgMapping],
field_resolver: &FieldResolver,
fields_c_types: &HashMap<String, String>,
fields_enum: &HashSet<String>,
result_type_name: &super::ResultTypeName,
options_type_name: &str,
client_factory: Option<&str>,
raw_c_result_type: Option<&str>,
c_free_fn: Option<&str>,
c_engine_factory: Option<&str>,
result_is_option: bool,
result_is_bytes: bool,
streaming: Option<bool>,
extra_args: &[String],
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
errors: &[crate::core::ir::ErrorDef],
documentation_snippet: bool,
config_sources: &FieldConfigSources,
target_params: TargetParams<'_>,
) -> anyhow::Result<()> {
let fn_name = sanitize_ident(&fixture.id);
let description = &fixture.description;
let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
let _ = writeln!(out, "void test_{fn_name}(void) {{");
let _ = writeln!(out, " /* {description} */");
let has_mock = fixture.needs_mock_server() && !documentation_snippet;
let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
if documentation_snippet && client_factory.is_some() {
let variable = crate::e2e::fixture::FixtureEnv::api_key_var_or_default(fixture.env.as_ref());
let _ = writeln!(out, " const char* api_key = getenv(\"{variable}\");");
let _ = writeln!(out, " assert(api_key != NULL && \"{variable} must be set\");");
}
if let Some(env) = &fixture.env
&& let Some(var) = &env.api_key_var
{
let fixture_id = &fixture.id;
if has_mock {
let _ = writeln!(out, " const char* api_key = getenv(\"{var}\");");
let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
let _ = writeln!(out, " char base_url_buf[512];");
let _ = writeln!(out, " int use_mock = !(api_key && api_key[0] != '\\0');");
let _ = writeln!(out, " if (!use_mock) {{");
let _ = writeln!(
out,
" fprintf(stderr, \"{fixture_id}: using real API ({var} is set)\\n\");"
);
let _ = writeln!(out, " }} else {{");
let _ = writeln!(
out,
" fprintf(stderr, \"{fixture_id}: using mock server ({var} not set)\\n\");"
);
let _ = writeln!(
out,
" snprintf(base_url_buf, sizeof(base_url_buf), \"%s/fixtures/{fixture_id}\", mock_base ? mock_base : \"\");"
);
let _ = writeln!(out, " api_key = \"test-key\";");
let _ = writeln!(out, " }}");
} else {
out.push_str(&crate::e2e::template_env::render(
"c/test_skip_if_env_missing.jinja",
minijinja::context! { env_var => var },
));
}
}
let prefix_upper = crate::codegen::c_consumer::export_type_prefix(prefix);
if result_type_name.returns_status_code() {
if let Some(field) = fixture
.assertions
.iter()
.filter_map(|assertion| assertion.field.as_deref())
.find(|field| !field.is_empty())
{
anyhow::bail!(
"C e2e generator: fixture `{}` asserts on field `{field}` of `{function_name}`, but \
that export is a trait-bridge registry function returning an `i32` status code -- \
it has no result object and therefore no fields. Assert on the status instead \
(`not_error` / `error`), or point the fixture at the call that returns the value \
being checked.",
fixture.id
);
}
let (typed_arg_handles, typed_arg_cleanup) = build_json_object_arg_handles(
out,
fixture,
prefix,
&prefix_upper,
args,
options_type_name,
documentation_snippet,
);
let mut arg_parts: Vec<String> = if args.is_empty() {
Vec::new()
} else {
vec![build_args_string_c(
&fixture.input,
args,
&typed_arg_handles,
config,
type_defs,
fixture,
function_name,
target_params,
)?]
};
arg_parts.extend(extra_args.iter().cloned());
let args_str = arg_parts.join(", ");
let _ = writeln!(out, " int32_t {result_var} = {function_name}({args_str});");
if expects_error {
let _ = writeln!(out, " assert({result_var} != 0 && \"expected call to fail\");");
emit_c_error_epilogue(out, prefix, fixture, errors, documentation_snippet);
} else {
let _ = writeln!(out, " assert({result_var} == 0 && \"expected call to succeed\");");
}
render_typed_arg_cleanup(out, prefix, &typed_arg_cleanup);
let _ = writeln!(out, "}}");
return Ok(());
}
if let Some(config_type) = c_engine_factory {
render_engine_factory_test_function(
out,
fixture,
prefix,
function_name,
result_var,
field_resolver,
fields_c_types,
fields_enum,
result_type_name.require_owned_handle()?,
config_type,
expects_error,
raw_c_result_type,
type_defs,
config_sources,
)?;
return Ok(());
}
if client_factory.is_some() && crate::e2e::codegen::streaming_assertions::resolve_is_streaming(fixture, streaming) {
let Some(streaming) = resolve_c_streaming_adapter(config, function_name) else {
render_c_diagnostic_skip(
out,
"streaming fixture requires matching [[crates.adapters]] metadata for C e2e codegen",
);
return Ok(());
};
render_streaming_test_function(
out,
fixture,
prefix,
result_var,
args,
client_factory.unwrap_or(""),
&streaming,
expects_error,
api_key_var,
documentation_snippet,
);
return Ok(());
}
if let Some(factory) = client_factory
&& result_is_bytes
{
let Some(client_owner_type) = resolve_c_client_owner_type(config, type_defs, function_name) else {
render_c_diagnostic_skip(
out,
"client_factory is configured but C e2e could not resolve the client owner type",
);
return Ok(());
};
render_bytes_test_function(
out,
fixture,
prefix,
function_name,
result_var,
args,
options_type_name,
result_type_name.require()?,
factory,
&client_owner_type,
expects_error,
errors,
documentation_snippet,
);
return Ok(());
}
if let Some(factory) = client_factory {
let Some(_client_owner_type) = resolve_c_client_owner_type(config, type_defs, function_name) else {
render_c_diagnostic_skip(
out,
"client_factory is configured but C e2e could not resolve the client owner type",
);
return Ok(());
};
let result_type_name = result_type_name.require_owned_handle()?;
let mut request_handle_vars: Vec<(String, String)> = Vec::new(); let mut inline_method_args: Vec<String> = Vec::new();
for arg in args {
if arg.arg_type == "json_object" {
let request_type_pascal = if !options_type_name.is_empty() {
options_type_name.to_string()
} else if let Some(stripped) = result_type_name.strip_suffix("Response") {
format!("{}Request", stripped)
} else {
format!("{result_type_name}Request")
};
let request_type_snake = request_type_pascal.to_snake_case();
let var_name = format!("{request_type_snake}_handle");
let json_val = crate::e2e::codegen::resolve_field(&fixture.input, &arg.field);
if !json_val.is_null() {
let val = json_val;
let normalized = transform_json_keys_for_language(val, "snake_case");
let (docs_setup, json_expr, docs_cleanup) = render_c_docs_json(
&arg.name,
&normalized,
&fixture.docs_files_for_arg(&arg.field),
documentation_snippet,
);
out.push_str(&docs_setup);
let _ = writeln!(
out,
" {prefix_upper}AlefHandle {var_name} = \
{prefix}_{request_type_snake}_from_json({json_expr});"
);
out.push_str(&docs_cleanup);
if expects_error {
out.push_str(&crate::e2e::template_env::render(
"c/test_pass_if_null.jinja",
minijinja::context! { variable => var_name },
));
} else {
let _ = writeln!(out, " assert({var_name} != 0 && \"failed to build request\");");
}
request_handle_vars.push((arg.name.clone(), var_name));
}
} else if arg.arg_type == "string"
|| (arg.arg_type == "mock_url"
&& crate::e2e::codegen::preserved_url_literal(
fixture.preserve_input_urls,
crate::e2e::codegen::resolve_field(&fixture.input, &arg.field),
)
.is_some())
{
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let val = fixture.input.get(field);
match val {
Some(v) if v.is_string() => {
let s = v.as_str().unwrap_or_default();
let escaped = escape_c(s);
inline_method_args.push(format!("\"{escaped}\""));
}
Some(serde_json::Value::Null) | None if arg.optional => {
inline_method_args.push("NULL".to_string());
}
None => {
inline_method_args.push("\"\"".to_string());
}
Some(other) => {
let s = serde_json::to_string(other).unwrap_or_default();
let escaped = escape_c(&s);
inline_method_args.push(format!("\"{escaped}\""));
}
}
} else if arg.optional {
inline_method_args.push(c_optional_sentinel(&arg.arg_type).to_string());
}
}
let fixture_id = &fixture.id;
if has_mock && api_key_var.is_some() {
let _ = writeln!(out, " const char* _base_url_arg = use_mock ? base_url_buf : NULL;");
let _ = writeln!(
out,
" {prefix_upper}AlefHandle client = {prefix}_{factory}(api_key, _base_url_arg, (uint64_t)-1, (uint32_t)-1, NULL);"
);
} else if has_mock {
let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
let _ = writeln!(out, " char base_url[1024];");
let _ = writeln!(
out,
" snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
);
let _ = writeln!(
out,
" {prefix_upper}AlefHandle client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
);
} else if documentation_snippet {
let _ = writeln!(
out,
" {prefix_upper}AlefHandle client = {prefix}_{factory}(api_key, NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
);
} else {
let _ = writeln!(
out,
" {prefix_upper}AlefHandle client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
);
}
let _ = writeln!(out, " assert(client != 0 && \"failed to create client\");");
let method_args = if request_handle_vars.is_empty() && inline_method_args.is_empty() && extra_args.is_empty() {
String::new()
} else {
let handles: Vec<String> = request_handle_vars.iter().map(|(_, v)| v.clone()).collect();
let parts: Vec<String> = handles
.into_iter()
.chain(inline_method_args.iter().cloned())
.chain(extra_args.iter().cloned())
.collect();
format!(", {}", parts.join(", "))
};
let call_fn = format!("{prefix}_default_client_{function_name}");
if expects_error {
let _ = writeln!(
out,
" {prefix_upper}AlefHandle {result_var} = {call_fn}(client{method_args});"
);
let _ = writeln!(out, " assert({result_var} == 0 && \"expected call to fail\");");
emit_c_error_epilogue(out, prefix, fixture, errors, documentation_snippet);
for (_, var_name) in &request_handle_vars {
let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
}
let _ = writeln!(out, " {prefix}_default_client_free(client);");
let _ = writeln!(out, "}}");
return Ok(());
}
let _ = writeln!(
out,
" {prefix_upper}AlefHandle {result_var} = {call_fn}(client{method_args});"
);
let _ = writeln!(out, " assert({result_var} != 0 && \"expected call to succeed\");");
let mut intermediate_handles: Vec<(String, String)> = Vec::new();
let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
let mut primitive_locals: HashMap<String, String> = HashMap::new();
let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
for assertion in &fixture.assertions {
if let Some(f) = &assertion.field
&& !f.is_empty()
&& !accessed_fields.iter().any(|(k, _, _)| k == f)
{
let resolved_raw = field_resolver.resolve(f);
let resolved = if let Some(stripped) = field_resolver.namespace_stripped_path(resolved_raw) {
let stripped_first = stripped.split('.').next().unwrap_or(stripped);
let stripped_first = stripped_first.split('[').next().unwrap_or(stripped_first);
if field_resolver.is_valid_for_result(stripped_first) {
stripped
} else {
resolved_raw
}
} else {
resolved_raw
};
let local_var = f.replace(['.', '['], "_").replace(']', "");
let has_map_access = resolved.contains('[');
if resolved.contains('.') {
let leaf_primitive = emit_nested_accessor(
out,
prefix,
resolved,
&local_var,
result_var,
fields_c_types,
fields_enum,
&mut intermediate_handles,
result_type_name,
f,
type_defs,
config_sources,
)?;
if let Some(prim) = leaf_primitive {
primitive_locals.insert(local_var.clone(), prim);
}
} else {
let result_type_snake = result_type_name.to_snake_case();
let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
let lookup_key = format!("{result_type_snake}.{resolved}");
if is_skipped_c_field(fields_c_types, &result_type_snake, resolved) {
primitive_locals.insert(local_var.clone(), "__skip__".to_string());
} else if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
primitive_locals.insert(local_var.clone(), t.clone());
} else if try_emit_enum_accessor(
out,
prefix,
&prefix_upper,
f,
resolved,
&result_type_snake,
&accessor_fn,
result_var,
&local_var,
fields_c_types,
fields_enum,
&mut intermediate_handles,
) {
} else if let Some(handle_pascal) =
infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
{
let _ = writeln!(
out,
" {prefix_upper}AlefHandle {local_var} = {accessor_fn}({result_var});"
);
opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
} else {
ensure_leaf_field_exists(LeafFieldCheck {
prefix,
accessor_fn: &accessor_fn,
resolved,
raw_field: f,
segment: resolved,
parent_snake_type: &result_type_snake,
parent_is_ir_type: type_defs.iter().any(|type_def| type_def.name == result_type_name),
declared_in_fields_c_types: fields_c_types.contains_key(&lookup_key),
result_type_name,
type_defs,
result_fields_source: &config_sources.result_fields,
fields_source: &config_sources.fields,
})?;
let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
}
}
accessed_fields.push((f.clone(), local_var, has_map_access));
}
}
for assertion in &fixture.assertions {
render_assertion(
out,
assertion,
result_var,
prefix,
field_resolver,
&accessed_fields,
&primitive_locals,
&opaque_handle_locals,
);
}
for (_f, local_var, from_json) in &accessed_fields {
if primitive_locals.contains_key(local_var) {
continue;
}
if let Some(snake_type) = opaque_handle_locals.get(local_var) {
let _ = writeln!(out, " {prefix}_{snake_type}_free({local_var});");
continue;
}
if *from_json {
let _ = writeln!(out, " free({local_var});");
} else {
let _ = writeln!(out, " {prefix}_free_string({local_var});");
}
}
for (handle_var, snake_type) in intermediate_handles.iter().rev() {
if snake_type == "free_string" {
let _ = writeln!(out, " {prefix}_free_string({handle_var});");
} else if snake_type == "free" {
let _ = writeln!(out, " free({handle_var});");
} else {
let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
}
}
let result_type_snake = result_type_name.to_snake_case();
let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
for (_, var_name) in &request_handle_vars {
let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
}
let _ = writeln!(out, " {prefix}_default_client_free(client);");
let _ = writeln!(out, "}}");
return Ok(());
}
if let Some(raw_type) = raw_c_result_type {
let args_str = if args.is_empty() {
String::new()
} else {
let parts: Vec<String> = args
.iter()
.filter_map(|arg| {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let val = fixture.input.get(field);
match val {
None if arg.optional => Some(c_optional_sentinel(&arg.arg_type).to_string()),
None => None,
Some(v) if v.is_null() && arg.optional => Some(c_optional_sentinel(&arg.arg_type).to_string()),
Some(v) => Some(json_to_c(v)),
}
})
.collect();
parts.join(", ")
};
let _ = writeln!(out, " {raw_type} {result_var} = {function_name}({args_str});");
if expects_error {
match raw_type {
"char*" => {
let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
}
"int32_t" => {
let _ = writeln!(out, " assert({result_var} < 0 && \"expected call to fail\");");
}
_ => {
let _ = writeln!(
out,
" assert({prefix}_last_error_code() != 0 && \"expected call to fail\");"
);
}
}
emit_c_error_epilogue(out, prefix, fixture, errors, documentation_snippet);
let _ = writeln!(out, "}}");
return Ok(());
}
let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
if has_not_error {
match raw_type {
"char*" if !result_is_option => {
let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
}
"int32_t" => {
let _ = writeln!(out, " assert({result_var} >= 0 && \"expected call to succeed\");");
}
"uintptr_t" => {
let _ = writeln!(
out,
" assert({prefix}_last_error_code() == 0 && \"expected call to succeed\");"
);
}
_ => {}
}
}
for assertion in &fixture.assertions {
match assertion.assertion_type.as_str() {
"not_error" | "error" => {} "not_empty" => {
let _ = writeln!(
out,
" assert({result_var} != NULL && strlen({result_var}) > 0 && \"expected non-empty value\");"
);
}
"is_empty" => {
if result_is_option && raw_type == "char*" {
let _ = writeln!(
out,
" assert({result_var} == NULL && \"expected empty/null value\");"
);
} else {
let _ = writeln!(
out,
" assert(strlen({result_var}) == 0 && \"expected empty value\");"
);
}
}
"count_min" => {
if let Some(val) = &assertion.value
&& let Some(n) = val.as_u64()
{
match raw_type {
"char*" => {
let _ = writeln!(out, " {{");
let _ = writeln!(
out,
" assert({result_var} != NULL && \"expected non-null JSON array\");"
);
let _ = writeln!(out, " int elem_count = alef_json_array_count({result_var});");
let _ = writeln!(
out,
" assert(elem_count >= {n} && \"expected at least {n} elements\");"
);
let _ = writeln!(out, " }}");
}
_ => {
let _ = writeln!(
out,
" assert((size_t){result_var} >= {n} && \"expected at least {n} elements\");"
);
}
}
}
}
"greater_than_or_equal" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert({result_var} >= {c_val} && \"expected greater than or equal\");"
);
}
}
"contains" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
);
}
}
"contains_all" => {
if let Some(values) = &assertion.values {
for val in values {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert(strstr({result_var}, {c_val}) != NULL && \"expected to contain substring\");"
);
}
}
}
"equals" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
if val.is_string() {
let _ = writeln!(
out,
" assert({result_var} != NULL && strcmp({result_var}, {c_val}) == 0 && \"equals assertion failed\");"
);
} else {
let _ = writeln!(
out,
" assert({result_var} == {c_val} && \"equals assertion failed\");"
);
}
}
}
"not_contains" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert(strstr({result_var}, {c_val}) == NULL && \"expected NOT to contain substring\");"
);
}
}
"starts_with" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert(strncmp({result_var}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
);
}
}
"is_true" => {
let _ = writeln!(out, " assert({result_var});");
}
"is_false" => {
let _ = writeln!(out, " assert(!{result_var});");
}
other => {
panic!("C e2e raw-result generator: unsupported assertion type: {other}");
}
}
}
if raw_type == "char*" {
let free_fn = c_free_fn
.map(|s| s.to_string())
.unwrap_or_else(|| format!("{prefix}_free_string"));
if result_is_option {
let _ = writeln!(out, " if ({result_var} != NULL) {{ {free_fn}({result_var}); }}");
} else {
let _ = writeln!(out, " {free_fn}({result_var});");
}
}
let _ = writeln!(out, "}}");
return Ok(());
}
let result_type_label = result_type_name.require()?;
let prefixed_fn = function_name.to_string();
let (typed_arg_handles, typed_arg_cleanup) = build_json_object_arg_handles(
out,
fixture,
prefix,
&prefix_upper,
args,
options_type_name,
documentation_snippet,
);
let configured_args = build_args_string_c(
&fixture.input,
args,
&typed_arg_handles,
config,
type_defs,
fixture,
function_name,
target_params,
)?;
let extra = extra_args.join(", ");
let args_str = match (configured_args.is_empty(), extra.is_empty()) {
(_, true) => configured_args,
(true, false) => extra,
(false, false) => format!("{configured_args}, {extra}"),
};
if let Some(capsule) = config.ffi.as_ref().and_then(|f| f.capsule_types.get(result_type_label)) {
let c_return_type = &capsule.c_return_type;
let _ = writeln!(
out,
" const {c_return_type} *{result_var} = {prefixed_fn}({args_str});"
);
if expects_error {
let _ = writeln!(out, " assert({result_var} == NULL && \"expected call to fail\");");
emit_c_error_epilogue(out, prefix, fixture, errors, documentation_snippet);
render_typed_arg_cleanup(out, prefix, &typed_arg_cleanup);
} else {
render_typed_arg_cleanup(out, prefix, &typed_arg_cleanup);
let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
}
let _ = writeln!(out, "}}");
return Ok(());
}
let result_type_name = result_type_name.require_owned_handle()?;
if expects_error {
let _ = writeln!(
out,
" {prefix_upper}AlefHandle {result_var} = {prefixed_fn}({args_str});"
);
let _ = writeln!(out, " assert({result_var} == 0 && \"expected call to fail\");");
emit_c_error_epilogue(out, prefix, fixture, errors, documentation_snippet);
render_typed_arg_cleanup(out, prefix, &typed_arg_cleanup);
let _ = writeln!(out, "}}");
return Ok(());
}
let _ = writeln!(
out,
" {prefix_upper}AlefHandle {result_var} = {prefixed_fn}({args_str});"
);
let _ = writeln!(out, " assert({result_var} != 0 && \"expected call to succeed\");");
let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
let mut intermediate_handles: Vec<(String, String)> = Vec::new();
let mut primitive_locals: HashMap<String, String> = HashMap::new();
let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
for assertion in &fixture.assertions {
if let Some(f) = &assertion.field
&& !f.is_empty()
&& !accessed_fields.iter().any(|(k, _, _)| k == f)
{
let resolved_raw = field_resolver.resolve(f);
let resolved = if let Some(stripped) = field_resolver.namespace_stripped_path(resolved_raw) {
let stripped_first = stripped.split('.').next().unwrap_or(stripped);
let stripped_first = stripped_first.split('[').next().unwrap_or(stripped_first);
if field_resolver.is_valid_for_result(stripped_first) {
stripped
} else {
resolved_raw
}
} else {
resolved_raw
};
let local_var = f.replace(['.', '['], "_").replace(']', "");
let has_map_access = resolved.contains('[');
if resolved.contains('.') {
let leaf_result = emit_nested_accessor(
out,
prefix,
resolved,
&local_var,
result_var,
fields_c_types,
fields_enum,
&mut intermediate_handles,
result_type_name,
f,
type_defs,
config_sources,
)?;
if let Some(returned_type) = leaf_result {
if is_primitive_c_type(&returned_type) {
primitive_locals.insert(local_var.clone(), returned_type);
} else {
opaque_handle_locals.insert(local_var.clone(), returned_type);
}
}
} else {
let result_type_snake = result_type_name.to_snake_case();
let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
let lookup_key = format!("{result_type_snake}.{resolved}");
if is_skipped_c_field(fields_c_types, &result_type_snake, resolved) {
primitive_locals.insert(local_var.clone(), "__skip__".to_string());
} else if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
primitive_locals.insert(local_var.clone(), t.clone());
} else if try_emit_enum_accessor(
out,
prefix,
&prefix_upper,
f,
resolved,
&result_type_snake,
&accessor_fn,
result_var,
&local_var,
fields_c_types,
fields_enum,
&mut intermediate_handles,
) {
} else if let Some(handle_pascal) =
infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
{
let _ = writeln!(
out,
" {prefix_upper}AlefHandle {local_var} = {accessor_fn}({result_var});"
);
opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
} else {
ensure_leaf_field_exists(LeafFieldCheck {
prefix,
accessor_fn: &accessor_fn,
resolved,
raw_field: f,
segment: resolved,
parent_snake_type: &result_type_snake,
parent_is_ir_type: type_defs.iter().any(|type_def| type_def.name == result_type_name),
declared_in_fields_c_types: fields_c_types.contains_key(&lookup_key),
result_type_name,
type_defs,
result_fields_source: &config_sources.result_fields,
fields_source: &config_sources.fields,
})?;
let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
}
}
accessed_fields.push((f.clone(), local_var.clone(), has_map_access));
}
}
for assertion in &fixture.assertions {
render_assertion(
out,
assertion,
result_var,
prefix,
field_resolver,
&accessed_fields,
&primitive_locals,
&opaque_handle_locals,
);
}
for (_f, local_var, from_json) in &accessed_fields {
if primitive_locals.contains_key(local_var) {
continue;
}
if let Some(snake_type) = opaque_handle_locals.get(local_var) {
let _ = writeln!(out, " {prefix}_{snake_type}_free({local_var});");
continue;
}
if *from_json {
let _ = writeln!(out, " free({local_var});");
} else {
let _ = writeln!(out, " {prefix}_free_string({local_var});");
}
}
for (handle_var, snake_type) in intermediate_handles.iter().rev() {
if snake_type == "free_string" {
let _ = writeln!(out, " {prefix}_free_string({handle_var});");
} else if snake_type == "free" {
let _ = writeln!(out, " free({handle_var});");
} else {
let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
}
}
render_typed_arg_cleanup(out, prefix, &typed_arg_cleanup);
let result_type_snake = result_type_name.to_snake_case();
let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
let _ = writeln!(out, "}}");
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn build_json_object_arg_handles(
out: &mut String,
fixture: &Fixture,
prefix: &str,
prefix_upper: &str,
args: &[crate::e2e::config::ArgMapping],
options_type_name: &str,
documentation_snippet: bool,
) -> (HashMap<String, String>, Vec<(String, String)>) {
let mut typed_arg_handles = HashMap::new();
let mut typed_arg_cleanup = Vec::new();
for arg in args {
if arg.arg_type != "json_object" {
continue;
}
let val = crate::e2e::codegen::resolve_field(&fixture.input, &arg.field);
if val.is_null() {
continue;
}
let normalized = transform_json_keys_for_language(val, "snake_case");
let (docs_setup, json_expr, docs_cleanup) = render_c_docs_json(
&arg.name,
&normalized,
&fixture.docs_files_for_arg(&arg.field),
documentation_snippet,
);
out.push_str(&docs_setup);
let Some(type_name) = arg
.element_type
.as_deref()
.or_else(|| (!options_type_name.is_empty()).then_some(options_type_name))
else {
panic!(
"C e2e generator: fixture `{}` declares a `json_object` arg `{}` with no resolvable type — \
`element_type` is unset and no `options_type_name` fallback is configured; cannot construct \
a typed `from_json` handle without knowing the target type",
fixture.id, arg.name
);
};
let type_snake = type_name.to_snake_case();
let handle = format!("{}_handle", sanitize_ident(&arg.name));
out.push_str(&crate::e2e::template_env::render(
"c/typed_handle.jinja",
minijinja::context! {
prefix_upper => prefix_upper,
type_name => type_name,
handle => handle,
prefix => prefix,
type_snake => type_snake,
json_expression => json_expr,
},
));
out.push_str(&docs_cleanup);
typed_arg_handles.insert(arg.name.clone(), handle.clone());
typed_arg_cleanup.push((handle, type_snake));
}
(typed_arg_handles, typed_arg_cleanup)
}
fn render_typed_arg_cleanup(out: &mut String, prefix: &str, handles: &[(String, String)]) {
for (handle, type_snake) in handles {
out.push_str(&crate::e2e::template_env::render(
"c/typed_handle_free.jinja",
minijinja::context! { prefix => prefix, type_snake => type_snake, handle => handle },
));
}
}
#[cfg(test)]
mod declared_error_variant_tests {
use super::emit_c_error_epilogue;
use crate::core::ir::{ErrorDef, ErrorVariant};
use crate::e2e::fixture::{Assertion, Fixture};
fn fixture_with_declared_error(value: &str) -> Fixture {
Fixture {
id: "declares_error".to_string(),
assertions: vec![Assertion {
assertion_type: "error".to_string(),
value: Some(serde_json::Value::String(value.to_string())),
..Assertion::default()
}],
..Fixture::default()
}
}
fn error_def_with(variant_name: &str, error_code: Option<u32>) -> Vec<ErrorDef> {
vec![ErrorDef {
name: "ApiError".to_string(),
rust_path: "lib::ApiError".to_string(),
original_rust_path: String::new(),
variants: vec![ErrorVariant {
name: variant_name.to_string(),
error_code,
is_unit: true,
..ErrorVariant::default()
}],
doc: String::new(),
methods: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}]
}
#[test]
fn message_style_value_still_asserts() {
let mut out = String::new();
let fixture = fixture_with_declared_error("rate limit");
emit_c_error_epilogue(&mut out, "sample", &fixture, &[], false);
assert!(out.contains("strstr(_err_message, \"rate limit\")"), "got: {out}");
}
#[test]
fn known_variant_renders_the_skip_even_when_coded() {
let mut out = String::new();
let fixture = fixture_with_declared_error("Authentication");
let errors = error_def_with("Authentication", Some(100));
emit_c_error_epilogue(&mut out, "sample", &fixture, &errors, false);
assert_eq!(
out,
" // skipped: declared error variant 'Authentication' not substantiated by this backend's \
generated error type\n"
);
assert!(!out.contains("strstr"), "must not render an assertion, got: {out}");
}
#[test]
fn documentation_snippet_renders_nothing_for_an_unsubstantiable_variant() {
let mut out = String::new();
let fixture = fixture_with_declared_error("Authentication");
let errors = error_def_with("Authentication", Some(100));
emit_c_error_epilogue(&mut out, "sample", &fixture, &errors, true);
assert_eq!(
out, "",
"documentation snippets must stay byte-identical to before this fix"
);
}
}
#[cfg(test)]
mod snippet_tests {
use super::{assemble_snippet_body, declared_variable, is_expected_result_assertion, snippet_declarations};
fn rendered_function(body_lines: &[&str]) -> String {
let mut function = String::from("void test_fixture(void) {\n /* Fixture */\n");
for line in body_lines {
function.push_str(" ");
function.push_str(line);
function.push('\n');
}
function.push_str("}\n");
function
}
#[test]
fn standalone_snippet_declares_success_guard() {
let declarations = snippet_declarations("if (request == 0) { ALEF_TEST_PASS(); }");
assert!(declarations.contains("return EXIT_SUCCESS"));
}
#[test]
fn error_rewrite_only_matches_declared_call_result_assertion() {
assert!(!is_expected_result_assertion(
"assert(client != 0 && \"failed to create client\");",
"result",
));
assert!(is_expected_result_assertion(
"assert(result == 0 && \"expected call to fail\");",
"result",
));
}
#[test]
fn declared_variable_reads_declarations_and_not_conditions() {
assert_eq!(
declared_variable("SAMPLEAlefHandle result = sample_chat(client);"),
Some("result")
);
assert_eq!(
declared_variable("char* result = sample_list_formats();"),
Some("result")
);
assert_eq!(
declared_variable("const TSLanguage *result = sample_language();"),
Some("result")
);
assert_eq!(
declared_variable("const char* api_key = getenv(\"API_KEY\");"),
Some("api_key")
);
assert_eq!(
declared_variable("assert(result == 0 && \"expected call to fail\");"),
None
);
assert_eq!(declared_variable("if (result != 0) { return EXIT_FAILURE; }"), None);
assert_eq!(declared_variable("result = sample_chat(client);"), None);
}
#[test]
fn failure_guard_is_emitted_with_the_declaration_it_names() {
let function = rendered_function(&[
"const char* api_key = getenv(\"API_KEY\");",
"assert(api_key != NULL && \"API_KEY must be set\");",
"SAMPLEAlefHandle client = sample_create_client(api_key, NULL, (uint64_t)-1, (uint32_t)-1, NULL);",
"assert(client != 0 && \"failed to create client\");",
"SAMPLEAlefHandle result = sample_default_client_chat(client);",
"sample_default_client_free(client);",
"assert(result == 0 && \"expected call to fail\");",
]);
let body =
assemble_snippet_body(&function, "result", true, "chat_auth_401", "result != 0").expect("body assembles");
let guard = body
.lines()
.position(|line| line.contains("if (result != 0) { return EXIT_FAILURE; }"))
.expect("failure guard is emitted");
let declaration = body
.lines()
.position(|line| line.contains("SAMPLEAlefHandle result ="))
.expect("result is declared");
assert!(declaration < guard, "guard precedes its declaration:\n{body}");
assert!(
body.contains("assert(client != 0 && \"failed to create client\");"),
"the client assertion must survive untouched:\n{body}"
);
}
#[test]
fn failure_guard_before_its_declaration_is_rejected() {
let function = rendered_function(&[
"SAMPLEAlefHandle client = sample_create_client(api_key, NULL, (uint64_t)-1, (uint32_t)-1, NULL);",
"assert(result == 0 && \"expected call to fail\");",
"SAMPLEAlefHandle result = sample_default_client_chat(client);",
]);
let error = assemble_snippet_body(&function, "result", true, "chat_auth_401", "result != 0")
.expect_err("a guard preceding its declaration must be rejected");
assert!(error.to_string().contains("before `result` is declared"), "{error}");
}
#[test]
fn success_snippets_drop_assertions_without_touching_declarations() {
let function = rendered_function(&[
"SAMPLEAlefHandle result = sample_default_client_chat(client);",
"assert(result != 0 && \"expected call to succeed\");",
"sample_chat_response_free(result);",
]);
let body =
assemble_snippet_body(&function, "result", false, "chat_basic", "result != 0").expect("body assembles");
assert!(!body.contains("assert("), "{body}");
assert!(
body.contains("SAMPLEAlefHandle result = sample_default_client_chat(client);"),
"{body}"
);
assert!(body.contains("sample_chat_response_free(result);"), "{body}");
}
}