use std::fs;
use std::path::Path;
use std::process::{Command, Output};
const RATCHET_FILE_NAME: &str = ".Cargo.lock.ratchet";
const LOCK_OLD: &str = "version = 4\n\n[[package]]\nname = \"serde\"\nversion = \"1.0.100\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\n";
const LOCK_NEW: &str = "version = 4\n\n[[package]]\nname = \"serde\"\nversion = \"1.0.219\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\n";
fn write_project(dir: &Path, lockfile: &str) {
fs::write(
dir.join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.0.0\"\nedition = \"2021\"\n\n[workspace]\n",
)
.unwrap();
fs::create_dir_all(dir.join("src")).unwrap();
fs::write(dir.join("src/lib.rs"), "").unwrap();
fs::write(dir.join("Cargo.lock"), lockfile).unwrap();
}
fn cargo_ratchet(project: &Path, extra_args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_cargo-ratchet"))
.arg("ratchet")
.arg("--manifest-path")
.arg(project.join("Cargo.toml"))
.args(extra_args)
.output()
.expect("failed to run cargo-ratchet")
}
fn stderr(output: &Output) -> String {
String::from_utf8(output.stderr.clone()).unwrap()
}
#[test]
fn bootstrap_creates_baseline() {
let dir = tempfile::tempdir().unwrap();
write_project(dir.path(), LOCK_NEW);
let output = cargo_ratchet(dir.path(), &[]);
assert!(output.status.success(), "stderr: {}", stderr(&output));
let baseline = fs::read_to_string(dir.path().join(RATCHET_FILE_NAME)).unwrap();
assert_eq!(baseline, LOCK_NEW);
}
#[test]
fn advance_on_pass() {
let dir = tempfile::tempdir().unwrap();
write_project(dir.path(), LOCK_NEW);
fs::write(dir.path().join(RATCHET_FILE_NAME), LOCK_OLD).unwrap();
let output = cargo_ratchet(dir.path(), &[]);
assert!(output.status.success(), "stderr: {}", stderr(&output));
let baseline = fs::read_to_string(dir.path().join(RATCHET_FILE_NAME)).unwrap();
assert_eq!(baseline, LOCK_NEW);
}
#[test]
fn baseline_untouched_on_fail() {
let dir = tempfile::tempdir().unwrap();
write_project(dir.path(), LOCK_OLD);
fs::write(dir.path().join(RATCHET_FILE_NAME), LOCK_NEW).unwrap();
let output = cargo_ratchet(dir.path(), &[]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("package version regressed"),
"stderr: {}",
stderr(&output)
);
let baseline = fs::read_to_string(dir.path().join(RATCHET_FILE_NAME)).unwrap();
assert_eq!(baseline, LOCK_NEW);
}
#[test]
fn fail_on_change_passes_when_baseline_matches() {
let dir = tempfile::tempdir().unwrap();
write_project(dir.path(), LOCK_NEW);
fs::write(dir.path().join(RATCHET_FILE_NAME), LOCK_NEW).unwrap();
let output = cargo_ratchet(dir.path(), &["--fail-on-change"]);
assert!(output.status.success(), "stderr: {}", stderr(&output));
}
#[test]
fn fail_on_change_fails_on_stale_baseline() {
let dir = tempfile::tempdir().unwrap();
write_project(dir.path(), LOCK_NEW);
fs::write(dir.path().join(RATCHET_FILE_NAME), LOCK_OLD).unwrap();
let output = cargo_ratchet(dir.path(), &["--fail-on-change"]);
assert!(!output.status.success());
assert!(
stderr(&output).contains("does not match"),
"stderr: {}",
stderr(&output)
);
let baseline = fs::read_to_string(dir.path().join(RATCHET_FILE_NAME)).unwrap();
assert_eq!(baseline, LOCK_OLD);
}
#[test]
fn fail_on_change_fails_on_missing_baseline() {
let dir = tempfile::tempdir().unwrap();
write_project(dir.path(), LOCK_NEW);
let output = cargo_ratchet(dir.path(), &["--fail-on-change"]);
assert!(!output.status.success());
assert!(!dir.path().join(RATCHET_FILE_NAME).exists());
}
#[test]
fn json_reports_violations_on_stdout() {
let dir = tempfile::tempdir().unwrap();
write_project(dir.path(), LOCK_OLD);
fs::write(dir.path().join(RATCHET_FILE_NAME), LOCK_NEW).unwrap();
let output = cargo_ratchet(dir.path(), &["--json"]);
assert!(!output.status.success());
let violations: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("stdout should be JSON");
assert_eq!(violations[0]["kind"], "package_version_regressed");
assert_eq!(violations[0]["name"], "serde");
assert_eq!(violations[0]["baseline_version"], "1.0.219");
assert_eq!(violations[0]["current_version"], "1.0.100");
}
#[test]
fn ratchet_file_override() {
let dir = tempfile::tempdir().unwrap();
write_project(dir.path(), LOCK_NEW);
let custom = dir.path().join("custom.ratchet");
let output = cargo_ratchet(dir.path(), &["--ratchet-file", custom.to_str().unwrap()]);
assert!(output.status.success(), "stderr: {}", stderr(&output));
assert_eq!(fs::read_to_string(&custom).unwrap(), LOCK_NEW);
assert!(!dir.path().join(RATCHET_FILE_NAME).exists());
}
#[test]
fn lockfile_override_skips_discovery_and_derives_baseline_name() {
let dir = tempfile::tempdir().unwrap();
let lockfile = dir.path().join("Custom.lock");
fs::write(&lockfile, LOCK_NEW).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_cargo-ratchet"))
.arg("ratchet")
.arg("--lockfile")
.arg(&lockfile)
.output()
.expect("failed to run cargo-ratchet");
assert!(output.status.success(), "stderr: {}", stderr(&output));
let baseline = fs::read_to_string(dir.path().join(".Custom.lock.ratchet")).unwrap();
assert_eq!(baseline, LOCK_NEW);
}