1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
//! The import block of a generated Kotlin e2e test file.
//!
//! Every import a test file emits is collected here rather than written straight to the output,
//! so that one funnel owns package qualification and de-duplication for all of them. Emitters
//! that each spelled the qualification themselves is exactly how a configured fully-qualified
//! `class` ended up imported both correctly AND as `<pkg>.<pkg>.<Class>` in the same file — one
//! path split the name, the other prefixed it unconditionally, and the bogus spelling failed the
//! Kotlin compile with an unresolved reference. ~keep
/// Ordered, de-duplicated import paths for one generated test file.
pub(super) struct ImportBlock {
/// Package the binding's public types live in. Test files live in its `.e2e` child package,
/// and Kotlin child packages do not see their parent's symbols, so binding types need an
/// explicit import.
binding_package: String,
paths: Vec<String>,
}
impl ImportBlock {
pub(super) fn new(binding_package: String) -> Self {
Self {
binding_package,
paths: Vec::new(),
}
}
/// Add an already-complete import path — a stdlib, JUnit or Jackson symbol.
pub(super) fn push(&mut self, path: &str) {
if !self.paths.iter().any(|existing| existing == path) {
self.paths.push(path.to_string());
}
}
/// Add a type from the binding package, qualifying it only when it is not already qualified.
pub(super) fn push_binding_type(&mut self, type_name: &str) {
let path = crate::codegen::naming::qualified_type_path(&self.binding_package, type_name);
self.push(&path);
}
/// Render the block. Empty when nothing was added.
pub(super) fn render(&self) -> String {
crate::e2e::template_env::render(
"kotlin/test_imports.kt.jinja",
minijinja::context! { imports => self.paths },
)
}
}
#[cfg(test)]
mod tests {
use super::ImportBlock;
const BINDING_PACKAGE: &str = "dev.sample.bindings";
fn block() -> ImportBlock {
ImportBlock::new(BINDING_PACKAGE.to_string())
}
#[test]
fn bare_binding_type_is_qualified_with_the_binding_package() {
let mut imports = block();
imports.push_binding_type("SampleClient");
assert_eq!(imports.render(), "import dev.sample.bindings.SampleClient\n");
}
/// The defect this module exists to prevent: prefixing a name that already carries the
/// package produces an unresolvable `dev.sample.bindings.dev.sample.bindings.SampleClient`.
#[test]
fn already_qualified_binding_type_is_left_alone() {
let mut imports = block();
imports.push_binding_type("dev.sample.bindings.SampleClient");
assert_eq!(imports.render(), "import dev.sample.bindings.SampleClient\n");
}
/// The two spellings of one configured name must collapse to a single import, not two —
/// which is what the file showed before the qualification was centralized here.
#[test]
fn the_two_spellings_of_one_type_collapse_to_a_single_import() {
let mut imports = block();
imports.push_binding_type("dev.sample.bindings.SampleClient");
imports.push_binding_type("SampleClient");
assert_eq!(imports.render(), "import dev.sample.bindings.SampleClient\n");
}
#[test]
fn insertion_order_is_preserved_and_non_binding_paths_are_untouched() {
let mut imports = block();
imports.push("org.junit.jupiter.api.Test");
imports.push_binding_type("SampleOptions");
imports.push("kotlin.test.assertEquals");
assert_eq!(
imports.render(),
"import org.junit.jupiter.api.Test\nimport dev.sample.bindings.SampleOptions\nimport kotlin.test.assertEquals\n"
);
}
#[test]
fn an_empty_block_renders_nothing() {
assert_eq!(block().render(), "");
}
}