use std::path::{Path, PathBuf};
use std::process::{Command, Output};
fn project(tag: &str) -> PathBuf {
let dir = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("target/test-tmp/it")
.join(tag);
std::fs::create_dir_all(dir.join("src")).unwrap();
std::fs::create_dir_all(dir.join(".ct")).unwrap();
dir
}
fn fresh_store(dir: &Path) {
std::fs::write(
dir.join(".ct/rules.jsonc"),
"{\n // test store\n \"defs\": {\n },\n \"rules\": [\n ]\n}\n",
)
.unwrap();
}
fn code(out: &Output) -> i32 {
out.status.code().expect("child exited via a signal")
}
fn stdout(out: &Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn stderr(out: &Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
fn ct_rules(dir: &Path) -> Command {
let mut c = Command::new(env!("CARGO_BIN_EXE_ct-rules"));
c.current_dir(dir);
c
}
fn ct_check(dir: &Path) -> Command {
let mut c = Command::new(env!("CARGO_BIN_EXE_ct-check"));
c.current_dir(dir);
c
}
#[test]
fn add_verifies_now_strict_refuses_failing_candidates() {
let dir = project("rules-add");
fresh_store(&dir);
std::fs::write(dir.join("src/main.rs"), "fn main() {}\n").unwrap();
let ok = ct_rules(&dir)
.args([
"--add",
"no-dbg",
"--question",
"No dbg! in src?",
"--why",
"hygiene",
])
.args([
"--",
"ct-search",
"--base",
"src",
"--grep",
"dbg!",
"--expect",
"none",
"--quiet",
])
.output()
.unwrap();
assert_eq!(code(&ok), 0, "stderr: {:?}", stderr(&ok));
let store = std::fs::read_to_string(dir.join(".ct/rules.jsonc")).unwrap();
assert!(
store.contains("// test store"),
"comments preserved: {store:?}"
);
assert!(store.contains("\"added\""), "provenance recorded");
std::fs::write(dir.join("src/main.rs"), "fn main() { dbg!(1); }\n").unwrap();
let refused = ct_rules(&dir)
.args(["--add", "no-dbg-2", "--question", "q"])
.args([
"--",
"ct-search",
"--base",
"src",
"--grep",
"dbg!",
"--expect",
"none",
"--quiet",
])
.output()
.unwrap();
assert_eq!(code(&refused), 1, "failing candidate refused");
assert!(stderr(&refused).contains("not recorded"));
assert!(
!std::fs::read_to_string(dir.join(".ct/rules.jsonc"))
.unwrap()
.contains("no-dbg-2")
);
let dup = ct_rules(&dir)
.args(["--add", "no-dbg", "--question", "q", "--", "true"])
.output()
.unwrap();
assert_eq!(code(&dup), 2, "duplicate id refused");
}
#[test]
fn pending_lane_and_promotion() {
let dir = project("rules-pending");
fresh_store(&dir);
std::fs::write(dir.join("src/lib.rs"), "fn f() { x.unwrap(); }\n").unwrap();
let add = ct_rules(&dir)
.args([
"--add",
"no-unwrap",
"--pending",
"--question",
"No unwrap?",
])
.args([
"--",
"ct-search",
"--base",
"src",
"--grep",
"unwrap",
"--expect",
"none",
"--quiet",
])
.output()
.unwrap();
assert_eq!(code(&add), 0, "stderr: {:?}", stderr(&add));
assert!(stdout(&add).contains("pending"));
let check = ct_check(&dir).output().unwrap();
assert_eq!(
code(&check),
0,
"pending must not fail: {:?}",
stderr(&check)
);
assert!(stdout(&check).contains("PENDING"));
assert!(stdout(&check).contains("not yet held"));
let early = ct_rules(&dir)
.args(["--promote", "no-unwrap"])
.output()
.unwrap();
assert_eq!(code(&early), 1, "premature promotion refused");
std::fs::write(dir.join("src/lib.rs"), "fn f() {}\n").unwrap();
let promote = ct_rules(&dir)
.args(["--promote", "no-unwrap"])
.output()
.unwrap();
assert_eq!(code(&promote), 0, "stderr: {:?}", stderr(&promote));
let check = ct_check(&dir).output().unwrap();
assert!(
stdout(&check).contains("SUCCESS no-unwrap"),
"now enforced: {:?}",
stdout(&check)
);
assert!(!stdout(&check).contains("PENDING"));
}
#[test]
fn lanes_map_to_exit_status_warn_soft_broken_hard() {
let dir = project("rules-lanes");
fresh_store(&dir);
std::fs::write(dir.join("src/lib.rs"), "fn f() { x.unwrap(); }\n").unwrap();
let add = ct_rules(&dir)
.args([
"--add",
"no-unwrap",
"--severity",
"warn",
"--question",
"No unwrap?",
"--pending",
])
.args([
"--",
"ct-search",
"--base",
"src",
"--grep",
"unwrap",
"--expect",
"none",
"--quiet",
])
.output()
.unwrap();
assert_eq!(code(&add), 0);
let store = std::fs::read_to_string(dir.join(".ct/rules.jsonc")).unwrap();
assert!(
store.contains("\n \"pending\": true,\n"),
"pretty store: {store:?}"
);
std::fs::write(
dir.join(".ct/rules.jsonc"),
store.replace("\n \"pending\": true,", ""),
)
.unwrap();
let check = ct_check(&dir).output().unwrap();
assert_eq!(code(&check), 0, "warn never reddens: {:?}", stderr(&check));
assert!(stdout(&check).contains("WARN"), "got {:?}", stdout(&check));
assert!(
stderr(&check).contains("'no-unwrap' WARN"),
"explained on stderr"
);
let store = std::fs::read_to_string(dir.join(".ct/rules.jsonc")).unwrap();
let with_broken = store.replace(
"\"rules\": [",
"\"rules\": [{\"id\":\"stale\",\"question\":\"q\",\"probe\":[\"ct-view\",\"src/gone.rs\"]},",
);
std::fs::write(dir.join(".ct/rules.jsonc"), with_broken).unwrap();
let check = ct_check(&dir).output().unwrap();
assert_eq!(code(&check), 2, "broken rule => exit 2");
assert!(stdout(&check).contains("BROKEN"));
assert!(stderr(&check).contains("fix or remove with ct-rules"));
}
#[test]
fn probe_gate_is_immutable_and_def_expansion_works() {
let dir = project("rules-gate");
fresh_store(&dir);
std::fs::write(dir.join("src/lib.rs"), "struct Parser; struct Lexer;\n").unwrap();
for probe in [
vec!["ct-edit", "--find", "a", "--replace", "b"],
vec!["ct-each", "--items", "x", "--mutating", "--", "ct-edit"],
vec!["rm", "-rf", "src"],
vec!["sh", "-c", "true"],
vec!["ct-check"], vec!["cargo", "publish"],
] {
let mut args = vec!["--add", "bad", "--question", "q", "--"];
args.extend(probe.iter().copied());
let out = ct_rules(&dir).args(&args).output().unwrap();
assert_eq!(code(&out), 2, "probe {probe:?} must be refused");
}
let def = ct_rules(&dir)
.args(["--def", r#"core-types=["Parser","Lexer"]"#])
.output()
.unwrap();
assert_eq!(code(&def), 0, "stderr: {:?}", stderr(&def));
let add = ct_rules(&dir)
.args([
"--add",
"types-used",
"--question",
"Core types referenced?",
])
.args([
"--",
"ct-each",
"--items",
"{def:core-types}",
"--quiet",
"--",
])
.args(["ct-search", "--base", "src", "--grep", "{ITEM}", "--quiet"])
.output()
.unwrap();
assert_eq!(code(&add), 0, "stderr: {:?}", stderr(&add));
let check = ct_check(&dir).output().unwrap();
assert_eq!(code(&check), 0, "stderr: {:?}", stderr(&check));
assert!(stdout(&check).contains("SUCCESS types-used"));
let store = std::fs::read_to_string(dir.join(".ct/rules.jsonc")).unwrap();
std::fs::write(
dir.join(".ct/rules.jsonc"),
store.replace("{def:core-types}", "{def:nope}"),
)
.unwrap();
let check = ct_check(&dir).output().unwrap();
assert_eq!(code(&check), 2);
assert!(
stderr(&check).contains("unknown def"),
"got {:?}",
stderr(&check)
);
}
#[test]
fn store_discovery_is_upward_and_probes_run_from_the_root() {
let dir = project("rules-discovery");
fresh_store(&dir);
std::fs::write(dir.join("src/lib.rs"), "fn clean() {}\n").unwrap();
let add = ct_rules(&dir)
.args(["--add", "no-dbg", "--question", "q"])
.args([
"--",
"ct-search",
"--base",
"src",
"--grep",
"dbg!",
"--expect",
"none",
"--quiet",
])
.output()
.unwrap();
assert_eq!(code(&add), 0, "stderr: {:?}", stderr(&add));
let sub = dir.join("src");
let check = ct_check(&sub).output().unwrap();
assert_eq!(code(&check), 0, "stderr: {:?}", stderr(&check));
assert!(stdout(&check).contains("SUCCESS no-dbg"));
let orphan = Path::new(env!("CARGO_MANIFEST_DIR")).join("target/test-tmp/it");
let lost = ct_check(&orphan).output().unwrap();
if code(&lost) == 2 {
assert!(
stderr(&lost).contains("no .ct directory") || stderr(&lost).contains("read"),
"got {:?}",
stderr(&lost)
);
}
}
#[test]
fn ct_check_is_allowlisted_and_composes() {
let dir = project("rules-compose");
fresh_store(&dir);
std::fs::write(dir.join("src/lib.rs"), "fn clean() {}\n").unwrap();
let add = ct_rules(&dir)
.args(["--add", "no-dbg", "--question", "q"])
.args([
"--",
"ct-search",
"--base",
"src",
"--grep",
"dbg!",
"--expect",
"none",
"--quiet",
])
.output()
.unwrap();
assert_eq!(code(&add), 0);
let mut wrap = Command::new(env!("CARGO_BIN_EXE_ct-test"));
wrap.current_dir(&dir)
.args(["--question", "Do all invariants hold?", "--quiet"])
.args(["--emit", "{RESULT}"])
.args(["--cmd", "ct-check", "--", "--quiet"]);
let out = wrap.output().unwrap();
assert_eq!(code(&out), 0, "stderr: {:?}", stderr(&out));
assert!(stdout(&out).contains("SUCCESS"));
let mut deny = Command::new(env!("CARGO_BIN_EXE_ct-test"));
deny.current_dir(&dir)
.args(["--cmd", "ct-rules", "--", "--list"]);
let out = deny.output().unwrap();
assert_eq!(code(&out), 2, "ct-rules must stay off the allowlist");
}
#[test]
fn store_is_human_friendly_jsonc_with_prompt_retention_and_flatten() {
let dir = project("rules-pretty");
fresh_store(&dir);
std::fs::write(dir.join("src/lib.rs"), "fn clean() {}\n").unwrap();
let add = ct_rules(&dir)
.args([
"--add",
"no-dbg",
"--question",
"No dbg! in src?",
"--why",
"hygiene",
])
.args([
"--prompt",
"please make sure we never ship debug prints again",
])
.args([
"--",
"ct-search",
"--base",
"src",
"--grep",
"dbg!",
"--expect",
"none",
"--quiet",
])
.output()
.unwrap();
assert_eq!(code(&add), 0, "stderr: {:?}", stderr(&add));
assert!(
stdout(&add).contains("retained in the rule's \"prompt\" field"),
"user is told about retention: {:?}",
stdout(&add)
);
let second = ct_rules(&dir)
.args(["--add", "second", "--question", "q2"])
.args(["--prompt", "second request"])
.args([
"--",
"ct-search",
"--base",
"src",
"--grep",
"clean",
"--quiet",
])
.output()
.unwrap();
assert_eq!(code(&second), 0);
let store = std::fs::read_to_string(dir.join(".ct/rules.jsonc")).unwrap();
assert!(
store.starts_with("// ct rule store"),
"header first: {store:?}"
);
assert!(store.contains("// Managed by `ct rules`"));
assert!(
store.contains("\n \"id\": \"no-dbg\",\n \"question\""),
"one field per line"
);
assert!(
store.contains("\"prompt\": \"please make sure we never ship debug prints again\""),
"prompt verbatim"
);
assert!(
store.contains(" },\n\n {"),
"blank line between rules: {store:?}"
);
assert!(
!store.lines().any(|l| l.ends_with(' ')),
"no trailing whitespace"
);
let headerless = store.lines().skip(4).collect::<Vec<_>>().join("\n");
std::fs::write(dir.join(".ct/rules.jsonc"), headerless).unwrap();
let def = ct_rules(&dir).args(["--def", "x=src"]).output().unwrap();
assert_eq!(code(&def), 0, "stderr: {:?}", stderr(&def));
let store = std::fs::read_to_string(dir.join(".ct/rules.jsonc")).unwrap();
assert!(
store.starts_with("// ct rule store"),
"header re-established"
);
let flat = ct_rules(&dir).args(["--flatten"]).output().unwrap();
assert_eq!(code(&flat), 0, "stderr: {:?}", stderr(&flat));
assert!(
stdout(&flat).contains("flattened 2 prompt(s)"),
"got {:?}",
stdout(&flat)
);
let store = std::fs::read_to_string(dir.join(".ct/rules.jsonc")).unwrap();
assert!(!store.contains("\"prompt\""), "prompts gone: {store:?}");
assert!(
store.contains("\"why\": \"hygiene\""),
"mechanical definition intact"
);
let check = ct_check(&dir).output().unwrap();
assert_eq!(
code(&check),
0,
"flattened store still verifies: {:?}",
stderr(&check)
);
let again = ct_rules(&dir).args(["--flatten"]).output().unwrap();
assert_eq!(code(&again), 0);
assert!(stdout(&again).contains("nothing to flatten"));
}
#[test]
fn cargo_hook_is_generated_and_refuses_foreign_files() {
let dir = project("rules-hook");
fresh_store(&dir);
std::fs::write(
dir.join("Cargo.toml"),
"[package]\nname=\"t\"\nversion=\"0.0.0\"\n",
)
.unwrap();
let _ = std::fs::remove_file(dir.join("tests/ct_invariants.rs"));
let hook = ct_rules(&dir).args(["--hook", "cargo"]).output().unwrap();
assert_eq!(code(&hook), 0, "stderr: {:?}", stderr(&hook));
let shim = std::fs::read_to_string(dir.join("tests/ct_invariants.rs")).unwrap();
assert!(shim.starts_with("// Generated by `ct rules --hook cargo`."));
assert!(shim.contains("ct check"), "shim runs the surface");
assert!(shim.contains("could not run `ct`"), "degrades loudly");
assert_eq!(
code(&ct_rules(&dir).args(["--hook", "cargo"]).output().unwrap()),
0
);
std::fs::write(dir.join("tests/ct_invariants.rs"), "// hand-written\n").unwrap();
let refused = ct_rules(&dir).args(["--hook", "cargo"]).output().unwrap();
assert_eq!(code(&refused), 2);
assert!(stderr(&refused).contains("not overwriting"));
}
#[test]
fn bridge_probes_run_real_cargo_with_hermetic_flags() {
let dir = project("rules-bridge");
fresh_store(&dir);
std::fs::write(
dir.join("Cargo.toml"),
"[package]\nname = \"bridge-probe\"\nversion = \"0.0.0\"\nedition = \"2021\"\n",
)
.unwrap();
std::fs::write(dir.join("src/lib.rs"), "").unwrap();
let lock = Command::new("cargo")
.args(["generate-lockfile", "--offline"])
.current_dir(&dir)
.output()
.unwrap();
assert!(lock.status.success(), "lockfile: {:?}", stderr(&lock));
let add = ct_rules(&dir)
.args([
"--add",
"no-duplicate-deps",
"--question",
"No duplicate crate versions?",
])
.args(["--expect", "empty"])
.args(["--", "cargo", "tree", "-d"])
.output()
.unwrap();
assert_eq!(code(&add), 0, "stderr: {:?}", stderr(&add));
let add = ct_rules(&dir)
.args([
"--add",
"metadata-resolves",
"--question",
"Does the crate graph resolve?",
])
.args(["--", "cargo", "metadata"])
.output()
.unwrap();
assert_eq!(code(&add), 0, "stderr: {:?}", stderr(&add));
let check = ct_check(&dir).output().unwrap();
assert_eq!(code(&check), 0, "stderr: {:?}", stderr(&check));
assert!(stdout(&check).contains("SUCCESS no-duplicate-deps"));
assert!(stdout(&check).contains("SUCCESS metadata-resolves"));
}
#[test]
fn builtin_mods_check_records_runs_and_prototypes() {
let dir = project("builtin-mods");
fresh_store(&dir);
std::fs::write(dir.join("src/lib.rs"), "mod a;\nmod b;\n").unwrap();
std::fs::write(dir.join("src/a.rs"), "use crate::b::X;\npub fn f() {}\n").unwrap();
std::fs::write(dir.join("src/b.rs"), "pub struct X;\n").unwrap();
let add = ct_rules(&dir)
.args([
"--add",
"mods-acyclic",
"--question",
"Is the module graph acyclic?",
])
.args(["--", "mods", "--acyclic"])
.output()
.unwrap();
assert_eq!(code(&add), 0, "stderr: {:?}", stderr(&add));
let bad = ct_rules(&dir)
.args(["--add", "no-a-to-b", "--question", "Does a stay off b?"])
.args(["--", "mods", "--forbid", "a=>b"])
.output()
.unwrap();
assert_eq!(code(&bad), 1, "stderr: {:?}", stderr(&bad));
let check = ct_check(&dir).output().unwrap();
assert_eq!(code(&check), 0, "stderr: {:?}", stderr(&check));
assert!(
stdout(&check).contains("SUCCESS mods-acyclic"),
"{:?}",
stdout(&check)
);
let store = std::fs::read_to_string(dir.join(".ct/rules.jsonc")).unwrap();
assert!(store.contains("\"mods\""), "stored probe head: {store}");
assert!(!store.contains("ct-mods"), "no binary reference: {store}");
let proto = ct_rules(&dir)
.args(["--", "mods", "--acyclic"])
.output()
.unwrap();
assert_eq!(code(&proto), 0, "stderr: {:?}", stderr(&proto));
assert!(stdout(&proto).contains("not saved"), "{:?}", stdout(&proto));
let proto_bad = ct_rules(&dir)
.args(["--", "mods", "--forbid", "a=>b"])
.output()
.unwrap();
assert_eq!(code(&proto_bad), 1, "stderr: {:?}", stderr(&proto_bad));
let store2 = std::fs::read_to_string(dir.join(".ct/rules.jsonc")).unwrap();
assert_eq!(
store2.matches("\"id\"").count(),
1,
"prototypes must not write: {store2}"
);
let guard = ct_rules(&dir)
.args(["--add", "x", "--question", "q", "--expect-ok", "foo"])
.args(["--", "mods", "--acyclic"])
.output()
.unwrap();
assert_eq!(code(&guard), 2, "stderr: {:?}", stderr(&guard));
assert!(
stderr(&guard).contains("classifies its own outcome"),
"stderr: {:?}",
stderr(&guard)
);
}
#[test]
fn prototyping_a_non_builtin_probe_runs_without_saving() {
let dir = project("prototype-search");
fresh_store(&dir);
std::fs::write(dir.join("src/lib.rs"), "pub fn clean() {}\n").unwrap();
let hold = ct_rules(&dir)
.args([
"--",
"ct-search",
"--base",
"src",
"--grep",
"ZZZNOTHERE",
"--expect",
"none",
"--quiet",
])
.output()
.unwrap();
assert_eq!(code(&hold), 0, "stderr: {:?}", stderr(&hold));
assert!(stdout(&hold).contains("not saved"), "{:?}", stdout(&hold));
let viol = ct_rules(&dir)
.args([
"--",
"ct-search",
"--base",
"src",
"--grep",
"clean",
"--expect",
"none",
"--quiet",
])
.output()
.unwrap();
assert_eq!(code(&viol), 1, "stderr: {:?}", stderr(&viol));
let store = std::fs::read_to_string(dir.join(".ct/rules.jsonc")).unwrap();
assert_eq!(
store.matches("\"id\"").count(),
0,
"prototypes must not write: {store}"
);
}
#[test]
fn ct_each_walker_source_feeds_per_file_rules() {
let dir = project("rules-walker");
fresh_store(&dir);
std::fs::write(
dir.join("src/a.rs"),
"// SPDX-License-Identifier: Apache-2.0\n",
)
.unwrap();
std::fs::write(
dir.join("src/b.rs"),
"// SPDX-License-Identifier: Apache-2.0\n",
)
.unwrap();
let add = ct_rules(&dir)
.args([
"--add",
"license-headers",
"--question",
"Every file carries the header?",
])
.args([
"--", "ct-each", "--base", "src", "--name", "*.rs", "--quiet", "--",
])
.args([
"ct-search",
"--base",
"{ITEM}",
"--grep",
"SPDX-License-Identifier",
"--quiet",
])
.output()
.unwrap();
assert_eq!(code(&add), 0, "stderr: {:?}", stderr(&add));
std::fs::write(dir.join("src/b.rs"), "fn nope() {}\n").unwrap();
let check = ct_check(&dir).output().unwrap();
assert_eq!(code(&check), 1, "violation => exit 1");
assert!(stdout(&check).contains("ERROR license-headers"));
}
#[test]
fn ct_check_id_mode_pins_interpretation() {
let dir = project("check-id-mode");
fresh_store(&dir);
std::fs::write(dir.join("src/lib.rs"), "fn clean() {}\n").unwrap();
let add = ct_rules(&dir)
.args(["--add", "abc", "--question", "q"])
.args([
"--",
"ct-search",
"--base",
"src",
"--grep",
"ZZZ",
"--expect",
"none",
"--quiet",
])
.output()
.unwrap();
assert_eq!(code(&add), 0, "stderr: {:?}", stderr(&add));
let re = ct_check(&dir)
.args(["--id", "a.c", "--mode", "regex", "--list"])
.output()
.unwrap();
assert!(
stdout(&re).contains("abc"),
"regex --id should match: {:?}",
stdout(&re)
);
let lit = ct_check(&dir)
.args(["--id", "a.c", "--mode", "literal", "--list"])
.output()
.unwrap();
assert!(
!stdout(&lit).contains("abc"),
"literal --id must not match: {:?}",
stdout(&lit)
);
}
#[test]
fn ct_each_refuses_builtin_check_with_guidance() {
let dir = project("each-builtin");
let out = Command::new(env!("CARGO_BIN_EXE_ct-each"))
.current_dir(&dir)
.args(["--items", "openssl", "--", "deps", "--deny", "{ITEM}"])
.output()
.unwrap();
assert_eq!(code(&out), 2, "refused dispatch => exit 2");
let err = stderr(&out);
assert!(err.contains("built-in check"), "{err:?}");
assert!(
err.contains("deps --deny"),
"should point to repeatable flags: {err:?}"
);
}