#![allow(dead_code)]
use std::collections::HashMap;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::Mutex;
use std::time::{Duration, Instant};
use tempfile::TempDir;
use windjammer::{build_project_ext, CompilationTarget};
const CARGO_TIMEOUT: Duration = Duration::from_secs(180);
static CARGO_CHECK_LOCK: Mutex<()> = Mutex::new(());
pub struct MultiFileTest {
project_root: PathBuf,
src_root: PathBuf,
build_dir: PathBuf,
_temp_dir: TempDir,
}
impl Default for MultiFileTest {
fn default() -> Self {
Self::new()
}
}
impl MultiFileTest {
pub fn new() -> Self {
let temp_dir = TempDir::new().expect("tempdir for MultiFileTest");
let project_root = temp_dir.path().to_path_buf();
let src_root = project_root.join("src");
let build_dir = project_root.join("build");
fs::create_dir_all(&src_root).expect("create src");
Self {
project_root,
src_root,
build_dir,
_temp_dir: temp_dir,
}
}
pub fn build_dir(&self) -> &Path {
&self.build_dir
}
pub fn add_file(&mut self, name: &str, content: &str) {
let path = self.src_root.join(name);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create parent dirs for .wj");
}
fs::write(&path, content).unwrap_or_else(|e| panic!("write {}: {}", path.display(), e));
}
pub fn compile(&self) -> Result<HashMap<String, String>, String> {
build_project_ext(
&self.src_root,
&self.build_dir,
CompilationTarget::Rust,
false,
true,
&[],
)
.map_err(|e| format!("build_project_ext failed: {:#}", e))?;
let mut out = HashMap::new();
collect_rs_files(&self.build_dir, &self.build_dir, &mut out)
.map_err(|e| format!("read generated .rs files: {}", e))?;
Ok(out)
}
pub fn assert_contains(&self, file: &str, pattern: &str) {
let map = self
.compile()
.unwrap_or_else(|e| panic!("compile() failed before assert_contains: {}", e));
let content = map.get(file).unwrap_or_else(|| {
panic!(
"no generated file {:?} (have keys: {:?})\nproject root: {}",
file,
map.keys().collect::<Vec<_>>(),
self.project_root.display()
)
});
assert!(
content.contains(pattern),
"expected {} to contain {:?}; project root {}\n\n----- {} -----\n{}",
file,
pattern,
self.project_root.display(),
file,
content
);
}
pub fn assert_compile_error(&self, err_substr: &str) {
let err = self.compile().expect_err("expected compile() to fail");
assert!(
err.contains(err_substr),
"expected error to contain {:?}, got:\n{}",
err_substr,
err
);
}
pub fn assert_compiles_without_error(&self) {
self.compile()
.unwrap_or_else(|e| panic!("compile failed before cargo check: {}", e));
write_flat_lib_rs(&self.build_dir).expect("write lib.rs");
write_verify_cargo_toml(&self.build_dir).expect("write Cargo.toml for cargo check");
let _guard = CARGO_CHECK_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let shared_target = shared_cargo_target_dir();
let mut child = Command::new("cargo")
.current_dir(&self.build_dir)
.env("CARGO_TARGET_DIR", &shared_target)
.args(["check", "--quiet"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap_or_else(|e| panic!("failed to spawn cargo check: {}", e));
let deadline = Instant::now() + CARGO_TIMEOUT;
loop {
match child.try_wait() {
Ok(Some(_)) => break,
Ok(None) => {
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
panic!(
"cargo check TIMED OUT after {}s in {}",
CARGO_TIMEOUT.as_secs(),
self.build_dir.display()
);
}
std::thread::sleep(Duration::from_millis(250));
}
Err(e) => panic!("error waiting for cargo check: {}", e),
}
}
let output = child.wait_with_output()
.unwrap_or_else(|e| panic!("failed to collect cargo check output: {}", e));
assert!(
output.status.success(),
"cargo check failed in {}.\nstderr:\n{}",
self.build_dir.display(),
String::from_utf8_lossy(&output.stderr),
);
}
}
fn collect_rs_files(root: &Path, dir: &Path, out: &mut HashMap<String, String>) -> io::Result<()> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
collect_rs_files(root, &path, out)?;
} else if path.extension().and_then(|s| s.to_str()) == Some("rs") {
let rel = path
.strip_prefix(root)
.expect("path under root")
.to_string_lossy()
.replace('\\', "/");
let text = fs::read_to_string(&path)?;
out.insert(rel, text);
}
}
Ok(())
}
fn write_flat_lib_rs(build_dir: &Path) -> io::Result<()> {
let mut stems: Vec<String> = Vec::new();
for entry in fs::read_dir(build_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file()
&& path.extension().and_then(|s| s.to_str()) == Some("rs")
&& path.file_stem().and_then(|s| s.to_str()) != Some("lib")
&& path.file_stem().and_then(|s| s.to_str()) != Some("mod")
{
stems.push(
path.file_stem()
.expect("stem")
.to_string_lossy()
.into_owned(),
);
}
}
stems.sort();
let body: String = stems
.into_iter()
.map(|s| format!("pub mod {};\n", s))
.collect();
fs::write(build_dir.join("lib.rs"), body)
}
fn path_to_toml_string(path: &Path) -> String {
let s = path.display().to_string();
let s = s.strip_prefix(r"\\?\").unwrap_or(&s);
s.replace('\\', "/")
}
fn write_verify_cargo_toml(build_dir: &Path) -> io::Result<()> {
let runtime = windjammer_runtime_path_for_integration_tests();
let runtime_display = path_to_toml_string(&runtime.canonicalize().unwrap_or(runtime));
let cargo = format!(
r#"[package]
name = "wj_multi_file_integration_verify"
version = "0.1.0"
edition = "2021"
[workspace]
[dependencies]
windjammer-runtime = {{ path = "{}" }}
smallvec = "1.13"
serde = {{ version = "1.0", features = ["derive"] }}
[lib]
path = "lib.rs"
name = "wj_multi_file_integration_verify"
"#,
runtime_display
);
fs::write(build_dir.join("Cargo.toml"), cargo)
}
fn windjammer_runtime_path_for_integration_tests() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("crates/windjammer-runtime")
}
fn shared_cargo_target_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("target")
.join("wj_integration_verify")
}