use std::path::{Path, PathBuf};
use std::process::Command;
const BIN: &str = env!("CARGO_BIN_EXE_roteiro");
fn git(dir: &Path, args: &[&str]) {
let status = Command::new("git")
.args([
"-c",
"user.name=Test",
"-c",
"user.email=test@example.com",
"-c",
"commit.gpgsign=false",
"-c",
"init.defaultBranch=main",
])
.args(args)
.current_dir(dir)
.status()
.expect("run git");
assert!(status.success(), "git {args:?} failed");
}
fn roteiro(dir: &Path, args: &[&str]) -> std::process::Output {
Command::new(BIN)
.args(args)
.current_dir(dir)
.env("ROTEIRO_HOME", dir)
.output()
.expect("run roteiro")
}
fn stdout(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn write(dir: &Path, rel: &str, content: &str) {
let path = dir.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).expect("mkdir");
std::fs::write(path, content).expect("write");
}
fn fresh_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("roteiro-wtread-{tag}-{}", std::process::id()));
std::fs::remove_dir_all(&dir).ok();
std::fs::create_dir_all(&dir).expect("mkdir");
dir
}
fn repo_with_uncommitted_marker(tag: &str) -> PathBuf {
let dir = fresh_dir(tag);
git(&dir, &["init", "-q"]);
write(&dir, "src/lib.rs", "pub struct Thing;\n");
git(&dir, &["add", "."]);
git(&dir, &["commit", "-q", "-m", "init"]);
write(
&dir,
"src/lib.rs",
"pub struct Thing;\n// TODO: uncommitted work in progress\npub struct Pending;\n",
);
dir
}
#[test]
fn debt_reports_the_marker_the_developer_is_looking_at() {
let dir = repo_with_uncommitted_marker("debt");
let out = roteiro(&dir, &["debt", "--json"]);
assert!(out.status.success(), "debt failed: {out:?}");
let report: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("debt --json is valid JSON");
assert_eq!(
report["total"], 1,
"an uncommitted marker is debt the moment it is written, not the moment \
it is committed: {report}"
);
std::fs::remove_dir_all(&dir).ok();
}
fn sync_json(dir: &Path) -> serde_json::Value {
let out = roteiro(dir, &["sync", "--json"]);
assert!(out.status.success(), "sync failed: {out:?}");
serde_json::from_slice(&out.stdout).expect("sync --json is valid JSON")
}
#[test]
fn a_read_does_not_discard_the_graph_sync_assembled() {
let dir = repo_with_uncommitted_marker("survive");
let first = sync_json(&dir);
assert_eq!(
first["blobs_dirty"], 1,
"the fixture must actually be dirty: {first}"
);
for read in [
vec!["debt"],
vec!["debt-density"],
vec!["coupling"],
vec!["config-secrets"],
vec!["search", "Pending"],
] {
let out = roteiro(&dir, &read);
assert!(out.status.success(), "{read:?} failed: {out:?}");
let again = sync_json(&dir);
assert_eq!(
again["no_op"],
serde_json::Value::Bool(true),
"`roteiro {}` rewrote the store to a different tree, so the next sync \
had to rebuild it: {again}",
read.join(" "),
);
}
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn search_finds_a_symbol_that_is_not_committed_yet() {
let dir = repo_with_uncommitted_marker("search");
let out = roteiro(&dir, &["search", "Pending"]);
assert!(out.status.success(), "search failed: {out:?}");
assert!(
stdout(&out).contains("Pending"),
"a symbol in the working tree is findable before it is committed \
(stdout: {:?}, stderr: {:?})",
stdout(&out),
String::from_utf8_lossy(&out.stderr)
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn committed_stays_reachable_and_says_which_tree_it_answered_about() {
let dir = repo_with_uncommitted_marker("committed");
let out = roteiro(&dir, &["debt", "--committed", "--json"]);
assert!(out.status.success(), "debt --committed failed: {out:?}");
let report: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("debt --json is valid JSON");
assert_eq!(
report["total"], 0,
"--committed still means HEAD, uncommitted work excluded: {report}"
);
let note = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(
note.contains("--committed"),
"the tree a report describes must be stated, by the flag that selected \
it, when it is not the default: {note}"
);
assert!(
note.contains("not the working tree"),
"and must say what it is *not*, since that is the default it departs \
from: {note}"
);
assert!(
stdout(&out).starts_with('{'),
"stdout must remain pure JSON: {}",
stdout(&out)
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn every_report_surface_names_the_tree_it_answered_about() {
let dir = repo_with_uncommitted_marker("announce");
for cmd in [
vec!["debt", "--committed"],
vec!["debt-density", "--committed"],
vec!["coupling", "--committed"],
vec!["config-secrets", "--committed"],
vec!["search", "--committed", "Pending"],
vec!["path", "--committed", "file:src/lib.rs", "file:src/lib.rs"],
] {
let out = roteiro(&dir, &cmd);
let note = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(
note.contains("--committed"),
"`roteiro {}` answered about a different tree without saying so \
(stderr: {note:?})",
cmd.join(" "),
);
}
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_read_that_was_not_refreshed_does_not_claim_a_tree() {
let dir = repo_with_uncommitted_marker("ahead");
let out = roteiro(&dir, &["sync"]);
assert!(out.status.success(), "sync failed: {out:?}");
let db = dir.join(".git/roteiro/graph.db");
let conn = rusqlite::Connection::open(&db).expect("open store");
conn.execute(
"INSERT INTO schema_migrations (version) VALUES (?1)",
[999_999],
)
.expect("record a version from the future");
drop(conn);
let out = roteiro(&dir, &["search", "--committed", "Thing"]);
assert!(out.status.success(), "search failed: {out:?}");
let note = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(
note.contains("has not been refreshed"),
"the schema-ahead path must still explain itself: {note:?}"
);
assert!(
!note.contains("--committed"),
"but it must not also claim to be reporting on the committed tree, \
because no rebuild for that tree happened: {note:?}"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn staged_reads_the_index_rather_than_the_disk() {
let dir = repo_with_uncommitted_marker("staged");
let unstaged = roteiro(&dir, &["debt", "--staged", "--json"]);
assert!(
unstaged.status.success(),
"debt --staged failed: {unstaged:?}"
);
let report: serde_json::Value =
serde_json::from_slice(&unstaged.stdout).expect("debt --json is valid JSON");
assert_eq!(
report["total"], 0,
"an unstaged edit is not what a commit would record: {report}"
);
git(&dir, &["add", "src/lib.rs"]);
let staged = roteiro(&dir, &["debt", "--staged", "--json"]);
assert!(staged.status.success(), "debt --staged failed: {staged:?}");
let report: serde_json::Value =
serde_json::from_slice(&staged.stdout).expect("debt --json is valid JSON");
assert_eq!(
report["total"], 1,
"staging the marker makes it what a commit would record: {report}"
);
std::fs::remove_dir_all(&dir).ok();
}