use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
const LIB_RS_MARKER_BEGIN: &str = "// WINDJAMMER_AUTO_RUST_TESTS_BEGIN";
const LIB_RS_MARKER_END: &str = "// WINDJAMMER_AUTO_RUST_TESTS_END";
const GENERATED_HEADER: &str = "// Auto-generated by Windjammer — do not edit manually.\n\
// Rust tests in `tests/*.rs` are discovered automatically (no manual `tests/lib.rs` registration).\n\n";
pub fn find_project_root_with_tests(start: &Path) -> Option<PathBuf> {
let mut candidates = Vec::new();
if start.is_dir() {
candidates.push(start.to_path_buf());
if let Some(parent) = start.parent() {
candidates.push(parent.to_path_buf());
}
} else if let Some(parent) = start.parent() {
candidates.push(parent.to_path_buf());
if let Some(grand) = parent.parent() {
candidates.push(grand.to_path_buf());
}
}
candidates
.into_iter()
.find(|root| root.join("tests").is_dir())
}
fn is_excluded_test_root_file(name: &str) -> bool {
name == "lib.rs" || name == "mod.rs" || name == "all.rs"
}
fn uses_consolidated_test_binary(project_root: &Path) -> bool {
project_root.join("tests").join("all.rs").exists()
}
fn is_helper_module(stem: &str, contents: &str) -> bool {
if stem.ends_with("_test") || stem.starts_with("test_") || stem.ends_with("_tests") {
return false;
}
!contents.contains("#[test]")
}
pub fn generate_tests_lib_rs(project_root: &Path) -> Result<Option<PathBuf>> {
if uses_consolidated_test_binary(project_root) {
return Ok(None);
}
let tests_dir = project_root.join("tests");
if !tests_dir.is_dir() {
return Ok(None);
}
let mut modules: Vec<(String, bool)> = Vec::new();
for entry in fs::read_dir(&tests_dir)? {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
if !name.ends_with(".rs") || is_excluded_test_root_file(name) {
continue;
}
let stem = name.strip_suffix(".rs").unwrap_or(name);
let contents =
fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
let helper = is_helper_module(stem, &contents);
modules.push((stem.to_string(), helper));
}
if modules.is_empty() {
return Ok(None);
}
modules.sort_by(|a, b| a.0.cmp(&b.0));
let mut out = String::from(GENERATED_HEADER);
for (stem, helper) in &modules {
if *helper {
out.push_str(&format!("pub mod {stem};\n"));
} else {
out.push_str("#[cfg(test)]\n");
out.push_str(&format!("pub mod {stem};\n"));
}
}
let helpers: Vec<_> = modules.iter().filter(|(_, h)| *h).collect();
if !helpers.is_empty() {
out.push_str("\n// Helper re-exports for test modules\n");
for (stem, _) in helpers {
out.push_str(&format!("pub use {stem}::*;\n"));
}
}
let lib_path = tests_dir.join("lib.rs");
fs::write(&lib_path, out).with_context(|| format!("write {}", lib_path.display()))?;
let mod_rs = tests_dir.join("mod.rs");
if mod_rs.exists() {
if let Ok(text) = fs::read_to_string(&mod_rs) {
if text.contains("Auto-generated mod.rs by Windjammer") {
let _ = fs::remove_file(&mod_rs);
}
}
}
Ok(Some(lib_path))
}
pub fn wire_tests_lib_into_crate_root(project_root: &Path) -> Result<()> {
let tests_lib = project_root.join("tests").join("lib.rs");
if !tests_lib.exists() {
return Ok(());
}
let lib_rs_path = project_root.join("lib.rs");
if !lib_rs_path.exists() {
return Ok(());
}
let hook = format!(
"{LIB_RS_MARKER_BEGIN}\n\
#[cfg(test)]\n\
#[path = \"tests/lib.rs\"]\n\
mod windjammer_rust_integration_tests;\n\
{LIB_RS_MARKER_END}\n"
);
let existing = fs::read_to_string(&lib_rs_path)?;
let updated = if existing.contains(LIB_RS_MARKER_BEGIN) {
replace_between_markers(&existing, LIB_RS_MARKER_BEGIN, LIB_RS_MARKER_END, &hook)
} else {
format!("{existing}\n{hook}")
};
if updated != existing {
fs::write(&lib_rs_path, updated)?;
}
ensure_autotests_disabled(&project_root.join("Cargo.toml"))?;
Ok(())
}
fn replace_between_markers(content: &str, begin: &str, end: &str, replacement: &str) -> String {
if let (Some(start), Some(end_pos)) = (content.find(begin), content.find(end)) {
if end_pos >= start {
let after = end_pos + end.len();
let mut out = String::new();
out.push_str(&content[..start]);
out.push_str(replacement);
if after < content.len() {
out.push_str(&content[after..]);
}
return out;
}
}
content.to_string()
}
fn ensure_autotests_disabled(cargo_toml_path: &Path) -> Result<()> {
if !cargo_toml_path.exists() {
return Ok(());
}
let content = fs::read_to_string(cargo_toml_path)?;
if content.contains("autotests") {
return Ok(());
}
let updated = if let Some(idx) = content.find("[package]") {
let insert_at = content[idx..]
.find('\n')
.map(|off| idx + off + 1)
.unwrap_or(content.len());
format!(
"{}{}{}",
&content[..insert_at],
"autotests = false\n",
&content[insert_at..]
)
} else {
format!("{content}\n[package]\nautotests = false\n")
};
fs::write(cargo_toml_path, updated)?;
Ok(())
}
pub fn sync_rust_integration_tests(project_root: &Path) -> Result<()> {
if uses_consolidated_test_binary(project_root) {
return Ok(());
}
if generate_tests_lib_rs(project_root)?.is_some() {
wire_tests_lib_into_crate_root(project_root)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn skips_lib_rs_when_consolidated_all_binary_present() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
fs::create_dir_all(root.join("tests")).unwrap();
fs::write(root.join("tests/all.rs"), "// consolidated\n").unwrap();
fs::write(
root.join("tests/feature_test.rs"),
"#[test]\nfn t() { assert!(true); }\n",
)
.unwrap();
assert!(generate_tests_lib_rs(root).unwrap().is_none());
assert!(!root.join("tests/lib.rs").exists());
}
#[test]
fn generates_lib_rs_from_test_files() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
fs::create_dir_all(root.join("tests")).unwrap();
fs::write(
root.join("tests/shader_file_registry_test.rs"),
"#[test]\nfn t() { assert!(true); }\n",
)
.unwrap();
fs::write(
root.join("tests/gpu_layout_assertions.rs"),
"pub fn helper() -> i32 { 1 }\n",
)
.unwrap();
fs::write(root.join("lib.rs"), "pub fn api() {}\n").unwrap();
fs::write(root.join("Cargo.toml"), "[package]\nname = \"demo\"\n").unwrap();
generate_tests_lib_rs(root).unwrap();
wire_tests_lib_into_crate_root(root).unwrap();
let lib = fs::read_to_string(root.join("tests/lib.rs")).unwrap();
assert!(lib.contains("pub mod shader_file_registry_test;"));
assert!(lib.contains("pub mod gpu_layout_assertions;"));
assert!(lib.contains("pub use gpu_layout_assertions::*;"));
let crate_lib = fs::read_to_string(root.join("lib.rs")).unwrap();
assert!(crate_lib.contains("WINDJAMMER_AUTO_RUST_TESTS_BEGIN"));
assert!(crate_lib.contains("tests/lib.rs"));
let cargo = fs::read_to_string(root.join("Cargo.toml")).unwrap();
assert!(cargo.contains("autotests = false"));
}
}