mod enum_fixtures;
mod gradle;
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)
}
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)
}
fn language_name(&self) -> &'static str {
"kotlin_android"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::e2e::config::{CallConfig, CallOverride};
use crate::e2e::fixture::Fixture;
#[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",
minijinja::context! {
package_name => "dev.sample",
fixtures => vec!["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() {}"));
}
}