use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::E2eConfig;
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::Fixture;
use heck::{ToLowerCamelCase, ToUpperCamelCase};
use std::collections::HashSet;
use std::fmt::Write as FmtWrite;
use super::args::{KotlinArgsContext, build_args_and_setup};
use super::assertions::render_assertion;
use crate::e2e::codegen::inert_example::{self, InertCause};
use crate::e2e::escape::escape_kotlin;
fn refuse_inert_example(out: &mut String, assertions_start: usize, fixture: &Fixture) {
let Some(refusal) =
inert_example::inert_verdict(&out[assertions_start..], "kotlin", &fixture.id, &fixture.assertions)
else {
return;
};
inert_example::record_refusal(&refusal);
let markers = out[assertions_start..].to_string();
let reason = escape_kotlin(&refusal.reason());
let statement = match refusal.cause {
InertCause::UnresolvedFieldPath => format!(" kotlin.test.assertTrue(false, \"{reason}\")\n"),
InertCause::AwaitedOrLimited | InertCause::RenderedNothing => {
format!(" org.junit.jupiter.api.Assumptions.assumeTrue(false, \"{reason}\")\n")
}
};
out.truncate(assertions_start);
out.push_str(&inert_example::refusal_body(&markers, &statement));
}
#[allow(clippy::too_many_arguments)]
pub(super) fn render_test_method(
out: &mut String,
fixture: &Fixture,
class_name: &str,
_function_name: &str,
_result_var: &str,
_args: &[crate::e2e::config::ArgMapping],
options_type: Option<&str>,
result_is_simple: bool,
e2e_config: &E2eConfig,
type_enum_fields: &std::collections::HashMap<String, HashSet<String>>,
kotlin_android_style: bool,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
) -> anyhow::Result<()> {
if let Some(http) = &fixture.http {
super::http::render_http_test_method(out, fixture, http);
return Ok(());
}
let call_config = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let (ir_reachable_fields, ir_known_excluded_fields, ir_optional_fields) = FieldResolver::ir_field_sets(type_defs);
let call_field_resolver = FieldResolver::new(
e2e_config.effective_fields(call_config),
e2e_config.effective_fields_optional(call_config),
e2e_config.effective_result_fields(call_config),
e2e_config.effective_fields_array(call_config),
&HashSet::new(),
)
.with_display_as_text_fields(e2e_config.effective_fields_display_as_text(call_config).clone())
.with_ir_fields(ir_reachable_fields, ir_known_excluded_fields, ir_optional_fields);
let field_resolver = &call_field_resolver;
let enum_fields = e2e_config.effective_fields_enum(call_config);
let json_scalar_fields = e2e_config.effective_fields_json_scalar(call_config);
let lang = if kotlin_android_style {
"kotlin_android"
} else {
"kotlin"
};
let call_overrides = call_config.overrides.get(lang);
let client_factory = call_overrides
.and_then(|o| o.client_factory.as_deref())
.or_else(|| {
e2e_config
.call
.overrides
.get(lang)
.and_then(|o| o.client_factory.as_deref())
})
.or_else(|| {
if !kotlin_android_style {
return None;
}
call_config
.overrides
.get("kotlin_android")
.and_then(|o| o.client_factory.as_deref())
.or_else(|| {
call_config
.overrides
.get("java")
.and_then(|o| o.client_factory.as_deref())
})
.or_else(|| {
e2e_config
.call
.overrides
.get("kotlin_android")
.and_then(|o| o.client_factory.as_deref())
})
.or_else(|| {
e2e_config
.call
.overrides
.get("java")
.and_then(|o| o.client_factory.as_deref())
})
});
let effective_function_name = call_overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| call_config.function.to_lower_camel_case());
let effective_class_name = call_overrides
.and_then(|o| o.class.as_ref())
.cloned()
.or_else(|| {
if kotlin_android_style {
call_config
.overrides
.get("kotlin_android")
.and_then(|o| o.class.as_ref())
.cloned()
} else {
None
}
})
.unwrap_or_else(|| class_name.to_string());
let function_name = effective_function_name.as_str();
let class_name_for_call = effective_class_name.as_str();
let result_var = call_config.effective_result_var();
let recipe = crate::e2e::codegen::recipe::ResolvedE2eCallRecipe::resolve(lang, fixture, call_config, type_defs);
let args: &[crate::e2e::config::ArgMapping] = recipe.args;
let compatible_options_languages: &[&str] = if kotlin_android_style {
&["kotlin_android", "kotlin", "java", "csharp", "c", "go", "php", "python"]
} else {
&["csharp", "c", "go", "php", "python"]
};
let fixture_options_type: Option<String> = call_overrides
.and_then(|o| o.options_type.clone())
.or_else(|| options_type.map(str::to_string))
.or_else(|| {
recipe
.compatible_options_type(compatible_options_languages)
.map(str::to_string)
});
let options_type = fixture_options_type.as_deref();
let effective_result_is_simple = call_overrides.is_some_and(|o| o.result_is_simple)
|| call_config.result_is_simple
|| result_is_simple
|| ["java", "csharp", "go"]
.iter()
.any(|cand| call_config.overrides.get(*cand).is_some_and(|o| o.result_is_simple));
let result_is_simple = effective_result_is_simple;
let result_is_option = call_overrides.is_some_and(|o| o.result_is_option) || call_config.result_is_option;
let adapter_lookup_name = call_config.core_lookup_name(lang);
let adapter = adapter_lookup_name
.as_deref()
.and_then(|name| config.adapters.iter().find(|adapter| adapter.name == name));
let streaming_request = adapter.and_then(|adapter| {
matches!(adapter.pattern, crate::core::config::extras::AdapterPattern::Streaming)
.then(|| adapter.params.first())
.flatten()
});
let method_name = fixture.id.to_upper_camel_case();
let description = &fixture.description;
let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
let is_streaming =
crate::e2e::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming_enabled());
let stream_lang = if kotlin_android_style {
"kotlin_android"
} else {
"kotlin"
};
let collect_snippet = if is_streaming && !expects_error {
crate::e2e::codegen::streaming_assertions::StreamingFieldResolver::collect_snippet(
stream_lang,
result_var,
"chunks",
)
.unwrap_or_default()
} else {
String::new()
};
let effective_enum_fields: std::borrow::Cow<HashSet<String>> = {
let result_type_name: Option<&str> = call_overrides
.and_then(|co| co.result_type.as_deref())
.or_else(|| call_config.overrides.get("java").and_then(|o| o.result_type.as_deref()))
.or_else(|| call_config.overrides.get("c").and_then(|o| o.result_type.as_deref()));
let auto_enum_fields: Option<&HashSet<String>> = result_type_name.and_then(|name| type_enum_fields.get(name));
let java_call_overrides = if kotlin_android_style {
call_config
.overrides
.get("java")
.or_else(|| call_config.overrides.get("kotlin_android"))
} else {
None
};
let has_per_call = call_overrides.is_some_and(|co| !co.enum_fields.is_empty())
|| java_call_overrides.is_some_and(|co| !co.enum_fields.is_empty());
let has_auto = auto_enum_fields.is_some_and(|f| !f.is_empty());
if has_per_call || has_auto {
let mut merged = enum_fields.clone();
if let Some(co) = call_overrides {
merged.extend(co.enum_fields.keys().cloned());
}
if let Some(co) = java_call_overrides {
merged.extend(co.enum_fields.keys().cloned());
}
if let Some(auto_fields) = auto_enum_fields {
merged.extend(auto_fields.iter().cloned());
}
std::borrow::Cow::Owned(merged)
} else {
std::borrow::Cow::Borrowed(enum_fields)
}
};
let enum_fields: &HashSet<String> = &effective_enum_fields;
let is_streaming_owner_adapter = adapter.is_some_and(|a| {
matches!(a.pattern, crate::core::config::extras::AdapterPattern::Streaming) && a.owner_type.is_some()
});
let streaming_owner_handle: Option<String> = if is_streaming_owner_adapter {
args.iter().find(|a| a.arg_type == "handle").map(|a| a.name.clone())
} else {
None
};
let _ = writeln!(out, " @Test");
if client_factory.is_some() || kotlin_android_style {
let _ = writeln!(out, " fun test{method_name}() = runBlocking {{");
} else {
let _ = writeln!(out, " fun test{method_name}() {{");
}
let _ = writeln!(out, " // {description}");
let call_args: Vec<_> = if streaming_owner_handle.is_some() && streaming_request.is_some() {
args.iter().filter(|arg| arg.arg_type == "handle").cloned().collect()
} else {
args.to_vec()
};
let (mut setup_lines, mut args_str) = build_args_and_setup(
&fixture.input,
&call_args,
KotlinArgsContext {
fixture,
class_name,
options_type,
fixture_id: &fixture.id,
kotlin_android_style,
config,
type_defs,
owner_handle_is_receiver: streaming_owner_handle.is_some(),
},
)?;
if streaming_owner_handle.is_some()
&& let Some(request) = streaming_request
{
let request_name = request.name.to_lower_camel_case();
let request_type = request.ty.rsplit("::").next().unwrap_or(&request.ty);
let mut request_input = fixture.input.clone();
if let Some(object) = request_input.as_object_mut() {
for handle in args.iter().filter(|arg| arg.arg_type == "handle") {
let field = handle.field.strip_prefix("input.").unwrap_or(&handle.field);
object.remove(field);
}
}
let normalized = crate::e2e::codegen::transform_json_keys_for_language(&request_input, "snake_case");
let request_json = serde_json::to_string(&normalized).unwrap_or_default();
let escaped_json = crate::e2e::escape::escape_kotlin(&request_json);
if crate::e2e::codegen::value_contains_mock_url_placeholder(&normalized) {
let env_key = crate::e2e::codegen::mock_url_env_key(&fixture.id);
setup_lines.push(format!(
"val {request_name}Json = \"{escaped_json}\".replace(\"{}\", System.getProperty(\"mockServer.{}\", System.getenv(\"{env_key}\") ?: \"\"))",
crate::e2e::escape::escape_kotlin(crate::e2e::codegen::MOCK_URL_PLACEHOLDER), fixture.id,
));
setup_lines.push(format!(
"val {request_name} = MAPPER.readValue({request_name}Json, {request_type}::class.java)"
));
} else {
setup_lines.push(format!(
"val {request_name} = MAPPER.readValue(\"{escaped_json}\", {request_type}::class.java)"
));
}
args_str = request_name;
}
if let Some(factory) = client_factory {
let fixture_id = &fixture.id;
let mock_url_expr = format!(
"System.getProperty(\"mockServer.{fixture_id}\", (System.getProperty(\"mockServerUrl\", System.getenv(\"MOCK_SERVER_URL\") ?: \"\") ?: \"\") + \"/fixtures/{fixture_id}\")"
);
let call_receiver = streaming_owner_handle.as_deref().unwrap_or("client");
if expects_error {
let call_expr = if is_streaming {
let collect_suffix = if kotlin_android_style {
".toList()"
} else {
".asSequence().toList()"
};
format!("{call_receiver}.{function_name}({args_str}){collect_suffix}")
} else {
format!("{call_receiver}.{function_name}({args_str})")
};
let _ = writeln!(out, " assertFailsWith<Exception> {{");
for line in &setup_lines {
let _ = writeln!(out, " {line}");
}
let _ = writeln!(
out,
" val client = {class_name_for_call}.{factory}(apiKey = \"test-key\", baseUrl = {mock_url_expr})"
);
let _ = writeln!(out, " {call_expr}");
let _ = writeln!(out, " client.close()");
let _ = writeln!(out, " }}");
crate::e2e::codegen::error_path_assertions::emit(out, fixture, " // ", "kotlin");
let _ = writeln!(out, " Unit");
let _ = writeln!(out, " }}");
return Ok(());
}
for line in &setup_lines {
let _ = writeln!(out, " {line}");
}
let _ = writeln!(
out,
" val client = {class_name_for_call}.{factory}(apiKey = \"test-key\", baseUrl = {mock_url_expr})"
);
let _ = writeln!(
out,
" val {result_var} = {call_receiver}.{function_name}({args_str})"
);
if !collect_snippet.is_empty() {
let _ = writeln!(out, " {collect_snippet}");
}
let assertions_start = out.len();
for assertion in &fixture.assertions {
render_assertion(
out,
assertion,
result_var,
class_name,
field_resolver,
result_is_simple,
result_is_option,
enum_fields,
json_scalar_fields,
e2e_config.effective_fields_c_types(call_config),
is_streaming,
kotlin_android_style,
);
}
crate::e2e::codegen::fail_on_unavailable_field_markers(
&out[assertions_start..],
"kotlin",
&fixture.id,
&fixture.assertions,
);
crate::e2e::codegen::fail_on_unsupported_assertion_type_markers(
&out[assertions_start..],
"kotlin",
&fixture.id,
);
refuse_inert_example(out, assertions_start, fixture);
let _ = writeln!(out, " client.close()");
let _ = writeln!(out, " }}");
return Ok(());
}
let call_receiver = streaming_owner_handle.as_deref().unwrap_or(class_name_for_call);
if expects_error {
let _ = writeln!(out, " assertFailsWith<Exception> {{");
for line in &setup_lines {
let _ = writeln!(out, " {line}");
}
let _ = writeln!(out, " {call_receiver}.{function_name}({args_str})");
let _ = writeln!(out, " }}");
crate::e2e::codegen::error_path_assertions::emit(out, fixture, " // ", "kotlin");
let _ = writeln!(out, " Unit");
let _ = writeln!(out, " }}");
return Ok(());
}
for line in &setup_lines {
let _ = writeln!(out, " {line}");
}
let _ = writeln!(
out,
" val {result_var} = {call_receiver}.{function_name}({args_str})"
);
if !collect_snippet.is_empty() {
let _ = writeln!(out, " {collect_snippet}");
}
let assertions_start = out.len();
for assertion in &fixture.assertions {
render_assertion(
out,
assertion,
result_var,
class_name,
field_resolver,
result_is_simple,
result_is_option,
enum_fields,
json_scalar_fields,
&e2e_config.fields_c_types,
is_streaming,
kotlin_android_style,
);
}
crate::e2e::codegen::fail_on_unsupported_assertion_type_markers(&out[assertions_start..], "kotlin", &fixture.id);
crate::e2e::codegen::fail_on_unavailable_field_markers(
&out[assertions_start..],
"kotlin",
&fixture.id,
&fixture.assertions,
);
refuse_inert_example(out, assertions_start, fixture);
let _ = writeln!(out, " }}");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::extras::{AdapterConfig, AdapterParam, AdapterPattern};
use crate::e2e::config::{ArgMapping, CallConfig};
fn handle_arg(name: &str) -> ArgMapping {
ArgMapping {
name: name.to_string(),
field: format!("input.{name}"),
arg_type: "handle".to_string(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}
}
fn string_arg(name: &str) -> ArgMapping {
ArgMapping {
name: name.to_string(),
field: format!("input.{name}"),
arg_type: "string".to_string(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}
}
fn streaming_owner_adapter(function_name: &str, owner_type: &str) -> AdapterConfig {
AdapterConfig {
name: function_name.to_string(),
pattern: AdapterPattern::Streaming,
core_path: format!("test_core::{function_name}"),
params: Vec::new(),
returns: None,
error_type: None,
owner_type: Some(owner_type.to_string()),
item_type: Some("Item".to_string()),
gil_release: false,
trait_name: None,
trait_method: None,
detect_async: false,
request_type: None,
skip_languages: Vec::new(),
}
}
fn render(call: CallConfig, adapters: Vec<AdapterConfig>) -> String {
let fixture = Fixture {
id: "call_fixture".to_string(),
description: "call fixture".to_string(),
input: serde_json::json!({ "handle": {}, "url": "https://example.com" }),
..Fixture::default()
};
let e2e_config = E2eConfig {
call,
..E2eConfig::default()
};
let config = ResolvedCrateConfig {
adapters,
..ResolvedCrateConfig::default()
};
let mut out = String::new();
render_test_method(
&mut out,
&fixture,
"Facade",
"",
"",
&[],
None,
false,
&e2e_config,
&std::collections::HashMap::new(),
false,
&config,
&[],
)
.expect("render_test_method succeeds");
out
}
#[test]
fn streaming_owner_type_adapter_uses_handle_as_instance_receiver() {
let call = CallConfig {
function: "stream_items".to_string(),
result_var: "result".to_string(),
args: vec![handle_arg("handle"), string_arg("url")],
..CallConfig::default()
};
let out = render(call, vec![streaming_owner_adapter("stream_items", "Engine")]);
assert!(
out.contains("val result = handle.streamItems(\"https://example.com\")"),
"expected an instance call on the owner handle, got:\n{out}"
);
assert!(
!out.contains("Facade.streamItems"),
"must not emit a static facade call for a streaming owner_type adapter, got:\n{out}"
);
assert!(
out.contains("val handle = Facade.createHandle(null)"),
"the handle's construction line must still be emitted, got:\n{out}"
);
assert!(
!out.contains("streamItems(handle, "),
"the handle must not also appear as a positional argument, got:\n{out}"
);
}
#[test]
fn non_owner_type_call_keeps_static_facade_call_with_handle_positional() {
let call = CallConfig {
function: "do_thing".to_string(),
result_var: "result".to_string(),
args: vec![handle_arg("handle"), string_arg("url")],
..CallConfig::default()
};
let out_no_adapter = render(call.clone(), Vec::new());
assert!(
out_no_adapter.contains("val result = Facade.doThing(handle, \"https://example.com\")"),
"expected an unchanged static facade call, got:\n{out_no_adapter}"
);
let mut non_streaming = streaming_owner_adapter("do_thing", "Engine");
non_streaming.pattern = AdapterPattern::SyncFunction;
let out_sync = render(call.clone(), vec![non_streaming]);
assert!(
out_sync.contains("val result = Facade.doThing(handle, \"https://example.com\")"),
"a non-streaming adapter must not switch to an instance receiver, got:\n{out_sync}"
);
let mut streaming_no_owner = streaming_owner_adapter("do_thing", "Engine");
streaming_no_owner.owner_type = None;
let out_no_owner = render(call, vec![streaming_no_owner]);
assert!(
out_no_owner.contains("val result = Facade.doThing(handle, \"https://example.com\")"),
"a streaming adapter without owner_type must not switch to an instance receiver, got:\n{out_no_owner}"
);
}
#[test]
fn streaming_owner_call_builds_declared_request_type() {
let call = CallConfig {
function: "stream_items".to_string(),
result_var: "result".to_string(),
args: vec![handle_arg("handle"), string_arg("url")],
..CallConfig::default()
};
let mut adapter = streaming_owner_adapter("stream_items", "Engine");
adapter.params.push(AdapterParam {
name: "request".to_string(),
ty: "sample::StreamRequest".to_string(),
optional: false,
});
let generated = render(call, vec![adapter]);
assert!(generated.contains("MAPPER.readValue("), "got:\n{generated}");
assert!(generated.contains("StreamRequest::class.java"), "got:\n{generated}");
assert!(generated.contains("handle.streamItems(request)"), "got:\n{generated}");
assert!(!generated.contains("handle.streamItems(\"https://example.com\")"));
let statements = generated
.lines()
.filter(|line| line.contains("val request =") || line.contains("val result ="))
.map(str::trim)
.collect::<Vec<_>>()
.join("\n ");
let source = format!(
"data class StreamRequest(val url: String = \"\")\nclass Engine {{ fun streamItems(request: StreamRequest): List<String> = listOf(request.url) }}\nobject MAPPER {{ fun <T : Any> readValue(value: String, type: Class<T>): T = type.getDeclaredConstructor().newInstance() }}\nfun main() {{\n val handle = Engine()\n {statements}\n}}\n"
);
let directory = tempfile::tempdir().expect("temporary Kotlin compile directory");
let source_path = directory.path().join("Assertions.kt");
std::fs::write(&source_path, source).expect("write generated Kotlin source");
let output = std::process::Command::new("kotlinc")
.arg(&source_path)
.arg("-d")
.arg(directory.path().join("assertions.jar"))
.current_dir(directory.path())
.output()
.expect("run kotlinc");
assert!(
output.status.success(),
"kotlinc failed ({})\n--- stdout ---\n{}\n--- stderr ---\n{}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
assert!(
!generated.contains("\\\"handle\\\""),
"owner handle config leaked into request JSON: {generated}"
);
}
fn test_backend_arg(name: &str, trait_name: &str) -> ArgMapping {
ArgMapping {
name: name.to_string(),
field: format!("input.{name}"),
arg_type: "test_backend".to_string(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: Some(trait_name.to_string()),
}
}
#[test]
fn kotlin_android_test_backend_arg_renders_concrete_stub_instantiation() {
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::{MethodDef, PrimitiveType, ReceiverKind, TypeDef, TypeRef};
let trait_bridge = TraitBridgeConfig {
trait_name: "Validator".to_string(),
super_trait: Some("Plugin".to_string()),
register_fn: Some("register_validator".to_string()),
..Default::default()
};
let method = MethodDef {
name: "validate".to_string(),
return_type: TypeRef::Primitive(PrimitiveType::Bool),
receiver: Some(ReceiverKind::Ref),
cfg: None,
..Default::default()
};
let type_def = TypeDef {
name: "Validator".to_string(),
methods: vec![method],
..Default::default()
};
let config = ResolvedCrateConfig {
trait_bridges: vec![trait_bridge],
..ResolvedCrateConfig::default()
};
let call = CallConfig {
function: "register_backend".to_string(),
result_var: "result".to_string(),
args: vec![test_backend_arg("backend", "Validator")],
..CallConfig::default()
};
let fixture = Fixture {
id: "register_validator".to_string(),
description: "register a validator backend".to_string(),
input: serde_json::json!({ "backend": { "name": "test-validator" } }),
..Fixture::default()
};
let e2e_config = E2eConfig {
call,
..E2eConfig::default()
};
let mut out = String::new();
render_test_method(
&mut out,
&fixture,
"Facade",
"",
"",
&[],
None,
false,
&e2e_config,
&std::collections::HashMap::new(),
true, &config,
&[type_def],
)
.expect("a registered, implemented trait bridge must render successfully");
assert!(
out.contains("TestStubRegisterValidator()"),
"expected a concrete kotlin_android stub instantiation as the call argument, got:\n{out}"
);
assert!(
!out.contains("/* test_backend unimplemented"),
"must never splice the unimplemented sentinel into generated Kotlin, got:\n{out}"
);
}
#[test]
fn kotlin_android_test_backend_arg_with_unregistered_trait_fails_loudly() {
let call = CallConfig {
function: "register_backend".to_string(),
result_var: "result".to_string(),
args: vec![test_backend_arg("backend", "Validator")],
..CallConfig::default()
};
let fixture = Fixture {
id: "register_validator".to_string(),
description: "register a validator backend".to_string(),
input: serde_json::json!({ "backend": { "name": "test-validator" } }),
..Fixture::default()
};
let e2e_config = E2eConfig {
call,
..E2eConfig::default()
};
let config = ResolvedCrateConfig::default();
let mut out = String::new();
let error = render_test_method(
&mut out,
&fixture,
"Facade",
"",
"",
&[],
None,
false,
&e2e_config,
&std::collections::HashMap::new(),
true,
&config,
&[],
)
.expect_err("an unregistered trait must fail generation loudly, not silently degrade");
assert!(
error.to_string().contains("Validator"),
"error should name the unresolved trait, got: {error}"
);
assert!(
!out.contains("null"),
"must not have emitted a silent `null` placeholder before failing, got:\n{out}"
);
}
fn json_object_arg(name: &str) -> ArgMapping {
ArgMapping {
name: name.to_string(),
field: format!("input.{name}"),
arg_type: "json_object".to_string(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}
}
#[test]
fn json_object_arg_emits_exactly_one_request_binding() {
let call = CallConfig {
function: "process".to_string(),
result_var: "result".to_string(),
args: vec![json_object_arg("request")],
..CallConfig::default()
};
let fixture = Fixture {
id: "process_request".to_string(),
description: "process a request".to_string(),
input: serde_json::json!({ "request": { "kind": "text", "value": "hello" } }),
..Fixture::default()
};
let e2e_config = E2eConfig {
call,
..E2eConfig::default()
};
let config = ResolvedCrateConfig::default();
let mut out = String::new();
render_test_method(
&mut out,
&fixture,
"Facade",
"",
"",
&[],
Some("Request"),
false,
&e2e_config,
&std::collections::HashMap::new(),
false,
&config,
&[],
)
.expect("render_test_method succeeds");
assert!(
out.contains("val request = MAPPER.readValue("),
"expected a request deserialization binding, got:\n{out}"
);
let binding_count = out.matches("val request =").count();
assert_eq!(
binding_count, 1,
"expected exactly one `val request =` binding, got {binding_count}:\n{out}"
);
}
#[test]
fn json_object_arg_emits_exactly_one_request_binding_in_error_test() {
let call = CallConfig {
function: "process".to_string(),
result_var: "result".to_string(),
args: vec![json_object_arg("request")],
..CallConfig::default()
};
let fixture = Fixture {
id: "process_request_error".to_string(),
description: "reject an invalid request".to_string(),
input: serde_json::json!({ "request": { "kind": "invalid", "value": "hello" } }),
assertions: vec![crate::e2e::fixture::Assertion {
assertion_type: "error".to_string(),
..Default::default()
}],
..Fixture::default()
};
let e2e_config = E2eConfig {
call,
..E2eConfig::default()
};
let config = ResolvedCrateConfig::default();
let mut out = String::new();
render_test_method(
&mut out,
&fixture,
"Facade",
"",
"",
&[],
Some("Request"),
false,
&e2e_config,
&std::collections::HashMap::new(),
false,
&config,
&[],
)
.expect("render_test_method succeeds");
assert!(
out.contains("val request = MAPPER.readValue("),
"expected a request deserialization binding inside assertFailsWith, got:\n{out}"
);
let binding_count = out.matches("val request =").count();
assert_eq!(
binding_count, 1,
"expected exactly one `val request =` binding, got {binding_count}:\n{out}"
);
}
fn render_error_method(extra: Vec<crate::e2e::fixture::Assertion>) -> String {
let mut assertions = vec![crate::e2e::fixture::Assertion {
assertion_type: "error".to_string(),
..Default::default()
}];
assertions.extend(extra);
let fixture = Fixture {
id: "rate_limited".to_string(),
description: "reject the request".to_string(),
input: serde_json::json!({}),
assertions,
..Fixture::default()
};
let e2e_config = E2eConfig {
call: CallConfig {
function: "process".to_string(),
result_var: "result".to_string(),
..CallConfig::default()
},
..E2eConfig::default()
};
let config = ResolvedCrateConfig::default();
let mut out = String::new();
let _ = crate::e2e::codegen::take_skip_records();
render_test_method(
&mut out,
&fixture,
"Facade",
"",
"",
&[],
None,
false,
&e2e_config,
&std::collections::HashMap::new(),
false,
&config,
&[],
)
.expect("render_test_method succeeds");
out
}
#[test]
fn kotlin_equals_on_an_error_field_is_named_instead_of_dropped() {
let out = render_error_method(vec![crate::e2e::fixture::Assertion {
assertion_type: "equals".to_string(),
field: Some("error.status_code".to_string()),
..Default::default()
}]);
assert!(
out.contains("assertFailsWith<Exception> {"),
"the error block must render: {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, "kotlin");
assert_eq!(records[0].field, "equals");
}
#[test]
fn kotlin_a_lone_error_assertion_renders_no_marker() {
let out = render_error_method(Vec::new());
assert!(
out.contains("assertFailsWith<Exception> {"),
"the error block must render: {out}"
);
assert!(!out.contains("has no accessor for error field"), "{out}");
}
}
#[cfg(test)]
mod inert_example_refusal_tests {
use super::render_test_method;
use crate::core::config::ResolvedCrateConfig;
use crate::e2e::codegen::inert_example::take_inert_examples;
use crate::e2e::config::{CallConfig, E2eConfig};
use crate::e2e::fixture::{Assertion, Fixture};
fn assertion(field: &str) -> Assertion {
Assertion {
assertion_type: "equals".to_string(),
field: Some(field.to_string()),
value: Some(serde_json::json!("x")),
..Default::default()
}
}
fn render(fixture_id: &str, assertions: Vec<Assertion>) -> String {
let fixture = Fixture {
id: fixture_id.to_string(),
description: "kotlin refusal fixture".to_string(),
assertions,
..Fixture::default()
};
let e2e_config = E2eConfig {
result_fields: std::collections::HashSet::from(["content".to_string()]),
call: CallConfig {
function: "process".to_string(),
result_var: "result".to_string(),
returns_result: true,
..CallConfig::default()
},
..E2eConfig::default()
};
let mut out = String::new();
render_test_method(
&mut out,
&fixture,
"Facade",
"",
"",
&[],
None,
false,
&e2e_config,
&std::collections::HashMap::new(),
false,
&ResolvedCrateConfig::default(),
&[],
)
.expect("render_test_method succeeds");
out
}
#[test]
fn a_resolvable_assertion_is_published_unchanged() {
let _ = take_inert_examples();
let out = render("kotlin_control", vec![assertion("content")]);
assert!(
out.contains("assertEquals("),
"the renderable assertion must still be emitted, got:\n{out}"
);
assert!(
!out.contains("assumeTrue(false"),
"a live example must not be refused, got:\n{out}"
);
assert!(
take_inert_examples().is_empty(),
"nothing may be recorded for a live example"
);
}
#[test]
fn an_unresolved_field_path_is_refused_with_a_failing_check() {
let _ = take_inert_examples();
let out = render(
"kotlin_unresolved",
vec![assertion("nonexistent_field"), assertion("another_missing_field")],
);
assert!(
out.contains("kotlin.test.assertTrue(false, \"alef resolved no assertion for fixture `kotlin_unresolved`"),
"a consumer-fixable gap must be refused with a FAILING check, got:\n{out}"
);
assert!(
out.contains("nonexistent_field") && out.contains("another_missing_field"),
"the markers must be carried into the refusal, got:\n{out}"
);
assert!(
out.contains("// skipped:"),
"the skip markers must still be emitted, not replaced by silence, got:\n{out}"
);
assert!(
!out.contains("assumeTrue(false"),
"a consumer-fixable gap must not be parked as skipped, got:\n{out}"
);
let refusals = take_inert_examples();
assert_eq!(refusals.len(), 1, "the refusal must be recorded once for the summary");
assert_eq!(refusals[0].fixture_id, "kotlin_unresolved");
}
#[test]
fn acknowledged_generator_debt_is_refused_as_a_skip() {
let _ = take_inert_examples();
let out = render(
"kotlin_generator_debt",
vec![Assertion {
assertion_type: "equals".to_string(),
field: Some("nonexistent_field".to_string()),
value: Some(serde_json::json!("x")),
skip: Some(crate::e2e::fixture::AssertionSkip::All(true)),
..Default::default()
}],
);
assert!(
out.contains(
"org.junit.jupiter.api.Assumptions.assumeTrue(false, \"alef rendered no runnable expectation for \
fixture `kotlin_generator_debt`"
),
"acknowledged debt must be parked as skipped, got:\n{out}"
);
assert!(
!out.contains("assertTrue(false"),
"alef's own debt must not fail a consumer's suite, got:\n{out}"
);
assert_eq!(take_inert_examples().len(), 1);
}
#[test]
fn a_fixture_with_no_declared_assertions_keeps_its_smoke_test_shape() {
let _ = take_inert_examples();
let out = render("kotlin_smoke_only", Vec::new());
assert!(
out.contains("Facade.process("),
"the call must still be emitted, got:\n{out}"
);
assert!(
!out.contains("assumeTrue(false") && !out.contains("assertTrue(false"),
"a fixture with no assertions must never be refused, got:\n{out}"
);
assert!(take_inert_examples().is_empty());
}
}