use crate::common::*;
use std::fs;
use tempfile::TempDir;
fn user_config(contents: &str) -> TempDir {
let xdg = TempDir::new().unwrap();
fs::create_dir_all(xdg.path().join("rsconstruct")).unwrap();
fs::write(xdg.path().join("rsconstruct/config.toml"), contents).unwrap();
xdg
}
fn header_cells(stdout: &str) -> Vec<String> {
let line = stdout
.lines()
.skip_while(|l| !l.starts_with('╭'))
.nth(1)
.unwrap_or_else(|| panic!("no table in:\n{stdout}"));
line.split('│')
.map(str::trim)
.filter(|c| !c.is_empty())
.map(str::to_string)
.collect()
}
fn processor_list(dir: &std::path::Path, args: &[&str], env: &[(&str, &str)]) -> String {
let mut full = args.to_vec();
full.extend(["processor", "list"]);
let mut env_vars = vec![("NO_COLOR", "1")];
env_vars.extend_from_slice(env);
let output = run_rsconstruct_with_env(dir, &full, &env_vars);
assert!(
output.status.success(),
"processor list failed: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).into_owned()
}
#[test]
fn all_shows_every_column() {
let dir = TempDir::new().unwrap();
let default = header_cells(&processor_list(dir.path(), &[], &[]));
assert_eq!(
default,
[
"Name",
"Type",
"Native",
"Rust",
"Batch",
"Cache",
"Rust alternative"
]
);
let all = header_cells(&processor_list(dir.path(), &["--all"], &[]));
assert_eq!(
all,
[
"Name",
"Type",
"Native",
"Rust",
"Batch",
"Cache",
"Rust alternative",
"Description"
]
);
let verbose = header_cells(&processor_list(dir.path(), &["--verbose"], &[]));
assert_eq!(verbose, all);
}
#[test]
fn fields_prints_names_only() {
let dir = TempDir::new().unwrap();
let stdout = processor_list(dir.path(), &["--fields"], &[]);
assert!(
stdout.contains("processor_list (rsconstruct processor list)"),
"{stdout}"
);
assert_eq!(
header_cells(&stdout),
[
"Name",
"Type",
"Native",
"Rust",
"Batch",
"Cache",
"Rust alternative",
"Description"
]
);
assert!(
!stdout.contains("processor.checker.ruff"),
"--fields must print no data rows:\n{stdout}"
);
let rows = stdout.lines().filter(|l| l.starts_with('│')).count();
assert_eq!(rows, 1, "only the header row:\n{stdout}");
}
#[test]
fn user_config_chooses_columns() {
let dir = TempDir::new().unwrap();
let xdg = user_config("[tables]\nprocessor_list = [\"Type\", \"Name\", \"Description\"]\n");
let env = [("XDG_CONFIG_HOME", xdg.path().to_str().unwrap())];
let chosen = header_cells(&processor_list(dir.path(), &[], &env));
assert_eq!(chosen, ["Type", "Name", "Description"]);
let all = header_cells(&processor_list(dir.path(), &["--all"], &env));
assert_eq!(all.len(), 8, "{all:?}");
let output = run_rsconstruct_with_env(dir.path(), &["processor", "types"], &env);
assert!(output.status.success());
let other = header_cells(&String::from_utf8_lossy(&output.stdout));
assert_eq!(other, ["Type", "Batch", "Description"]);
}
#[test]
fn user_config_tables_beside_build() {
let dir = TempDir::new().unwrap();
let xdg =
user_config("[build]\nreject_dot_src_dirs = true\n\n[tables]\nhook_list = [\"Hook\"]\n");
let env = [
("XDG_CONFIG_HOME", xdg.path().to_str().unwrap()),
("NO_COLOR", "1"),
];
let output = run_rsconstruct_with_env(dir.path(), &["hook"], &env);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
header_cells(&String::from_utf8_lossy(&output.stdout)),
["Hook"]
);
}
#[test]
fn user_config_rejects_unknown_table_or_field() {
let dir = TempDir::new().unwrap();
let xdg = user_config("[tables]\nprocessor_lst = [\"Name\"]\n");
let output = run_rsconstruct_with_env(
dir.path(),
&["processor", "list"],
&[("XDG_CONFIG_HOME", xdg.path().to_str().unwrap())],
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(output.status.code(), Some(2), "{stderr}");
assert!(
stderr.contains("processor_lst") && stderr.contains("no table has this name"),
"{stderr}"
);
assert!(
stderr.contains("processor_list") && stderr.contains("--fields"),
"the error must list the real names and point at --fields: {stderr}"
);
let xdg = user_config("[tables]\nprocessor_list = [\"Name\", \"Colour\"]\n");
let output = run_rsconstruct_with_env(
dir.path(),
&["processor", "list"],
&[("XDG_CONFIG_HOME", xdg.path().to_str().unwrap())],
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(output.status.code(), Some(2), "{stderr}");
assert!(
stderr.contains("\"Colour\"") && stderr.contains("\"Rust alternative\""),
"{stderr}"
);
let xdg = user_config("[tables]\nprocessor_list = \"Name\"\n");
let output = run_rsconstruct_with_env(
dir.path(),
&["processor", "list"],
&[("XDG_CONFIG_HOME", xdg.path().to_str().unwrap())],
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(output.status.code(), Some(2), "{stderr}");
assert!(stderr.contains("list of field names"), "{stderr}");
}
#[test]
fn total_row_follows_the_chosen_columns() {
let temp_dir =
setup_project_with_config("[processor.generator.tera]\nsrc_dirs = [\"tera.templates\"]\n");
write_file(temp_dir.path(), "tera.templates/hello.txt.tera", "hello");
let xdg = user_config("[tables]\nproduct_status = [\"new\", \"Processor\"]\n");
let output = run_rsconstruct_with_env(
temp_dir.path(),
&["status"],
&[
("XDG_CONFIG_HOME", xdg.path().to_str().unwrap()),
("NO_COLOR", "1"),
],
);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(header_cells(&stdout), ["new", "Processor"]);
let total = stdout
.lines()
.find(|l| l.contains("Total"))
.unwrap_or_else(|| panic!("no Total row:\n{stdout}"));
let cells: Vec<&str> = total
.split('│')
.map(str::trim)
.filter(|c| !c.is_empty())
.collect();
assert_eq!(cells, ["1", "Total"], "{stdout}");
}
#[test]
fn tables_drop_the_processor_prefix() {
let dir = TempDir::new().unwrap();
let stdout = processor_list(dir.path(), &[], &[]);
assert!(stdout.contains("│ checker.ruff "), "{stdout}");
assert!(
!stdout.lines().any(|l| l.contains("│ processor.")),
"no cell starts with the prefix:\n{stdout}"
);
let output = run_rsconstruct_with_env(
dir.path(),
&["--json", "processor", "list"],
&[("NO_COLOR", "1")],
);
let entries: Vec<serde_json::Value> =
serde_json::from_str(&String::from_utf8_lossy(&output.stdout)).unwrap();
assert!(
entries
.iter()
.any(|e| e["name"] == "processor.checker.ruff")
);
}
#[test]
fn cache_column_marks_the_installers() {
let dir = TempDir::new().unwrap();
let output = run_rsconstruct_with_env(
dir.path(),
&["--json", "processor", "list"],
&[("NO_COLOR", "1")],
);
let entries: Vec<serde_json::Value> =
serde_json::from_str(&String::from_utf8_lossy(&output.stdout)).unwrap();
let caches = |name: &str| -> bool {
entries
.iter()
.find(|e| e["name"] == name)
.unwrap_or_else(|| panic!("no {name}"))["caches_outputs"]
.as_bool()
.unwrap()
};
assert!(caches("processor.generator.tera"));
assert!(caches("processor.checker.ruff"));
assert!(caches("processor.mass_generator.cargo"));
for installer in ["pip", "npm", "gem"] {
assert!(
!caches(&format!("processor.mass_generator.{installer}")),
"{installer} installs into an environment the cache cannot restore"
);
}
let stdout = processor_list(dir.path(), &[], &[]);
let pip = stdout
.lines()
.find(|l| l.contains("│ mass_generator.pip "))
.unwrap_or_else(|| panic!("no pip row:\n{stdout}"));
let cells: Vec<&str> = pip
.split('│')
.map(str::trim)
.filter(|c| !c.is_empty())
.collect();
assert_eq!(cells[5], "No", "{pip}");
}