270 lines
8.2 KiB
Rust
270 lines
8.2 KiB
Rust
//! End-to-end proof of the never-worse guard (src/core/guard.rs).
|
|
|
|
use std::io::Write;
|
|
use std::process::{Command, Stdio};
|
|
|
|
fn rtk_stdin(args: &[&str], input: &str) -> String {
|
|
let mut child = Command::new(env!("CARGO_BIN_EXE_rtk"))
|
|
.args(args)
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::null())
|
|
.spawn()
|
|
.expect("spawn rtk");
|
|
child
|
|
.stdin
|
|
.take()
|
|
.expect("stdin")
|
|
.write_all(input.as_bytes())
|
|
.expect("write stdin");
|
|
let out = child.wait_with_output().expect("wait rtk");
|
|
String::from_utf8_lossy(&out.stdout).into_owned()
|
|
}
|
|
|
|
#[test]
|
|
fn guard_shows_raw_when_filter_would_bloat_tiny_input() {
|
|
let input = "{\"a\":1,\"b\":2,\"c\":3,\"d\":4}";
|
|
let out = rtk_stdin(&["json", "-"], input);
|
|
|
|
assert_eq!(
|
|
out.trim(),
|
|
input,
|
|
"guard should emit the raw minified JSON, not a larger pretty-printed form"
|
|
);
|
|
assert!(
|
|
out.trim().len() <= input.len(),
|
|
"never-worse violated: {} chars emitted for a {}-char raw input",
|
|
out.trim().len(),
|
|
input.len()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn guard_does_not_block_real_compression() {
|
|
let mut input = String::from("{");
|
|
for i in 0..60 {
|
|
input.push_str(&format!("\"key_{i}\":\"value_{i}\","));
|
|
}
|
|
input.push_str("\"last\":1}");
|
|
|
|
let out = rtk_stdin(&["json", "-"], &input);
|
|
assert!(
|
|
out.len() < input.len(),
|
|
"filter should compress large input (guard must not over-trigger): {} vs {}",
|
|
out.len(),
|
|
input.len()
|
|
);
|
|
}
|
|
|
|
fn rtk_output_in_dir(dir: &std::path::Path, args: &[&str]) -> (String, String, Option<i32>) {
|
|
let out = Command::new(env!("CARGO_BIN_EXE_rtk"))
|
|
.args(args)
|
|
.current_dir(dir)
|
|
.output()
|
|
.expect("spawn rtk");
|
|
(
|
|
String::from_utf8_lossy(&out.stdout).into_owned(),
|
|
String::from_utf8_lossy(&out.stderr).into_owned(),
|
|
out.status.code(),
|
|
)
|
|
}
|
|
|
|
fn rtk_in_dir(dir: &std::path::Path, args: &[&str]) -> (String, Option<i32>) {
|
|
let (stdout, _, code) = rtk_output_in_dir(dir, args);
|
|
(stdout, code)
|
|
}
|
|
|
|
fn rg_available() -> bool {
|
|
Command::new("rg")
|
|
.arg("--version")
|
|
.output()
|
|
.map(|o| o.status.success())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn init_git_repo() -> tempfile::TempDir {
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
for args in [
|
|
&["init", "-q", "-b", "main"][..],
|
|
&["config", "user.email", "t@t.t"][..],
|
|
&["config", "user.name", "t"][..],
|
|
&["commit", "-q", "--allow-empty", "-m", "init"][..],
|
|
] {
|
|
let ok = Command::new("git")
|
|
.args(args)
|
|
.current_dir(dir.path())
|
|
.output()
|
|
.map(|o| o.status.success())
|
|
.unwrap_or(false);
|
|
assert!(ok, "git setup failed: {args:?}");
|
|
}
|
|
dir
|
|
}
|
|
|
|
fn git_in_dir(dir: &std::path::Path, args: &[&str]) {
|
|
let out = Command::new("git")
|
|
.args(args)
|
|
.current_dir(dir)
|
|
.output()
|
|
.expect("spawn git");
|
|
assert!(
|
|
out.status.success(),
|
|
"git command failed: {args:?}\nstdout: {}\nstderr: {}",
|
|
String::from_utf8_lossy(&out.stdout),
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
}
|
|
|
|
fn read_text_normalized(path: &std::path::Path) -> String {
|
|
std::fs::read_to_string(path).unwrap().replace("\r\n", "\n")
|
|
}
|
|
|
|
#[test]
|
|
fn grep_no_match_emits_empty_not_a_message() {
|
|
if !rg_available() {
|
|
return;
|
|
}
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
std::fs::write(dir.path().join("a.txt"), "hello world\n").expect("write");
|
|
// Faithful grep needs -r to descend a directory; we no longer force recursion
|
|
// by routing through rg (the engine-faithful contract).
|
|
let (out, code) = rtk_in_dir(dir.path(), &["grep", "-r", "zzz_no_match_xyz", "."]);
|
|
assert!(
|
|
out.trim().is_empty(),
|
|
"no-match grep must emit empty, not a '0 matches' line: {out:?}"
|
|
);
|
|
assert_eq!(code, Some(1), "grep no-match must preserve exit 1");
|
|
}
|
|
|
|
#[test]
|
|
fn find_no_results_emits_empty() {
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
std::fs::write(dir.path().join("a.txt"), "x").expect("write");
|
|
let (out, _) = rtk_in_dir(dir.path(), &["find", ".", "-name", "zzz_no_match_xyz"]);
|
|
assert!(
|
|
out.trim().is_empty(),
|
|
"no-result find must emit empty, not a '0 for' line: {out:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn git_stash_list_no_stashes_emits_empty() {
|
|
let dir = init_git_repo();
|
|
let (out, code) = rtk_in_dir(dir.path(), &["git", "stash", "list"]);
|
|
assert!(
|
|
out.trim().is_empty(),
|
|
"no-stashes must emit empty, not 'No stashes': {out:?}"
|
|
);
|
|
assert_eq!(code, Some(0));
|
|
}
|
|
|
|
#[test]
|
|
fn git_stash_show_no_stash_emits_empty_and_propagates_failure() {
|
|
// Regression: previously printed "Empty stash" and returned Ok(0), masking
|
|
// the underlying `git stash show` failure.
|
|
let dir = init_git_repo();
|
|
let (out, code) = rtk_in_dir(dir.path(), &["git", "stash", "show"]);
|
|
assert!(
|
|
out.trim().is_empty(),
|
|
"must emit empty, not 'Empty stash': {out:?}"
|
|
);
|
|
assert_ne!(
|
|
code,
|
|
Some(0),
|
|
"a real git stash show failure must not be masked as exit 0"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn git_checkout_branch_switch_emits_compact_ok() {
|
|
let dir = init_git_repo();
|
|
git_in_dir(dir.path(), &["checkout", "-q", "-b", "feature/test"]);
|
|
|
|
let (out, code) = rtk_in_dir(dir.path(), &["git", "checkout", "main"]);
|
|
|
|
assert_eq!(code, Some(0));
|
|
assert_eq!(out.trim(), "ok main");
|
|
}
|
|
|
|
#[test]
|
|
fn git_checkout_new_branch_emits_compact_ok() {
|
|
let dir = init_git_repo();
|
|
|
|
let (out, code) = rtk_in_dir(dir.path(), &["git", "checkout", "-b", "feature/test"]);
|
|
|
|
assert_eq!(code, Some(0));
|
|
assert_eq!(out.trim(), "ok feature/test (new)");
|
|
}
|
|
|
|
#[test]
|
|
fn git_checkout_reset_branch_does_not_claim_new_branch() {
|
|
let dir = init_git_repo();
|
|
|
|
let (out, code) = rtk_in_dir(dir.path(), &["git", "checkout", "-B", "feature/test"]);
|
|
|
|
assert_eq!(code, Some(0));
|
|
assert_eq!(out.trim(), "ok feature/test");
|
|
}
|
|
|
|
#[test]
|
|
fn git_checkout_file_restore_emits_restored_count() {
|
|
let dir = init_git_repo();
|
|
std::fs::write(dir.path().join("a.txt"), "original\n").expect("write a");
|
|
std::fs::write(dir.path().join("b.txt"), "original\n").expect("write b");
|
|
git_in_dir(dir.path(), &["add", "a.txt", "b.txt"]);
|
|
git_in_dir(dir.path(), &["commit", "-q", "-m", "add files"]);
|
|
|
|
std::fs::write(dir.path().join("a.txt"), "changed\n").expect("write a");
|
|
std::fs::write(dir.path().join("b.txt"), "changed\n").expect("write b");
|
|
|
|
let (out, code) = rtk_in_dir(
|
|
dir.path(),
|
|
&["git", "checkout", "HEAD", "--", "a.txt", "b.txt"],
|
|
);
|
|
|
|
assert_eq!(code, Some(0));
|
|
assert!(
|
|
out.trim().is_empty() || out.trim() == "ok 2 files restored",
|
|
"guarded output may stay empty when native git emits no success text: {out:?}"
|
|
);
|
|
assert_eq!(
|
|
read_text_normalized(&dir.path().join("a.txt")),
|
|
"original\n"
|
|
);
|
|
assert_eq!(
|
|
read_text_normalized(&dir.path().join("b.txt")),
|
|
"original\n"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn git_checkout_dirty_tree_error_keeps_file_list() {
|
|
let dir = init_git_repo();
|
|
std::fs::write(dir.path().join("a.txt"), "main\n").expect("write a");
|
|
git_in_dir(dir.path(), &["add", "a.txt"]);
|
|
git_in_dir(dir.path(), &["commit", "-q", "-m", "add a"]);
|
|
git_in_dir(dir.path(), &["checkout", "-q", "-b", "feature/test"]);
|
|
std::fs::write(dir.path().join("a.txt"), "feature\n").expect("write feature");
|
|
git_in_dir(dir.path(), &["commit", "-am", "feature change"]);
|
|
git_in_dir(dir.path(), &["checkout", "-q", "main"]);
|
|
std::fs::write(dir.path().join("a.txt"), "dirty\n").expect("write dirty");
|
|
|
|
let (stdout, stderr, code) =
|
|
rtk_output_in_dir(dir.path(), &["git", "checkout", "feature/test"]);
|
|
let combined = format!("{stdout}{stderr}");
|
|
|
|
assert_ne!(code, Some(0));
|
|
assert!(
|
|
combined.contains("error:"),
|
|
"dirty checkout failure should keep error header: {combined:?}"
|
|
);
|
|
assert!(
|
|
combined.contains("a.txt"),
|
|
"dirty checkout failure should keep conflicting filename: {combined:?}"
|
|
);
|
|
assert!(
|
|
combined.contains("Aborting"),
|
|
"dirty checkout failure should keep abort line: {combined:?}"
|
|
);
|
|
}
|