#[cfg(test)]
mod assertion_guard_tests;
mod enum_fixtures;
#[cfg(test)]
mod full_dispatch_list_tests;
mod gradle;
#[cfg(test)]
mod gradle_tests;
mod gradle_wrapper;
mod project;
mod stubs;
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::{Fixture, FixtureGroup};
use anyhow::Result;
use super::E2eCodegen;
pub use stubs::emit_test_backend;
pub struct KotlinAndroidE2eCodegen;
impl E2eCodegen for KotlinAndroidE2eCodegen {
fn generate(
&self,
groups: &[FixtureGroup],
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
enums: &[crate::core::ir::EnumDef],
functions: &[crate::core::ir::FunctionDef],
_errors: &[crate::core::ir::ErrorDef],
) -> Result<Vec<GeneratedFile>> {
project::generate(groups, e2e_config, config, type_defs, enums, functions)
}
fn render_snippet_body(
&self,
fixture: &Fixture,
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
enums: &[crate::core::ir::EnumDef],
) -> Result<String> {
crate::e2e::codegen::kotlin::snippet::render_snippet_body(fixture, e2e_config, config, type_defs, enums, true)
}
forward_snippet_rendering!(|fixture, e2e_config, config, session| {
crate::e2e::codegen::kotlin::snippet::render_in_session(fixture, e2e_config, config, session, true)
});
fn renders_client_config(&self) -> bool {
true
}
fn language_name(&self) -> &'static str {
"kotlin_android"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{FieldDef, FunctionDef, MethodDef, TypeDef, TypeRef};
use crate::e2e::codegen::kotlin::KotlinE2eCodegen;
use crate::e2e::config::{CallConfig, CallOverride};
use crate::e2e::fixture::Fixture;
use std::collections::HashSet;
#[test]
fn snippet_uses_android_coroutine_call_without_junit_harness() {
let fixture = Fixture {
id: "quick_start".into(),
description: "Quick start".into(),
input: serde_json::Value::Null,
..Fixture::default()
};
let mut call = CallConfig {
function: "load_document".into(),
result_var: "document".into(),
r#async: true,
..CallConfig::default()
};
call.overrides.insert(
"kotlin_android".into(),
CallOverride {
class: Some("DocumentApi".into()),
..CallOverride::default()
},
);
let body = KotlinAndroidE2eCodegen
.render_snippet_body(
&fixture,
&E2eConfig {
call,
..E2eConfig::default()
},
&ResolvedCrateConfig::default(),
&[],
&[],
)
.expect("snippet renders");
assert!(body.contains("kotlinx.coroutines.runBlocking"));
assert!(body.contains("DocumentApi.loadDocument()"));
assert!(!body.contains("@Test"));
assert!(!body.contains("assert"));
}
#[test]
fn client_factory_snippet_never_points_the_reader_at_the_mock_server() {
let fixture = Fixture {
id: "rate_limit_429".into(),
description: "Rate limited".into(),
input: serde_json::Value::Null,
..Fixture::default()
};
let mut call = CallConfig {
function: "chat".into(),
result_var: "result".into(),
..CallConfig::default()
};
call.overrides.insert(
"kotlin_android".into(),
CallOverride {
client_factory: Some("create_client".into()),
..CallOverride::default()
},
);
let body = KotlinAndroidE2eCodegen
.render_snippet_body(
&fixture,
&E2eConfig {
call,
..E2eConfig::default()
},
&ResolvedCrateConfig::default(),
&[],
&[],
)
.expect("snippet renders");
assert!(!body.contains("MOCK_SERVER"), "mock-server env var leaked:\n{body}");
assert!(!body.contains("mockServer"), "mock-server property leaked:\n{body}");
assert!(
!body.contains("/fixtures/rate_limit_429"),
"mock-server fixture route leaked:\n{body}"
);
assert!(!body.contains("\"test-key\""), "literal credential leaked:\n{body}");
assert!(
body.contains("System.getenv(\"API_KEY\")"),
"credential is not read from the environment:\n{body}"
);
assert!(
body.contains("createClient(apiKey = apiKey)"),
"an unconfigured project must construct the client without a mock base URL:\n{body}"
);
}
#[test]
fn client_factory_snippet_releases_the_client_in_a_use_block() {
let fixture = Fixture {
id: "rate_limit_429".into(),
description: "Rate limited".into(),
input: serde_json::Value::Null,
..Fixture::default()
};
let mut call = CallConfig {
function: "chat".into(),
result_var: "result".into(),
..CallConfig::default()
};
call.overrides.insert(
"kotlin_android".into(),
CallOverride {
client_factory: Some("create_client".into()),
..CallOverride::default()
},
);
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let body = KotlinAndroidE2eCodegen
.render_snippet_body(
&fixture,
&E2eConfig {
call,
..E2eConfig::default()
},
&config,
&[],
&[],
)
.expect("snippet renders");
assert!(
body.contains("Sample.createClient(apiKey = apiKey).use { client -> client.chat() }"),
"the client must be released via a `use` block around the call:\n{body}"
);
assert!(
!body.contains("client.close()"),
"no bare close() call must remain:\n{body}"
);
}
#[test]
fn excluded_binding_fixture_uses_native_disabled_test() {
let rendered = crate::e2e::template_env::render(
"kotlin_android/excluded_fixtures.kt.jinja",
crate::alef_context! {
package_name => "dev.sample",
entries => vec![crate::alef_context! {
name => "visitor_round_trip",
reason => "visitor is excluded by crates.kotlin_android.exclude_functions",
}],
},
);
assert!(rendered.contains("@Disabled(\"visitor is excluded by crates.kotlin_android.exclude_functions\")"));
assert!(rendered.contains("fun visitor_round_trip() {}"));
}
fn batch_result_ir() -> (Vec<TypeDef>, Vec<FunctionDef>) {
let type_defs = vec![
TypeDef {
name: "BatchResult".into(),
fields: vec![FieldDef {
name: "results".into(),
ty: TypeRef::Vec(Box::new(TypeRef::Named("ExtractionResult".into()))),
..FieldDef::default()
}],
..TypeDef::default()
},
TypeDef {
name: "ExtractionResult".into(),
fields: vec![FieldDef {
name: "metadata".into(),
ty: TypeRef::Named("Metadata".into()),
..FieldDef::default()
}],
methods: vec![MethodDef {
name: "extract_batch".into(),
return_type: TypeRef::Named("ExtractionResult".into()),
..MethodDef::default()
}],
..TypeDef::default()
},
TypeDef {
name: "Metadata".into(),
fields: vec![FieldDef {
name: "output_format".into(),
ty: TypeRef::String,
..FieldDef::default()
}],
..TypeDef::default()
},
];
let functions = vec![FunctionDef {
name: "extract_batch".into(),
return_type: TypeRef::Named("BatchResult".into()),
..FunctionDef::default()
}];
(type_defs, functions)
}
fn batch_result_fixture(field: &str) -> Fixture {
serde_json::from_value(serde_json::json!({
"id": "batch_extract",
"description": "Batch extract",
"input": {},
"assertions": [{"type": "equals", "field": field, "value": "html"}],
"docs": {"topic": "smoke", "stem": "batch_extract"}
}))
.expect("fixture must parse")
}
fn batch_result_e2e_config() -> E2eConfig {
E2eConfig {
call: CallConfig {
function: "extract_batch".into(),
result_var: "result".into(),
..CallConfig::default()
},
result_fields: HashSet::from(["results".to_string()]),
..E2eConfig::default()
}
}
#[test]
fn kotlin_android_docs_snippet_shows_the_same_declared_field_kotlin_does() {
let (type_defs, functions) = batch_result_ir();
let fixture = batch_result_fixture("results[0].metadata.output_format");
let e2e_config = batch_result_e2e_config();
let config = ResolvedCrateConfig::default();
let kotlin_body = KotlinE2eCodegen
.render_snippet_body_with_functions(&fixture, &e2e_config, &config, &type_defs, &[], &functions, &[])
.expect("kotlin snippet renders");
assert!(
kotlin_body.contains(".results"),
"control: the plain kotlin backend must show the `results` field `extract_batch` \
genuinely declares:\n{kotlin_body}"
);
let android_body = KotlinAndroidE2eCodegen
.render_snippet_body_with_functions(&fixture, &e2e_config, &config, &type_defs, &[], &functions, &[])
.expect("kotlin_android snippet renders");
assert!(
android_body.contains(".results"),
"kotlin_android must show the same `results` field the plain kotlin backend shows -- \
both back the identical Rust `extract_batch` result type:\n{android_body}"
);
}
#[test]
fn kotlin_android_docs_snippet_still_omits_a_field_the_result_type_never_declares() {
let (type_defs, functions) = batch_result_ir();
let fixture = batch_result_fixture("results[0].metadata.nonexistent_field");
let mut e2e_config = batch_result_e2e_config();
e2e_config.result_fields.insert("nonexistent_field".to_string());
let config = ResolvedCrateConfig::default();
let kotlin_body = KotlinE2eCodegen
.render_snippet_body_with_functions(&fixture, &e2e_config, &config, &type_defs, &[], &functions, &[])
.expect("kotlin snippet renders");
assert!(
!kotlin_body.contains("nonexistent_field"),
"control: plain kotlin must never derive an accessor for a field no type declares:\n{kotlin_body}"
);
let android_body = KotlinAndroidE2eCodegen
.render_snippet_body_with_functions(&fixture, &e2e_config, &config, &type_defs, &[], &functions, &[])
.expect("kotlin_android snippet renders");
assert!(
!android_body.contains("nonexistent_field"),
"a genuinely undeclared field must stay rejected after the functions-wiring fix, not \
just the false positive on `results`:\n{android_body}"
);
}
}