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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
#![cfg(any(
not(any(
feature = "parser_tests",
feature = "analyzer_tests",
feature = "codegen_tests",
feature = "interpreter_tests",
feature = "conformance_tests",
feature = "integration_tests",
)),
feature = "analyzer_tests",
))]
// This test file is part of the TDD process for Bug #12: Module discovery scope.
//
// Problem:
// --------
// The Windjammer compiler incorrectly discovers hand-written .rs files from the
// parent directory (e.g., src/app.rs) and:
// 1. Copies them to the generated output directory (e.g., src/components/generated/app.rs)
// 2. Declares them in the generated mod.rs (e.g., `pub mod app;`)
//
// This causes duplicate symbol errors when these files are already declared as
// top-level modules in src/lib.rs.
//
// Root Cause:
// -----------
// The module discovery logic in `discover_hand_written_modules` and/or the
// directory copying logic in `copy_dir_recursive` doesn't properly scope its
// search to only the immediate directory being compiled.
//
// Solution:
// ---------
// When generating src/components/generated/mod.rs, the compiler should only
// discover hand-written .rs files that are WITHIN src/components/, not from
// the parent src/ directory.
//
// Test Strategy:
// --------------
// 1. Create a project structure that mimics windjammer-ui:
// - src/lib.rs (declares `pub mod app;`)
// - src/app.rs (hand-written top-level module)
// - src/components_wj/button.wj (Windjammer component)
// - Output to src/components/generated/
//
// 2. Compile with `wj build`
//
// 3. Assert:
// - src/components/generated/mod.rs does NOT contain `pub mod app;`
// - src/components/generated/app.rs does NOT exist
// - src/components/generated/button.rs DOES exist
// - src/components/generated/mod.rs DOES contain `pub mod button;`
use anyhow::Result;
use std::fs;
use std::path::Path;
use tempfile::tempdir;
// Helper function to compile a Windjammer project
fn compile_wj_project(source_dir: &Path, output_dir: &Path) -> Result<(), String> {
use std::process::Command;
let output = Command::new(env!("CARGO_BIN_EXE_wj"))
.args([
"build",
source_dir.to_str().unwrap(),
"--output",
output_dir.to_str().unwrap(),
"--no-cargo", // Skip cargo build to focus on wj compilation
])
.output()
.map_err(|e| format!("Failed to run wj CLI: {}", e))?;
if !output.status.success() {
return Err(format!(
"Compilation failed:\n{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
));
}
Ok(())
}
#[test]
#[cfg_attr(tarpaulin, ignore)] // Skip during coverage runs due to timeout
fn test_parent_directory_modules_not_discovered() {
// Scenario (mimics windjammer-ui structure):
// src/
// lib.rs (declares `pub mod app;`)
// app.rs (hand-written top-level module)
// components_wj/
// button.wj (Windjammer component)
// mod.wj
// components/
// generated/ <- output directory
//
// Expected behavior:
// - generated/mod.rs should NOT declare `pub mod app;`
// - generated/app.rs should NOT exist
// - generated/button.rs SHOULD exist
// - generated/mod.rs SHOULD declare `pub mod button;`
let temp_dir = tempdir().unwrap();
let project_root = temp_dir.path();
// Create src/lib.rs with top-level app module
let src_dir = project_root.join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(
src_dir.join("lib.rs"),
r#"// Top-level library file
pub mod app; // Hand-written top-level module
// Re-export generated components
pub mod components {
pub mod generated;
}
"#,
)
.unwrap();
// Create src/app.rs (hand-written top-level module)
fs::write(
src_dir.join("app.rs"),
r#"// Hand-written app module (top-level in src/lib.rs)
pub struct App {
pub name: string,
}
impl App {
pub fn new(name: string) -> Self {
Self { name }
}
}
"#,
)
.unwrap();
// Create src/components_wj/button.wj (Windjammer component)
let components_wj_dir = src_dir.join("components_wj");
fs::create_dir_all(&components_wj_dir).unwrap();
fs::write(
components_wj_dir.join("button.wj"),
r#"pub struct Button {
pub label: string
}
impl Button {
pub fn new(label: string) -> Button {
Button { label }
}
pub fn render(self) -> string {
format!("<button>{}</button>", self.label)
}
}
"#,
)
.unwrap();
fs::write(components_wj_dir.join("mod.wj"), "").unwrap();
// Create output directory structure
let output_dir = src_dir.join("components").join("generated");
fs::create_dir_all(&output_dir).unwrap();
// Compile
compile_wj_project(&components_wj_dir, &output_dir).expect("Compilation should succeed");
// Debug: List all generated files
eprintln!("=== Generated files in {:?} ===", output_dir);
if let Ok(entries) = fs::read_dir(&output_dir) {
for entry in entries.flatten() {
eprintln!(" - {}", entry.file_name().to_string_lossy());
}
}
// CRITICAL ASSERTIONS: Parent directory modules should NOT be discovered
// 1. app.rs should NOT be copied to generated directory
let generated_app_rs = output_dir.join("app.rs");
assert!(
!generated_app_rs.exists(),
"BUG: app.rs from parent src/ was incorrectly copied to generated/: {:?}",
generated_app_rs
);
// 2. mod.rs should NOT declare app module
let mod_rs_path = output_dir.join("mod.rs");
assert!(
mod_rs_path.exists(),
"mod.rs should be generated: {:?}",
mod_rs_path
);
let mod_rs_content = fs::read_to_string(&mod_rs_path).unwrap();
eprintln!("=== mod.rs content ===\n{}", mod_rs_content);
assert!(
!mod_rs_content.contains("pub mod app;"),
"BUG: mod.rs incorrectly declares parent directory module 'app'"
);
assert!(
!mod_rs_content.contains("pub use app::*;"),
"BUG: mod.rs incorrectly re-exports parent directory module 'app'"
);
// POSITIVE ASSERTIONS: Actual components should be discovered
// 3. button.rs SHOULD exist
let button_rs_path = output_dir.join("button.rs");
assert!(
button_rs_path.exists(),
"button.rs should be generated: {:?}",
button_rs_path
);
// 4. mod.rs SHOULD declare button module
assert!(
mod_rs_content.contains("pub mod button;"),
"mod.rs should declare button module"
);
assert!(
mod_rs_content.contains("pub use button::*;"),
"mod.rs should re-export button module"
);
}
#[test]
#[cfg_attr(tarpaulin, ignore)] // Skip during coverage runs due to timeout
fn test_sibling_hand_written_modules_are_discovered() {
// Scenario:
// src/
// components_wj/
// button.wj
// mod.wj
// components/
// platform.rs <- Hand-written sibling (should be discovered)
// generated/ <- output directory
//
// Expected behavior:
// - generated/mod.rs SHOULD declare `pub mod platform;` (sibling in components/)
// - generated/mod.rs should NOT declare anything from src/
let temp_dir = tempdir().unwrap();
let project_root = temp_dir.path();
// Create src structure
let src_dir = project_root.join("src");
fs::create_dir_all(&src_dir).unwrap();
// Create src/components_wj/button.wj
let components_wj_dir = src_dir.join("components_wj");
fs::create_dir_all(&components_wj_dir).unwrap();
fs::write(
components_wj_dir.join("button.wj"),
"pub struct Button { pub label: string }",
)
.unwrap();
fs::write(components_wj_dir.join("mod.wj"), "").unwrap();
// Create src/components/platform.rs (hand-written sibling)
let components_dir = src_dir.join("components");
fs::create_dir_all(&components_dir).unwrap();
fs::write(
components_dir.join("platform.rs"),
r#"pub fn get_platform_name() -> string {
"test".to_string()
}
"#,
)
.unwrap();
// Create output directory
let output_dir = components_dir.join("generated");
fs::create_dir_all(&output_dir).unwrap();
// Compile
compile_wj_project(&components_wj_dir, &output_dir).expect("Compilation should succeed");
// Check mod.rs
let mod_rs_path = output_dir.join("mod.rs");
let mod_rs_content = fs::read_to_string(&mod_rs_path).unwrap();
eprintln!("=== mod.rs content ===\n{}", mod_rs_content);
// Platform SHOULD be discovered (it's a sibling in components/)
assert!(
mod_rs_content.contains("pub mod platform;"),
"mod.rs should declare sibling platform module"
);
}