//! Git collector acceptance test. //! //! Builds a real temp git repo, drives the collector against it, and asserts the //! aggregate scalars — commits-in-window, commits-today, branch count, and //! days-since-last-commit — match the repo's true state. Also covers the stale //! repo (an old commit) that the garden renders as wilt. use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::atomic::{AtomicU64, Ordering}; use signal_schema::{Signal, SignalName}; use signald::collectors::git; #[test] fn fresh_repo_reports_today_commits_and_no_staleness() { let repo = TempRepo::init(); repo.commit_now("first"); repo.commit_now("second"); repo.commit_now("third"); let signals = git::collect(repo.path()); assert_eq!(value(&signals, SignalName::CommitsToday), 3.0); assert_eq!(value(&signals, SignalName::CommitsWindow), 3.0); assert_eq!(value(&signals, SignalName::BranchCount), 1.0); assert_eq!(value(&signals, SignalName::DaysSinceLastCommit), 0.0); } #[test] fn stale_repo_reports_zero_recent_commits_and_high_staleness() { let repo = TempRepo::init(); repo.commit_days_ago("ancient", 40); let signals = git::collect(repo.path()); assert_eq!(value(&signals, SignalName::CommitsToday), 0.0); assert_eq!(value(&signals, SignalName::CommitsWindow), 0.0); let stale = value(&signals, SignalName::DaysSinceLastCommit); assert!(stale >= 39.0, "expected ~40 days stale, got {stale}"); } #[test] fn non_git_directory_yields_no_signals() { let dir = unique_dir("ambient-nongit"); std::fs::create_dir_all(&dir).unwrap(); let signals = git::collect(&dir); assert!(signals.is_empty()); let _ = std::fs::remove_dir_all(&dir); } // --- helpers --- fn value(signals: &[Signal], name: SignalName) -> f64 { signals .iter() .find(|s| s.name == name) .unwrap_or_else(|| panic!("signal {name:?} missing")) .value .0 } struct TempRepo { dir: PathBuf, } impl TempRepo { fn init() -> TempRepo { let dir = unique_dir("ambient-gitcollector"); std::fs::create_dir_all(&dir).unwrap(); run(&dir, &["init", "-q"], &[]); // Local identity so commits work without global git config. run(&dir, &["config", "user.email", "test@example.com"], &[]); run(&dir, &["config", "user.name", "Test"], &[]); TempRepo { dir } } fn path(&self) -> &Path { &self.dir } fn commit_now(&self, msg: &str) { run(&self.dir, &["commit", "--allow-empty", "-q", "-m", msg], &[]); } fn commit_days_ago(&self, msg: &str, days: u64) { let secs = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() - days * 86_400; let date = format!("{secs} +0000"); run( &self.dir, &["commit", "--allow-empty", "-q", "-m", msg], &[("GIT_AUTHOR_DATE", &date), ("GIT_COMMITTER_DATE", &date)], ); } } impl Drop for TempRepo { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.dir); } } fn run(repo: &Path, args: &[&str], envs: &[(&str, &str)]) { let mut cmd = Command::new("git"); cmd.arg("-C").arg(repo).args(args); for (k, v) in envs { cmd.env(k, v); } let out = cmd.output().expect("run git"); assert!( out.status.success(), "git {args:?} failed: {}", String::from_utf8_lossy(&out.stderr) ); } fn unique_dir(prefix: &str) -> PathBuf { // pid and a timestamp are not enough on their own: tests run in parallel // threads of one process, so the pid is shared, and the clock is coarser // than a nanosecond, so two tests starting together can read the same // value. Two `git init`s into one directory then fail with "File exists". // The counter makes the name unique within the process; pid keeps it // unique across concurrent cargo runs. static SEQ: AtomicU64 = AtomicU64::new(0); let nanos = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos(); let seq = SEQ.fetch_add(1, Ordering::Relaxed); std::env::temp_dir().join(format!("{prefix}-{}-{nanos}-{seq}", std::process::id())) }