use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::hash::{self, CommentStyle};
use crate::e2e::config::{CallConfig, E2eConfig};
use crate::e2e::escape::{escape_c, sanitize_filename};
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{Fixture, FixtureGroup};
use anyhow::Result;
use heck::{ToPascalCase, ToSnakeCase};
use std::collections::{HashMap, HashSet};
use std::fmt::Write as FmtWrite;
use std::path::PathBuf;
use super::E2eCodegen;
pub struct CCodegen;
pub(crate) fn is_primitive_c_type(t: &str) -> bool {
matches!(
t,
"uint8_t"
| "uint16_t"
| "uint32_t"
| "uint64_t"
| "int8_t"
| "int16_t"
| "int32_t"
| "int64_t"
| "uintptr_t"
| "intptr_t"
| "size_t"
| "ssize_t"
| "double"
| "float"
| "bool"
| "int"
)
}
fn is_skipped_c_field(fields_c_types: &HashMap<String, String>, parent_snake: &str, field_snake: &str) -> bool {
let key = format!("{parent_snake}.{field_snake}");
fields_c_types.get(&key).is_some_and(|t| t == "skip")
}
fn enum_fields_from_ir(
fields_c_types: &HashMap<String, String>,
enums: &[crate::core::ir::EnumDef],
) -> HashSet<String> {
fields_c_types
.iter()
.filter(|(_, type_name)| enums.iter().any(|e| &e.name == *type_name))
.filter_map(|(key, _)| key.rsplit('.').next().map(str::to_string))
.collect()
}
use optional_arg::{c_optional_sentinel, ir_declares_a_json_string_param, resolve_optional_sentinel};
fn infer_opaque_handle_type(
fields_c_types: &HashMap<String, String>,
parent_snake_type: &str,
field_snake: &str,
) -> Option<String> {
let lookup_key = format!("{parent_snake_type}.{field_snake}");
if let Some(t) = fields_c_types.get(&lookup_key) {
if !is_primitive_c_type(t) && t != "char*" {
return Some(t.clone());
}
return None;
}
let nested_prefix = format!("{field_snake}.");
if fields_c_types.keys().any(|k| k.starts_with(&nested_prefix)) {
return Some(field_snake.to_pascal_case());
}
None
}
#[allow(clippy::too_many_arguments)]
fn try_emit_enum_accessor(
out: &mut String,
prefix: &str,
prefix_upper: &str,
raw_field: &str,
resolved_field: &str,
parent_snake_type: &str,
accessor_fn: &str,
parent_handle: &str,
local_var: &str,
fields_c_types: &HashMap<String, String>,
fields_enum: &HashSet<String>,
intermediate_handles: &mut Vec<(String, String)>,
) -> bool {
if !(fields_enum.contains(raw_field) || fields_enum.contains(resolved_field)) {
return false;
}
let lookup_key = format!("{parent_snake_type}.{resolved_field}");
let Some(enum_pascal) = fields_c_types.get(&lookup_key) else {
return false;
};
if is_primitive_c_type(enum_pascal) || enum_pascal == "char*" {
return false;
}
let enum_snake = enum_pascal.to_snake_case();
let handle_var = format!("{local_var}_handle");
let _ = writeln!(
out,
" {prefix_upper}AlefHandle {handle_var} = {accessor_fn}({parent_handle});"
);
let _ = writeln!(out, " assert({handle_var} != 0);");
let _ = writeln!(
out,
" char* {local_var} = {prefix}_{enum_snake}_to_string({handle_var});"
);
intermediate_handles.push((handle_var, enum_snake));
true
}
impl E2eCodegen for CCodegen {
fn generate(
&self,
groups: &[FixtureGroup],
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
enums: &[crate::core::ir::EnumDef],
functions: &[crate::core::ir::FunctionDef],
errors: &[crate::core::ir::ErrorDef],
) -> Result<Vec<GeneratedFile>> {
let lang = self.language_name();
let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
let ir = CallIr { functions, type_defs };
let mut files = Vec::new();
let call = &e2e_config.call;
let overrides = call.overrides.get(lang);
let result_var = call.effective_result_var();
let prefix = effective_c_prefix(call, config);
let header = overrides
.and_then(|o| o.header.as_ref())
.cloned()
.unwrap_or_else(|| config.ffi_header_name());
let c_pkg = e2e_config.resolve_package("c");
let lib_name = config.ffi_lib_name();
let ffi_pkg_name = c_pkg
.as_ref()
.and_then(|p| p.name.as_ref())
.cloned()
.unwrap_or_else(|| lib_name.clone());
let active_groups: Vec<(&FixtureGroup, Vec<&Fixture>)> = groups
.iter()
.filter_map(|group| {
let active: Vec<&Fixture> = group
.fixtures
.iter()
.filter(|f| super::should_include_fixture(f, lang, e2e_config))
.filter(|f| f.visitor.is_none())
.collect();
if active.is_empty() { None } else { Some((group, active)) }
})
.collect();
let visitor_fixtures: Vec<&Fixture> = groups
.iter()
.flat_map(|group| group.fixtures.iter())
.filter(|f| super::should_include_fixture(f, lang, e2e_config))
.filter(|f| f.visitor.is_some())
.filter(|f| c_visitor_fixture_has_typed_call(f, e2e_config, ir))
.collect();
let ffi_crate_path = c_pkg
.as_ref()
.and_then(|p| p.path.as_ref())
.cloned()
.map(Ok)
.unwrap_or_else(|| config.ffi_crate_path_from(&format!("{}/c", e2e_config.effective_output())))
.map_err(anyhow::Error::msg)?;
let mut category_names: Vec<String> = active_groups
.iter()
.map(|(g, _)| sanitize_filename(&g.category))
.collect();
if !visitor_fixtures.is_empty() {
category_names.push("visitor".to_string());
}
let needs_mock_server = active_groups
.iter()
.flat_map(|(_, fixtures)| fixtures.iter())
.any(|f| f.needs_mock_server());
files.push(GeneratedFile {
path: output_base.join("Makefile"),
content: render_makefile(&category_names, &header, &ffi_crate_path, &lib_name, needs_mock_server)?,
generated_header: true,
});
let github_repo = config.github_repo();
let version = config.resolved_version().unwrap_or_else(|| "0.0.0".to_string());
files.push(GeneratedFile {
path: output_base.join("download_ffi.sh"),
content: render_download_script(&github_repo, &version, &ffi_pkg_name),
generated_header: true,
});
files.push(GeneratedFile {
path: output_base.join("test_runner.h"),
content: render_test_runner_header(&active_groups, &visitor_fixtures),
generated_header: true,
});
files.push(GeneratedFile {
path: output_base.join("main.c"),
content: render_main_c(&active_groups, &visitor_fixtures, &e2e_config.env),
generated_header: true,
});
files.push(GeneratedFile {
path: output_base.join(".gitignore"),
content: render_gitignore(),
generated_header: false,
});
let field_resolver = FieldResolver::new(
&e2e_config.fields,
&e2e_config.fields_optional,
&e2e_config.result_fields,
&e2e_config.fields_array,
&std::collections::HashSet::new(),
);
for (group, active) in &active_groups {
let filename = format!("test_{}.c", sanitize_filename(&group.category));
let content = render_test_file(
&group.category,
active,
&header,
&prefix,
result_var,
e2e_config,
lang,
&field_resolver,
config,
type_defs,
enums,
errors,
ir,
)?;
files.push(GeneratedFile {
path: output_base.join(filename),
content,
generated_header: true,
});
}
if !visitor_fixtures.is_empty() {
files.push(GeneratedFile {
path: output_base.join("test_visitor.c"),
content: render_visitor_test_file(&visitor_fixtures, &header, &prefix, e2e_config, config, ir)?,
generated_header: true,
});
}
Ok(files)
}
fn render_snippet_body(
&self,
fixture: &Fixture,
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
_enums: &[crate::core::ir::EnumDef],
) -> Result<String> {
render_c_snippet(fixture, e2e_config, config, type_defs, &[])
}
fn render_snippet_body_with_functions(
&self,
fixture: &Fixture,
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
_enums: &[crate::core::ir::EnumDef],
functions: &[crate::core::ir::FunctionDef],
_errors: &[crate::core::ir::ErrorDef],
) -> Result<String> {
render_c_snippet(fixture, e2e_config, config, type_defs, functions)
}
fn language_name(&self) -> &'static str {
"c"
}
}
fn render_c_snippet(
fixture: &Fixture,
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
functions: &[crate::core::ir::FunctionDef],
) -> Result<String> {
let ir = CallIr { functions, type_defs };
let mut info = resolve_fixture_call_info(fixture, e2e_config, config, "c", ir);
let call = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let prefix = effective_c_prefix(call, config);
if info.client_factory.is_none()
&& info.c_engine_factory.is_none()
&& !prefix.is_empty()
&& !info.function_name.starts_with(&format!("{prefix}_"))
{
info.function_name = crate::codegen::naming::abi_symbol(&prefix, &info.function_name);
}
let header = call
.overrides
.get("c")
.and_then(|value| value.header.clone())
.unwrap_or_else(|| config.ffi_header_name());
let (ir_reachable_fields, ir_known_excluded_fields, ir_optional_fields) = FieldResolver::ir_field_sets(type_defs);
let resolver = FieldResolver::new(
e2e_config.effective_fields(call),
e2e_config.effective_fields_optional(call),
e2e_config.effective_result_fields(call),
e2e_config.effective_fields_array(call),
e2e_config.effective_fields_method_calls(call),
)
.with_ir_fields(ir_reachable_fields, ir_known_excluded_fields, ir_optional_fields);
test_function::render_snippet_body(test_function::SnippetContext {
fixture,
e2e_config,
header: &header,
prefix: &prefix,
info: &info,
field_resolver: &resolver,
config,
type_defs,
ir,
})
}
fn effective_c_prefix(call: &CallConfig, config: &ResolvedCrateConfig) -> String {
call.overrides
.get("c")
.and_then(|value| value.prefix.clone())
.unwrap_or_else(|| config.ffi_prefix())
}
fn effective_c_header(call: &CallConfig, config: &ResolvedCrateConfig) -> String {
call.overrides
.get("c")
.and_then(|value| value.header.clone())
.unwrap_or_else(|| config.ffi_header_name())
}
struct ResolvedCallInfo {
function_name: String,
result_type_name: ResultTypeName,
options_type_name: String,
client_factory: Option<String>,
args: Vec<crate::e2e::config::ArgMapping>,
raw_c_result_type: Option<String>,
c_free_fn: Option<String>,
c_engine_factory: Option<String>,
result_is_option: bool,
returns_void: bool,
result_is_bytes: bool,
streaming: Option<bool>,
extra_args: Vec<String>,
}
pub(super) use super::call_ir::{CallIr, TargetParams, named_type};
fn resolve_call_info(
call: &CallConfig,
lang: &str,
ir: CallIr<'_>,
trait_bridge_registry_identity: Option<&str>,
) -> ResolvedCallInfo {
let overrides = call.overrides.get(lang);
let function_name = overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| call.function.clone());
let result_type_name = overrides
.and_then(|o| o.result_type.as_ref())
.cloned()
.inspect(|configured| warn_if_result_type_override_disables_verification(configured, call, lang))
.or_else(|| resolve_ir_result_type(call, lang, ir))
.map(ResultTypeName::Resolved)
.unwrap_or_else(|| unresolved_result_type_name(call, lang, ir, trait_bridge_registry_identity));
let options_type_name = overrides
.and_then(|o| o.options_type.as_deref())
.or(call.options_type.as_deref())
.unwrap_or_default()
.to_string();
let client_factory = overrides.and_then(|o| o.client_factory.as_ref()).cloned();
let raw_c_result_type = overrides
.and_then(|o| o.raw_c_result_type.clone())
.or_else(|| return_shape::resolve_raw_c_result_type(call, lang, ir));
let c_free_fn = overrides.and_then(|o| o.c_free_fn.clone());
let c_engine_factory = overrides.and_then(|o| o.c_engine_factory.clone());
let result_is_option = overrides
.and_then(|o| if o.result_is_option { Some(true) } else { None })
.unwrap_or(call.result_is_option);
let returns_void = call.returns_void;
let result_is_bytes = call.result_is_bytes || overrides.is_some_and(|o| o.result_is_bytes);
let extra_args = overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
let mut args = call.args.clone();
let core_lookup_name = call.core_lookup_name(lang);
if let Some(signature) = core_lookup_name.as_deref().and_then(|name| ir.signature(name)) {
for (index, arg) in args.iter_mut().enumerate() {
if arg.element_type.is_some() || arg.arg_type != "json_object" {
continue;
}
let parameter = signature
.params
.iter()
.find(|parameter| parameter.name == arg.name)
.or_else(|| signature.params.get(index));
arg.element_type = parameter
.and_then(|parameter| named_type(¶meter.ty))
.map(str::to_string);
}
}
ResolvedCallInfo {
function_name,
result_type_name,
options_type_name,
client_factory,
args,
raw_c_result_type,
c_free_fn,
c_engine_factory,
result_is_option,
returns_void,
result_is_bytes,
streaming: call.streaming_enabled(),
extra_args,
}
}
fn resolve_ir_result_type(call: &CallConfig, lang: &str, ir: CallIr<'_>) -> Option<String> {
let lookup_name = call.core_lookup_name(lang)?;
let signature = ir.signature(&lookup_name)?;
named_type(signature.return_type).map(str::to_string)
}
fn warn_if_result_type_override_disables_verification(configured: &str, call: &CallConfig, lang: &str) {
if is_primitive_c_type(configured) || configured == "char*" || configured.ends_with('*') {
tracing::warn!(
call = %call.function,
language = %lang,
result_type = %configured,
"call/override declares `result_type` as a primitive/pointer C spelling rather than \
a PascalCase IR type name, which disables nested-field verification for this call \
because no IR type will ever match this name — if the result genuinely carries no \
named fields to verify, declare that with `result_is_bytes` / `result_is_simple` \
instead"
);
}
}
const UNNAMED_CALL_DIAGNOSTIC: &str = "<call with no configured name>";
pub(super) enum ResultTypeName {
Resolved(String),
Unverified { name: String, basis: UnverifiedBasis },
Unresolvable { call: String, language: String },
}
pub(super) enum UnverifiedBasis {
IrAbsent,
DeclaredNonStruct,
TraitBridgeRegistry,
}
impl ResultTypeName {
pub(super) fn require(&self) -> Result<&str> {
match self {
Self::Resolved(name) | Self::Unverified { name, .. } => Ok(name),
Self::Unresolvable { call, language } => anyhow::bail!(
"C e2e codegen cannot name the result type of call `{call}` for language \
`{language}`: it resolves to no core IR function or method with a named return \
type. Naming it after the call would emit `{{prefix}}_{{result}}_{{field}}` \
accessors and a `{{prefix}}_{{result}}_free` cleanup for a type the generated \
header never declares, and would switch nested-field verification off for this \
fixture because no IR type matches an invented name. Fix by setting \
`result_type` on the call's `{language}` override to the real type, or by \
declaring the result carries no named fields (`result_is_bytes` / \
`result_is_simple`)."
),
}
}
pub(super) fn require_owned_handle(&self) -> Result<&str> {
let name = self.require()?;
match self {
Self::Unverified {
basis: UnverifiedBasis::DeclaredNonStruct,
..
} => anyhow::bail!(
"C e2e codegen would bind the result of a call it cannot name to an opaque \
handle and free it with `{{prefix}}_{{result}}_free`, but the call already \
declares its result carries no named fields (`result_is_bytes` / \
`result_is_simple` / `result_is_json_struct`) -- so there is no handle to own \
and the free would be passed a value that was never an alef `Box`. Fix by \
setting `raw_c_result_type` on the call's `c` override to the C spelling the \
export actually returns (`char*`, `int32_t`, `uintptr_t`, ...), or by setting \
`result_type` to the real handle type if the result IS a named struct."
),
Self::Unverified {
basis: UnverifiedBasis::TraitBridgeRegistry,
..
} => anyhow::bail!(
"C e2e codegen would bind the result of trait-bridge registry export \
`{name}` to an opaque handle and free it, but `register_fn` / `unregister_fn` \
/ `clear_fn` exports return an `i32` status code (see \
`src/backends/ffi/templates/`), not a handle. The status-code emission in \
`test_function.rs` should have claimed this call before any handle path did."
),
Self::Resolved(_) | Self::Unverified { .. } | Self::Unresolvable { .. } => Ok(name),
}
}
pub(super) fn returns_status_code(&self) -> bool {
matches!(
self,
Self::Unverified {
basis: UnverifiedBasis::TraitBridgeRegistry,
..
}
)
}
}
fn unresolved_result_type_name(
call: &CallConfig,
lang: &str,
ir: CallIr<'_>,
trait_bridge_registry_identity: Option<&str>,
) -> ResultTypeName {
let result_type = call.function.to_pascal_case();
if let Some(identity) = trait_bridge_registry_identity {
let result_type = identity.to_pascal_case();
tracing::debug!(
call = %call.function,
language = %lang,
%result_type,
"call resolves to a trait-bridge registry function (register_fn / unregister_fn / \
clear_fn), which is a generated FFI export with no core IR counterpart and no named \
result to verify"
);
return ResultTypeName::Unverified {
name: result_type,
basis: UnverifiedBasis::TraitBridgeRegistry,
};
}
if ir.is_absent() {
tracing::debug!(
call = %call.function,
language = %lang,
%result_type,
"no core IR available to this generator; result type derived from the call name"
);
return ResultTypeName::Unverified {
name: result_type,
basis: UnverifiedBasis::IrAbsent,
};
}
if call_declares_non_struct_result(call, lang) {
tracing::debug!(
call = %call.function,
language = %lang,
%result_type,
"call did not resolve to a core IR function or method with a named return type, but \
the call/override already declares the result carries no named fields \
(result_is_bytes / result_is_simple / result_is_json_struct) — there is no named \
type to set and no nested field for the derived type to hide"
);
return ResultTypeName::Unverified {
name: result_type,
basis: UnverifiedBasis::DeclaredNonStruct,
};
}
let call_name = call.effective_function(lang).unwrap_or(UNNAMED_CALL_DIAGNOSTIC);
tracing::warn!(
call = %call_name,
language = %lang,
"call did not resolve to a core IR function or method with a named return type and \
declares no non-struct result; there is no real type to name, so any emission path that \
needs one now fails rather than inventing it — set `result_type` on the call override"
);
ResultTypeName::Unresolvable {
call: call_name.to_string(),
language: lang.to_string(),
}
}
fn call_declares_non_struct_result(call: &CallConfig, lang: &str) -> bool {
if call.result_is_simple || call.result_is_bytes {
return true;
}
call.overrides
.get(lang)
.is_some_and(|o| o.result_is_simple || o.result_is_bytes || o.result_is_json_struct)
}
fn resolve_fixture_call_info(
fixture: &Fixture,
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
lang: &str,
ir: CallIr<'_>,
) -> ResolvedCallInfo {
let call = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let call_skipped_for_lang = crate::e2e::codegen::call_skip_reason(fixture, lang, e2e_config).is_some();
let trait_bridge_identity = (!call_skipped_for_lang)
.then(|| crate::e2e::codegen::recipe::trait_bridge_derived_c_identity(config, fixture))
.flatten();
let mut info = resolve_call_info(
call,
lang,
ir,
trait_bridge_identity.as_ref().map(|(_, name)| name.as_str()),
);
trait_bridge_registry_identity::apply(&mut info, call, lang, trait_bridge_identity);
let default_overrides = e2e_config.call.overrides.get(lang);
let returns_status_code = info.result_type_name.returns_status_code();
if info.client_factory.is_none()
&& !returns_status_code
&& let Some(factory) = default_overrides.and_then(|o| o.client_factory.as_ref())
{
info.client_factory = Some(factory.clone());
}
if info.c_engine_factory.is_none()
&& !returns_status_code
&& let Some(factory) = default_overrides.and_then(|o| o.c_engine_factory.as_ref())
{
info.c_engine_factory = Some(factory.clone());
}
info
}
fn c_visitor_fixture_has_typed_call(fixture: &Fixture, e2e_config: &E2eConfig, ir: CallIr<'_>) -> bool {
let call = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let info = resolve_call_info(call, "c", ir, None);
let has_function = call
.overrides
.get("c")
.and_then(|override_config| override_config.function.as_deref())
.is_some_and(|function| !function.is_empty());
has_function && !info.options_type_name.is_empty()
}
mod assertions;
mod call_patterns;
#[cfg(test)]
mod client_factory_optional_arg_tests;
#[cfg(test)]
mod collection_empty_assertion_tests;
mod collection_wildcard;
mod docs_input;
mod enum_field_inference;
mod ffi_constructors;
mod optional_arg;
mod primitive_field_inference;
mod project;
mod return_shape;
mod runner;
#[cfg(test)]
mod snippet_regressions;
#[cfg(test)]
mod std_arg_tests;
mod streaming;
mod test_function;
mod trait_bridge_registry_identity;
#[cfg(test)]
mod trait_bridge_registry_symbol_tests;
mod trait_bridge_snippet;
mod visitor;
mod void_call_status;
#[cfg(test)]
mod wildcard_collection_regression_tests;
use assertions::{
FieldConfigSources, LeafFieldCheck, build_args_string_c, emit_nested_accessor, ensure_leaf_field_exists,
render_assertion,
};
use collection_wildcard::{NestedLeafOutcome, classify_nested_leaf, render_wildcard_assertion};
#[cfg(test)]
use assertions::EffectiveConfigSource;
use call_patterns::{render_bytes_test_function, render_engine_factory_test_function};
use enum_field_inference::enum_fields_c_types_from_ir;
use primitive_field_inference::primitive_fields_c_types_from_ir;
use project::{render_download_script, render_gitignore, render_makefile};
use runner::{render_main_c, render_test_runner_header};
use streaming::{
render_c_diagnostic_skip, render_streaming_test_function, resolve_c_client_owner_type, resolve_c_streaming_adapter,
validate_c_snippet_metadata,
};
use test_function::render_test_function_impl;
use visitor::render_visitor_test_file;
#[allow(clippy::too_many_arguments)]
fn render_test_file(
category: &str,
fixtures: &[&Fixture],
header: &str,
_prefix: &str,
result_var: &str,
e2e_config: &E2eConfig,
lang: &str,
field_resolver: &FieldResolver,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
enums: &[crate::core::ir::EnumDef],
errors: &[crate::core::ir::ErrorDef],
ir: CallIr<'_>,
) -> anyhow::Result<String> {
let mut out = String::new();
out.push_str(&hash::header(CommentStyle::Block));
let _ = writeln!(out, "/* E2e tests for category: {category} */");
let _ = writeln!(out);
let _ = writeln!(out, "#include <assert.h>");
let _ = writeln!(out, "#include <stdint.h>");
let _ = writeln!(out, "#include <string.h>");
let _ = writeln!(out, "#include <stdio.h>");
let _ = writeln!(out, "#include <stdlib.h>");
let mut headers = std::collections::BTreeSet::from([header.to_string()]);
for fixture in fixtures {
let call = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
headers.insert(effective_c_header(call, config));
}
for header in headers {
let _ = writeln!(out, "#include \"{header}\"");
}
let _ = writeln!(out, "#include \"test_runner.h\"");
let _ = writeln!(out);
let mut effective_fields_c_types = e2e_config.fields_c_types.clone();
for (key, type_name) in enum_fields_c_types_from_ir(type_defs, enums) {
effective_fields_c_types.entry(key).or_insert(type_name);
}
for (key, type_name) in primitive_fields_c_types_from_ir(type_defs) {
effective_fields_c_types.entry(key).or_insert(type_name);
}
for (i, fixture) in fixtures.iter().enumerate() {
if fixture.visitor.is_some() {
panic!(
"C e2e generator: visitor pattern not supported for fixture: {}",
fixture.id
);
}
let call_info = resolve_fixture_call_info(fixture, e2e_config, config, lang, ir);
let mut effective_fields_enum = e2e_config.fields_enum.clone();
let fixture_call = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let prefix = effective_c_prefix(fixture_call, config);
if let Some(co) = fixture_call.overrides.get(lang) {
for k in co.enum_fields.keys() {
effective_fields_enum.insert(k.clone());
}
}
effective_fields_enum.extend(enum_fields_from_ir(&effective_fields_c_types, enums));
let (ir_reachable_fields, ir_known_excluded_fields, ir_optional_fields) =
FieldResolver::ir_field_sets(type_defs);
let per_call_field_resolver = FieldResolver::new(
e2e_config.effective_fields(fixture_call),
e2e_config.effective_fields_optional(fixture_call),
e2e_config.effective_result_fields(fixture_call),
e2e_config.effective_fields_array(fixture_call),
&std::collections::HashSet::new(),
)
.with_ir_fields(ir_reachable_fields, ir_known_excluded_fields, ir_optional_fields);
let _ = field_resolver; let field_resolver = &per_call_field_resolver;
let config_sources = FieldConfigSources::resolve(e2e_config, fixture_call);
let fixture_start = out.len();
let target_params = if crate::e2e::codegen::recipe::trait_bridge_derived_c_identity(config, fixture).is_some() {
TargetParams::Known(&[])
} else {
TargetParams::resolve(fixture_call, lang, ir)
};
render_test_function_impl(
&mut out,
fixture,
&prefix,
&call_info.function_name,
result_var,
&call_info.args,
field_resolver,
&effective_fields_c_types,
&effective_fields_enum,
&call_info.result_type_name,
&call_info.options_type_name,
call_info.client_factory.as_deref(),
call_info.raw_c_result_type.as_deref(),
call_info.c_free_fn.as_deref(),
call_info.c_engine_factory.as_deref(),
call_info.result_is_option,
call_info.result_is_bytes,
call_info.streaming,
&call_info.extra_args,
config,
type_defs,
errors,
false,
&config_sources,
target_params,
)?;
crate::e2e::codegen::fail_on_unavailable_field_markers(
&out[fixture_start..],
"c",
&fixture.id,
&fixture.assertions,
);
crate::e2e::codegen::fail_on_unsupported_assertion_type_markers(&out[fixture_start..], "c", &fixture.id);
if i + 1 < fixtures.len() {
let _ = writeln!(out);
}
}
Ok(out)
}
#[allow(clippy::too_many_arguments)]
fn json_to_c(value: &serde_json::Value) -> String {
match value {
serde_json::Value::String(s) => format!("\"{}\"", escape_c(s)),
serde_json::Value::Bool(true) => "1".to_string(),
serde_json::Value::Bool(false) => "0".to_string(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Null => "NULL".to_string(),
other => format!("\"{}\"", escape_c(&other.to_string())),
}
}
pub fn emit_test_backend(
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&crate::core::ir::MethodDef],
fixture: &crate::e2e::fixture::Fixture,
) -> super::TestBackendEmission {
trait_bridge_snippet::emit_test_backend(trait_bridge, methods, fixture)
}
#[cfg(test)]
mod snippet_tests {
use super::*;
#[test]
fn full_generation_uses_the_derived_ffi_prefix() {
let group = FixtureGroup {
category: "basic".into(),
fixtures: vec![Fixture {
id: "clear".into(),
description: "Clear".into(),
..Fixture::default()
}],
};
let mut e2e = E2eConfig::default();
e2e.call.function = "clear".into();
e2e.call.returns_void = true;
let config = ResolvedCrateConfig {
name: "sample-core".into(),
..ResolvedCrateConfig::default()
};
let generated = CCodegen
.generate(&[group], &e2e, &config, &[], &[], &[], &[])
.expect("C harness renders");
let test = generated
.iter()
.find(|file| file.path.ends_with("test_basic.c"))
.expect("category test is emitted");
assert!(test.content.contains("SAMPLE_COREAlefHandle"), "{}", test.content);
assert!(test.content.contains("sample_core_clear_free("), "{}", test.content);
assert!(!test.content.contains(" _clear_free("), "{}", test.content);
}
#[test]
fn full_generation_uses_named_call_prefix_and_header() {
let group = FixtureGroup {
category: "basic".into(),
fixtures: vec![Fixture {
id: "secondary".into(),
call: Some("secondary".into()),
..Fixture::default()
}],
};
let mut e2e = E2eConfig::default();
e2e.call.function = "default_call".into();
e2e.calls.insert(
"secondary".into(),
CallConfig {
function: "secondary_call".into(),
returns_void: true,
overrides: HashMap::from([(
"c".into(),
crate::e2e::config::CallOverride {
prefix: Some("secondary".into()),
header: Some("secondary.h".into()),
..Default::default()
},
)]),
..Default::default()
},
);
let config = ResolvedCrateConfig {
name: "sample-core".into(),
..Default::default()
};
let files = CCodegen.generate(&[group], &e2e, &config, &[], &[], &[], &[]).unwrap();
let test = files.iter().find(|file| file.path.ends_with("test_basic.c")).unwrap();
assert!(test.content.contains("#include \"secondary.h\""), "{}", test.content);
assert!(test.content.contains("SECONDARYAlefHandle"), "{}", test.content);
assert!(
test.content.contains("secondary_secondary_call_free("),
"{}",
test.content
);
}
#[test]
fn snippet_keeps_header_and_call_without_test_harness() {
let fixture = Fixture {
id: "count".into(),
description: "Count".into(),
..Fixture::default()
};
let mut e2e = E2eConfig::default();
e2e.call.function = "sample_count".into();
e2e.call.result_var = "result".into();
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let rendered = CCodegen
.render_snippet_body(&fixture, &e2e, &config, &[], &[])
.expect("snippet renders");
assert!(rendered.contains("#include \""));
assert!(rendered.contains("sample_count("));
assert!(rendered.contains("int main(void)"));
assert!(!rendered.contains("void test_"));
assert!(!rendered.contains("assert("));
assert!(rendered.contains("_free(result)"), "{rendered}");
}
fn unrelated_ir() -> [crate::core::ir::FunctionDef; 1] {
[crate::core::ir::FunctionDef {
name: "unrelated".into(),
return_type: crate::core::ir::TypeRef::Named("Unrelated".into()),
..crate::core::ir::FunctionDef::default()
}]
}
#[test]
fn should_refuse_to_emit_a_snippet_whose_result_type_resolves_to_nothing_real() {
let fixture = Fixture {
id: "list_backends".into(),
description: "List backends".into(),
..Fixture::default()
};
let mut e2e = E2eConfig::default();
e2e.call.function = "sample_list_backends".into();
e2e.call.result_var = "result".into();
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let error = render_c_snippet(&fixture, &e2e, &config, &[], &unrelated_ir())
.expect_err("a result type nothing real names must fail generation, not emit a snippet");
let message = format!("{error:#}");
assert!(
message.contains("sample_list_backends"),
"the failure must name the call an operator has to fix: {message}"
);
assert!(
message.contains("result_type"),
"the failure must name the config key that fixes it: {message}"
);
assert!(
!message.contains("SampleListBackends"),
"the failure must not hand back the PascalCased call name as if it were a type: {message}"
);
}
#[test]
fn should_still_emit_a_snippet_when_the_ir_names_the_result_type() {
let fixture = Fixture {
id: "list_backends".into(),
description: "List backends".into(),
..Fixture::default()
};
let mut e2e = E2eConfig::default();
e2e.call.function = "sample_list_backends".into();
e2e.call.result_var = "result".into();
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let functions = [crate::core::ir::FunctionDef {
name: "sample_list_backends".into(),
return_type: crate::core::ir::TypeRef::Named("BackendList".into()),
..crate::core::ir::FunctionDef::default()
}];
let rendered =
render_c_snippet(&fixture, &e2e, &config, &[], &functions).expect("a call the IR names must still render");
assert!(rendered.contains("sample_list_backends("), "{rendered}");
assert!(
rendered.contains("sample_backend_list_free(result)"),
"cleanup must be derived from the IR-declared type, not the call name: {rendered}"
);
assert!(
!rendered.contains("sample_list_backends_free"),
"the call-name-derived cleanup symbol must never appear: {rendered}"
);
}
#[test]
fn trait_bridge_operation_uses_declared_abi_identity() {
let fixture = Fixture {
id: "clear_sample_backends".into(),
description: "Clear registered sample backends".into(),
call: Some("clear_sample_backends".into()),
..Fixture::default()
};
let mut e2e = E2eConfig::default();
e2e.calls.insert(
"clear_sample_backends".into(),
CallConfig {
returns_result: false,
returns_void: true,
..CallConfig::default()
},
);
let config = ResolvedCrateConfig {
name: "sample".into(),
trait_bridges: vec![crate::core::config::TraitBridgeConfig {
trait_name: "SampleBackend".into(),
clear_fn: Some("clear_sample_backends".into()),
..Default::default()
}],
..ResolvedCrateConfig::default()
};
let rendered = render_c_snippet(&fixture, &e2e, &config, &[], &[]).expect("C snippet renders");
assert!(rendered.contains("sample_clear_sample_backend(NULL)"), "{rendered}");
assert!(!rendered.contains("sample_clear_sample_backends("), "{rendered}");
assert!(!rendered.contains("has no function identity"), "{rendered}");
}
#[test]
fn trait_bridge_unregister_appends_out_error_out_param() {
let fixture = Fixture {
id: "unregister_sample_backend".into(),
description: "Unregister a sample backend".into(),
call: Some("unregister_sample_backend".into()),
input: serde_json::json!({ "name": "nonexistent-backend" }),
..Fixture::default()
};
let mut e2e = E2eConfig::default();
e2e.calls.insert(
"unregister_sample_backend".into(),
CallConfig {
returns_result: false,
returns_void: true,
args: vec![crate::core::config::e2e::ArgMapping {
name: "name".into(),
field: "input.name".into(),
arg_type: "string".into(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}],
..CallConfig::default()
},
);
let config = ResolvedCrateConfig {
name: "sample".into(),
trait_bridges: vec![crate::core::config::TraitBridgeConfig {
trait_name: "SampleBackend".into(),
unregister_fn: Some("unregister_sample_backend".into()),
..Default::default()
}],
..ResolvedCrateConfig::default()
};
let rendered = render_c_snippet(&fixture, &e2e, &config, &[], &[]).expect("C snippet renders");
assert!(
rendered.contains("sample_unregister_sample_backend(\"nonexistent-backend\", NULL)"),
"{rendered}"
);
}
#[test]
fn expected_error_snippet_checks_the_native_null_result() {
let mut fixture = Fixture {
id: "invalid".into(),
description: "Invalid".into(),
..Fixture::default()
};
fixture.assertions.push(crate::e2e::fixture::Assertion {
assertion_type: "error".into(),
..Default::default()
});
let mut e2e = E2eConfig::default();
e2e.call.function = "sample_parse".into();
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let rendered = CCodegen
.render_snippet_body(&fixture, &e2e, &config, &[], &[])
.expect("snippet renders");
assert!(rendered.contains("!= 0) { return EXIT_FAILURE; }"), "{rendered}");
assert!(!rendered.contains("assert("));
}
#[test]
fn engine_factory_snippet_reuses_native_call_preparation() {
let fixture = Fixture {
id: "engine_call".into(),
description: "Engine call".into(),
input: serde_json::json!({ "url": "https://example.test" }),
..Fixture::default()
};
let mut e2e = E2eConfig::default();
e2e.call.function = "sample_scrape".into();
e2e.call.result_var = "result".into();
e2e.call.overrides.insert(
"c".into(),
crate::core::config::e2e::CallOverride {
c_engine_factory: Some("EngineConfig".into()),
..Default::default()
},
);
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let rendered = CCodegen
.render_snippet_body(&fixture, &e2e, &config, &[], &[])
.expect("engine-factory snippet renders");
assert!(rendered.contains("create_engine"), "{rendered}");
assert!(rendered.contains("sample_scrape(engine"), "{rendered}");
assert!(rendered.contains("crawl_engine_handle_free(engine)"), "{rendered}");
}
#[test]
fn simple_result_snippet_uses_prefixed_string_api() {
let fixture = Fixture {
id: "list_formats".into(),
description: "List formats".into(),
..Fixture::default()
};
let mut e2e = E2eConfig::default();
e2e.call.function = "list_formats".into();
e2e.call.result_var = "result".into();
e2e.call.result_is_simple = true;
e2e.call.overrides.insert(
"c".into(),
crate::core::config::e2e::CallOverride {
raw_c_result_type: Some("char*".into()),
..Default::default()
},
);
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let rendered = CCodegen
.render_snippet_body(&fixture, &e2e, &config, &[], &[])
.expect("simple-result snippet renders");
assert!(rendered.contains("char* result = sample_list_formats();"), "{rendered}");
assert!(rendered.contains("sample_free_string(result);"), "{rendered}");
assert!(!rendered.contains("SAMPLEListFormats"), "{rendered}");
}
#[test]
fn scalar_result_snippets_preserve_numeric_types_without_string_cleanup() {
for raw_type in ["int32_t", "bool"] {
let fixture = Fixture {
id: "count_formats".into(),
description: "Count formats".into(),
..Fixture::default()
};
let mut e2e = E2eConfig::default();
e2e.call.function = "count_formats".into();
e2e.call.result_var = "result".into();
e2e.call.result_is_simple = true;
e2e.call.overrides.insert(
"c".into(),
crate::core::config::e2e::CallOverride {
raw_c_result_type: Some(raw_type.into()),
..Default::default()
},
);
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let rendered = CCodegen
.render_snippet_body(&fixture, &e2e, &config, &[], &[])
.expect("numeric-result snippet renders");
assert!(
rendered.contains(&format!("{raw_type} result = sample_count_formats();")),
"{rendered}"
);
assert!(!rendered.contains("free_string"), "{rendered}");
}
}
#[test]
fn raw_result_error_snippet_fails_on_unexpected_success() {
for (raw_type, expected_failure_check) in [
("char*", "if (result != 0) { return EXIT_FAILURE; }"),
("int32_t", "if (result != 0) { return EXIT_FAILURE; }"),
("uintptr_t", "assert(sample_last_error_code() != 0"),
] {
let mut fixture = Fixture {
id: "invalid_input".into(),
description: "Invalid input".into(),
..Fixture::default()
};
fixture.assertions.push(crate::e2e::fixture::Assertion {
assertion_type: "error".into(),
..Default::default()
});
let mut e2e = E2eConfig::default();
e2e.call.function = "parse_input".into();
e2e.call.result_var = "result".into();
e2e.call.result_is_simple = true;
e2e.call.overrides.insert(
"c".into(),
crate::core::config::e2e::CallOverride {
raw_c_result_type: Some(raw_type.into()),
..Default::default()
},
);
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let rendered = CCodegen
.render_snippet_body(&fixture, &e2e, &config, &[], &[])
.expect("raw-result error snippet renders");
assert!(
rendered.contains(expected_failure_check),
"raw_type={raw_type}: {rendered}"
);
}
}
const GUARD_FREE_IDENTIFIERS: &[&str] = &["NULL", "EXIT_FAILURE", "EXIT_SUCCESS", "true", "false", "sizeof"];
fn guard_condition(line: &str) -> Option<&str> {
let rest = line.trim().strip_prefix("if (")?;
let mut depth = 1usize;
for (index, character) in rest.char_indices() {
match character {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return Some(&rest[..index]);
}
}
_ => {}
}
}
None
}
fn condition_identifiers(condition: &str) -> Vec<String> {
let characters: Vec<char> = condition.chars().collect();
let mut identifiers = Vec::new();
let mut index = 0;
let mut in_string = false;
while index < characters.len() {
let character = characters[index];
if in_string {
index += if character == '\\' { 2 } else { 1 };
if character == '"' {
in_string = false;
}
continue;
}
if character == '"' {
in_string = true;
index += 1;
continue;
}
if character.is_alphabetic() || character == '_' {
let start = index;
while index < characters.len() && (characters[index].is_alphanumeric() || characters[index] == '_') {
index += 1;
}
if characters.get(index) != Some(&'(') {
identifiers.push(characters[start..index].iter().collect());
}
continue;
}
index += 1;
}
identifiers
}
fn declared_name(line: &str) -> Option<String> {
let statement = line.trim().trim_end_matches(';');
let declarator = statement.split('=').next()?.trim();
if declarator.contains(['(', ')', '{', '}', '!', '<', '>', ',', '#'])
|| declarator.split_whitespace().count() < 2
{
return None;
}
let last = declarator.split_whitespace().next_back()?;
let name = last.trim_start_matches('*').split('[').next()?;
(!name.is_empty()).then(|| name.to_string())
}
fn guard_uses_before_declaration(snippet: &str) -> Vec<String> {
let mut declared: HashSet<String> = HashSet::new();
let mut violations = Vec::new();
for line in snippet.lines() {
if let Some(condition) = guard_condition(line) {
for identifier in condition_identifiers(condition) {
if !GUARD_FREE_IDENTIFIERS.contains(&identifier.as_str()) && !declared.contains(&identifier) {
violations.push(format!("`{identifier}` read by guard `{}`", line.trim()));
}
}
}
if let Some(name) = declared_name(line) {
declared.insert(name);
}
}
violations
}
fn error_fixture(id: &str) -> Fixture {
let mut fixture = Fixture {
id: id.into(),
description: "Expected to fail".into(),
..Fixture::default()
};
fixture.assertions.push(crate::e2e::fixture::Assertion {
assertion_type: "error".into(),
..Default::default()
});
fixture
}
#[test]
fn every_guard_identifier_in_a_generated_snippet_is_declared_before_it_is_read() {
let sample = || ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let mut client_e2e = E2eConfig::default();
client_e2e.call.function = "chat".into();
client_e2e.call.overrides.insert(
"c".into(),
crate::core::config::e2e::CallOverride {
client_factory: Some("create_client".into()),
..Default::default()
},
);
let client_config = ResolvedCrateConfig {
adapters: vec![
serde_json::from_value(serde_json::json!({
"name": "chat",
"pattern": "async_method",
"core_path": "sample::chat",
"owner_type": "DefaultClient"
}))
.expect("client adapter config"),
],
..sample()
};
let mut raw_e2e = E2eConfig::default();
raw_e2e.call.function = "parse_input".into();
raw_e2e.call.result_var = "result".into();
raw_e2e.call.result_is_simple = true;
raw_e2e.call.overrides.insert(
"c".into(),
crate::core::config::e2e::CallOverride {
raw_c_result_type: Some("char*".into()),
..Default::default()
},
);
let mut handle_e2e = E2eConfig::default();
handle_e2e.call.function = "sample_parse".into();
let cases: Vec<(&str, Fixture, E2eConfig, ResolvedCrateConfig)> = vec![
(
"client-factory error",
error_fixture("chat_auth_401"),
client_e2e,
client_config,
),
("raw-result error", error_fixture("parse_invalid"), raw_e2e, sample()),
(
"opaque-handle error",
error_fixture("parse_failed"),
handle_e2e,
sample(),
),
];
for (label, fixture, e2e, config) in cases {
let rendered = render_c_snippet(&fixture, &e2e, &config, &[], &[]).expect("snippet renders");
let violations = guard_uses_before_declaration(&rendered);
assert!(
violations.is_empty(),
"{label}: guard reads an undeclared identifier: {violations:?}\n{rendered}"
);
}
}
#[test]
fn guard_checker_rejects_the_historic_use_before_declaration_snippet() {
let historic = concat!(
"int main(void) {\n",
" SAMPLEDefaultClient* client = sample_create_client(\"test-key\", NULL);\n",
" if (result != NULL) { return EXIT_FAILURE; }\n",
" SAMPLEBatchObject* result = sample_default_client_cancel_batch(client, \"batch-1\");\n",
" sample_default_client_free(client);\n",
" if (result != NULL) { return EXIT_FAILURE; }\n",
" return EXIT_SUCCESS;\n",
"}\n",
);
let violations = guard_uses_before_declaration(historic);
assert_eq!(violations.len(), 1, "{violations:?}");
assert!(violations[0].contains("`result`"), "{violations:?}");
}
#[test]
fn raw_result_test_function_asserts_failure_per_result_type() {
let cases: &[(&str, &str)] = &[
("char*", "assert(result == NULL && \"expected call to fail\");"),
("int32_t", "assert(result < 0 && \"expected call to fail\");"),
(
"uintptr_t",
"assert(sample_last_error_code() != 0 && \"expected call to fail\");",
),
];
for (raw_type, expected_assert) in cases {
let mut fixture = Fixture {
id: "invalid_input".into(),
description: "Invalid input".into(),
..Fixture::default()
};
fixture.assertions.push(crate::e2e::fixture::Assertion {
assertion_type: "error".into(),
..Default::default()
});
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let field_resolver = FieldResolver::new(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
);
let mut out = String::new();
render_test_function_impl(
&mut out,
&fixture,
"sample",
"sample_parse_input",
"result",
&[],
&field_resolver,
&HashMap::new(),
&HashSet::new(),
&ResultTypeName::Resolved("Result".into()),
"",
None,
Some(raw_type),
None,
None,
false,
false,
None,
&[],
&config,
&[],
&[],
false,
&FieldConfigSources {
result_fields: EffectiveConfigSource::Global,
fields: EffectiveConfigSource::Global,
},
TargetParams::IrAbsent,
)
.expect("test fixture renders");
assert!(
out.contains(expected_assert),
"raw_type={raw_type}: expected `{expected_assert}` in:\n{out}"
);
assert!(
!out.contains("expected call to succeed"),
"raw_type={raw_type}: unexpected success-path assertion in:\n{out}"
);
}
}
#[test]
fn raw_result_test_function_falls_back_to_last_error_code_for_unmodeled_raw_types() {
for raw_type in ["bool", "uint64_t", "size_t"] {
let mut fixture = Fixture {
id: "invalid_input".into(),
description: "Invalid input".into(),
..Fixture::default()
};
fixture.assertions.push(crate::e2e::fixture::Assertion {
assertion_type: "error".into(),
..Default::default()
});
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let field_resolver = FieldResolver::new(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
);
let mut out = String::new();
render_test_function_impl(
&mut out,
&fixture,
"sample",
"sample_parse_input",
"result",
&[],
&field_resolver,
&HashMap::new(),
&HashSet::new(),
&ResultTypeName::Resolved("Result".into()),
"",
None,
Some(raw_type),
None,
None,
false,
false,
None,
&[],
&config,
&[],
&[],
false,
&FieldConfigSources {
result_fields: EffectiveConfigSource::Global,
fields: EffectiveConfigSource::Global,
},
TargetParams::IrAbsent,
)
.expect("test fixture renders");
assert!(
out.contains("assert(sample_last_error_code() != 0 && \"expected call to fail\");"),
"raw_type={raw_type}: expected last_error_code fallback assert in:\n{out}"
);
}
}
fn render_c_error_fixture(extra: Vec<crate::e2e::fixture::Assertion>, declared: Option<&str>) -> String {
let mut fixture = Fixture {
id: "rate_limited".into(),
description: "Rejects the request".into(),
..Fixture::default()
};
fixture.assertions.push(crate::e2e::fixture::Assertion {
assertion_type: "error".into(),
value: declared.map(|v| serde_json::Value::String(v.to_string())),
..Default::default()
});
fixture.assertions.extend(extra);
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let field_resolver = FieldResolver::new(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
);
let mut out = String::new();
let _ = crate::e2e::codegen::take_skip_records();
render_test_function_impl(
&mut out,
&fixture,
"sample",
"sample_parse_input",
"result",
&[],
&field_resolver,
&HashMap::new(),
&HashSet::new(),
&ResultTypeName::Resolved("Result".into()),
"",
None,
Some("char*"),
None,
None,
false,
false,
None,
&[],
&config,
&[],
&[],
false,
&FieldConfigSources {
result_fields: EffectiveConfigSource::Global,
fields: EffectiveConfigSource::Global,
},
TargetParams::IrAbsent,
)
.expect("test fixture renders");
out
}
#[test]
fn a_declared_error_value_is_compared_against_the_ffi_error_message() {
let out = render_c_error_fixture(Vec::new(), Some("rate limit"));
assert!(
out.contains("assert(result == NULL && \"expected call to fail\");"),
"the failure check must still render: {out}"
);
assert!(
out.contains("const char* _err_message = sample_last_error_context();"),
"the FFI message must be bound: {out}"
);
assert!(
out.contains("assert(strstr(_err_message, \"rate limit\") != NULL && \"error message mismatch\");"),
"the declared value must be compared: {out}"
);
}
#[test]
fn an_error_assertion_without_a_value_emits_no_message_check() {
let out = render_c_error_fixture(Vec::new(), None);
assert!(
out.contains("assert(result == NULL && \"expected call to fail\");"),
"the failure check must still render: {out}"
);
assert!(!out.contains("last_error_context"), "{out}");
}
#[test]
fn an_equals_on_an_error_field_is_named_instead_of_dropped() {
let out = render_c_error_fixture(
vec![crate::e2e::fixture::Assertion {
assertion_type: "equals".into(),
field: Some("error.status_code".into()),
..Default::default()
}],
Some("rate limit"),
);
assert!(
out.contains("assert(result == NULL && \"expected call to fail\");"),
"the error block must render before we assert anything about the second assertion: {out}"
);
assert!(
out.contains(
"// skipped: assertion type 'equals' has no accessor for error field error.status_code in this backend"
),
"{out}"
);
let records = crate::e2e::codegen::take_skip_records();
assert_eq!(records.len(), 1, "got: {records:?}");
assert_eq!(records[0].language, "c");
assert_eq!(records[0].field, "equals");
}
#[test]
fn void_result_snippet_calls_api_without_placeholder_result() {
let fixture = Fixture {
id: "clear_formats".into(),
description: "Clear formats".into(),
..Fixture::default()
};
let mut e2e = E2eConfig::default();
e2e.call.function = "clear_formats".into();
e2e.call.returns_void = true;
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let rendered = CCodegen
.render_snippet_body(&fixture, &e2e, &config, &[], &[])
.expect("void-result snippet renders");
assert!(rendered.contains("sample_clear_formats();"), "{rendered}");
assert!(!rendered.contains("result ="), "{rendered}");
assert!(!rendered.contains("_free("), "{rendered}");
}
#[test]
fn enum_fields_from_ir_recovers_field_missing_from_declared_fields_enum() {
let fields_c_types = HashMap::from([("batch_object.status".to_string(), "BatchStatus".to_string())]);
let enums = vec![crate::core::ir::EnumDef {
name: "BatchStatus".into(),
..crate::core::ir::EnumDef::default()
}];
let derived = enum_fields_from_ir(&fields_c_types, &enums);
assert_eq!(derived, HashSet::from(["status".to_string()]));
}
#[test]
fn enum_fields_from_ir_ignores_a_field_whose_type_is_not_a_registered_enum() {
let fields_c_types = HashMap::from([("batch_object.usage".to_string(), "BatchUsage".to_string())]);
let enums = vec![crate::core::ir::EnumDef {
name: "BatchStatus".into(),
..crate::core::ir::EnumDef::default()
}];
let derived = enum_fields_from_ir(&fields_c_types, &enums);
assert!(derived.is_empty(), "got: {derived:?}");
}
#[test]
fn try_emit_enum_accessor_fires_for_a_field_ir_proves_is_an_enum_even_when_fields_enum_omits_it() {
let fields_c_types = HashMap::from([("batch_object.status".to_string(), "BatchStatus".to_string())]);
let enums = vec![crate::core::ir::EnumDef {
name: "BatchStatus".into(),
..crate::core::ir::EnumDef::default()
}];
let mut fields_enum: HashSet<String> = HashSet::new();
fields_enum.extend(enum_fields_from_ir(&fields_c_types, &enums));
let mut out = String::new();
let mut handles = Vec::new();
let fired = try_emit_enum_accessor(
&mut out,
"sample",
"SAMPLE",
"status",
"status",
"batch_object",
"sample_batch_object_status",
"result",
"status",
&fields_c_types,
&fields_enum,
&mut handles,
);
assert!(
fired,
"enum accessor must fire once the IR-derived override is unioned in"
);
assert!(
out.contains("sample_batch_status_to_string("),
"must convert via _to_string, not leave a bare handle for strcmp: {out}"
);
assert!(!out.contains("strcmp"), "{out}");
}
}
#[cfg(test)]
mod result_type_resolution_tests;