use super::*;
use crate::vcs::options::{FileTypeScope, Options};
fn event(oid: &str, time: i64, touched: &[(&str, u64)], renames: &[(&str, &str)]) -> CommitEvent {
CommitEvent {
oid: oid.to_owned(),
time,
authors: vec![format!("digest-of-{oid}")],
bug_fix: false,
security_fix: false,
revert: false,
renames: renames
.iter()
.map(|(s, d)| (PathBuf::from(s), PathBuf::from(d)))
.collect(),
touched: touched
.iter()
.map(|(p, c)| (PathBuf::from(p), *c))
.collect(),
}
}
fn windowed_options(long: i64, recent: i64) -> Options {
Options {
long_window_secs: long,
recent_window_secs: recent,
..Options::default()
}
}
#[test]
fn replay_counts_in_window_commits_and_prunes_old_ones() {
let now = 1_000_000;
let options = windowed_options(100_000, 10_000);
let seed = HashMap::from([(PathBuf::from("a.rs"), 50)]);
let events = vec![
event("c2", now - 5_000, &[("a.rs", 4)], &[]), event("c1", now - 50_000, &[("a.rs", 6)], &[]), event("old", now - 200_000, &[("a.rs", 99)], &[]), ];
let out = replay(seed, &events, &options, now);
let stats = out.files.get(Path::new("a.rs")).expect("a.rs present");
assert_eq!(stats.commits_long, 2);
assert_eq!(stats.commits_recent, 1);
assert_eq!(stats.churn_long, 10);
assert_eq!(stats.churn_recent, 4);
}
#[test]
fn replay_seeds_inactive_files_with_zero_counts() {
let now = 1_000_000;
let options = windowed_options(100_000, 10_000);
let seed = HashMap::from([(PathBuf::from("idle.rs"), 12)]);
let out = replay(seed, &[], &options, now);
let stats = out
.files
.get(Path::new("idle.rs"))
.expect("idle.rs present");
assert_eq!(stats.commits_long, 0);
assert_eq!(stats.churn_long, 0);
}
#[test]
fn replay_stitches_renames_across_the_event_log() {
let now = 1_000_000;
let options = windowed_options(500_000, 10_000);
let seed = HashMap::from([(PathBuf::from("b.rs"), 50)]);
let events = vec![
event("rename", now - 1_000, &[("b.rs", 2)], &[("a.rs", "b.rs")]),
event("old", now - 100_000, &[("a.rs", 5)], &[]),
];
let out = replay(seed, &events, &options, now);
let stats = out.files.get(Path::new("b.rs")).expect("b.rs present");
assert_eq!(
stats.commits_long, 2,
"the older a.rs edit must attribute to the renamed b.rs"
);
assert_eq!(stats.churn_long, 7);
assert!(!out.files.contains_key(Path::new("a.rs")));
}
#[test]
fn replay_reclamps_window_to_the_current_now() {
let options = windowed_options(1_000_000, 10_000);
let seed = HashMap::from([(PathBuf::from("a.rs"), 50)]);
let commit_time = 500_000;
let events = vec![event("c", commit_time, &[("a.rs", 4)], &[])];
let fresh = replay(seed.clone(), &events, &options, commit_time + 5_000);
assert_eq!(
fresh.files.get(Path::new("a.rs")).unwrap().commits_recent,
1,
"within the recent window at the earlier now"
);
let later = replay(seed, &events, &options, commit_time + 500_000);
assert_eq!(
later.files.get(Path::new("a.rs")).unwrap().commits_recent,
0,
"the same commit has aged out of the recent window at the later now"
);
}
#[test]
fn replay_include_deleted_respects_file_type_scope() {
let now = 1_000_000;
let options = Options {
long_window_secs: 100_000,
recent_window_secs: 10_000,
include_deleted: true,
file_types: FileTypeScope::Metrics,
..Options::default()
};
let seed = HashMap::new();
let events = vec![event(
"c1",
now - 5_000,
&[("gone.rs", 7), ("notes.md", 9)],
&[],
)];
let out = replay(seed, &events, &options, now);
assert!(
out.files.contains_key(Path::new("gone.rs")),
"a deleted source file is re-admitted under the metrics scope"
);
assert!(
!out.files.contains_key(Path::new("notes.md")),
"a deleted non-source file stays out of a metrics-scoped ranking"
);
}
#[test]
fn replay_include_deleted_all_scope_admits_every_deleted_file() {
let now = 1_000_000;
let options = Options {
long_window_secs: 100_000,
recent_window_secs: 10_000,
include_deleted: true,
file_types: FileTypeScope::All,
..Options::default()
};
let events = vec![event("c1", now - 5_000, &[("notes.md", 9)], &[])];
let out = replay(HashMap::new(), &events, &options, now);
assert!(
out.files.contains_key(Path::new("notes.md")),
"the `all` scope keeps deleted non-source files"
);
}
#[test]
fn resolve_alias_terminates_on_a_rename_back_cycle() {
let mut alias: HashMap<PathBuf, PathBuf> = HashMap::new();
alias.insert(PathBuf::from("x"), PathBuf::from("a"));
alias.insert(PathBuf::from("a"), PathBuf::from("b"));
alias.insert(PathBuf::from("b"), PathBuf::from("a"));
assert_eq!(
resolve_alias(&alias, Path::new("x")).as_ref(),
Path::new("a")
);
assert_eq!(
resolve_alias(&alias, Path::new("a")).as_ref(),
Path::new("a")
);
assert_eq!(
resolve_alias(&alias, Path::new("b")).as_ref(),
Path::new("b")
);
}
#[test]
fn resolve_alias_terminates_on_a_multi_step_cycle() {
let mut alias: HashMap<PathBuf, PathBuf> = HashMap::new();
alias.insert(PathBuf::from("a"), PathBuf::from("b"));
alias.insert(PathBuf::from("b"), PathBuf::from("c"));
alias.insert(PathBuf::from("c"), PathBuf::from("a"));
assert_eq!(
resolve_alias(&alias, Path::new("a")).as_ref(),
Path::new("a")
);
assert_eq!(
resolve_alias(&alias, Path::new("b")).as_ref(),
Path::new("b")
);
assert_eq!(
resolve_alias(&alias, Path::new("c")).as_ref(),
Path::new("c")
);
}
#[test]
fn resolve_alias_follows_an_acyclic_chain_to_its_end() {
let mut alias: HashMap<PathBuf, PathBuf> = HashMap::new();
alias.insert(PathBuf::from("old"), PathBuf::from("mid"));
alias.insert(PathBuf::from("mid"), PathBuf::from("new"));
assert_eq!(
resolve_alias(&alias, Path::new("old")).as_ref(),
Path::new("new"),
"an acyclic rename chain resolves to its terminal path"
);
}
#[test]
fn replay_extreme_negative_now_saturates_window_boundaries() {
let now = i64::MIN;
let options = windowed_options(100_000, 10_000);
let seed = HashMap::from([(PathBuf::from("a.rs"), 50)]);
let events = vec![event("c1", now + 1, &[("a.rs", 7)], &[])];
let out = replay(seed, &events, &options, now);
let stats = out.files.get(Path::new("a.rs")).expect("a.rs present");
assert_eq!(stats.commits_long, 1);
assert_eq!(stats.churn_long, 7);
}