use std::path::{Path, PathBuf};
use std::process::{Command, Output};
fn scratch(tag: &str) -> PathBuf {
let dir = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("target/test-tmp/it")
.join(tag);
std::fs::create_dir_all(&dir).unwrap();
dir
}
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()
}
#[test]
fn ct_search_expect_none_inverts_the_verdict() {
let dir = scratch("ct-search-expect");
std::fs::write(dir.join("hit.txt"), "a NEEDLE lives here\n").unwrap();
std::fs::write(dir.join("clean.txt"), "nothing to see\n").unwrap();
let run = |grep: &str, expect: &str| -> Output {
Command::new(env!("CARGO_BIN_EXE_ct-search"))
.args(["--base", dir.to_str().unwrap()])
.args(["--type", "f", "--grep", grep, "--expect", expect, "--quiet"])
.output()
.unwrap()
};
assert_eq!(code(&run("NEEDLE", "none")), 1, "found one => ERROR");
assert_eq!(
code(&run("ABSENT_TOKEN", "none")),
0,
"found none => SUCCESS"
);
let emit = Command::new(env!("CARGO_BIN_EXE_ct-search"))
.args(["--base", dir.to_str().unwrap()])
.args(["--type", "f", "--grep", "NEEDLE"])
.args(["--emit", "{RESULT} {COUNT}"])
.output()
.unwrap();
assert_eq!(code(&emit), 0);
assert!(
stdout(&emit).contains("SUCCESS 1"),
"got: {:?}",
stdout(&emit)
);
}
#[test]
fn ct_patch_yaml_set_and_delete_preserve_comments() {
let dir = scratch("ct-patch-yaml");
let file = dir.join("cfg.yaml");
let original =
"# top comment\nserver:\n host: localhost # inline\n port: 8080\n debug: true\n";
std::fs::write(&file, original).unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_ct-patch"))
.args([
"--base",
file.to_str().unwrap(),
"--set",
".server.port=9090",
"--delete",
".server.debug",
"--expect",
"=2",
])
.output()
.unwrap();
assert_eq!(code(&out), 0, "stderr: {:?}", stderr(&out));
let after = std::fs::read_to_string(&file).unwrap();
assert!(
after.contains("# top comment"),
"leading comment kept: {after:?}"
);
assert!(
after.contains("port: 9090"),
"number set unquoted: {after:?}"
);
assert!(after.contains("# inline"), "inline comment kept: {after:?}");
assert!(!after.contains("debug:"), "debug deleted: {after:?}");
let add = Command::new(env!("CARGO_BIN_EXE_ct-patch"))
.args(["--base", file.to_str().unwrap(), "--add", ".server.tags=x"])
.output()
.unwrap();
assert_eq!(code(&add), 2, "YAML --add should error");
assert!(stderr(&add).contains("not yet supported for YAML"));
}
#[test]
fn ct_patch_preserves_comments_and_is_expect_gated() {
let dir = scratch("ct-patch");
let file = dir.join("config.jsonc");
let original = "{\n // service config\n \"port\": 8080,\n \"debug\": true\n}\n";
std::fs::write(&file, original).unwrap();
let patch = |args: &[&str]| -> Output {
let mut a = vec!["--base", file.to_str().unwrap()];
a.extend_from_slice(args);
Command::new(env!("CARGO_BIN_EXE_ct-patch"))
.args(a)
.output()
.unwrap()
};
let denied = patch(&["--set", ".port=9090", "--expect", "=2"]);
assert_eq!(code(&denied), 1, "expect mismatch => ERROR");
assert_eq!(
std::fs::read_to_string(&file).unwrap(),
original,
"ERROR must not write"
);
let dry = patch(&["--set", ".port=9090", "--dry-run"]);
assert_eq!(code(&dry), 0);
assert_eq!(
std::fs::read_to_string(&file).unwrap(),
original,
"dry-run must not write"
);
let ok = patch(&["--set", ".port=9090"]);
assert_eq!(code(&ok), 0);
let after = std::fs::read_to_string(&file).unwrap();
assert_eq!(
after, "{\n // service config\n \"port\": 9090,\n \"debug\": true\n}\n",
"only the value should change; comment preserved"
);
}
#[test]
fn ct_test_wraps_read_only_suite_tools_by_sibling_resolution() {
let dir = scratch("ct-compose");
std::fs::write(dir.join("big.rs"), "x\n".repeat(50)).unwrap();
std::fs::write(dir.join("small.rs"), "x\n".repeat(3)).unwrap();
let wrap = |args: &[&str]| -> Output {
let mut a = vec!["--quiet", "--emit", "{RESULT}", "--cmd", "ct-tree", "--"];
a.extend_from_slice(args);
Command::new(env!("CARGO_BIN_EXE_ct-test"))
.args(a)
.output()
.unwrap()
};
let yes = wrap(&[
"--base",
dir.to_str().unwrap(),
"--ext",
"rs",
"--min-lines",
"40",
"--flat",
]);
assert_eq!(
code(&yes),
0,
"sibling ct-tree should run; got {:?}",
stderr(&yes)
);
assert!(stdout(&yes).contains("SUCCESS"));
let no = wrap(&[
"--base",
dir.to_str().unwrap(),
"--ext",
"rs",
"--min-lines",
"500",
"--flat",
]);
assert_eq!(code(&no), 1);
assert!(stdout(&no).contains("ERROR"));
}
#[test]
fn ct_test_allow_gate_is_fixed_and_immutable() {
let ct_test = || Command::new(env!("CARGO_BIN_EXE_ct-test"));
let denied = ct_test()
.args(["--quiet", "--cmd", "seq", "--", "1", "2"])
.output()
.unwrap();
assert_eq!(code(&denied), 2, "non-allowlisted command must be refused");
assert!(
stderr(&denied).contains("not on the allowlist"),
"deny message missing; got: {:?}",
stderr(&denied)
);
assert!(
stderr(&denied).contains("immutable"),
"deny message must state the list is fixed; got: {:?}",
stderr(&denied)
);
let no_allow = ct_test().args(["--allow", "seq"]).output().unwrap();
assert_eq!(code(&no_allow), 2, "--allow must no longer exist");
let builtin = ct_test()
.args(["--quiet", "--cmd", "true", "--emit", "{RESULT}"])
.output()
.unwrap();
assert_eq!(code(&builtin), 0, "built-in command should run");
assert!(stdout(&builtin).contains("SUCCESS"));
}
#[test]
fn ct_test_diagnoses_and_lets_caller_set_inconclusive_outcome() {
let run = |extra: &[&str]| -> Output {
let mut args = vec![
"--ok-match-stderr",
"hi",
"--cmd",
"echo",
"--emit",
"{RESULT}",
];
args.extend_from_slice(extra);
args.extend_from_slice(&["--", "hi"]);
Command::new(env!("CARGO_BIN_EXE_ct-test"))
.args(args)
.output()
.unwrap()
};
let default = run(&[]);
assert_eq!(
code(&default),
1,
"absent required ok-match => ERROR on exit 0"
);
assert!(stdout(&default).contains("ERROR"));
let why = stderr(&default);
assert!(
why.contains("not found in stderr"),
"reason names the stream: {why:?}"
);
assert!(why.contains("exit=0"), "reason includes exit code: {why:?}");
assert_eq!(
code(&run(&["--otherwise", "exit"])),
0,
"exit policy => SUCCESS on exit 0"
);
assert_eq!(code(&run(&["--otherwise", "success"])), 0, "success policy");
assert_eq!(code(&run(&["--otherwise", "error"])), 1, "error policy");
let err = Command::new(env!("CARGO_BIN_EXE_ct-test"))
.args([
"--err-match",
"BAD",
"--otherwise",
"success",
"--cmd",
"echo",
"--emit",
"{RESULT}",
"--",
"BAD news",
])
.output()
.unwrap();
assert_eq!(
code(&err),
1,
"err-match must not be overridden by --otherwise"
);
}