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()
}
fn ct_outline() -> Command {
Command::new(env!("CARGO_BIN_EXE_ct-outline"))
}
const RUST_SAMPLE: &str = "pub struct Point {\n x: i32,\n}\n\nimpl Point {\n pub fn norm(&self) -> i32 {\n self.x\n }\n}\n";
#[test]
fn matched_only_counting_with_context_ancestors() {
let dir = scratch("outline-context");
std::fs::write(dir.join("point.rs"), RUST_SAMPLE).unwrap();
let out = ct_outline()
.args(["--base", dir.to_str().unwrap()])
.args(["--match", "norm", "--emit", "count={COUNT} {RESULT}"])
.output()
.unwrap();
assert_eq!(code(&out), 0, "stderr: {:?}", stderr(&out));
let text = stdout(&out);
assert!(text.contains("impl Point (context)"), "got {text:?}");
assert!(text.contains("fn norm"), "got {text:?}");
assert!(text.contains("count=1 SUCCESS"), "ancestors must not count: {text:?}");
let flat = ct_outline()
.args(["--base", dir.to_str().unwrap(), "--match", "norm", "--flat"])
.output()
.unwrap();
let rows: Vec<String> = stdout(&flat).lines().map(String::from).collect();
assert_eq!(rows.len(), 1, "flat carries matched entries only: {rows:?}");
assert!(rows[0].ends_with(":6:8:fn:norm"), "got {rows:?}");
let json = ct_outline()
.args(["--base", dir.to_str().unwrap(), "--match", "norm", "--json"])
.output()
.unwrap();
let v: serde_json::Value = serde_json::from_str(stdout(&json).trim()).unwrap();
assert_eq!(v["count"], 1);
assert_eq!(v["files"][0]["entries"][0]["name"], "norm");
}
#[test]
fn anchored_match_keeps_expect_counts_predictable() {
let dir = scratch("outline-anchored");
std::fs::write(
dir.join("v.rs"),
"enum Verdict { A }\ntrait VerdictExt {}\n",
)
.unwrap();
let exact = ct_outline()
.args(["--base", dir.to_str().unwrap(), "--match", "Verdict"])
.args(["--expect", "=1", "--quiet"])
.output()
.unwrap();
assert_eq!(code(&exact), 0, "stderr: {:?}", stderr(&exact));
let prefix = ct_outline()
.args(["--base", dir.to_str().unwrap(), "--match", "Verdict*"])
.args(["--expect", "=2", "--quiet"])
.output()
.unwrap();
assert_eq!(code(&prefix), 0, "stderr: {:?}", stderr(&prefix));
}
#[test]
fn underivable_end_renders_as_question_mark_never_a_guess() {
let dir = scratch("outline-unknown-end");
std::fs::write(dir.join("broken.rs"), "fn broken() {\n let x = 1;\n").unwrap();
let out = ct_outline()
.args(["--base", dir.to_str().unwrap(), "--flat"])
.output()
.unwrap();
assert!(stdout(&out).contains(":1:?:fn:broken"), "got {:?}", stdout(&out));
let json = ct_outline()
.args(["--base", dir.to_str().unwrap(), "--json"])
.output()
.unwrap();
let v: serde_json::Value = serde_json::from_str(stdout(&json).trim()).unwrap();
assert!(v["files"][0]["entries"][0]["end"].is_null());
}
#[test]
fn unrecognised_language_errors_directly_but_skips_in_walks() {
let dir = scratch("outline-langs");
std::fs::write(dir.join("known.py"), "def f():\n pass\n").unwrap();
std::fs::write(dir.join("unknown.zig"), "fn f() void {}\n").unwrap();
let direct = ct_outline()
.args(["--base", dir.join("unknown.zig").to_str().unwrap()])
.output()
.unwrap();
assert_eq!(code(&direct), 2);
assert!(stderr(&direct).contains("no outline rules"), "got {:?}", stderr(&direct));
let walked = ct_outline()
.args(["--base", dir.to_str().unwrap(), "--flat"])
.output()
.unwrap();
assert_eq!(code(&walked), 0);
assert!(stdout(&walked).contains(":1:2:def:f"), "got {:?}", stdout(&walked));
assert!(!stdout(&walked).contains("zig"), "unrecognised file skipped");
}
#[test]
fn composes_through_ct_test_and_ct_each() {
let dir = scratch("outline-compose");
std::fs::write(dir.join("point.rs"), RUST_SAMPLE).unwrap();
let base = dir.to_str().unwrap();
let wrapped = Command::new(env!("CARGO_BIN_EXE_ct-test"))
.args(["--quiet", "--emit", "{RESULT}"])
.args(["--cmd", "ct-outline", "--"])
.args(["--base", base, "--match", "Point", "--kind", "struct", "--expect", "=1", "--quiet"])
.output()
.unwrap();
assert_eq!(code(&wrapped), 0, "stderr: {:?}", stderr(&wrapped));
assert!(stdout(&wrapped).contains("SUCCESS"));
let swept = Command::new(env!("CARGO_BIN_EXE_ct-each"))
.args(["--items", "Point", "norm", "--quiet", "--emit", "{OK}/{TOTAL}"])
.args(["--", "ct-outline", "--base", base, "--match", "{ITEM}", "--quiet"])
.output()
.unwrap();
assert_eq!(code(&swept), 0, "stderr: {:?}", stderr(&swept));
assert!(stdout(&swept).contains("2/2"), "got {:?}", stdout(&swept));
}