use crate::common::{
run_rsconstruct_json_with_env, run_rsconstruct_with_env, setup_project_with_config,
setup_test_project,
};
use std::fs;
use tempfile::TempDir;
#[test]
fn processors_list_shows_declared() {
let temp_dir = setup_test_project();
let project_path = temp_dir.path();
let output =
run_rsconstruct_with_env(project_path, &["processor", "list"], &[("NO_COLOR", "1")]);
assert!(
output.status.success(),
"processor list failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("tera"), "Expected tera processor in list");
}
#[test]
fn processors_files_shows_products() {
let temp_dir = setup_test_project();
let project_path = temp_dir.path();
fs::write(project_path.join("config/test.py"), "value = 42").expect("Failed to write config");
fs::write(
project_path.join("tera.templates/output.txt.tera"),
"{% set c = load_python(path='config/test.py') %}{{ c.value }}",
)
.expect("Failed to write template");
let output = run_rsconstruct_with_env(
project_path,
&["processor", "files", "--headers"],
&[("NO_COLOR", "1")],
);
assert!(
output.status.success(),
"processor files failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("[processor.generator.tera]"),
"Expected [tera] header in output"
);
assert!(
stdout.contains("output.txt"),
"Expected output file in listing"
);
}
#[test]
fn processors_files_no_files_message() {
let temp_dir = setup_test_project();
let project_path = temp_dir.path();
let output =
run_rsconstruct_with_env(project_path, &["processor", "files"], &[("NO_COLOR", "1")]);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("No files discovered") || stdout.contains("(no files)"),
"Expected empty message, got: {}",
stdout
);
}
#[test]
fn processors_files_unknown_processor_fails() {
let temp_dir = setup_test_project();
let project_path = temp_dir.path();
let output = run_rsconstruct_with_env(
project_path,
&["processor", "files", "nonexistent"],
&[("NO_COLOR", "1")],
);
assert!(
!output.status.success(),
"Expected failure for unknown processor"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Unknown processor"),
"Expected 'Unknown processor' error, got: {}",
stderr
);
}
#[test]
fn processors_list_shows_descriptions() {
let temp_dir = setup_test_project();
let project_path = temp_dir.path();
let output =
run_rsconstruct_with_env(project_path, &["processor", "list"], &[("NO_COLOR", "1")]);
assert!(
output.status.success(),
"processor list failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("tera"), "Expected tera processor");
}
#[test]
fn processors_files_json_output() {
let temp_dir = setup_test_project();
let project_path = temp_dir.path();
fs::write(project_path.join("config/test.py"), "value = 42").expect("Failed to write config");
fs::write(
project_path.join("tera.templates/output.txt.tera"),
"{% set c = load_python(path='config/test.py') %}{{ c.value }}",
)
.expect("Failed to write template");
let output = run_rsconstruct_with_env(
project_path,
&["--json", "processor", "files"],
&[("NO_COLOR", "1")],
);
assert!(
output.status.success(),
"processor files --json failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let entries: Vec<serde_json::Value> =
serde_json::from_str(&stdout).expect("Expected valid JSON array");
assert!(!entries.is_empty(), "Expected at least one entry");
let entry = &entries[0];
assert!(
entry.get("processor").is_some(),
"Entry should have 'processor' field"
);
assert!(
entry.get("processor_type").is_some(),
"Entry should have 'processor_type' field"
);
assert!(
entry.get("inputs").is_some(),
"Entry should have 'inputs' field"
);
assert!(
entry.get("outputs").is_some(),
"Entry should have 'outputs' field"
);
assert_eq!(entry["processor"], "processor.generator.tera");
assert_eq!(entry["processor_type"], "generator");
}
#[test]
fn processors_files_named_includes_generated_inputs() {
let temp_dir = TempDir::new().unwrap();
let project = temp_dir.path();
fs::write(
project.join("rsconstruct.toml"),
r#"[build]
hash_tool_versions = false
[processor.generator.generic.make]
command = "cp"
output_dir = "gen"
output_extension = "mid"
batch = false
src_extensions = [".raw"]
src_dirs = ["raw"]
[processor.generator.generic.use]
command = "cp"
output_dir = "out/use"
output_extension = "txt"
batch = false
src_extensions = [".mid"]
src_dirs = ["gen"]
"#,
)
.unwrap();
fs::create_dir_all(project.join("raw")).unwrap();
fs::write(project.join("raw/a.raw"), "a\n").unwrap();
let output = run_rsconstruct_with_env(
project,
&[
"--json",
"processor",
"files",
"processor.generator.generic.use",
],
&[("NO_COLOR", "1")],
);
assert!(
output.status.success(),
"processor files failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let entries: Vec<serde_json::Value> =
serde_json::from_slice(&output.stdout).expect("Expected valid JSON array");
assert_eq!(entries.len(), 1, "only the named processor: {entries:?}");
assert_eq!(entries[0]["processor"], "processor.generator.generic.use");
assert_eq!(entries[0]["inputs"][0], "gen/a.mid");
}
#[test]
fn processor_info_shows_description_and_fields() {
let temp_dir = TempDir::new().unwrap();
let output = run_rsconstruct_with_env(
temp_dir.path(),
&["processor", "info", "processor.checker.ruff"],
&[("NO_COLOR", "1")],
);
assert!(
output.status.success(),
"processor info failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("processor.checker.ruff (checker)"),
"{stdout}"
);
assert!(stdout.contains("Lint and format Python files"), "{stdout}");
assert!(stdout.contains("src_extensions"), "{stdout}");
assert!(
stdout.contains("File extensions to match during scanning"),
"field descriptions are shown: {stdout}"
);
}
#[test]
fn processor_info_without_name_covers_every_processor() {
let temp_dir = TempDir::new().unwrap();
let listed = run_rsconstruct_with_env(
temp_dir.path(),
&["--json", "processor", "list"],
&[("NO_COLOR", "1")],
);
assert!(listed.status.success());
let listed: Vec<serde_json::Value> = serde_json::from_slice(&listed.stdout).unwrap();
let output = run_rsconstruct_with_env(
temp_dir.path(),
&["--json", "processor", "info"],
&[("NO_COLOR", "1")],
);
assert!(
output.status.success(),
"processor info failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let entries: Vec<serde_json::Value> =
serde_json::from_slice(&output.stdout).expect("Expected valid JSON array");
assert_eq!(entries.len(), listed.len(), "one entry per processor");
for entry in &entries {
assert_ne!(entry["description"], "", "{entry}");
let fields = entry["fields"].as_array().expect("fields array");
assert!(
fields.iter().any(|f| f["name"] == "enabled"),
"every processor has the standard fields: {entry}"
);
}
}
#[test]
fn processors_files_json_empty() {
let temp_dir = setup_test_project();
let project_path = temp_dir.path();
let output = run_rsconstruct_with_env(
project_path,
&["--json", "processor", "files"],
&[("NO_COLOR", "1")],
);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let entries: Vec<serde_json::Value> =
serde_json::from_str(&stdout).expect("Expected valid JSON array");
assert!(
entries.is_empty(),
"Expected empty JSON array, got: {}",
stdout
);
}
#[test]
fn processors_list_works_without_config() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let output = run_rsconstruct_with_env(
temp_dir.path(),
&["processor", "list"],
&[("NO_COLOR", "1")],
);
assert!(
output.status.success(),
"processor list should work without rsconstruct.toml: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("tera"), "Expected tera processor in output");
assert!(stdout.contains("ruff"), "Expected ruff processor in output");
assert!(
stdout.contains("shellcheck"),
"Expected shellcheck processor in output"
);
}
#[test]
fn processors_list_reports_rust() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let output = run_rsconstruct_with_env(
temp_dir.path(),
&["--json", "processor", "list"],
&[("NO_COLOR", "1")],
);
assert!(
output.status.success(),
"processor list --json failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let entries: Vec<serde_json::Value> =
serde_json::from_str(&stdout).expect("Expected valid JSON array");
let by_name = |name: &str| -> &serde_json::Value {
entries
.iter()
.find(|e| e["name"] == name)
.unwrap_or_else(|| panic!("no '{name}' entry in processor list --json"))
};
assert_eq!(by_name("processor.generator.tera")["native"], true);
assert_eq!(by_name("processor.generator.tera")["rust"], true);
assert_eq!(by_name("processor.checker.ruff")["native"], false);
assert_eq!(by_name("processor.checker.ruff")["rust"], true);
assert_eq!(by_name("processor.checker.pylint")["native"], false);
assert_eq!(by_name("processor.checker.pylint")["rust"], false);
for entry in &entries {
assert!(
entry["rust"].is_boolean(),
"'rust' must be a boolean on every entry, got {entry}"
);
if entry["native"] == true {
assert_eq!(
entry["rust"], true,
"native processor '{}' must report rust: true",
entry["name"]
);
}
}
let table = run_rsconstruct_with_env(
temp_dir.path(),
&["processor", "list"],
&[("NO_COLOR", "1")],
);
let table_out = String::from_utf8_lossy(&table.stdout);
assert!(
table_out.contains("Rust"),
"processor list table should have a Rust column: {table_out}"
);
}
#[test]
fn no_processor_section_means_no_products() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
fs::create_dir_all(project_path.join("tera.templates")).unwrap();
fs::write(project_path.join("tera.templates/quick.txt.tera"), "hello").unwrap();
fs::write(project_path.join("rsconstruct.toml"), "\n").unwrap();
let result = run_rsconstruct_json_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(result.exit_success, "Build should succeed");
assert_eq!(
result.total_products, 0,
"Expected 0 products when no processor is declared"
);
}
#[test]
fn per_processor_enabled_true_is_default() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
fs::create_dir_all(project_path.join("tera.templates")).unwrap();
fs::write(project_path.join("tera.templates/quick.txt.tera"), "hello").unwrap();
fs::write(
project_path.join("rsconstruct.toml"),
"[processor.generator.tera]\nsrc_dirs = [\"tera.templates\"]\n",
)
.unwrap();
let result = run_rsconstruct_json_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(result.exit_success, "Build should succeed");
assert_eq!(
result.total_products, 1,
"Expected 1 product when processor defaults to enabled"
);
}
#[test]
fn disabled_processor_skips_tool_preflight() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
fs::create_dir_all(project_path.join("src")).unwrap();
fs::write(project_path.join("src/a.txt"), "hello\n").unwrap();
let config = |enabled: &str| {
format!(
"[processor.checker.script]\ncommand = \"definitely-not-a-real-tool-xyz\"\nsrc_dirs = [\"src\"]\nsrc_extensions = [\".txt\"]\nenabled = {enabled}\n"
)
};
fs::write(project_path.join("rsconstruct.toml"), config("true")).unwrap();
let result = run_rsconstruct_json_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(
!result.exit_success,
"enabled instance with missing tool must fail"
);
fs::write(project_path.join("rsconstruct.toml"), config("false")).unwrap();
let result = run_rsconstruct_json_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(
result.exit_success,
"disabled instance must not fail the tool pre-flight"
);
assert_eq!(
result.total_products, 0,
"disabled instance must produce no products"
);
}
#[test]
fn processors_list_json() {
let temp_dir = setup_test_project();
let project_path = temp_dir.path();
let output = run_rsconstruct_with_env(
project_path,
&["--json", "processor", "list"],
&[("NO_COLOR", "1")],
);
assert!(
output.status.success(),
"processor list --json failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let entries: Vec<serde_json::Value> =
serde_json::from_str(&stdout).expect("Expected valid JSON array");
assert!(!entries.is_empty(), "Expected at least one entry");
for entry in &entries {
assert!(
entry.get("name").is_some(),
"Entry should have 'name' field"
);
assert!(
entry.get("processor_type").is_some(),
"Entry should have 'processor_type' field"
);
assert!(
entry.get("enabled").is_some(),
"Entry should have 'enabled' field"
);
assert!(
entry.get("detected").is_some(),
"Entry should have 'detected' field"
);
assert!(
entry.get("batch").is_some(),
"Entry should have 'batch' field"
);
assert!(
entry.get("description").is_some(),
"Entry should have 'description' field"
);
}
let tera = entries
.iter()
.find(|e| e["name"] == "processor.generator.tera")
.expect("Expected tera in list");
assert!(tera.get("name").is_some());
}
#[test]
fn processors_list_all_json_without_config() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let output = run_rsconstruct_with_env(
temp_dir.path(),
&["--json", "processor", "list"],
&[("NO_COLOR", "1")],
);
assert!(
output.status.success(),
"processor list --json failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let entries: Vec<serde_json::Value> =
serde_json::from_str(&stdout).expect("Expected valid JSON array");
assert!(!entries.is_empty(), "Expected at least one entry");
for entry in &entries {
assert!(
entry.get("name").is_some(),
"Entry should have 'name' field"
);
assert!(
entry.get("processor_type").is_some(),
"Entry should have 'processor_type' field"
);
assert!(
entry.get("batch").is_some(),
"Entry should have 'batch' field"
);
assert!(
entry.get("description").is_some(),
"Entry should have 'description' field"
);
}
let tera = entries
.iter()
.find(|e| e["name"] == "processor.generator.tera")
.expect("Expected tera in list");
assert!(tera.get("name").is_some());
}
#[test]
fn removing_processor_section_disables_it() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_path = temp_dir.path();
fs::create_dir_all(project_path.join("tera.templates")).unwrap();
fs::write(project_path.join("tera.templates/output.txt.tera"), "hello").unwrap();
fs::write(
project_path.join("rsconstruct.toml"),
"[processor.generator.tera]\nsrc_dirs = [\"tera.templates\"]\n",
)
.unwrap();
let result = run_rsconstruct_json_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(result.exit_success, "Build should succeed");
assert_eq!(
result.total_products, 1,
"Expected 1 product with tera declared"
);
fs::write(project_path.join("rsconstruct.toml"), "\n").unwrap();
let result = run_rsconstruct_json_with_env(project_path, &["build"], &[("NO_COLOR", "1")]);
assert!(result.exit_success, "Build should succeed");
assert_eq!(
result.total_products, 0,
"Expected 0 products with no processor declared"
);
}
#[test]
fn remove_no_file_processors_keeps_disabled_stanza() {
let temp_dir = setup_project_with_config(
"[processor.generator.tera]\nsrc_dirs = [\"tera.templates\"]\n\n[processor.checker.shellcheck]\nsrc_dirs = [\"src\"]\nenabled = false\n",
);
let project_path = temp_dir.path();
fs::create_dir_all(project_path.join("tera.templates")).unwrap();
fs::write(project_path.join("tera.templates/output.txt.tera"), "hello").unwrap();
let output = run_rsconstruct_with_env(
project_path,
&["smart", "remove-no-file-processors"],
&[("NO_COLOR", "1")],
);
assert!(
output.status.success(),
"command failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("shellcheck"),
"Disabled processor must not be reported as having no files, got: {stdout}"
);
let toml = fs::read_to_string(project_path.join("rsconstruct.toml")).unwrap();
assert!(
toml.contains("[processor.checker.shellcheck]"),
"Disabled stanza must be preserved, got: {toml}"
);
assert!(
toml.contains("enabled = false"),
"enabled = false must be preserved, got: {toml}"
);
}
#[test]
fn remove_no_file_processors_removes_enabled_stanza_with_no_files() {
let temp_dir = setup_project_with_config(
"[processor.generator.tera]\nsrc_dirs = [\"tera.templates\"]\n\n[processor.checker.shellcheck]\nsrc_dirs = [\"src\"]\n",
);
let project_path = temp_dir.path();
fs::create_dir_all(project_path.join("tera.templates")).unwrap();
fs::write(project_path.join("tera.templates/output.txt.tera"), "hello").unwrap();
let output = run_rsconstruct_with_env(
project_path,
&["smart", "remove-no-file-processors"],
&[("NO_COLOR", "1")],
);
assert!(
output.status.success(),
"command failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let toml = fs::read_to_string(project_path.join("rsconstruct.toml")).unwrap();
assert!(
!toml.contains("[processor.checker.shellcheck]"),
"Enabled stanza with no files must be removed, got: {toml}"
);
assert!(
toml.contains("[processor.generator.tera]"),
"Processor with files must be kept, got: {toml}"
);
}