use crate::codegen::keywords::swift_ident;
use crate::core::config::ResolvedCrateConfig;
use crate::e2e::codegen::field_skip::FieldSkip;
use crate::e2e::codegen::inert_example::{self, InertCause, InertExample};
use crate::e2e::config::E2eConfig;
use crate::e2e::field_access::{FieldResolver, SwiftFirstClassMap};
use crate::e2e::fixture::Fixture;
use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
use regex::Regex;
use std::collections::{HashMap, HashSet};
use std::fmt::Write as _;
use super::args::build_args_and_setup;
use super::assertions::render_assertion;
use super::empty_field_accessor_map;
use super::values::{escape_swift, resolve_streaming_adapter, swift_call_result_type, swift_client_factory_call};
fn render_error_catch_block(out: &mut String, fixture: &Fixture, errors: &[crate::core::ir::ErrorDef]) {
use crate::e2e::codegen::declared_error_variant::{DeclaredErrorAssertion, classify, skip_line};
let _ = writeln!(out, " }} catch {{");
match classify("swift", fixture, errors) {
DeclaredErrorAssertion::Assert(declared) => {
let escaped = escape_swift(declared);
let _ = writeln!(out, " let _errorMessage = String(describing: error)");
let _ = writeln!(out, " let _errorType = String(describing: type(of: error))");
let _ = writeln!(
out,
" XCTAssertTrue(_errorMessage.contains(\"{escaped}\") || _errorType.contains(\"{escaped}\"), \"expected error to mention \\\"{escaped}\\\", got message: \\(_errorMessage), type: \\(_errorType)\")"
);
}
DeclaredErrorAssertion::Unsubstantiable(variant) => {
let _ = writeln!(
out,
"{}",
skip_line(" ", "//", variant, &fixture.id, "swift")
);
}
DeclaredErrorAssertion::Undeclared => {
let _ = writeln!(out, " // success");
}
}
let _ = writeln!(out, " }}");
}
#[allow(clippy::too_many_arguments)]
pub(super) fn render_test_method(
out: &mut String,
fixture: &Fixture,
e2e_config: &E2eConfig,
_function_name: &str,
_result_var: &str,
_args: &[crate::e2e::config::ArgMapping],
result_is_simple: bool,
global_client_factory: Option<&str>,
swift_first_class_map: &SwiftFirstClassMap,
module_name: &str,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
enums: &[crate::core::ir::EnumDef],
functions: &[crate::core::ir::FunctionDef],
errors: &[crate::core::ir::ErrorDef],
) {
let call_config = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let lang = "swift";
let call_overrides = call_config.overrides.get(lang);
let mut effective_enum_fields: HashSet<String> = e2e_config.effective_fields_enum(call_config).clone();
if let Some(o) = call_overrides {
effective_enum_fields.extend(o.enum_fields.keys().cloned());
}
let call_root_type = crate::e2e::codegen::call_ir::resolve_declared_result_type(
call_config,
lang,
crate::e2e::codegen::call_ir::CallIr { functions, type_defs },
);
let (ir_reachable_fields, ir_known_excluded_fields, ir_optional_fields) = FieldResolver::ir_field_sets(type_defs);
let call_field_resolver = FieldResolver::new_with_swift_first_class(
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),
e2e_config.effective_fields_method_calls(call_config),
&HashMap::new(),
swift_first_class_map.clone(),
)
.with_display_as_text_fields(e2e_config.effective_fields_display_as_text(call_config).clone())
.with_enum_fields(effective_enum_fields)
.with_ir_enum_map(FieldResolver::ir_enum_fields(type_defs, enums), call_root_type)
.with_ir_fields(ir_reachable_fields, ir_known_excluded_fields, ir_optional_fields);
let field_resolver = &call_field_resolver;
let function_name = call_overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| swift_ident(&call_config.function.to_lower_camel_case()));
let client_factory: Option<&str> = call_overrides
.and_then(|o| o.client_factory.as_deref())
.or(global_client_factory);
let result_var = call_config.effective_result_var();
let recipe = crate::e2e::codegen::recipe::ResolvedE2eCallRecipe::resolve(lang, fixture, call_config, type_defs)
.with_functions(functions);
let target_params = recipe.target_params(lang);
let args = recipe.args;
let result_is_bytes_any_lang =
call_config.result_is_bytes || call_config.overrides.values().any(|o| o.result_is_bytes);
let result_is_simple = call_config.result_is_simple
|| call_overrides.is_some_and(|o| o.result_is_simple)
|| result_is_simple
|| result_is_bytes_any_lang;
let result_is_array = call_config.result_is_array;
let result_is_option = call_config.result_is_option || call_overrides.is_some_and(|o| o.result_is_option);
let result_element_is_string =
call_config.result_element_is_string || call_overrides.is_some_and(|o| o.result_element_is_string);
let result_field_accessor: &HashMap<String, String> = call_overrides
.map(|o| &o.result_field_accessor)
.unwrap_or_else(|| empty_field_accessor_map());
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_async = call_overrides.and_then(|o| o.r#async).unwrap_or(call_config.r#async);
let is_streaming =
crate::e2e::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming_enabled());
let streaming_adapter = if is_streaming && !expects_error {
resolve_streaming_adapter(config, call_config, &function_name, client_factory)
} else {
None
};
let chunk_item_type = streaming_adapter.and_then(|adapter| adapter.item_type.as_deref());
let collect_snippet_opt = if is_streaming && !expects_error {
crate::e2e::codegen::streaming_assertions::StreamingFieldResolver::collect_snippet_typed(
lang,
result_var,
"chunks",
chunk_item_type,
)
} else {
None
};
if is_streaming && !expects_error && collect_snippet_opt.is_none() {
if is_async {
let _ = writeln!(out, " func test{method_name}() async throws {{");
} else {
let _ = writeln!(out, " func test{method_name}() throws {{");
}
let _ = writeln!(out, " // {description}");
let _ = writeln!(
out,
" try XCTSkipIf(true, \"swift: streaming chunk collection is not yet supported via the swift-bridge surface (fixture: {})\")",
fixture.id
);
let _ = writeln!(out, " }}");
return;
}
let collect_snippet = collect_snippet_opt.unwrap_or_default();
let collect_snippet = if collect_snippet.is_empty() {
collect_snippet
} else {
let re = Regex::new(r"\[([A-Za-z][A-Za-z0-9]*)\]").expect("valid regex");
let module_qualifier = module_name;
re.replace_all(&collect_snippet, |caps: ®ex::Captures| {
format!("[{}.{}]", module_qualifier, &caps[1])
})
.to_string()
};
let has_unresolvable_json_object_arg = {
let options_via = call_overrides.and_then(|o| o.options_via.as_deref());
options_via.is_none()
&& args.iter().any(|a| {
a.arg_type == "json_object" && a.name != "config" && a.element_type.is_none() && options_via.is_none()
})
};
if has_unresolvable_json_object_arg {
if is_async {
let _ = writeln!(out, " func test{method_name}() async throws {{");
} else {
let _ = writeln!(out, " func test{method_name}() throws {{");
}
let _ = writeln!(out, " // {description}");
let _ = writeln!(
out,
" try XCTSkipIf(true, \"swift: json_object requires options_via configuration (fixture: {})\");",
fixture.id
);
let _ = writeln!(out, " }}");
return;
}
let mut visitor_setup_lines: Vec<String> = Vec::new();
let visitor_handle_expr: Option<String> = fixture.visitor.as_ref().map(|spec| {
let visitor_config =
super::super::swift_visitors::resolve_swift_visitor_config(config, call_overrides, type_defs, spec);
super::super::swift_visitors::build_swift_visitor(
&mut visitor_setup_lines,
spec,
&fixture.id,
&visitor_config,
module_name,
)
});
let extra_args = recipe.extra_args;
let options_via_str: Option<&str> = Some(recipe.options_via).filter(|value| *value != "kwargs");
let options_type_str: Option<&str> = recipe.options_type;
let handle_config_fn_owned: Option<String> = call_config
.overrides
.get("c")
.and_then(|c| c.c_engine_factory.as_deref())
.map(|ty| format!("{}_from_json", ty.to_snake_case()).to_lower_camel_case());
let unnamed_arg_indices: &[usize] = call_overrides.map(|o| &o.unnamed_arg_indices[..]).unwrap_or(&[]);
let arg_name_map = call_overrides.map(|o| &o.arg_name_map);
let streaming_request_type = resolve_streaming_adapter(config, call_config, &function_name, client_factory)
.and_then(|adapter| adapter.request_type.as_deref())
.map(|request_type| request_type.rsplit("::").next().unwrap_or(request_type));
let (mut setup_lines, args_str) = build_args_and_setup(
&fixture.input,
args,
&fixture.id,
fixture.has_host_root_route(),
&function_name,
options_via_str,
options_type_str,
handle_config_fn_owned.as_deref(),
visitor_handle_expr.as_deref(),
client_factory.is_some(),
module_name,
unnamed_arg_indices,
config,
type_defs,
fixture,
arg_name_map,
streaming_request_type,
enums,
target_params,
);
if !visitor_setup_lines.is_empty() {
visitor_setup_lines.extend(setup_lines);
setup_lines = visitor_setup_lines;
}
let args_str = if extra_args.is_empty() {
args_str
} else if args_str.is_empty() {
extra_args.join(", ")
} else {
format!("{args_str}, {}", extra_args.join(", "))
};
let has_mock = fixture.mock_response.is_some();
let (call_setup, call_expr) = if let Some(factory) = client_factory {
let env_key = format!("MOCK_SERVER_{}", fixture.id.to_ascii_uppercase().replace('-', "_"));
let mock_url = if fixture.has_host_root_route() {
format!(
"ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (AlefE2EMockServer.baseURL + \"/fixtures/{}\")",
fixture.id
)
} else {
format!("AlefE2EMockServer.baseURL + \"/fixtures/{}\"", fixture.id)
};
let client_constructor = if has_mock {
swift_client_factory_call(factory, "\"test-key\"", &mock_url)
} else {
if let Some(env_var) = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref()) {
format!(
"let _apiKey = ProcessInfo.processInfo.environment[\"{env_var}\"]\n \
let _baseUrl: String? = _apiKey != nil ? nil : {mock_url}\n \
{}",
swift_client_factory_call(factory, "_apiKey ?? \"test-key\"", "_baseUrl")
)
} else {
swift_client_factory_call(factory, "\"test-key\"", &mock_url)
}
};
let expr = if is_async {
format!("try await _client.{function_name}({args_str})")
} else {
format!("try _client.{function_name}({args_str})")
};
(Some(client_constructor), expr)
} else {
let expr = if is_async {
format!("try await {module_name}.{function_name}({args_str})")
} else {
format!("try {module_name}.{function_name}({args_str})")
};
(None, expr)
};
let _ = function_name;
if is_async {
let _ = writeln!(out, " func test{method_name}() async throws {{");
} else {
let _ = writeln!(out, " func test{method_name}() throws {{");
}
let _ = writeln!(out, " // {description}");
if expects_error {
if is_async {
let _ = writeln!(out, " do {{");
for line in &setup_lines {
let _ = writeln!(out, " {line}");
}
if let Some(setup) = &call_setup {
let _ = writeln!(out, " {setup}");
}
let _ = writeln!(out, " _ = {call_expr}");
let _ = writeln!(out, " XCTFail(\"expected to throw\")");
render_error_catch_block(out, fixture, errors);
} else {
let _ = writeln!(out, " do {{");
for line in &setup_lines {
let _ = writeln!(out, " {line}");
}
if let Some(setup) = &call_setup {
let _ = writeln!(out, " {setup}");
}
let _ = writeln!(out, " _ = {call_expr}");
let _ = writeln!(out, " XCTFail(\"expected to throw\")");
render_error_catch_block(out, fixture, errors);
}
crate::e2e::codegen::error_path_assertions::emit(out, fixture, " // ", "swift");
let _ = writeln!(out, " }}");
return;
}
for line in &setup_lines {
let _ = writeln!(out, " {line}");
}
if let Some(setup) = &call_setup {
let _ = writeln!(out, " {setup}");
}
let fixture_root_type: Option<String> = swift_call_result_type(call_config);
let fixture_resolver = field_resolver.with_swift_root_type(fixture_root_type);
let swift_excluded_fields_by_type: HashMap<String, HashSet<String>> = config
.swift
.as_ref()
.map(|s| {
let mut map: HashMap<String, HashSet<String>> = HashMap::new();
for entry in &s.exclude_fields {
if let Some((type_name, field_name)) = entry.split_once('.') {
map.entry(type_name.to_string())
.or_default()
.insert(field_name.to_string());
}
}
map
})
.unwrap_or_default();
let swift_excluded_types: HashSet<String> = config
.swift
.as_ref()
.map(|s| s.exclude_types.iter().cloned().collect())
.unwrap_or_default();
let mut body_buffer = String::new();
if !collect_snippet.is_empty() {
for line in collect_snippet.lines() {
let _ = writeln!(body_buffer, " {line}");
}
}
let mut void_skip_comment_emitted = false;
for assertion in &fixture.assertions {
if call_config.returns_void {
if !void_skip_comment_emitted {
let _ = writeln!(
body_buffer,
" // skipped: void-returning function has no result to assert on"
);
void_skip_comment_emitted = true;
}
continue;
}
if let Some(f) = assertion.field.as_deref()
&& !f.is_empty()
{
let resolved_f = fixture_resolver.resolve(f);
if is_assertion_field_swift_excluded(
resolved_f,
fixture_resolver.swift_root_type().map(String::as_str),
&swift_first_class_map.field_types,
&swift_excluded_fields_by_type,
&swift_excluded_types,
) {
let _ = writeln!(
body_buffer,
" // skipped: {}",
FieldSkip::ExcludedFromSwiftBinding.message(f)
);
continue;
}
}
let mut assertion_out = String::new();
render_assertion(
&mut assertion_out,
assertion,
result_var,
&fixture_resolver,
result_is_simple,
result_is_array,
result_is_option,
result_element_is_string,
result_field_accessor,
is_streaming,
call_config.returns_void,
);
for unqualified in ["StreamToolCall", "ToolCall"] {
assertion_out =
assertion_out.replace(&format!("[{unqualified}]"), &format!("[{module_name}.{unqualified}]"));
}
body_buffer.push_str(&assertion_out);
}
crate::e2e::codegen::fail_on_unavailable_field_markers(&body_buffer, "swift", &fixture.id, &fixture.assertions);
crate::e2e::codegen::fail_on_unsupported_assertion_type_markers(&body_buffer, "swift", &fixture.id);
let refusal = inert_example::inert_verdict(&body_buffer, "swift", &fixture.id, &fixture.assertions);
if call_config.returns_void {
let _ = writeln!(out, " {call_expr}");
} else if body_buffer.contains(result_var) {
let _ = writeln!(out, " let {result_var} = {call_expr}");
} else {
let _ = writeln!(out, " _ = {call_expr}");
}
if let Some(refusal) = &refusal {
inert_example::record_refusal(refusal);
body_buffer = render_swift_refusal(&body_buffer, refusal);
}
out.push_str(&body_buffer);
for arg in args {
if arg.arg_type == "test_backend"
&& let Some(trait_name) = &arg.trait_name
&& let Some(trait_bridge) = config.trait_bridges.iter().find(|tb| tb.trait_name == *trait_name)
{
let unregister_fn = format!("unregister{}", trait_bridge.trait_name.to_upper_camel_case());
let plugin_name = fixture
.input
.get("name")
.and_then(|v| v.as_str())
.unwrap_or(&fixture.id);
let _ = writeln!(out, " try? {module_name}.{unregister_fn}(\"{plugin_name}\")");
}
}
let _ = writeln!(out, " }}");
}
fn render_swift_refusal(markers: &str, refusal: &InertExample) -> String {
let reason = escape_swift(&refusal.reason());
let statement = match refusal.cause {
InertCause::UnresolvedFieldPath => format!(" XCTFail(\"{reason}\")\n"),
InertCause::AwaitedOrLimited | InertCause::RenderedNothing => {
format!(" try XCTSkipIf(true, \"{reason}\")\n")
}
};
inert_example::refusal_body(markers, &statement)
}
fn is_assertion_field_swift_excluded(
field_path: &str,
root_type: Option<&str>,
field_types: &HashMap<String, HashMap<String, String>>,
excluded_fields_by_type: &HashMap<String, HashSet<String>>,
excluded_types: &HashSet<String>,
) -> bool {
if excluded_fields_by_type.is_empty() && excluded_types.is_empty() {
return false;
}
let segments: Vec<&str> = field_path
.split(['.', '[', ']'])
.filter(|s| !s.is_empty() && !s.chars().all(|c: char| c.is_ascii_digit()))
.collect();
for segment in &segments {
if excluded_fields_by_type.values().any(|fields| fields.contains(*segment)) {
return true;
}
}
let mut current_type: Option<String> = root_type.map(|s| s.to_string());
for segment in segments {
let Some(owner_str) = current_type.as_deref() else {
break;
};
if excluded_fields_by_type
.get(owner_str)
.is_some_and(|fields| fields.contains(segment))
{
return true;
}
let next: Option<String> = field_types.get(owner_str).and_then(|m| m.get(segment).cloned());
if let Some(ref next_type) = next
&& excluded_types.contains(next_type.as_str())
{
return true;
}
current_type = next;
}
false
}
#[cfg(test)]
mod error_catch_block_tests {
use super::render_error_catch_block;
use crate::core::ir::{ErrorDef, ErrorVariant};
use crate::e2e::fixture::{Assertion, Fixture};
fn fixture_with_declared_error(value: &str) -> Fixture {
Fixture {
id: "declares_error".to_string(),
assertions: vec![Assertion {
assertion_type: "error".to_string(),
value: Some(serde_json::Value::String(value.to_string())),
..Assertion::default()
}],
..Fixture::default()
}
}
fn coded_error_def(variant_name: &str) -> ErrorDef {
ErrorDef {
name: "ApiError".to_string(),
rust_path: "lib::ApiError".to_string(),
original_rust_path: String::new(),
variants: vec![ErrorVariant {
name: variant_name.to_string(),
error_code: Some(100),
is_unit: true,
..ErrorVariant::default()
}],
doc: String::new(),
methods: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}
}
#[test]
fn no_declared_value_renders_the_success_stub() {
let fixture = Fixture {
id: "no_error".to_string(),
..Fixture::default()
};
let mut out = String::new();
render_error_catch_block(&mut out, &fixture, &[]);
assert_eq!(out, " } catch {\n // success\n }\n");
}
#[test]
fn message_style_value_renders_the_message_or_type_assertion() {
let fixture = fixture_with_declared_error("BadRequest");
let mut out = String::new();
render_error_catch_block(&mut out, &fixture, &[]);
assert!(
out.contains("_errorMessage.contains(\"BadRequest\") || _errorType.contains(\"BadRequest\")"),
"got: {out}"
);
}
#[test]
fn declared_value_naming_a_known_variant_renders_the_registered_skip() {
let fixture = fixture_with_declared_error("Authentication");
let errors = vec![coded_error_def("Authentication")];
let mut out = String::new();
render_error_catch_block(&mut out, &fixture, &errors);
assert_eq!(
out,
" } catch {\n // skipped: declared error variant 'Authentication' not yet preserved \
as a distinct identity by this backend's generator\n }\n"
);
assert!(
!out.contains("XCTAssertTrue"),
"must not render an assertion that can never pass, got: {out}"
);
}
}
#[cfg(test)]
mod inert_example_refusal_tests {
use super::render_test_method;
use crate::e2e::codegen::inert_example::take_inert_examples;
use crate::e2e::config::{CallConfig, E2eConfig};
use crate::e2e::field_access::SwiftFirstClassMap;
use crate::e2e::fixture::{Assertion, Fixture};
fn assertion(assertion_type: &str, field: &str) -> Assertion {
Assertion {
assertion_type: assertion_type.to_string(),
field: Some(field.to_string()),
value: Some(serde_json::json!("x")),
..Default::default()
}
}
fn field_gated_e2e_config() -> E2eConfig {
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()
}
}
fn render(assertions: Vec<Assertion>, fixture_id: &str) -> String {
let fixture = Fixture {
id: fixture_id.to_string(),
description: "swift refusal fixture".to_string(),
assertions,
..Fixture::default()
};
let mut out = String::new();
render_test_method(
&mut out,
&fixture,
&field_gated_e2e_config(),
"process",
"result",
&[],
false,
None,
&SwiftFirstClassMap::default(),
"SampleModule",
&crate::core::config::ResolvedCrateConfig::default(),
&[],
&[],
&[],
&[],
);
out
}
#[test]
fn a_resolvable_assertion_is_published_unchanged() {
let _ = take_inert_examples();
let out = render(vec![assertion("equals", "content")], "swift_control");
assert!(
out.contains("XCTAssert"),
"the renderable assertion must still be emitted, got:\n{out}"
);
assert!(
!out.contains("XCTSkipIf(true, \"alef "),
"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_example_whose_every_assertion_skips_is_refused_as_a_skipped_test() {
let _ = take_inert_examples();
let out = render(
vec![
assertion("equals", "nonexistent_field"),
assertion("equals", "another_missing_field"),
],
"swift_all_skipped",
);
assert!(
out.contains("XCTFail(\"alef resolved no assertion for fixture `swift_all_skipped`"),
"an unresolved field path must be refused with a FAILING check, got:\n{out}"
);
assert!(
out.contains("skipped:") && out.contains("nonexistent_field") && out.contains("another_missing_field"),
"the markers must be carried into the refusal, got:\n{out}"
);
assert!(
out.contains("SampleModule.process("),
"the call itself still fails on throw and must not be deleted, 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, "swift_all_skipped");
}
#[test]
fn generator_debt_is_refused_as_a_skip_rather_than_a_failure() {
let _ = take_inert_examples();
let out = render(
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()
}],
"swift_generator_debt",
);
assert!(
out.contains("XCTSkipIf(true, \"alef rendered no runnable expectation for fixture `swift_generator_debt`"),
"acknowledged debt must be parked as skipped, got:\n{out}"
);
assert!(
!out.contains("XCTFail(\"alef "),
"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(Vec::new(), "swift_smoke_only");
assert!(
out.contains("SampleModule.process("),
"the call must still be emitted, got:\n{out}"
);
assert!(
!out.contains("XCTSkipIf(true, \"alef ") && !out.contains("XCTFail(\"alef "),
"a fixture with no assertions must never be refused, got:\n{out}"
);
assert!(take_inert_examples().is_empty());
}
}