use std::path::Path;
use tempfile::TempDir;
use super::*;
use crate::core::build_env::BuildConfig;
fn env_at(home: &Path) -> ResolvedBuildEnv {
ResolvedBuildEnv {
cargo_target_dir: home
.join(".trusty-tools")
.join("cargo-target")
.join("bobmatnyc")
.join("trusty-tools"),
build_jobs: 8,
sccache: false,
target_dir_configured: false,
}
}
fn config_path_at(home: &Path) -> std::path::PathBuf {
home.join(".trusty-tools")
.join("trusty-mpm")
.join("config.yaml")
}
#[test]
fn rust_build_env_repair_steps_name_the_check() {
let home = TempDir::new().expect("temp home");
let steps = repair_rust_build_env(
&config_path_at(home.path()),
&env_at(home.path()),
RepairMode::DryRun,
);
assert!(!steps.is_empty(), "a bare temp home must have work to do");
for step in &steps {
assert_eq!(step.check, "rust_build_env");
}
}
#[test]
fn rust_build_env_repair_creates_dir_and_writes_defaults_once() {
let home = TempDir::new().expect("temp home");
let env = env_at(home.path());
let config = config_path_at(home.path());
std::fs::create_dir_all(config.parent().expect("parent")).expect("config dir");
std::fs::write(&config, "auto_resume: true\n").expect("seed an existing config");
let steps = repair_rust_build_env(&config, &env, RepairMode::Apply);
assert_eq!(
steps.len(),
2,
"one directory step, one config step: {steps:?}"
);
for step in &steps {
assert!(
matches!(step.status, StepStatus::Applied { .. }),
"every step must apply: {step:?}"
);
}
assert!(env.cargo_target_dir.is_dir(), "the target directory exists");
let raw = std::fs::read_to_string(&config).expect("read back");
assert!(raw.contains("auto_resume: true"), "{raw}");
#[derive(serde::Deserialize)]
struct Doc {
auto_resume: Option<bool>,
build: BuildConfig,
}
let doc: Doc = serde_yaml::from_str(&raw).expect("the appended file must still parse");
assert_eq!(doc.auto_resume, Some(true));
assert_eq!(doc.build.build_jobs, Some(8));
assert_eq!(
doc.build.cargo_target_dir.as_deref(),
Some(env.cargo_target_dir.display().to_string().as_str())
);
assert_eq!(doc.build.sccache, Some(false));
let backups: Vec<_> = std::fs::read_dir(config.parent().expect("parent"))
.expect("list")
.filter_map(Result::ok)
.filter(|e| e.file_name().to_string_lossy().contains(".bak-"))
.collect();
assert_eq!(backups.len(), 1, "exactly one backup: {backups:?}");
assert_eq!(
std::fs::read_to_string(backups[0].path()).expect("read backup"),
"auto_resume: true\n"
);
}
#[test]
fn rust_build_env_repair_is_idempotent() {
let home = TempDir::new().expect("temp home");
let env = env_at(home.path());
let config = config_path_at(home.path());
let first = repair_rust_build_env(&config, &env, RepairMode::Apply);
assert_eq!(first.len(), 2, "the first run does both halves: {first:?}");
let after_first = std::fs::read_to_string(&config).expect("written");
let second = repair_rust_build_env(&config, &env, RepairMode::Apply);
assert!(
second.is_empty(),
"a second run must find nothing to repair: {second:?}"
);
assert_eq!(
std::fs::read_to_string(&config).expect("unchanged"),
after_first,
"the second run must not touch the file"
);
assert!(repair_rust_build_env(&config, &env, RepairMode::DryRun).is_empty());
}
#[test]
fn rust_build_env_repair_never_overwrites_existing_build_keys() {
let home = TempDir::new().expect("temp home");
let env = env_at(home.path());
let config = config_path_at(home.path());
std::fs::create_dir_all(config.parent().expect("parent")).expect("config dir");
let original = "build:\n build_jobs: 1\n";
std::fs::write(&config, original).expect("seed");
let steps = repair_rust_build_env(&config, &env, RepairMode::Apply);
assert!(
!steps.iter().any(|s| s.path == config),
"no step may target the config file: {steps:?}"
);
assert_eq!(
std::fs::read_to_string(&config).expect("read back"),
original,
"the file must be byte-identical"
);
assert!(env.cargo_target_dir.is_dir());
}
#[test]
fn rust_build_env_repair_refuses_an_unparseable_config() {
let home = TempDir::new().expect("temp home");
let env = env_at(home.path());
let config = config_path_at(home.path());
std::fs::create_dir_all(config.parent().expect("parent")).expect("config dir");
let broken = "auto_resume: true\n bad: [indent\n";
std::fs::write(&config, broken).expect("seed");
let steps = repair_rust_build_env(&config, &env, RepairMode::Apply);
let step = steps
.iter()
.find(|s| s.path == config)
.expect("a config step");
assert!(
matches!(step.status, StepStatus::Refused(_)),
"must refuse: {step:?}"
);
assert_eq!(
std::fs::read_to_string(&config).expect("read back"),
broken,
"a refused file must not be touched"
);
}
#[test]
fn rust_build_env_repair_refuses_to_wire_the_cargo_config() {
let home = TempDir::new().expect("temp home");
let mut env = env_at(home.path());
env.sccache = true;
let config = config_path_at(home.path());
let steps = repair_rust_build_env(&config, &env, RepairMode::Apply);
let cargo_config = home.path().join(".cargo").join("config.toml");
let step = steps
.iter()
.find(|s| s.path == cargo_config)
.expect("a step naming ~/.cargo/config.toml");
match &step.status {
StepStatus::Refused(why) => {
assert_eq!(why, CARGO_CONFIG_REFUSAL);
assert!(
why.contains("machine-global"),
"the reason must say why: {why}"
);
}
other => panic!("must be refused, not {other:?}"),
}
assert!(
!cargo_config.exists(),
"the repair must never create ~/.cargo/config.toml"
);
let mut off = env.clone();
off.sccache = false;
assert!(
!repair_rust_build_env(&config, &off, RepairMode::DryRun)
.iter()
.any(|s| s.path == cargo_config)
);
}
#[test]
fn rust_build_env_repair_reports_an_uncreatable_directory_as_failed() {
let home = TempDir::new().expect("temp home");
let mut env = env_at(home.path());
let blocked = home.path().join("blocked");
std::fs::write(&blocked, b"not a directory").expect("seed a file");
env.cargo_target_dir = blocked.clone();
let steps = repair_rust_build_env(&config_path_at(home.path()), &env, RepairMode::Apply);
let step = steps
.iter()
.find(|s| s.path == blocked)
.expect("a directory step");
assert!(
matches!(step.status, StepStatus::Refused(_)),
"an occupied path must be refused, not overwritten: {step:?}"
);
assert_eq!(
std::fs::read(&blocked).expect("still there"),
b"not a directory",
"the refused file must be untouched"
);
}
#[test]
fn rust_build_env_repair_seeds_an_absent_config_file() {
let home = TempDir::new().expect("temp home");
let env = env_at(home.path());
let config = config_path_at(home.path());
assert!(!config.exists(), "precondition: no config yet");
let steps = repair_rust_build_env(&config, &env, RepairMode::Apply);
assert!(steps.iter().any(|s| s.path == config && s.changed()));
let raw = std::fs::read_to_string(&config).expect("created");
assert!(raw.starts_with("build:"), "{raw}");
assert!(
!std::fs::read_dir(config.parent().expect("parent"))
.expect("list")
.filter_map(Result::ok)
.any(|e| e.file_name().to_string_lossy().contains(".bak-")),
"there was nothing to back up"
);
}
#[test]
fn rust_build_env_repair_dry_run_writes_nothing() {
let home = TempDir::new().expect("temp home");
let env = env_at(home.path());
let config = config_path_at(home.path());
let steps = repair_rust_build_env(&config, &env, RepairMode::DryRun);
assert_eq!(steps.len(), 2, "{steps:?}");
assert!(steps.iter().all(|s| s.status == StepStatus::Planned));
assert!(!config.exists(), "dry run must not create the config");
assert!(
!env.cargo_target_dir.exists(),
"dry run must not create the directory"
);
}