#![allow(dead_code)]
use std::fs;
use std::path::Path;
use std::process::Command;
use tempfile::TempDir;
use serde::Deserialize;
pub fn require_tool(name: &str) {
let available = Command::new(name)
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok();
assert!(
available,
"required tool '{name}' is not installed — install it; missing tools fail tests, they never skip"
);
}
pub fn setup_test_project() -> TempDir {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
fs::create_dir_all(temp_dir.path().join("tera.templates")).expect("Failed to create tera.templates dir");
fs::create_dir_all(temp_dir.path().join("config")).expect("Failed to create config dir");
fs::write(
temp_dir.path().join("rsconstruct.toml"),
"[processor.tera]\nsrc_dirs = [\"tera.templates\"]\n"
).expect("Failed to write rsconstruct.toml");
temp_dir
}
pub fn run_rsconstruct(dir: &Path, args: &[&str]) -> std::process::Output {
let rsconstruct_path = env!("CARGO_BIN_EXE_rsconstruct");
Command::new(rsconstruct_path)
.current_dir(dir)
.args(args)
.output()
.expect("Failed to execute rsconstruct")
}
pub fn run_rsconstruct_with_env(dir: &Path, args: &[&str], env_vars: &[(&str, &str)]) -> std::process::Output {
let rsconstruct_path = env!("CARGO_BIN_EXE_rsconstruct");
let mut cmd = Command::new(rsconstruct_path);
cmd.current_dir(dir).args(args);
for (key, val) in env_vars {
cmd.env(key, val);
}
cmd.output().expect("Failed to execute rsconstruct")
}
pub fn setup_project_with_config(config: &str) -> TempDir {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
fs::write(temp_dir.path().join("rsconstruct.toml"), config).unwrap();
temp_dir
}
pub fn make_executable(path: &Path) {
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(path, perms).unwrap();
}
pub fn write_file(base: &Path, relative: &str, content: &str) {
let path = base.join(relative);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(path, content).unwrap();
}
pub fn setup_cc_project(project_path: &Path) {
fs::create_dir_all(project_path.join("src")).unwrap();
fs::write(
project_path.join("rsconstruct.toml"),
"[processor.cc_single_file]\nsrc_dirs = [\"src\"]\n[analyzer.icpp]\n"
).unwrap();
}
pub fn run_rsconstruct_json(dir: &Path, args: &[&str]) -> BuildResult {
let mut full_args = vec!["--json"];
full_args.extend(args);
let output = run_rsconstruct(dir, &full_args);
BuildResult::parse(&output)
}
pub fn run_rsconstruct_json_with_env(dir: &Path, args: &[&str], env_vars: &[(&str, &str)]) -> BuildResult {
let mut full_args = vec!["--json"];
full_args.extend(args);
let output = run_rsconstruct_with_env(dir, &full_args, env_vars);
BuildResult::parse(&output)
}
#[derive(Debug, Deserialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum BuildEvent {
BuildStart {
total_products: usize,
},
ProductComplete {
product: String,
processor: String,
status: String,
#[serde(default)]
duration_ms: Option<u64>,
#[serde(default)]
error: Option<String>,
},
BuildSummary {
total: usize,
success: usize,
failed: usize,
skipped: usize,
restored: usize,
duration_ms: u64,
#[serde(default)]
errors: Vec<String>,
},
}
#[derive(Debug, Default)]
pub struct BuildResult {
pub exit_success: bool,
pub total_products: usize,
pub success: usize,
pub failed: usize,
pub skipped: usize,
pub restored: usize,
pub duration_ms: u64,
pub errors: Vec<String>,
pub products: Vec<ProductResult>,
}
#[derive(Debug, Clone)]
pub struct ProductResult {
pub product: String,
pub processor: String,
pub status: String,
pub duration_ms: Option<u64>,
pub error: Option<String>,
}
impl BuildResult {
pub fn parse(output: &std::process::Output) -> Self {
let mut result = BuildResult {
exit_success: output.status.success(),
..Default::default()
};
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.trim().is_empty() {
continue;
}
if let Ok(event) = serde_json::from_str::<BuildEvent>(line) {
match event {
BuildEvent::BuildStart { total_products } => {
result.total_products = total_products;
}
BuildEvent::ProductComplete { product, processor, status, duration_ms, error } => {
result.products.push(ProductResult {
product,
processor,
status,
duration_ms,
error,
});
}
BuildEvent::BuildSummary { total: _, success, failed, skipped, restored, duration_ms, errors } => {
result.success = success;
result.failed = failed;
result.skipped = skipped;
result.restored = restored;
result.duration_ms = duration_ms;
result.errors = errors;
}
}
}
}
result
}
pub fn count_status(&self, status: &str) -> usize {
self.products.iter().filter(|p| p.status == status).count()
}
pub fn has_product(&self, name: &str, status: &str) -> bool {
self.products.iter().any(|p| p.product.contains(name) && p.status == status)
}
pub fn products_with_status(&self, status: &str) -> Vec<&ProductResult> {
self.products.iter().filter(|p| p.status == status).collect()
}
}
macro_rules! test_checker {
($mod_name:ident, tool: $tool:expr, processor: $proc:expr,
files: [ $( ($fname:expr, $content:expr) ),+ $(,)? ]) => {
paste::paste! {
#[test]
fn [<$mod_name _valid>]() {
crate::common::require_tool($tool);
let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
std::fs::write(
project_path.join("rsconstruct.toml"),
format!("[processor.{}]\nsrc_dirs = [\".\"]\n", $proc),
).unwrap();
$( std::fs::write(project_path.join($fname), $content).unwrap(); )+
let output = crate::common::run_rsconstruct_with_env(
project_path, &["build", "-v"], &[("NO_COLOR", "1")]);
assert!(
output.status.success(),
"Build should succeed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn [<$mod_name _incremental_skip>]() {
crate::common::require_tool($tool);
let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
std::fs::write(
project_path.join("rsconstruct.toml"),
format!("[processor.{}]\nsrc_dirs = [\".\"]\n", $proc),
).unwrap();
$( std::fs::write(project_path.join($fname), $content).unwrap(); )+
let output1 = crate::common::run_rsconstruct_with_env(
project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output1.status.success());
let output2 = crate::common::run_rsconstruct_with_env(
project_path, &["build", "--verbose"], &[("NO_COLOR", "1")]);
assert!(output2.status.success());
let stdout2 = String::from_utf8_lossy(&output2.stdout);
assert!(
stdout2.contains(&format!("[{}] Skipping (unchanged):", $proc)),
"Second build should skip: {}", stdout2
);
}
}
};
($mod_name:ident, tool: $tool:expr, processor: $proc:expr, no_project) => {
paste::paste! {
#[test]
fn [<$mod_name _no_project_discovered>]() {
crate::common::require_tool($tool);
let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
std::fs::write(
project_path.join("rsconstruct.toml"),
format!("[processor.{}]\nsrc_dirs = [\".\"]\n", $proc),
).unwrap();
let output = crate::common::run_rsconstruct_with_env(
project_path, &["build", "-v"], &[("NO_COLOR", "1")]);
assert!(
output.status.success(),
"Build should succeed with no files: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
}
};
($mod_name:ident, tool: $tool:expr, processor: $proc:expr,
config: $config:expr,
files: [ $( ($fname:expr, $content:expr) ),+ $(,)? ]) => {
paste::paste! {
#[test]
fn [<$mod_name _valid>]() {
crate::common::require_tool($tool);
let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
std::fs::write(project_path.join("rsconstruct.toml"), $config).unwrap();
$( std::fs::write(project_path.join($fname), $content).unwrap(); )+
let output = crate::common::run_rsconstruct_with_env(
project_path, &["build", "-v"], &[("NO_COLOR", "1")]);
assert!(
output.status.success(),
"Build should succeed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn [<$mod_name _incremental_skip>]() {
crate::common::require_tool($tool);
let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
std::fs::write(project_path.join("rsconstruct.toml"), $config).unwrap();
$( std::fs::write(project_path.join($fname), $content).unwrap(); )+
let output1 = crate::common::run_rsconstruct_with_env(
project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(output1.status.success());
let output2 = crate::common::run_rsconstruct_with_env(
project_path, &["build", "--verbose"], &[("NO_COLOR", "1")]);
assert!(output2.status.success());
let stdout2 = String::from_utf8_lossy(&output2.stdout);
assert!(
stdout2.contains(&format!("[{}] Skipping (unchanged):", $proc)),
"Second build should skip: {}", stdout2
);
}
}
};
}