use crate::core::config::{ResolvedCrateConfig, SwiftConfig};
use crate::core::ir::{FieldDef, FunctionDef, TypeDef, TypeRef};
use crate::e2e::config::{CallConfig, E2eConfig};
use crate::e2e::fixture::{Assertion, Fixture};
use std::collections::HashSet;
fn field(name: &str, ty: TypeRef, optional: bool) -> FieldDef {
FieldDef {
name: name.to_string(),
ty,
optional,
..FieldDef::default()
}
}
fn type_defs() -> Vec<TypeDef> {
let entry = || TypeRef::Vec(Box::new(TypeRef::Named("Entry".to_string())));
vec![
TypeDef {
name: "Report".to_string(),
fields: vec![
field("section", TypeRef::Named("Section".to_string()), true),
field("archive", TypeRef::Named("Archive".to_string()), false),
field("title", TypeRef::String, true),
],
..TypeDef::default()
},
TypeDef {
name: "Section".to_string(),
fields: vec![field("entries", entry(), false)],
..TypeDef::default()
},
TypeDef {
name: "Archive".to_string(),
fields: vec![field("entries", entry(), true)],
..TypeDef::default()
},
TypeDef {
name: "Entry".to_string(),
fields: vec![field("label", TypeRef::String, false)],
..TypeDef::default()
},
]
}
fn functions() -> Vec<FunctionDef> {
vec![FunctionDef {
name: "analyze".to_string(),
return_type: TypeRef::Named("Report".to_string()),
..FunctionDef::default()
}]
}
fn result_fields() -> HashSet<String> {
["section", "archive", "title"]
.into_iter()
.map(str::to_string)
.collect()
}
fn e2e_config() -> E2eConfig {
E2eConfig {
call: CallConfig {
function: "analyze".to_string(),
result_var: "result".to_string(),
returns_result: true,
result_fields: result_fields(),
..CallConfig::default()
},
result_fields: result_fields(),
..E2eConfig::default()
}
}
fn render_with_config(assertion: Assertion, config: &ResolvedCrateConfig) -> String {
let fixture = Fixture {
id: "analyze_entries".to_string(),
description: "entries reached through an optional parent".to_string(),
assertions: vec![assertion],
..Fixture::default()
};
let e2e_config = e2e_config();
let type_defs = type_defs();
let functions = functions();
let swift_first_class_map =
super::values::build_swift_first_class_map(&type_defs, &[], &e2e_config, &e2e_config.call);
let mut out = String::new();
super::test_method::render_test_method(
&mut out,
&fixture,
&e2e_config,
"analyze",
"result",
&[],
false,
None,
&swift_first_class_map,
"SampleModule",
config,
&type_defs,
&[],
&functions,
&[],
);
out
}
fn render(assertion: Assertion) -> String {
render_with_config(assertion, &ResolvedCrateConfig::default())
}
fn assertion(assertion_type: &str, field: &str) -> Assertion {
Assertion {
assertion_type: assertion_type.to_string(),
field: Some(field.to_string()),
..Assertion::default()
}
}
fn count_min(field: &str, minimum: u64) -> Assertion {
Assertion {
value: Some(serde_json::json!(minimum)),
..assertion("count_min", field)
}
}
fn config_excluding(entry: &str) -> ResolvedCrateConfig {
ResolvedCrateConfig {
name: "sample".into(),
swift: Some(SwiftConfig {
exclude_fields: vec![entry.to_string()],
..SwiftConfig::default()
}),
..ResolvedCrateConfig::default()
}
}
#[test]
fn not_empty_on_a_collection_reached_through_an_optional_parent_stays_discriminating() {
let out = render(assertion("not_empty", "section.entries"));
assert!(
out.contains("isEmpty == false"),
"a genuine Vec must keep a real emptiness check, got:\n{out}"
);
assert!(
!out.contains("!= nil"),
"must not degrade to a non-nil check that passes for an empty collection, got:\n{out}"
);
assert!(
!out.contains(".toString()"),
"must not stringify a field `Section` declares as Vec<Entry>, got:\n{out}"
);
}
#[test]
fn not_empty_on_a_genuinely_optional_scalar_still_checks_non_nil() {
let out = render(assertion("not_empty", "title"));
assert!(
out.contains("!= nil"),
"an optional String's not_empty must keep the non-nil check, got:\n{out}"
);
assert!(
!out.contains("isEmpty == false"),
"a scalar must not take the collection branch, got:\n{out}"
);
}
#[test]
fn not_empty_on_a_genuinely_bridged_collection_decodes_and_counts() {
let out = render(assertion("not_empty", "archive.entries"));
assert!(
out.contains("JSONSerialization.jsonObject") && out.contains("XCTAssertGreaterThan("),
"a bridged collection's not_empty must decode and count instead of being refused, got:\n{out}"
);
assert!(
!out.contains("// skipped: field 'archive.entries' has no countable Swift leaf"),
"the count recovery makes the old blanket skip obsolete for this shape, got:\n{out}"
);
assert!(
!out.contains("!= nil") && !out.contains(".isEmpty") && !out.contains(".toString().count"),
"must never emit a check that cannot fail, got:\n{out}"
);
}
#[test]
fn count_min_on_a_collection_counts_elements_not_characters() {
let out = render(count_min("section.entries", 2));
assert!(
out.contains("XCTAssertGreaterThanOrEqual") && out.contains(".count"),
"count_min must still emit a count comparison, got:\n{out}"
);
assert!(
!out.contains(".toString()"),
"must count the Vec's elements, not the characters of a stringified Vec, got:\n{out}"
);
}
#[test]
fn count_min_on_a_string_field_still_counts_characters() {
let out = render(count_min("title", 2));
assert!(
out.contains(".toString().count"),
"a scalar String's count must stay a character count, got:\n{out}"
);
}
#[test]
fn an_exclusion_on_an_unrelated_type_does_not_drop_a_same_named_field() {
let out = render_with_config(
assertion("not_empty", "section.entries"),
&config_excluding("Archive.entries"),
);
assert!(
!out.contains("excluded from the Swift binding"),
"excluding Archive.entries must not drop Section.entries, got:\n{out}"
);
assert!(
out.contains("isEmpty == false"),
"the assertion must still be emitted, got:\n{out}"
);
}
#[test]
fn an_exclusion_on_the_real_owner_type_still_drops_the_field() {
let out = render_with_config(
assertion("not_empty", "section.entries"),
&config_excluding("Section.entries"),
);
assert!(
out.contains("// skipped: field 'section.entries' references a field or type excluded from the Swift binding"),
"excluding Section.entries must still drop it, got:\n{out}"
);
}
fn count_equals(field: &str, expected: u64) -> Assertion {
Assertion {
value: Some(serde_json::json!(expected)),
..assertion("count_equals", field)
}
}
#[test]
fn count_on_a_genuinely_bridged_collection_decodes_elements_not_characters() {
for (label, out) in [
("count_equals", render(count_equals("archive.entries", 2))),
("count_min", render(count_min("archive.entries", 2))),
] {
assert!(
!out.contains(".toString().count"),
"{label} must not count the characters of a stringified collection, got:\n{out}"
);
assert!(
out.contains("JSONSerialization.jsonObject(with: Data(result.archive().entries().toString().utf8))"),
"{label} on a bridged leaf must decode it and count elements, got:\n{out}"
);
assert!(
!out.contains("// skipped: field 'archive.entries' has no countable Swift leaf"),
"{label} on a bridged collection leaf is no longer unspellable, got:\n{out}"
);
}
}
#[test]
fn count_equals_on_a_string_field_still_counts_characters() {
let out = render(count_equals("title", 5));
assert!(
out.contains(".toString().count"),
"a scalar String's count_equals must stay a character count, got:\n{out}"
);
}