crates/signald/tests/git_collector.rs
135 lines · 4328 bytes
1//! Git collector acceptance test.
2//!
3//! Builds a real temp git repo, drives the collector against it, and asserts the
4//! aggregate scalars — commits-in-window, commits-today, branch count, and
5//! days-since-last-commit — match the repo's true state. Also covers the stale
6//! repo (an old commit) that the garden renders as wilt.
7
8use std::path::{Path, PathBuf};
9use std::process::Command;
10use std::sync::atomic::{AtomicU64, Ordering};
11
12use signal_schema::{Signal, SignalName};
13use signald::collectors::git;
14
15#[test]
16fn fresh_repo_reports_today_commits_and_no_staleness() {
17 let repo = TempRepo::init();
18 repo.commit_now("first");
19 repo.commit_now("second");
20 repo.commit_now("third");
21
22 let signals = git::collect(repo.path());
23
24 assert_eq!(value(&signals, SignalName::CommitsToday), 3.0);
25 assert_eq!(value(&signals, SignalName::CommitsWindow), 3.0);
26 assert_eq!(value(&signals, SignalName::BranchCount), 1.0);
27 assert_eq!(value(&signals, SignalName::DaysSinceLastCommit), 0.0);
28}
29
30#[test]
31fn stale_repo_reports_zero_recent_commits_and_high_staleness() {
32 let repo = TempRepo::init();
33 repo.commit_days_ago("ancient", 40);
34
35 let signals = git::collect(repo.path());
36
37 assert_eq!(value(&signals, SignalName::CommitsToday), 0.0);
38 assert_eq!(value(&signals, SignalName::CommitsWindow), 0.0);
39 let stale = value(&signals, SignalName::DaysSinceLastCommit);
40 assert!(stale >= 39.0, "expected ~40 days stale, got {stale}");
41}
42
43#[test]
44fn non_git_directory_yields_no_signals() {
45 let dir = unique_dir("ambient-nongit");
46 std::fs::create_dir_all(&dir).unwrap();
47 let signals = git::collect(&dir);
48 assert!(signals.is_empty());
49 let _ = std::fs::remove_dir_all(&dir);
50}
51
52// --- helpers ---
53
54fn value(signals: &[Signal], name: SignalName) -> f64 {
55 signals
56 .iter()
57 .find(|s| s.name == name)
58 .unwrap_or_else(|| panic!("signal {name:?} missing"))
59 .value
60 .0
61}
62
63struct TempRepo {
64 dir: PathBuf,
65}
66
67impl TempRepo {
68 fn init() -> TempRepo {
69 let dir = unique_dir("ambient-gitcollector");
70 std::fs::create_dir_all(&dir).unwrap();
71 run(&dir, &["init", "-q"], &[]);
72 // Local identity so commits work without global git config.
73 run(&dir, &["config", "user.email", "test@example.com"], &[]);
74 run(&dir, &["config", "user.name", "Test"], &[]);
75 TempRepo { dir }
76 }
77
78 fn path(&self) -> &Path {
79 &self.dir
80 }
81
82 fn commit_now(&self, msg: &str) {
83 run(&self.dir, &["commit", "--allow-empty", "-q", "-m", msg], &[]);
84 }
85
86 fn commit_days_ago(&self, msg: &str, days: u64) {
87 let secs = std::time::SystemTime::now()
88 .duration_since(std::time::UNIX_EPOCH)
89 .unwrap()
90 .as_secs()
91 - days * 86_400;
92 let date = format!("{secs} +0000");
93 run(
94 &self.dir,
95 &["commit", "--allow-empty", "-q", "-m", msg],
96 &[("GIT_AUTHOR_DATE", &date), ("GIT_COMMITTER_DATE", &date)],
97 );
98 }
99}
100
101impl Drop for TempRepo {
102 fn drop(&mut self) {
103 let _ = std::fs::remove_dir_all(&self.dir);
104 }
105}
106
107fn run(repo: &Path, args: &[&str], envs: &[(&str, &str)]) {
108 let mut cmd = Command::new("git");
109 cmd.arg("-C").arg(repo).args(args);
110 for (k, v) in envs {
111 cmd.env(k, v);
112 }
113 let out = cmd.output().expect("run git");
114 assert!(
115 out.status.success(),
116 "git {args:?} failed: {}",
117 String::from_utf8_lossy(&out.stderr)
118 );
119}
120
121fn unique_dir(prefix: &str) -> PathBuf {
122 // pid and a timestamp are not enough on their own: tests run in parallel
123 // threads of one process, so the pid is shared, and the clock is coarser
124 // than a nanosecond, so two tests starting together can read the same
125 // value. Two `git init`s into one directory then fail with "File exists".
126 // The counter makes the name unique within the process; pid keeps it
127 // unique across concurrent cargo runs.
128 static SEQ: AtomicU64 = AtomicU64::new(0);
129 let nanos = std::time::SystemTime::now()
130 .duration_since(std::time::UNIX_EPOCH)
131 .unwrap()
132 .as_nanos();
133 let seq = SEQ.fetch_add(1, Ordering::Relaxed);
134 std::env::temp_dir().join(format!("{prefix}-{}-{nanos}-{seq}", std::process::id()))
135}