use crate::core::config::ResolvedCrateConfig;
use crate::e2e::codegen::field_skip::FieldSkip;
use crate::e2e::config::{CallConfig, E2eConfig};
use crate::e2e::escape::escape_c;
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{Assertion, Fixture};
use heck::{ToPascalCase, ToSnakeCase};
use std::collections::{HashMap, HashSet};
use std::fmt::Write as FmtWrite;
use super::{c_optional_sentinel, is_primitive_c_type, is_skipped_c_field, json_to_c, try_emit_enum_accessor};
#[allow(clippy::too_many_arguments)]
pub(super) fn emit_nested_accessor(
out: &mut String,
prefix: &str,
resolved: &str,
local_var: &str,
result_var: &str,
fields_c_types: &HashMap<String, String>,
fields_enum: &HashSet<String>,
intermediate_handles: &mut Vec<(String, String)>,
result_type_name: &str,
raw_field: &str,
type_defs: &[crate::core::ir::TypeDef],
config_sources: &FieldConfigSources,
) -> anyhow::Result<Option<String>> {
let segments: Vec<&str> = resolved.split('.').collect();
let prefix_upper = crate::codegen::c_consumer::export_type_prefix(prefix);
let mut current_snake_type = result_type_name.to_snake_case();
let mut current_handle = result_var.to_string();
let mut current_type_from_ir = type_defs.iter().any(|type_def| type_def.name == result_type_name);
let mut json_extract_mode = false;
for (i, segment) in segments.iter().enumerate() {
let is_leaf = i + 1 == segments.len();
if json_extract_mode {
let (bare_segment, bracket_key): (&str, Option<&str>) = match segment.find('[') {
Some(pos) => (&segment[..pos], Some(segment[pos + 1..].trim_end_matches(']'))),
None => (segment, None),
};
let seg_snake = bare_segment.to_snake_case();
if is_leaf {
let _ = writeln!(
out,
" char* {local_var} = alef_json_get_string({current_handle}, \"{seg_snake}\");"
);
return Ok(None); }
let json_var = format!("{seg_snake}_json");
if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
let _ = writeln!(
out,
" char* {json_var} = alef_json_get_object({current_handle}, \"{seg_snake}\");"
);
intermediate_handles.push((json_var.clone(), "free".to_string()));
}
if let Some(key) = bracket_key
&& let Ok(idx) = key.parse::<usize>()
{
let elem_var = format!("{seg_snake}_{idx}_json");
if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
let _ = writeln!(
out,
" char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
);
intermediate_handles.push((elem_var.clone(), "free".to_string()));
}
current_handle = elem_var;
continue;
}
current_handle = json_var;
continue;
}
if let Some(bracket_pos) = segment.find('[') {
let field_name = &segment[..bracket_pos];
let key = segment[bracket_pos + 1..].trim_end_matches(']');
let field_snake = field_name.to_snake_case();
let accessor_fn = format!("{prefix}_{current_snake_type}_{field_snake}");
let json_var = format!("{field_snake}_json");
if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
let _ = writeln!(out, " assert({json_var} != NULL);");
intermediate_handles.push((json_var.clone(), "free_string".to_string()));
}
if key.is_empty() {
if !is_leaf {
current_handle = json_var;
json_extract_mode = true;
continue;
}
return Ok(None);
}
if let Ok(idx) = key.parse::<usize>() {
let elem_var = format!("{field_snake}_{idx}_json");
if !intermediate_handles.iter().any(|(h, _)| h == &elem_var) {
let _ = writeln!(
out,
" char* {elem_var} = alef_json_array_get_index({json_var}, {idx});"
);
intermediate_handles.push((elem_var.clone(), "free".to_string()));
}
if !is_leaf {
current_handle = elem_var;
json_extract_mode = true;
continue;
}
return Ok(None);
}
let _ = writeln!(
out,
" char* {local_var} = alef_json_get_string({json_var}, \"{key}\");"
);
return Ok(None); }
let seg_snake = segment.to_snake_case();
let accessor_fn = format!("{prefix}_{current_snake_type}_{seg_snake}");
if is_skipped_c_field(fields_c_types, ¤t_snake_type, &seg_snake) {
return Ok(Some("__skip__".to_string())); }
if is_leaf {
let lookup_key = format!("{current_snake_type}.{seg_snake}");
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}({current_handle});");
return Ok(Some(t.clone()));
}
if try_emit_enum_accessor(
out,
prefix,
&prefix_upper,
raw_field,
&seg_snake,
¤t_snake_type,
&accessor_fn,
¤t_handle,
local_var,
fields_c_types,
fields_enum,
intermediate_handles,
) {
return Ok(None);
}
if let Some(opaque_type) = fields_c_types.get(&lookup_key).filter(|t| {
*t != "char*"
&& *t != "skip"
&& !is_primitive_c_type(t)
&& t.chars().next().is_some_and(|c| c.is_uppercase())
}) {
let handle_var = format!("{seg_snake}_handle");
let opaque_snake = opaque_type.to_snake_case();
if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
let _ = writeln!(
out,
" {prefix_upper}AlefHandle {handle_var} = {accessor_fn}({current_handle});"
);
intermediate_handles.push((handle_var.clone(), opaque_snake.clone()));
}
if local_var != handle_var {
let _ = writeln!(out, " {prefix_upper}AlefHandle {local_var} = {handle_var};");
}
return Ok(Some(opaque_snake)); }
ensure_leaf_field_exists(LeafFieldCheck {
prefix,
accessor_fn: &accessor_fn,
resolved,
raw_field,
segment,
parent_snake_type: ¤t_snake_type,
parent_is_ir_type: current_type_from_ir,
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}({current_handle});");
} else {
let lookup_key = format!("{current_snake_type}.{seg_snake}");
let return_type_pascal = match fields_c_types
.get(&lookup_key)
.cloned()
.or_else(|| resolve_intermediate_type(¤t_snake_type, &seg_snake, type_defs))
{
Some(return_type) => return_type,
None => {
anyhow::bail!(
"{}",
missing_intermediate_type_diagnostic(MissingIntermediateType {
prefix,
lookup_key: &lookup_key,
accessor_fn: &accessor_fn,
resolved,
raw_field,
segment,
seg_snake: &seg_snake,
segments_walked: &segments[..=i],
current_snake_type: ¤t_snake_type,
result_type_name,
type_defs,
fields_source: &config_sources.fields,
})
);
}
};
if return_type_pascal == "char*" {
let json_var = format!("{seg_snake}_json");
if !intermediate_handles.iter().any(|(h, _)| h == &json_var) {
let _ = writeln!(out, " char* {json_var} = {accessor_fn}({current_handle});");
intermediate_handles.push((json_var.clone(), "free_string".to_string()));
}
if i + 2 == segments.len() && segments[i + 1] == "length" {
let _ = writeln!(out, " int {local_var} = alef_json_array_count({json_var});");
return Ok(Some("int".to_string()));
}
current_snake_type = seg_snake.clone();
current_type_from_ir = false;
current_handle = json_var;
continue;
}
let return_snake = return_type_pascal.to_snake_case();
let handle_var = format!("{seg_snake}_handle");
if !intermediate_handles.iter().any(|(h, _)| h == &handle_var) {
let _ = writeln!(
out,
" {prefix_upper}AlefHandle {handle_var} = \
{accessor_fn}({current_handle});"
);
let _ = writeln!(out, " assert({handle_var} != 0);");
intermediate_handles.push((handle_var.clone(), return_snake.clone()));
}
current_type_from_ir = type_defs.iter().any(|type_def| type_def.name == return_type_pascal);
current_snake_type = return_snake;
current_handle = handle_var;
}
}
Ok(None)
}
fn resolve_intermediate_type(
parent_snake: &str,
field_snake: &str,
type_defs: &[crate::core::ir::TypeDef],
) -> Option<String> {
let parent = type_defs
.iter()
.find(|type_def| type_def.name.to_snake_case() == parent_snake)?;
let field = parent
.fields
.iter()
.find(|field| field.name.to_snake_case() == field_snake)?;
super::named_type(&field.ty).map(str::to_string)
}
const MAX_FIELD_PATH_SEARCH_DEPTH: usize = 6;
struct ResolvedFieldChain {
path: String,
owner_type: String,
}
fn find_all_field_paths(
root_type: &str,
field_snake: &str,
type_defs: &[crate::core::ir::TypeDef],
) -> Vec<ResolvedFieldChain> {
fn walk(
type_name: &str,
field_snake: &str,
type_defs: &[crate::core::ir::TypeDef],
depth: usize,
seen: &mut HashSet<String>,
out: &mut Vec<ResolvedFieldChain>,
) {
if depth == 0 || !seen.insert(type_name.to_string()) {
return;
}
let Some(type_def) = type_defs.iter().find(|type_def| type_def.name == type_name) else {
return;
};
if let Some(field) = type_def
.fields
.iter()
.find(|field| field.name.to_snake_case() == field_snake)
{
out.push(ResolvedFieldChain {
path: field.name.to_snake_case(),
owner_type: type_def.name.clone(),
});
}
for field in &type_def.fields {
let Some(nested) = super::named_type(&field.ty) else {
continue;
};
let before = out.len();
walk(nested, field_snake, type_defs, depth - 1, seen, out);
for chain in &mut out[before..] {
chain.path = format!("{}.{}", field.name.to_snake_case(), chain.path);
}
}
}
let mut out = Vec::new();
walk(
root_type,
field_snake,
type_defs,
MAX_FIELD_PATH_SEARCH_DEPTH,
&mut HashSet::new(),
&mut out,
);
out.sort_by_key(|chain| chain.path.matches('.').count());
out
}
#[cfg(test)]
fn find_field_path(
root_type: &str,
field_snake: &str,
type_defs: &[crate::core::ir::TypeDef],
) -> Option<ResolvedFieldChain> {
let mut chains = find_all_field_paths(root_type, field_snake, type_defs);
if chains.len() == 1 { chains.pop() } else { None }
}
fn stripped_namespace_prefix<'a>(raw_field: &'a str, resolved: &str) -> Option<&'a str> {
let prefix_len = raw_field.len().checked_sub(resolved.len())?;
if prefix_len == 0 || !raw_field.ends_with(resolved) {
return None;
}
raw_field
.get(..prefix_len)
.and_then(|prefix| prefix.strip_suffix('.'))
.filter(|prefix| !prefix.is_empty())
}
fn why_the_type_is_unknown(parent_snake: &str, field_snake: &str, type_defs: &[crate::core::ir::TypeDef]) -> String {
let Some(parent) = type_defs
.iter()
.find(|type_def| type_def.name.to_snake_case() == parent_snake)
else {
return format!("No IR type has the snake_case name `{parent_snake}`");
};
let Some(field) = parent
.fields
.iter()
.find(|field| field.name.to_snake_case() == field_snake)
else {
return format!("Type `{}` has no field `{field_snake}`", parent.name);
};
if super::named_type(&field.ty).is_none() {
return format!(
"Field `{}.{field_snake}` is not a named struct type, so no opaque accessor type can be derived from it",
parent.name
);
}
format!("Type `{}` does have a field `{field_snake}`", parent.name)
}
struct MissingIntermediateType<'a> {
prefix: &'a str,
lookup_key: &'a str,
accessor_fn: &'a str,
resolved: &'a str,
raw_field: &'a str,
segment: &'a str,
seg_snake: &'a str,
segments_walked: &'a [&'a str],
current_snake_type: &'a str,
result_type_name: &'a str,
type_defs: &'a [crate::core::ir::TypeDef],
fields_source: &'a EffectiveConfigSource,
}
fn missing_intermediate_type_diagnostic(context: MissingIntermediateType<'_>) -> String {
let MissingIntermediateType {
prefix,
lookup_key,
accessor_fn,
resolved,
raw_field,
segment,
seg_snake,
segments_walked,
current_snake_type,
result_type_name,
type_defs,
fields_source,
} = context;
let mut message = format!(
"e2e c codegen: fields_c_types is missing key \"{lookup_key}\" (path \"{resolved}\", segment \"{segment}\"), \
reached while walking fixture field \"{raw_field}\" from result type `{result_type_name}`. {why}, so \
declaring \"{lookup_key}\" would make the generated test call `{accessor_fn}()`. (The old fallback guessed \
`{guess}` from the field name, which silently miscompiled whenever the Rust return type differed, e.g. \
`DataNode` vs `Data`.)",
why = why_the_type_is_unknown(current_snake_type, seg_snake, type_defs),
guess = segment.to_pascal_case(),
);
if let Some(namespace) = stripped_namespace_prefix(raw_field, resolved) {
let _ = write!(
message,
" alef stripped the leading \"{namespace}\" from \"{raw_field}\" as a virtual namespace, because no \
`[crates.e2e.fields]` alias maps it onto a real path and its first segment is not a `result_fields` \
entry -- which is why the walk started at `{result_type_name}` instead of inside `{namespace}`."
);
}
match find_all_field_paths(result_type_name, seg_snake, type_defs).as_slice() {
[chain] => {
let alias_key = match stripped_namespace_prefix(raw_field, resolved) {
Some(namespace) => format!("{namespace}.{}", segments_walked.join(".")),
None => segments_walked.join("."),
};
let real_path = &chain.path;
let real_symbol = format!("{prefix}_{}_{seg_snake}", chain.owner_type.to_snake_case());
let fields_key = match fields_source {
EffectiveConfigSource::Global => "`[crates.e2e.fields]`".to_string(),
EffectiveConfigSource::PerCall(label) => format!("`{label}.fields`"),
};
let _ = write!(
message,
" Field `{seg_snake}` does exist below `{result_type_name}`, at \"{real_path}\" -- it is declared on \
`{owner}`, so the accessor that really exists is `{real_symbol}()`. Fix: add \
\"{alias_key}\" = \"{real_path}\" under {fields_key} so the fixture path resolves to the \
real chain. Only add \"{lookup_key}\" to `[crates.e2e.fields_c_types]` if `{accessor_fn}()` really \
is in the generated header.",
owner = chain.owner_type,
);
}
[] => {
let _ = write!(
message,
" No type reachable from `{result_type_name}` has a field named `{seg_snake}` either, so the \
fixture's field path is the thing to check first -- declaring \"{lookup_key}\" cannot make \
`{accessor_fn}()` exist."
);
}
chains => {
let _ = write!(
message,
"{}",
ambiguous_field_name_suffix(seg_snake, result_type_name, chains, fields_source)
);
}
}
message
}
fn ambiguous_field_name_suffix(
seg_snake: &str,
result_type_name: &str,
chains: &[ResolvedFieldChain],
fields_source: &EffectiveConfigSource,
) -> String {
let candidates: Vec<String> = chains
.iter()
.map(|chain| format!("\"{}\" (declared on `{}`)", chain.path, chain.owner_type))
.collect();
let fields_key = match fields_source {
EffectiveConfigSource::Global => "`[crates.e2e.fields]`".to_string(),
EffectiveConfigSource::PerCall(label) => format!("`{label}.fields`"),
};
format!(
" Field `{seg_snake}` is declared on {count} unrelated types reachable from `{result_type_name}`, with \
different chains: {candidates} -- alef cannot tell which one the fixture means, and guessing risks \
binding the assertion to a field with a different value domain than intended. Fix: add \
\"<fixture path>\" = \"<the correct chain from the list above>\" under {fields_key} yourself, \
after checking which candidate actually matches this fixture's data.",
count = chains.len(),
candidates = candidates.join(", "),
)
}
pub(super) enum EffectiveConfigSource {
Global,
PerCall(String),
}
pub(super) fn describe_effective_config_source(
e2e_config: &E2eConfig,
call: &CallConfig,
call_has_override: bool,
) -> EffectiveConfigSource {
if !call_has_override {
return EffectiveConfigSource::Global;
}
match e2e_config
.calls
.iter()
.find(|(_, candidate)| std::ptr::eq(*candidate, call))
{
Some((name, _)) => EffectiveConfigSource::PerCall(format!("[crates.e2e.calls.{name}]")),
None => EffectiveConfigSource::PerCall("[crates.e2e.call]".to_string()),
}
}
pub(super) struct FieldConfigSources {
pub result_fields: EffectiveConfigSource,
pub fields: EffectiveConfigSource,
}
impl FieldConfigSources {
pub(super) fn resolve(e2e_config: &E2eConfig, call: &CallConfig) -> Self {
Self {
result_fields: describe_effective_config_source(e2e_config, call, !call.result_fields.is_empty()),
fields: describe_effective_config_source(e2e_config, call, !call.fields.is_empty()),
}
}
}
pub(super) struct LeafFieldCheck<'a> {
pub prefix: &'a str,
pub accessor_fn: &'a str,
pub resolved: &'a str,
pub raw_field: &'a str,
pub segment: &'a str,
pub parent_snake_type: &'a str,
pub parent_is_ir_type: bool,
pub declared_in_fields_c_types: bool,
pub result_type_name: &'a str,
pub type_defs: &'a [crate::core::ir::TypeDef],
pub result_fields_source: &'a EffectiveConfigSource,
pub fields_source: &'a EffectiveConfigSource,
}
pub(super) fn ensure_leaf_field_exists(check: LeafFieldCheck<'_>) -> anyhow::Result<()> {
if !check.parent_is_ir_type || check.declared_in_fields_c_types || check.resolved.contains('[') {
return Ok(());
}
let seg_snake = check.segment.to_snake_case();
let Some(parent) = check
.type_defs
.iter()
.find(|type_def| type_def.name.to_snake_case() == check.parent_snake_type)
else {
return Ok(());
};
if parent
.fields
.iter()
.any(|field| field.name.to_snake_case() == seg_snake)
{
return Ok(());
}
anyhow::bail!(
"{}",
unknown_leaf_field_diagnostic(UnknownLeafField {
prefix: check.prefix,
accessor_fn: check.accessor_fn,
resolved: check.resolved,
raw_field: check.raw_field,
segment: check.segment,
seg_snake: &seg_snake,
parent_type: &parent.name,
result_type_name: check.result_type_name,
type_defs: check.type_defs,
result_fields_source: check.result_fields_source,
fields_source: check.fields_source,
})
)
}
struct UnknownLeafField<'a> {
prefix: &'a str,
accessor_fn: &'a str,
resolved: &'a str,
raw_field: &'a str,
segment: &'a str,
seg_snake: &'a str,
parent_type: &'a str,
result_type_name: &'a str,
type_defs: &'a [crate::core::ir::TypeDef],
result_fields_source: &'a EffectiveConfigSource,
fields_source: &'a EffectiveConfigSource,
}
fn unknown_leaf_field_diagnostic(context: UnknownLeafField<'_>) -> String {
let UnknownLeafField {
prefix,
accessor_fn,
resolved,
raw_field,
segment,
seg_snake,
parent_type,
result_type_name,
type_defs,
result_fields_source,
fields_source,
} = context;
let mut message = format!(
"e2e c codegen: fixture field \"{raw_field}\" (path \"{resolved}\") ends at segment \"{segment}\", but IR \
type `{parent_type}` has no field `{seg_snake}`. The walk was about to emit `{accessor_fn}()`, a C symbol \
no binding generates, so this assertion would have been rendered against a function that does not exist. \
Nothing upstream rejects it: the field-availability oracle (`FieldResolver::is_valid_for_result`) only \
inspects a path's FIRST segment, which is a real field here."
);
let namespace = stripped_namespace_prefix(raw_field, resolved);
if let Some(namespace) = namespace {
let _ = write!(
message,
" alef stripped the leading \"{namespace}\" from \"{raw_field}\" as a virtual namespace, because no \
`[crates.e2e.fields]` alias maps it onto a real path and its first segment is not a `result_fields` \
entry -- which is why the walk started at `{result_type_name}` instead of inside `{namespace}`."
);
}
let chains = find_all_field_paths(result_type_name, seg_snake, type_defs);
let chain = match chains.as_slice() {
[chain] => chain,
[] => {
let _ = write!(
message,
" No type reachable from `{result_type_name}` has a field named `{seg_snake}` either, so the \
fixture's field path is the thing to fix -- there is no config entry that can spell a chain which \
does not exist."
);
return message;
}
chains => {
let _ = write!(
message,
"{}",
ambiguous_field_name_suffix(seg_snake, result_type_name, chains, fields_source)
);
return message;
}
};
let real_path = &chain.path;
let real_symbol = format!("{prefix}_{}_{seg_snake}", chain.owner_type.to_snake_case());
let _ = write!(
message,
" Field `{seg_snake}` does exist below `{result_type_name}`, at \"{real_path}\" -- it is declared on \
`{owner}`, so the accessor that really exists is `{real_symbol}()`.",
owner = chain.owner_type,
);
match namespace.filter(|namespace| real_path.starts_with(&format!("{namespace}."))) {
Some(namespace) => {
let result_fields_key = match result_fields_source {
EffectiveConfigSource::Global => "`[crates.e2e].result_fields`".to_string(),
EffectiveConfigSource::PerCall(label) => format!("`{label}.result_fields`"),
};
let _ = write!(
message,
" Fix: add \"{namespace}\" to {result_fields_key} so alef stops treating it as a virtual \
namespace prefix and walks it as the real field it is. An alias here would be an identity mapping \
and would not stop the stripping."
);
}
None => {
let fields_key = match fields_source {
EffectiveConfigSource::Global => "`[crates.e2e.fields]`".to_string(),
EffectiveConfigSource::PerCall(label) => format!("`{label}.fields`"),
};
let _ = write!(
message,
" Fix: add \"{raw_field}\" = \"{real_path}\" under {fields_key} so the fixture path \
resolves to the real chain."
);
}
}
message
}
pub(super) use crate::e2e::codegen::call_ir::TargetParams;
fn args_config_key(fixture: &Fixture) -> String {
match fixture.call.as_deref() {
Some(name) => format!("[crates.e2e.calls.{name}].args"),
None => "[crates.e2e.call].args".to_string(),
}
}
fn missing_args_for_known_params_diagnostic(
fixture: &Fixture,
function_name: &str,
params: &[crate::core::ir::ParamDef],
) -> String {
let names: Vec<&str> = params.iter().map(|p| p.name.as_str()).collect();
let call_key = args_config_key(fixture);
format!(
"e2e c codegen: fixture \"{id}\" calls `{function_name}` with no configured `args`, but the Rust core \
signature for `{function_name}` declares {count} parameter(s): {joined_names}. With no `args` \
configured, alef used to splice the fixture's whole `input` JSON as a single C string literal \
regardless of what the target actually takes, which does not compile against anything but a lone \
string parameter. Fix: add an `args` entry under `{call_key}` for each parameter, mapping it to the \
fixture input field that supplies it.",
id = fixture.id,
count = params.len(),
joined_names = names.join(", "),
)
}
fn missing_args_unresolvable_signature_diagnostic(fixture: &Fixture, function_name: &str) -> String {
let call_key = args_config_key(fixture);
format!(
"e2e c codegen: fixture \"{id}\" calls `{function_name}` with no configured `args`, and alef could not \
resolve `{function_name}` against the Rust core IR, so it cannot tell a genuine zero-argument call from \
a missing `args` configuration -- guessing risks splicing the fixture's whole `input` JSON as one C \
literal against a target that takes real, typed parameters. Fix: configure `args` under `{call_key}`, \
one entry per parameter `{function_name}` actually takes. If it genuinely takes none, check that this \
call's `function` name (and any per-language override) matches a real core function or method name -- \
an unresolvable name is why alef cannot confirm that on its own.",
id = fixture.id,
)
}
fn handle_param_type_name(ty: &crate::core::ir::TypeRef) -> Option<&str> {
match ty {
crate::core::ir::TypeRef::Named(name) => Some(name),
crate::core::ir::TypeRef::Optional(inner) => match inner.as_ref() {
crate::core::ir::TypeRef::Named(name) => Some(name),
_ => None,
},
_ => None,
}
}
const MAX_DIAGNOSTIC_VALUE_CHARS: usize = 80;
fn handle_param_type_mismatch_diagnostic(
fixture: &Fixture,
function_name: &str,
arg: &crate::e2e::config::ArgMapping,
param: &crate::core::ir::ParamDef,
param_type: &crate::core::ir::TypeDef,
rendered: &str,
) -> String {
let call_key = args_config_key(fixture);
let quoted: String = rendered.chars().take(MAX_DIAGNOSTIC_VALUE_CHARS).collect();
let elided = if quoted.len() < rendered.len() { "..." } else { "" };
let type_name = ¶m_type.name;
let mut message = format!(
"e2e c codegen: fixture \"{id}\" maps `args` entry \"{arg_name}\" (type = \"{arg_type}\", field = \
\"{field}\") onto parameter `{param_name}` of `{function_name}`, which the Rust core declares as \
`{type_name}` and the C ABI exports as `AlefHandle` -- an unsigned integer handle, not a pointer or \
a string. The fixture value lowers to the C literal {quoted}{elided}, and passing a literal where a \
handle is expected does not compile (`incompatible pointer to integer conversion`). A handle only \
exists once something constructs it, and alef will not fabricate one.",
id = fixture.id,
arg_name = arg.name,
arg_type = arg.arg_type,
field = arg.field,
param_name = param.name,
);
if arg.arg_type == "json_object" {
let _ = write!(
message,
" This entry already declares `type = \"json_object\"`, so the gap is on alef's side: this call \
path rendered the arguments without constructing any typed handle first (the `returns_void` \
snippet path in `c/test_function.rs` passes an empty handle map, unlike the free-function path, \
which emits the `from_json` construction ahead of the call). Until that path constructs handles, \
this fixture needs an extension-owned documentation recipe for C, or a documented \
`coverage_exceptions` entry."
);
} else if param_type.has_serde {
let _ = write!(
message,
" Fix: set `type = \"json_object\"` and `element_type = \"{type_name}\"` on that entry under \
{call_key}, so alef constructs the handle with the generated `from_json` helper and passes that \
instead of the literal."
);
} else {
let _ = write!(
message,
" `{type_name}` derives no serde, so the FFI crate exports no `from_json` constructor for it and \
`type = \"json_object\"` would name a symbol that does not exist. This fixture needs an \
extension-owned documentation recipe for C, or a documented `coverage_exceptions` entry."
);
}
message
}
fn ensure_arg_matches_param_type(
fixture: &Fixture,
function_name: &str,
arg: &crate::e2e::config::ArgMapping,
index: usize,
params: &[crate::core::ir::ParamDef],
type_defs: &[crate::core::ir::TypeDef],
rendered: &str,
) -> anyhow::Result<()> {
let Some(param) = TargetParams::Known(params).param_for(&arg.name, index) else {
return Ok(());
};
let Some(type_name) = handle_param_type_name(¶m.ty) else {
return Ok(());
};
let Some(param_type) = type_defs.iter().find(|type_def| type_def.name == type_name) else {
return Ok(());
};
anyhow::bail!(
"{}",
handle_param_type_mismatch_diagnostic(fixture, function_name, arg, param, param_type, rendered)
)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn build_args_string_c(
input: &serde_json::Value,
args: &[crate::e2e::config::ArgMapping],
typed_arg_handles: &HashMap<String, String>,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
fixture: &Fixture,
function_name: &str,
target_params: TargetParams<'_>,
) -> anyhow::Result<String> {
if args.is_empty() {
return match target_params {
TargetParams::Known([]) => Ok(String::new()),
TargetParams::Known(params) => {
anyhow::bail!(
"{}",
missing_args_for_known_params_diagnostic(fixture, function_name, params)
)
}
TargetParams::IrAbsent => Ok(json_to_c(input)),
TargetParams::Unresolvable => {
anyhow::bail!(
"{}",
missing_args_unresolvable_signature_diagnostic(fixture, function_name)
)
}
};
}
let known_params = target_params.known();
let mut parts: Vec<String> = Vec::new();
for (index, arg) in args.iter().enumerate() {
if arg.arg_type == "test_backend" {
let Some(trait_name) = &arg.trait_name else {
panic!(
"C e2e generator: fixture `{}` declares a `test_backend` arg `{}` with no `trait_name` configured; cannot generate a C stub without knowing which trait to implement",
fixture.id, arg.name
);
};
let Some(trait_bridge) = config.trait_bridges.iter().find(|tb| tb.trait_name == *trait_name) else {
panic!(
"C e2e generator: fixture `{}` requires trait `{trait_name}` for its `test_backend` arg `{}`, but no `[[crates.trait_bridges]]` entry named `{trait_name}` is configured",
fixture.id, arg.name
);
};
let mut methods: Vec<&crate::core::ir::MethodDef> = type_defs
.iter()
.find(|t| t.name == *trait_name)
.map(|t| t.methods.iter().collect())
.unwrap_or_default();
if let Some(super_trait) = &trait_bridge.super_trait
&& let Some(super_type) = type_defs.iter().find(|t| &t.rust_path == super_trait)
{
for method in &super_type.methods {
if !methods.iter().any(|m| m.name == method.name) {
methods.push(method);
}
}
}
let emission = crate::e2e::codegen::emit_test_backend("c", trait_bridge, &methods, fixture, &[]);
parts.push(emission.arg_expr);
continue;
}
let val = crate::e2e::codegen::resolve_field(input, &arg.field);
match val {
v if v.is_null() && arg.optional => parts.push(c_optional_sentinel(&arg.arg_type).to_string()),
v if v.is_null() => {}
v => {
if let Some(handle) = typed_arg_handles.get(&arg.name) {
parts.push(handle.clone())
} else {
let rendered = json_to_c(v);
if let Some(params) = known_params {
ensure_arg_matches_param_type(
fixture,
function_name,
arg,
index,
params,
type_defs,
&rendered,
)?;
}
parts.push(rendered)
}
}
}
}
Ok(parts.join(", "))
}
#[allow(clippy::too_many_arguments)]
pub(super) fn render_assertion(
out: &mut String,
assertion: &Assertion,
result_var: &str,
ffi_prefix: &str,
_field_resolver: &FieldResolver,
accessed_fields: &[(String, String, bool)],
primitive_locals: &HashMap<String, String>,
opaque_handle_locals: &HashMap<String, String>,
) {
if let Some(f) = &assertion.field
&& !f.is_empty()
&& !_field_resolver.is_valid_for_result(f)
{
let _ = writeln!(
out,
" // skipped: {}",
FieldSkip::NotAvailableOnResultType.message(f)
);
return;
}
let field_expr = match &assertion.field {
Some(f) if !f.is_empty() => {
accessed_fields
.iter()
.find(|(k, _, _)| k == f)
.map(|(_, local, _)| local.clone())
.unwrap_or_else(|| result_var.to_string())
}
_ => result_var.to_string(),
};
if primitive_locals.get(&field_expr).is_some_and(|t| t == "__skip__") {
let _ = writeln!(
out,
" // skipped: {}",
FieldSkip::NotAvailableInCFfi.message(&field_expr)
);
return;
}
let field_is_primitive = primitive_locals.contains_key(&field_expr);
let field_primitive_type = primitive_locals.get(&field_expr).cloned();
let field_is_opaque_handle = opaque_handle_locals.contains_key(&field_expr);
let field_is_map_access = if let Some(f) = &assertion.field {
accessed_fields.iter().any(|(k, _, m)| k == f && *m)
} else {
false
};
let assertion_field_is_optional = assertion
.field
.as_deref()
.map(|f| {
if f.is_empty() {
return false;
}
if _field_resolver.is_optional(f) {
return true;
}
let resolved = _field_resolver.resolve(f);
_field_resolver.is_optional(resolved)
})
.unwrap_or(false);
match assertion.assertion_type.as_str() {
"equals" => {
if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
if field_is_primitive {
let cmp_val = if field_primitive_type.as_deref() == Some("bool") {
match expected.as_bool() {
Some(true) => "1".to_string(),
Some(false) => "0".to_string(),
None => c_val,
}
} else {
c_val
};
let is_numeric = field_primitive_type.as_deref().map(|t| t != "bool").unwrap_or(false);
if assertion_field_is_optional && is_numeric {
let _ = writeln!(
out,
" assert(({field_expr} == 0 || {field_expr} == {cmp_val}) && \"equals assertion failed\");"
);
} else {
let _ = writeln!(
out,
" assert({field_expr} == {cmp_val} && \"equals assertion failed\");"
);
}
} else if field_is_opaque_handle {
if expected.is_number() {
let _ = writeln!(
out,
" assert({field_expr} == {c_val} && \"equals assertion failed\");"
);
} else {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
}
} else if expected.is_string() {
let _ = writeln!(
out,
" assert({field_expr} != NULL && strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
);
} else if field_is_map_access && expected.is_boolean() {
let lit = match expected.as_bool() {
Some(true) => "\"true\"",
_ => "\"false\"",
};
let _ = writeln!(
out,
" assert({field_expr} != NULL && strcmp({field_expr}, {lit}) == 0 && \"equals assertion failed\");"
);
} else if field_is_map_access && expected.is_number() {
if expected.is_f64() {
let _ = writeln!(
out,
" assert({field_expr} != NULL && atof({field_expr}) == {c_val} && \"equals assertion failed\");"
);
} else {
let _ = writeln!(
out,
" assert({field_expr} != NULL && atoll({field_expr}) == {c_val} && \"equals assertion failed\");"
);
}
} else {
let _ = writeln!(
out,
" assert(strcmp({field_expr}, {c_val}) == 0 && \"equals assertion failed\");"
);
}
}
}
"contains" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
let _ = writeln!(
out,
" assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
);
}
}
"contains_all" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(values) = &assertion.values {
for val in values {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert({field_expr} != NULL && strstr({field_expr}, {c_val}) != NULL && \"expected to contain substring\");"
);
}
}
}
"not_contains" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
let _ = writeln!(
out,
" assert({field_expr} != NULL && strstr({field_expr}, {c_val}) == NULL && \"expected non-null value without substring\");"
);
}
}
"not_empty" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else {
let _ = writeln!(
out,
" assert({field_expr} != NULL && strlen({field_expr}) > 0 && \"expected non-empty value\");"
);
}
}
"is_empty" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} == 0 && \"expected null handle\");");
} else if assertion_field_is_optional || !field_is_primitive {
let _ = writeln!(
out,
" assert(({field_expr} == NULL || strlen({field_expr}) == 0) && \"expected empty value\");"
);
} else {
let _ = writeln!(
out,
" assert(strlen({field_expr}) == 0 && \"expected empty value\");"
);
}
}
"contains_any" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(values) = &assertion.values {
let _ = writeln!(out, " {{");
let _ = writeln!(out, " int found = 0;");
for val in values {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" if (strstr({field_expr}, {c_val}) != NULL) {{ found = 1; }}"
);
}
let _ = writeln!(
out,
" assert(found && \"expected to contain at least one of the specified values\");"
);
let _ = writeln!(out, " }}");
}
}
"greater_than" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
if field_is_map_access && val.is_number() && !field_is_primitive {
let _ = writeln!(
out,
" assert({field_expr} != NULL && atof({field_expr}) > {c_val} && \"expected greater than\");"
);
} else {
let _ = writeln!(out, " assert({field_expr} > {c_val} && \"expected greater than\");");
}
}
}
"less_than" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
if field_is_map_access && val.is_number() && !field_is_primitive {
let _ = writeln!(
out,
" assert({field_expr} != NULL && atof({field_expr}) < {c_val} && \"expected less than\");"
);
} else {
let _ = writeln!(out, " assert({field_expr} < {c_val} && \"expected less than\");");
}
}
}
"greater_than_or_equal" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
if field_is_map_access && val.is_number() && !field_is_primitive {
let _ = writeln!(
out,
" assert({field_expr} != NULL && atof({field_expr}) >= {c_val} && \"expected greater than or equal\");"
);
} else {
let _ = writeln!(
out,
" assert({field_expr} >= {c_val} && \"expected greater than or equal\");"
);
}
}
}
"less_than_or_equal" => {
if let Some(val) = &assertion.value {
let c_val = json_to_c(val);
if field_is_map_access && val.is_number() && !field_is_primitive {
let _ = writeln!(
out,
" assert({field_expr} != NULL && atof({field_expr}) <= {c_val} && \"expected less than or equal\");"
);
} else {
let _ = writeln!(
out,
" assert({field_expr} <= {c_val} && \"expected less than or equal\");"
);
}
}
}
"starts_with" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
let _ = writeln!(
out,
" assert(strncmp({field_expr}, {c_val}, strlen({c_val})) == 0 && \"expected to start with\");"
);
}
}
"ends_with" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
let _ = writeln!(out, " assert(strlen({field_expr}) >= strlen({c_val}) && ");
let _ = writeln!(
out,
" strcmp({field_expr} + strlen({field_expr}) - strlen({c_val}), {c_val}) == 0 && \"expected to end with\");"
);
}
}
"min_length" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(val) = &assertion.value
&& let Some(n) = val.as_u64()
{
let _ = writeln!(
out,
" assert(strlen({field_expr}) >= {n} && \"expected minimum length\");"
);
}
}
"max_length" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(val) = &assertion.value
&& let Some(n) = val.as_u64()
{
let _ = writeln!(
out,
" assert(strlen({field_expr}) <= {n} && \"expected maximum length\");"
);
}
}
"count_min" => {
if let Some(val) = &assertion.value
&& let Some(n) = val.as_u64()
{
let _ = writeln!(out, " {{");
let _ = writeln!(out, " /* count_min: count top-level JSON array elements */");
let _ = writeln!(
out,
" assert({field_expr} != NULL && \"expected non-null collection JSON\");"
);
let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
let _ = writeln!(
out,
" assert(elem_count >= {n} && \"expected at least {n} elements\");"
);
let _ = writeln!(out, " }}");
}
}
"count_equals" => {
if let Some(val) = &assertion.value
&& let Some(n) = val.as_u64()
{
let _ = writeln!(out, " {{");
let _ = writeln!(out, " /* count_equals: count elements in array */");
let _ = writeln!(
out,
" assert({field_expr} != NULL && \"expected non-null collection JSON\");"
);
let _ = writeln!(out, " int elem_count = alef_json_array_count({field_expr});");
let _ = writeln!(out, " assert(elem_count == {n} && \"expected {n} elements\");");
let _ = writeln!(out, " }}");
}
}
"is_true" => {
let _ = writeln!(out, " assert({field_expr});");
}
"is_false" => {
let _ = writeln!(out, " assert(!{field_expr});");
}
"method_result" => {
if let Some(method_name) = &assertion.method {
render_method_result_assertion(
out,
result_var,
ffi_prefix,
method_name,
assertion.args.as_ref(),
assertion.return_type.as_deref(),
assertion.check.as_deref().unwrap_or("is_true"),
assertion.value.as_ref(),
);
} else {
panic!("C e2e generator: method_result assertion missing 'method' field");
}
}
"matches_regex" => {
if field_is_opaque_handle {
let _ = writeln!(out, " assert({field_expr} != 0 && \"expected non-null handle\");");
} else if let Some(expected) = &assertion.value {
let c_val = json_to_c(expected);
let _ = writeln!(out, " {{");
let _ = writeln!(out, " regex_t _re;");
let _ = writeln!(
out,
" assert(regcomp(&_re, {c_val}, REG_EXTENDED) == 0 && \"regex compile failed\");"
);
let _ = writeln!(
out,
" assert(regexec(&_re, {field_expr}, 0, NULL, 0) == 0 && \"expected value to match regex\");"
);
let _ = writeln!(out, " regfree(&_re);");
let _ = writeln!(out, " }}");
}
}
"not_error" => {
}
"error" => {
}
other => {
panic!("C e2e generator: unsupported assertion type: {other}");
}
}
}
#[allow(clippy::too_many_arguments)]
fn render_method_result_assertion(
out: &mut String,
result_var: &str,
ffi_prefix: &str,
method_name: &str,
args: Option<&serde_json::Value>,
return_type: Option<&str>,
check: &str,
value: Option<&serde_json::Value>,
) {
let call_expr = build_c_method_call(result_var, ffi_prefix, method_name, args);
if return_type == Some("string") {
let _ = writeln!(out, " {{");
let _ = writeln!(out, " char* _method_result = {call_expr};");
if check == "is_error" {
let _ = writeln!(
out,
" assert(_method_result == NULL && \"expected method to return error\");"
);
let _ = writeln!(out, " }}");
return;
}
let _ = writeln!(
out,
" assert(_method_result != NULL && \"method_result returned NULL\");"
);
match check {
"contains" => {
if let Some(val) = value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert(strstr(_method_result, {c_val}) != NULL && \"method_result contains assertion failed\");"
);
}
}
"equals" => {
if let Some(val) = value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert(strcmp(_method_result, {c_val}) == 0 && \"method_result equals assertion failed\");"
);
}
}
"is_true" => {
let _ = writeln!(
out,
" assert(_method_result != NULL && strlen(_method_result) > 0 && \"method_result is_true assertion failed\");"
);
}
"count_min" => {
if let Some(val) = value {
let n = val.as_u64().unwrap_or(0);
let _ = writeln!(out, " int _elem_count = alef_json_array_count(_method_result);");
let _ = writeln!(
out,
" assert(_elem_count >= {n} && \"method_result count_min assertion failed\");"
);
}
}
other_check => {
panic!("C e2e generator: unsupported method_result check type for string return: {other_check}");
}
}
let _ = writeln!(out, " free(_method_result);");
let _ = writeln!(out, " }}");
return;
}
match check {
"equals" => {
if let Some(val) = value {
let c_val = json_to_c(val);
let _ = writeln!(
out,
" assert({call_expr} == {c_val} && \"method_result equals assertion failed\");"
);
}
}
"is_true" => {
let _ = writeln!(
out,
" assert({call_expr} && \"method_result is_true assertion failed\");"
);
}
"is_false" => {
let _ = writeln!(
out,
" assert(!{call_expr} && \"method_result is_false assertion failed\");"
);
}
"greater_than_or_equal" => {
if let Some(val) = value {
let n = val.as_u64().unwrap_or(0);
let _ = writeln!(
out,
" assert({call_expr} >= {n} && \"method_result >= {n} assertion failed\");"
);
}
}
"count_min" => {
if let Some(val) = value {
let n = val.as_u64().unwrap_or(0);
let _ = writeln!(
out,
" assert({call_expr} >= {n} && \"method_result count_min assertion failed\");"
);
}
}
other_check => {
panic!("C e2e generator: unsupported method_result check type: {other_check}");
}
}
}
fn build_c_method_call(
result_var: &str,
ffi_prefix: &str,
method_name: &str,
args: Option<&serde_json::Value>,
) -> String {
let extra_args = if let Some(args_val) = args {
args_val
.as_object()
.map(|obj| {
obj.values()
.map(|v| match v {
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())),
})
.collect::<Vec<_>>()
.join(", ")
})
.unwrap_or_default()
} else {
String::new()
};
if extra_args.is_empty() {
format!("{ffi_prefix}_{method_name}({result_var})")
} else {
format!("{ffi_prefix}_{method_name}({result_var}, {extra_args})")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{FieldDef, ParamDef, TypeDef, TypeRef};
fn global_sources() -> FieldConfigSources {
FieldConfigSources {
result_fields: EffectiveConfigSource::Global,
fields: EffectiveConfigSource::Global,
}
}
#[test]
fn c_ir_reachable_field_absent_from_result_fields_is_not_skipped() {
let reachable: HashSet<String> = ["data".to_string()].into_iter().collect();
let resolver = FieldResolver::new(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
)
.with_ir_fields(reachable, HashSet::new());
let assertion = Assertion {
assertion_type: "equals".to_string(),
field: Some("data".to_string()),
value: Some(serde_json::Value::String("hello".to_string())),
..Default::default()
};
let mut out = String::new();
render_assertion(
&mut out,
&assertion,
"result",
"sample",
&resolver,
&[],
&HashMap::new(),
&HashMap::new(),
);
assert!(!out.contains("skipped"), "got: {out}");
}
#[test]
fn c_ir_excluded_field_present_in_result_fields_is_still_skipped() {
let result_fields: HashSet<String> = ["internal_diagnostics".to_string()].into_iter().collect();
let excluded: HashSet<String> = ["internal_diagnostics".to_string()].into_iter().collect();
let resolver = FieldResolver::new(
&HashMap::new(),
&HashSet::new(),
&result_fields,
&HashSet::new(),
&HashSet::new(),
)
.with_ir_fields(HashSet::new(), excluded);
let assertion = Assertion {
assertion_type: "equals".to_string(),
field: Some("internal_diagnostics".to_string()),
value: Some(serde_json::Value::String("hello".to_string())),
..Default::default()
};
let mut out = String::new();
render_assertion(
&mut out,
&assertion,
"result",
"sample",
&resolver,
&[],
&HashMap::new(),
&HashMap::new(),
);
assert!(out.contains("skipped"), "got: {out}");
}
#[test]
fn equals_assertion_on_opaque_handle_compares_numerically_not_via_strcmp() {
let reachable: HashSet<String> = ["status".to_string()].into_iter().collect();
let resolver = FieldResolver::new(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
)
.with_ir_fields(reachable, HashSet::new());
let assertion = Assertion {
assertion_type: "equals".to_string(),
field: Some("status".to_string()),
value: Some(serde_json::json!(2)),
..Default::default()
};
let accessed_fields = [("status".to_string(), "status".to_string(), false)];
let mut opaque_handle_locals = HashMap::new();
opaque_handle_locals.insert("status".to_string(), "batch_status".to_string());
let mut out = String::new();
render_assertion(
&mut out,
&assertion,
"result",
"sample",
&resolver,
&accessed_fields,
&HashMap::new(),
&opaque_handle_locals,
);
assert!(out.contains("status == 2"), "got: {out}");
assert!(!out.contains("strcmp"), "must not strcmp a uint64_t handle: {out}");
}
#[test]
fn equals_assertion_on_opaque_handle_with_string_value_falls_back_to_existence_check() {
let reachable: HashSet<String> = ["status".to_string()].into_iter().collect();
let resolver = FieldResolver::new(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
)
.with_ir_fields(reachable, HashSet::new());
let assertion = Assertion {
assertion_type: "equals".to_string(),
field: Some("status".to_string()),
value: Some(serde_json::Value::String("completed".to_string())),
..Default::default()
};
let accessed_fields = [("status".to_string(), "status".to_string(), false)];
let mut opaque_handle_locals = HashMap::new();
opaque_handle_locals.insert("status".to_string(), "batch_status".to_string());
let mut out = String::new();
render_assertion(
&mut out,
&assertion,
"result",
"sample",
&resolver,
&accessed_fields,
&HashMap::new(),
&opaque_handle_locals,
);
assert!(out.contains("status != 0"), "got: {out}");
assert!(
!out.contains("strcmp"),
"must not compare a uint64_t handle to a string literal: {out}"
);
}
#[test]
fn nested_optional_handle_type_comes_from_ir_when_config_mapping_is_absent() {
let types = [
TypeDef {
name: "ExtractionResult".into(),
fields: vec![FieldDef {
name: "summary".into(),
ty: TypeRef::Optional(Box::new(TypeRef::Named("ExtractionSummary".into()))),
..FieldDef::default()
}],
..TypeDef::default()
},
TypeDef {
name: "ExtractionSummary".into(),
fields: vec![FieldDef {
name: "processed".into(),
ty: TypeRef::Primitive(crate::core::ir::PrimitiveType::U64),
..FieldDef::default()
}],
..TypeDef::default()
},
];
let mut output = String::new();
let mut handles = Vec::new();
emit_nested_accessor(
&mut output,
"sample",
"summary.processed",
"summary_processed",
"result",
&HashMap::from([("extraction_summary.processed".into(), "uint64_t".into())]),
&HashSet::new(),
&mut handles,
"ExtractionResult",
"summary.processed",
&types,
&global_sources(),
)
.expect("every hop resolves");
assert!(output.contains("SAMPLEAlefHandle summary_handle"), "{output}");
assert!(output.contains("sample_extraction_result_summary(result)"), "{output}");
assert!(output.contains("uint64_t summary_processed"), "{output}");
}
fn crawlberg_article_types() -> Vec<TypeDef> {
vec![
TypeDef {
name: "ScrapeResult".into(),
fields: vec![FieldDef {
name: "metadata".into(),
ty: TypeRef::Optional(Box::new(TypeRef::Named("PageMetadata".into()))),
..FieldDef::default()
}],
..TypeDef::default()
},
TypeDef {
name: "PageMetadata".into(),
fields: vec![FieldDef {
name: "article".into(),
ty: TypeRef::Optional(Box::new(TypeRef::Named("ArticleMetadata".into()))),
..FieldDef::default()
}],
..TypeDef::default()
},
TypeDef {
name: "ArticleMetadata".into(),
fields: vec![FieldDef {
name: "tags".into(),
ty: TypeRef::Vec(Box::new(TypeRef::String)),
..FieldDef::default()
}],
..TypeDef::default()
},
]
}
fn walk_crawlberg_article_tags() -> anyhow::Error {
walk_crawlberg_article_tags_with_sources(&global_sources())
}
fn walk_crawlberg_article_tags_with_sources(config_sources: &FieldConfigSources) -> anyhow::Error {
let mut output = String::new();
let mut handles = Vec::new();
emit_nested_accessor(
&mut output,
"cberg",
"tags.length",
"article_tags_length",
"result",
&HashMap::new(),
&HashSet::new(),
&mut handles,
"ScrapeResult",
"article.tags.length",
&crawlberg_article_types(),
config_sources,
)
.expect_err("`tags` is not a field of ScrapeResult")
}
#[test]
fn missing_intermediate_type_returns_an_error_instead_of_panicking() {
let message = walk_crawlberg_article_tags().to_string();
assert!(message.contains("fields_c_types"), "{message}");
assert!(message.contains("scrape_result.tags"), "{message}");
assert!(message.contains("tags.length"), "{message}");
}
#[test]
fn missing_intermediate_type_keeps_the_original_panic_facts() {
let message = walk_crawlberg_article_tags().to_string();
assert!(message.contains("path \"tags.length\""), "{message}");
assert!(message.contains("segment \"tags\""), "{message}");
assert!(message.contains("`Tags`"), "guessed-name rationale is gone: {message}");
assert!(message.contains("`DataNode` vs `Data`"), "{message}");
}
#[test]
fn missing_intermediate_type_names_the_real_chain_not_the_phantom_key() {
let message = walk_crawlberg_article_tags().to_string();
assert!(
message.contains("Type `ScrapeResult` has no field `tags`"),
"must say why the key is missing: {message}"
);
assert!(
message.contains("stripped the leading \"article\""),
"must name the namespace stripping that produced the path: {message}"
);
assert!(
message.contains("cberg_scrape_result_tags()"),
"must name the C symbol declaring the key would conjure: {message}"
);
assert!(
message.contains("cberg_article_metadata_tags()"),
"must name the C symbol that really exists: {message}"
);
assert!(
message.contains("\"metadata.article.tags\""),
"must name the real resolved chain: {message}"
);
assert!(
message.contains("\"article.tags\" = \"metadata.article.tags\""),
"must spell the alias that fixes it: {message}"
);
assert!(
message.contains("[crates.e2e.fields]"),
"must name the alias table, not just fields_c_types: {message}"
);
}
#[test]
fn missing_intermediate_type_names_the_per_call_fields_when_that_is_what_shadows() {
let sources = FieldConfigSources {
result_fields: EffectiveConfigSource::Global,
fields: EffectiveConfigSource::PerCall("[crates.e2e.calls.scrape]".to_string()),
};
let message = walk_crawlberg_article_tags_with_sources(&sources).to_string();
assert!(
message.contains("\"article.tags\" = \"metadata.article.tags\" under `[crates.e2e.calls.scrape].fields`"),
"must name the per-call key that actually governs this call: {message}"
);
assert!(
!message.contains("under `[crates.e2e.fields]`"),
"must not point at the global key when a per-call override shadows it: {message}"
);
}
#[test]
fn missing_intermediate_type_says_so_when_no_type_carries_the_field() {
let mut output = String::new();
let mut handles = Vec::new();
let error = emit_nested_accessor(
&mut output,
"cberg",
"nowhere.length",
"nowhere_length",
"result",
&HashMap::new(),
&HashSet::new(),
&mut handles,
"ScrapeResult",
"nowhere.length",
&crawlberg_article_types(),
&global_sources(),
)
.expect_err("`nowhere` is not a field of anything");
let message = error.to_string();
assert!(
message.contains("No type reachable from `ScrapeResult` has a field named `nowhere`"),
"{message}"
);
assert!(
!message.contains("under `[crates.e2e.fields]`"),
"must not suggest an alias it cannot spell: {message}"
);
assert!(
!message.contains("stripped the leading"),
"nothing was stripped here: {message}"
);
}
#[test]
fn stripped_namespace_prefix_recovers_only_a_real_stripped_prefix() {
assert_eq!(
stripped_namespace_prefix("article.tags.length", "tags.length"),
Some("article")
);
assert_eq!(
stripped_namespace_prefix("interaction.action_results[0].x", "action_results[0].x"),
Some("interaction")
);
assert_eq!(stripped_namespace_prefix("tags.length", "tags.length"), None);
assert_eq!(
stripped_namespace_prefix("metadata.title", "something.else"),
None,
"a raw field that does not end with the resolved path was not produced by stripping"
);
}
#[test]
fn find_field_path_returns_the_shallowest_chain_and_its_declaring_type() {
let types = crawlberg_article_types();
let tags = find_field_path("ScrapeResult", "tags", &types).expect("tags is reachable");
assert_eq!(tags.path, "metadata.article.tags");
assert_eq!(
tags.owner_type, "ArticleMetadata",
"the C accessor symbol is built from the declaring type, not the root"
);
let metadata = find_field_path("ScrapeResult", "metadata", &types).expect("metadata is a direct field");
assert_eq!(metadata.path, "metadata");
assert_eq!(metadata.owner_type, "ScrapeResult");
assert!(find_field_path("ScrapeResult", "nowhere", &types).is_none());
}
fn completion_response_types() -> Vec<TypeDef> {
vec![
TypeDef {
name: "CompletionResponse".into(),
fields: vec![
FieldDef {
name: "id".into(),
ty: TypeRef::String,
..FieldDef::default()
},
FieldDef {
name: "metadata".into(),
ty: TypeRef::Named("Metadata".into()),
..FieldDef::default()
},
],
..TypeDef::default()
},
TypeDef {
name: "Metadata".into(),
fields: vec![FieldDef {
name: "document".into(),
ty: TypeRef::Named("Document".into()),
..FieldDef::default()
}],
..TypeDef::default()
},
TypeDef {
name: "Document".into(),
fields: vec![FieldDef {
name: "title".into(),
ty: TypeRef::String,
..FieldDef::default()
}],
..TypeDef::default()
},
]
}
fn completion_response_c_types() -> HashMap<String, String> {
HashMap::from([
("completion_response.metadata".to_string(), "Metadata".to_string()),
("metadata.document".to_string(), "Document".to_string()),
])
}
fn walk_completion_response(
resolved: &str,
raw_field: &str,
fields_c_types: &HashMap<String, String>,
) -> anyhow::Result<(String, Option<String>)> {
walk_completion_response_with_sources(resolved, raw_field, fields_c_types, &global_sources())
}
fn walk_completion_response_with_sources(
resolved: &str,
raw_field: &str,
fields_c_types: &HashMap<String, String>,
config_sources: &FieldConfigSources,
) -> anyhow::Result<(String, Option<String>)> {
let mut output = String::new();
let mut handles = Vec::new();
let leaf = emit_nested_accessor(
&mut output,
"gatelib",
resolved,
"metadata_title",
"result",
fields_c_types,
&HashSet::new(),
&mut handles,
"CompletionResponse",
raw_field,
&completion_response_types(),
config_sources,
)?;
Ok((output, leaf))
}
#[test]
fn unknown_leaf_field_is_an_error_not_a_phantom_accessor() {
let error = walk_completion_response("metadata.title", "metadata.title", &completion_response_c_types())
.expect_err("`title` is not a field of `Metadata`");
let message = error.to_string();
assert!(
message.contains("IR type `Metadata` has no field `title`"),
"must name the type and the field it lacks: {message}"
);
assert!(
message.contains("gatelib_metadata_title()"),
"must name the phantom symbol it refused to emit: {message}"
);
assert!(
message.contains("only inspects a path's FIRST segment"),
"must say why nothing upstream caught it: {message}"
);
}
#[test]
fn unknown_leaf_field_diagnostic_spells_the_alias_that_fixes_it() {
let message = walk_completion_response("metadata.title", "metadata.title", &completion_response_c_types())
.expect_err("`title` is not a field of `Metadata`")
.to_string();
assert!(
message.contains("\"metadata.title\" = \"metadata.document.title\""),
"must spell the alias that reconnects the fixture path: {message}"
);
assert!(
message.contains("`[crates.e2e.fields]`"),
"must name the table the alias goes in: {message}"
);
assert!(
message.contains("gatelib_document_title()"),
"must name the accessor that really exists: {message}"
);
}
#[test]
fn unknown_leaf_field_diagnostic_names_the_per_call_fields_when_that_is_what_shadows() {
let sources = FieldConfigSources {
result_fields: EffectiveConfigSource::Global,
fields: EffectiveConfigSource::PerCall("[crates.e2e.calls.complete]".to_string()),
};
let message = walk_completion_response_with_sources(
"metadata.title",
"metadata.title",
&completion_response_c_types(),
&sources,
)
.expect_err("`title` is not a field of `Metadata`")
.to_string();
assert!(
message.contains(
"\"metadata.title\" = \"metadata.document.title\" under `[crates.e2e.calls.complete].fields`"
),
"must name the per-call key that actually governs this call: {message}"
);
assert!(
!message.contains("`[crates.e2e.fields]`"),
"must not point at the global key when a per-call override shadows it: {message}"
);
}
#[test]
fn resolvable_leaf_still_renders_its_accessor() {
let (output, leaf) = walk_completion_response(
"metadata.document.title",
"metadata.title",
&completion_response_c_types(),
)
.expect("every hop and the leaf resolve");
assert_eq!(
leaf, None,
"a plain string leaf is a char*, not a primitive or a handle"
);
assert!(
output.contains("char* metadata_title = gatelib_document_title(document_handle);"),
"{output}"
);
}
#[test]
fn explicitly_declared_leaf_type_overrides_the_ir_check() {
let mut fields_c_types = completion_response_c_types();
fields_c_types.insert("metadata.title".to_string(), "char*".to_string());
let (output, _) = walk_completion_response("metadata.title", "metadata.title", &fields_c_types)
.expect("an explicit fields_c_types declaration is authoritative");
assert!(
output.contains("char* metadata_title = gatelib_metadata_title(metadata_handle);"),
"{output}"
);
}
#[test]
fn leaf_on_a_type_the_ir_does_not_declare_is_not_rejected() {
let mut output = String::new();
let mut handles = Vec::new();
emit_nested_accessor(
&mut output,
"gatelib",
"metadata.title",
"metadata_title",
"result",
&HashMap::from([("unmodelled_result.metadata".to_string(), "AlsoUnmodelled".to_string())]),
&HashSet::new(),
&mut handles,
"UnmodelledResult",
"metadata.title",
&completion_response_types(),
&global_sources(),
)
.expect("an unmodelled parent type must not be treated as proof the leaf is absent");
assert!(
output.contains("char* metadata_title = gatelib_also_unmodelled_title(metadata_handle);"),
"{output}"
);
}
fn ts_pack_types() -> Vec<TypeDef> {
vec![
TypeDef {
name: "ProcessResult".into(),
fields: vec![
FieldDef {
name: "language".into(),
ty: TypeRef::String,
..FieldDef::default()
},
FieldDef {
name: "data".into(),
ty: TypeRef::Named("DataNode".into()),
..FieldDef::default()
},
],
..TypeDef::default()
},
TypeDef {
name: "DataNode".into(),
fields: vec![FieldDef {
name: "kind".into(),
ty: TypeRef::String,
..FieldDef::default()
}],
..TypeDef::default()
},
]
}
fn check_ts_pack_stripped_leaf(
declared_in_fields_c_types: bool,
result_fields_source: &EffectiveConfigSource,
) -> anyhow::Result<()> {
let types = ts_pack_types();
ensure_leaf_field_exists(LeafFieldCheck {
prefix: "ts_pack",
accessor_fn: "ts_pack_process_result_kind",
resolved: "kind",
raw_field: "data.kind",
segment: "kind",
parent_snake_type: "process_result",
parent_is_ir_type: true,
declared_in_fields_c_types,
result_type_name: "ProcessResult",
type_defs: &types,
result_fields_source,
fields_source: &EffectiveConfigSource::Global,
})
}
#[test]
fn namespace_stripped_leaf_that_is_not_a_result_type_field_is_rejected() {
let message = check_ts_pack_stripped_leaf(false, &EffectiveConfigSource::Global)
.expect_err("`kind` is a field of `DataNode`, not of `ProcessResult`")
.to_string();
assert!(
message.contains("IR type `ProcessResult` has no field `kind`"),
"must name the type the accessor would have been called on: {message}"
);
assert!(
message.contains("stripped the leading \"data\""),
"must name the stripping that produced the bare leaf: {message}"
);
assert!(
message.contains("ts_pack_data_node_kind()"),
"must name the accessor that really exists: {message}"
);
}
#[test]
fn stripped_leaf_diagnostic_names_result_fields_not_an_identity_alias() {
let message = check_ts_pack_stripped_leaf(false, &EffectiveConfigSource::Global)
.expect_err("`kind` is a field of `DataNode`, not of `ProcessResult`")
.to_string();
assert!(
message.contains("add \"data\" to `[crates.e2e].result_fields`"),
"must name the config entry that stops the stripping: {message}"
);
assert!(
!message.contains("\"data.kind\" = \"data.kind\""),
"must not suggest an identity alias that changes nothing: {message}"
);
}
#[test]
fn stripped_leaf_diagnostic_names_the_per_call_result_fields_when_that_is_what_shadows() {
let source = EffectiveConfigSource::PerCall("[crates.e2e.calls.crawl]".to_string());
let message = check_ts_pack_stripped_leaf(false, &source)
.expect_err("`kind` is a field of `DataNode`, not of `ProcessResult`")
.to_string();
assert!(
message.contains("add \"data\" to `[crates.e2e.calls.crawl].result_fields`"),
"must name the per-call key that actually governs this call: {message}"
);
assert!(
!message.contains("`[crates.e2e].result_fields`"),
"must not point at the global key when a per-call override shadows it: {message}"
);
}
#[test]
fn describe_effective_config_source_names_the_unnamed_default_call() {
let e2e_config = E2eConfig::default();
let call = CallConfig {
result_fields: HashSet::from(["pages".to_string()]),
..CallConfig::default()
};
let source = describe_effective_config_source(&e2e_config, &call, !call.result_fields.is_empty());
match source {
EffectiveConfigSource::PerCall(label) => assert_eq!(label, "[crates.e2e.call]"),
EffectiveConfigSource::Global => panic!("call_has_override == true must never resolve to Global"),
}
}
#[test]
fn describe_effective_config_source_names_a_call_matched_by_pointer_identity() {
let mut e2e_config = E2eConfig::default();
let crawl_call = CallConfig {
result_fields: HashSet::from(["pages".to_string()]),
..CallConfig::default()
};
e2e_config.calls.insert("crawl".to_string(), crawl_call);
let source = describe_effective_config_source(&e2e_config, &e2e_config.calls["crawl"], true);
match source {
EffectiveConfigSource::PerCall(label) => assert_eq!(label, "[crates.e2e.calls.crawl]"),
EffectiveConfigSource::Global => panic!("call_has_override == true must never resolve to Global"),
}
}
#[test]
fn describe_effective_config_source_is_global_when_the_caller_says_there_is_no_override() {
let e2e_config = E2eConfig::default();
let call = CallConfig {
result_fields: HashSet::from(["pages".to_string()]),
..CallConfig::default()
};
assert!(matches!(
describe_effective_config_source(&e2e_config, &call, false),
EffectiveConfigSource::Global
));
}
#[test]
fn field_config_sources_resolve_derives_each_collection_independently() {
let mut e2e_config = E2eConfig::default();
let call = CallConfig {
result_fields: HashSet::from(["pages".to_string()]),
..CallConfig::default()
};
e2e_config.calls.insert("crawl".to_string(), call);
let sources = FieldConfigSources::resolve(&e2e_config, &e2e_config.calls["crawl"]);
assert!(
matches!(sources.result_fields, EffectiveConfigSource::PerCall(ref label) if label == "[crates.e2e.calls.crawl]")
);
assert!(matches!(sources.fields, EffectiveConfigSource::Global));
}
#[test]
fn explicitly_declared_flat_leaf_type_overrides_the_ir_check() {
check_ts_pack_stripped_leaf(true, &EffectiveConfigSource::Global)
.expect("an explicit fields_c_types declaration is authoritative");
}
fn ts_pack_types_with_optional_data_and_enum_kind() -> Vec<TypeDef> {
vec![
TypeDef {
name: "ProcessResult".into(),
fields: vec![FieldDef {
name: "data".into(),
ty: TypeRef::Optional(Box::new(TypeRef::Named("DataNode".into()))),
..FieldDef::default()
}],
..TypeDef::default()
},
TypeDef {
name: "DataNode".into(),
fields: vec![
FieldDef {
name: "kind".into(),
ty: TypeRef::Named("DataNodeKind".into()),
..FieldDef::default()
},
FieldDef {
name: "children".into(),
ty: TypeRef::Vec(Box::new(TypeRef::Named("DataNode".into()))),
..FieldDef::default()
},
],
..TypeDef::default()
},
]
}
#[test]
fn dotted_path_through_optional_field_reaches_enum_leaf() {
let types = ts_pack_types_with_optional_data_and_enum_kind();
let fields_c_types = HashMap::from([
("process_result.data".to_string(), "DataNode".to_string()),
("data_node.kind".to_string(), "DataNodeKind".to_string()),
]);
let fields_enum: HashSet<String> = ["data.kind".to_string()].into_iter().collect();
let mut output = String::new();
let mut handles = Vec::new();
let result = emit_nested_accessor(
&mut output,
"ts_pack",
"data.kind",
"data_kind",
"result",
&fields_c_types,
&fields_enum,
&mut handles,
"ProcessResult",
"data.kind",
&types,
&global_sources(),
)
.expect("the Option<DataNode> hop and the enum leaf both resolve");
assert_eq!(
result, None,
"an enum leaf returns Ok(None) (render_assertion reads it as a plain char*), not \
Ok(Some(opaque_type)) -- a Some here would mean the opaque-struct branch fired instead"
);
assert!(
output.contains("data_handle = ts_pack_process_result_data(result)"),
"must walk into the Option<DataNode> field via the FFI accessor: {output}"
);
assert!(
output.contains("ts_pack_data_node_kind_to_string("),
"must convert the enum leaf via its _to_string accessor, proving the enum branch \
(not the opaque-struct branch) fired: {output}"
);
assert!(
!output.contains("AlefHandle data_kind = kind_handle"),
"must not fall through to the opaque-struct branch's bare handle assignment: {output}"
);
}
#[test]
fn ambiguous_leaf_field_name_does_not_suggest_a_specific_alias() {
let types = vec![
TypeDef {
name: "ProcessResult".into(),
fields: vec![
FieldDef {
name: "data".into(),
ty: TypeRef::Named("DataNode".into()),
..FieldDef::default()
},
FieldDef {
name: "structure".into(),
ty: TypeRef::Named("StructureItem".into()),
..FieldDef::default()
},
],
..TypeDef::default()
},
TypeDef {
name: "DataNode".into(),
fields: vec![FieldDef {
name: "kind".into(),
ty: TypeRef::String,
..FieldDef::default()
}],
..TypeDef::default()
},
TypeDef {
name: "StructureItem".into(),
fields: vec![FieldDef {
name: "kind".into(),
ty: TypeRef::String,
..FieldDef::default()
}],
..TypeDef::default()
},
];
let message = ensure_leaf_field_exists(LeafFieldCheck {
prefix: "ts_pack",
accessor_fn: "ts_pack_process_result_kind",
resolved: "kind",
raw_field: "data.kind",
segment: "kind",
parent_snake_type: "process_result",
parent_is_ir_type: true,
declared_in_fields_c_types: false,
result_type_name: "ProcessResult",
type_defs: &types,
result_fields_source: &EffectiveConfigSource::Global,
fields_source: &EffectiveConfigSource::Global,
})
.expect_err("`kind` is not a field of `ProcessResult` itself")
.to_string();
assert!(
!message.contains("\"data.kind\" = \"structure.kind\""),
"must never suggest binding DataNode.kind's field onto the unrelated \
StructureItem.kind: {message}"
);
assert!(
message.contains("\"data.kind\""),
"must still name the ambiguous candidate chain rooted at `data`: {message}"
);
assert!(
message.contains("\"structure.kind\""),
"must still name the ambiguous candidate chain rooted at `structure`: {message}"
);
assert!(
message.contains("DataNode") && message.contains("StructureItem"),
"must name both declaring types so the operator can tell them apart: {message}"
);
}
fn test_backend_arg(trait_name: &str) -> crate::e2e::config::ArgMapping {
crate::e2e::config::ArgMapping {
name: "backend".into(),
field: "backend".into(),
arg_type: "test_backend".into(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: Some(trait_name.to_string()),
}
}
#[test]
#[should_panic(expected = "test-backend emitter is unimplemented")]
fn registered_test_backend_trait_panics_because_c_backend_is_unimplemented() {
use crate::core::config::TraitBridgeConfig;
let bridge = TraitBridgeConfig {
trait_name: "SampleBackend".into(),
..TraitBridgeConfig::default()
};
let config = ResolvedCrateConfig {
trait_bridges: vec![bridge],
..ResolvedCrateConfig::default()
};
let fixture = Fixture {
id: "register_sample_backend".into(),
..Fixture::default()
};
let args = vec![test_backend_arg("SampleBackend")];
let _ = build_args_string_c(
&fixture.input,
&args,
&HashMap::new(),
&config,
&[],
&fixture,
"register_sample_backend",
TargetParams::IrAbsent,
);
}
#[test]
#[should_panic(expected = "no `[[crates.trait_bridges]]` entry")]
fn unregistered_test_backend_trait_panics_instead_of_falling_back_to_null() {
let config = ResolvedCrateConfig::default();
let fixture = Fixture {
id: "register_sample_backend".into(),
..Fixture::default()
};
let args = vec![test_backend_arg("SampleBackend")];
let _ = build_args_string_c(
&fixture.input,
&args,
&HashMap::new(),
&config,
&[],
&fixture,
"register_sample_backend",
TargetParams::IrAbsent,
);
}
#[test]
fn should_emit_empty_parens_when_args_unconfigured_and_target_takes_no_parameters() {
let fixture = Fixture {
id: "list_ocr_backends".into(),
input: serde_json::json!({"cache_dir": "/tmp/sample_cache"}),
..Fixture::default()
};
let config = ResolvedCrateConfig::default();
let result = build_args_string_c(
&fixture.input,
&[],
&HashMap::new(),
&config,
&[],
&fixture,
"list_ocr_backends",
TargetParams::Known(&[]),
)
.expect("a genuinely zero-argument target must not fail generation");
assert_eq!(
result, "",
"a zero-argument call must emit `()`, not a fabricated literal"
);
}
#[test]
fn should_refuse_when_args_unconfigured_and_target_takes_a_typed_parameter() {
let fixture = Fixture {
id: "pack_configure_defaults".into(),
input: serde_json::json!({"cache_dir": "/tmp/sample_cache"}),
..Fixture::default()
};
let config = ResolvedCrateConfig::default();
let params = [ParamDef {
name: "config".into(),
..ParamDef::default()
}];
let error = build_args_string_c(
&fixture.input,
&[],
&HashMap::new(),
&config,
&[],
&fixture,
"ts_pack_configure",
TargetParams::Known(¶ms),
)
.expect_err("a known non-empty parameter list must not be papered over with a JSON literal")
.to_string();
assert!(
!error.contains("cache_dir"),
"must not leak the fixture JSON into a diagnostic that replaces splicing it: {error}"
);
assert!(error.contains("ts_pack_configure"), "must name the call: {error}");
assert!(error.contains("config"), "must name the unfilled parameter: {error}");
assert!(error.contains("args"), "must point at the `args` config knob: {error}");
}
#[test]
fn should_refuse_when_args_unconfigured_and_target_signature_is_unresolvable() {
let fixture = Fixture {
id: "mystery_call".into(),
input: serde_json::json!({"cache_dir": "/tmp/sample_cache"}),
..Fixture::default()
};
let config = ResolvedCrateConfig::default();
let error = build_args_string_c(
&fixture.input,
&[],
&HashMap::new(),
&config,
&[],
&fixture,
"mystery_fn",
TargetParams::Unresolvable,
)
.expect_err("an unresolvable signature must not fall back to guessing")
.to_string();
assert!(error.contains("mystery_fn"), "must name the call: {error}");
assert!(error.contains("args"), "must point at the `args` config knob: {error}");
}
#[test]
fn should_keep_prior_behaviour_when_there_is_no_ir_to_consult() {
let fixture = Fixture {
id: "no_ir".into(),
input: serde_json::json!({"cache_dir": "/tmp/sample_cache"}),
..Fixture::default()
};
let config = ResolvedCrateConfig::default();
let rendered = build_args_string_c(
&fixture.input,
&[],
&HashMap::new(),
&config,
&[],
&fixture,
"sample_fn",
TargetParams::IrAbsent,
)
.expect("an absent IR must not fail generation on a path that never had a signature");
assert_eq!(
rendered,
json_to_c(&fixture.input),
"with no IR consulted the emitter must render exactly what it rendered before"
);
}
#[test]
fn should_still_emit_configured_args_unchanged_when_args_are_present() {
let fixture = Fixture {
id: "chat_basic".into(),
input: serde_json::json!({"text": "hello"}),
..Fixture::default()
};
let config = ResolvedCrateConfig::default();
let args = vec![crate::e2e::config::ArgMapping {
name: "text".into(),
field: "text".into(),
arg_type: "string".into(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}];
let result = build_args_string_c(
&fixture.input,
&args,
&HashMap::new(),
&config,
&[],
&fixture,
"chat",
TargetParams::Unresolvable,
)
.expect("configured args must still render");
assert_eq!(
result, "\"hello\"",
"a configured string arg must still emit its real typed literal"
);
}
fn string_arg(name: &str, field: &str) -> crate::e2e::config::ArgMapping {
crate::e2e::config::ArgMapping {
name: name.into(),
field: field.into(),
arg_type: "string".into(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}
}
#[test]
fn should_refuse_a_string_literal_configured_against_a_handle_parameter() {
let fixture = Fixture {
id: "configure_cache_dir".into(),
input: serde_json::json!({"config": {"cache_dir": "/tmp/sample_cache"}}),
..Fixture::default()
};
let config = ResolvedCrateConfig::default();
let args = vec![string_arg("config", "config")];
let params = [ParamDef {
name: "config".into(),
ty: TypeRef::Named("SampleConfig".into()),
..ParamDef::default()
}];
let type_defs = [TypeDef {
name: "SampleConfig".into(),
has_serde: true,
..TypeDef::default()
}];
let error = build_args_string_c(
&fixture.input,
&args,
&HashMap::new(),
&config,
&type_defs,
&fixture,
"sample_configure",
TargetParams::Known(¶ms),
)
.expect_err("a JSON object must not be lowered into a handle parameter")
.to_string();
assert!(error.contains("sample_configure"), "must name the call: {error}");
assert!(error.contains("`config`"), "must name the parameter: {error}");
assert!(
error.contains("AlefHandle"),
"must name the parameter's C type: {error}"
);
assert!(
error.contains("cache_dir"),
"must quote the offending value so the operator can find the entry: {error}"
);
assert!(
error.contains("json_object"),
"must name the configuration that constructs the handle: {error}"
);
}
#[test]
fn should_not_refuse_a_json_literal_against_a_vec_parameter() {
let fixture = Fixture {
id: "rank_documents".into(),
input: serde_json::json!({"documents": ["alpha", "beta"]}),
..Fixture::default()
};
let config = ResolvedCrateConfig::default();
let args = vec![string_arg("documents", "documents")];
let params = [ParamDef {
name: "documents".into(),
ty: TypeRef::Vec(Box::new(TypeRef::Named("Document".into()))),
..ParamDef::default()
}];
let type_defs = [TypeDef {
name: "Document".into(),
has_serde: true,
..TypeDef::default()
}];
let rendered = build_args_string_c(
&fixture.input,
&args,
&HashMap::new(),
&config,
&type_defs,
&fixture,
"sample_rank",
TargetParams::Known(¶ms),
)
.expect("a JSON-string parameter must keep rendering its literal");
assert_eq!(
rendered,
json_to_c(&fixture.input["documents"]),
"a `Vec<T>` parameter crosses as a JSON `const char *`, so the literal is correct"
);
}
#[test]
fn should_not_refuse_a_named_parameter_the_ir_carries_no_type_def_for() {
let fixture = Fixture {
id: "set_level".into(),
input: serde_json::json!({"level": "debug"}),
..Fixture::default()
};
let config = ResolvedCrateConfig::default();
let args = vec![string_arg("level", "level")];
let params = [ParamDef {
name: "level".into(),
ty: TypeRef::Named("LogLevel".into()),
..ParamDef::default()
}];
let rendered = build_args_string_c(
&fixture.input,
&args,
&HashMap::new(),
&config,
&[],
&fixture,
"sample_set_level",
TargetParams::Known(¶ms),
)
.expect("a name with no `TypeDef` behind it licenses no claim about the C type");
assert_eq!(rendered, "\"debug\"", "the rendering must be left exactly as it was");
}
}