use std::cell::RefCell;
use super::{BaseFreshness, BaseRefs, RefreshMode, check};
struct FakeRefs {
local: RefCell<Vec<Result<Option<String>, String>>>,
remote: Result<String, String>,
fetch: Result<(), String>,
fetched: RefCell<bool>,
}
impl FakeRefs {
fn new(first: &str, then: &str, remote: &str) -> Self {
Self {
local: RefCell::new(vec![
Ok(Some(first.to_string())),
Ok(Some(then.to_string())),
]),
remote: Ok(remote.to_string()),
fetch: Ok(()),
fetched: RefCell::new(false),
}
}
}
impl BaseRefs for FakeRefs {
fn local(&self, _base: &str) -> Result<Option<String>, String> {
let mut queue = self.local.borrow_mut();
if queue.is_empty() {
return Ok(None);
}
queue.remove(0)
}
fn remote(&self, _base: &str) -> Result<String, String> {
self.remote.clone()
}
fn fetch(&self, _base: &str) -> Result<(), String> {
*self.fetched.borrow_mut() = true;
self.fetch.clone()
}
}
#[test]
fn fresh_when_the_local_ref_already_matches_the_remote() {
let refs = FakeRefs::new("aaa111", "aaa111", "aaa111");
let verdict = check(&refs, "main", RefreshMode::FetchOnDrift);
assert_eq!(
verdict,
BaseFreshness::Fresh {
sha: "aaa111".to_string()
}
);
assert!(verdict.refusal().is_none());
assert!(
!*refs.fetched.borrow(),
"an already-current base must cost no fetch"
);
}
#[test]
fn a_stale_ref_is_refreshed_by_the_fetch() {
let refs = FakeRefs::new("old111", "new222", "new222");
let verdict = check(&refs, "main", RefreshMode::FetchOnDrift);
assert_eq!(
verdict,
BaseFreshness::Refreshed {
was: "old111".to_string(),
now: "new222".to_string()
}
);
assert!(
verdict.refusal().is_none(),
"a base this call brought current is diffable"
);
assert!(*refs.fetched.borrow(), "the gap must be closed by a fetch");
}
#[test]
fn a_ref_that_stays_behind_refuses_and_names_both_shas() {
let refs = FakeRefs::new("old111", "old111", "new222");
let refusal = check(&refs, "main", RefreshMode::FetchOnDrift)
.refusal()
.expect("a base that is still behind must refuse");
assert!(refusal.contains("old111"), "{refusal}");
assert!(refusal.contains("new222"), "{refusal}");
}
#[test]
fn an_unanswerable_comparison_refuses_rather_than_passing() {
let refs = FakeRefs {
local: RefCell::new(vec![Ok(Some("old111".to_string()))]),
remote: Err("`git ls-remote origin refs/heads/main` failed: no such remote".to_string()),
fetch: Ok(()),
fetched: RefCell::new(false),
};
let verdict = check(&refs, "main", RefreshMode::FetchOnDrift);
assert!(
matches!(verdict, BaseFreshness::Undetermined { .. }),
"{verdict:?}"
);
let refusal = verdict.refusal().expect("an unverified base must refuse");
assert!(refusal.contains("no such remote"), "{refusal}");
assert!(
!*refs.fetched.borrow(),
"nothing to fetch toward when the remote never answered"
);
}
#[test]
fn an_empty_remote_answer_is_undetermined() {
let refs = FakeRefs {
local: RefCell::new(vec![Ok(None)]),
remote: Ok(String::new()),
fetch: Ok(()),
fetched: RefCell::new(false),
};
assert!(
check(&refs, "main", RefreshMode::FetchOnDrift)
.refusal()
.is_some()
);
}
#[test]
fn a_failed_fetch_reports_stale_rather_than_undetermined() {
let refs = FakeRefs {
local: RefCell::new(vec![Ok(Some("old111".to_string()))]),
remote: Ok("new222".to_string()),
fetch: Err("could not read from remote repository".to_string()),
fetched: RefCell::new(false),
};
let verdict = check(&refs, "main", RefreshMode::FetchOnDrift);
let refusal = verdict.refusal().expect("a failed refresh must refuse");
assert!(refusal.contains("old111"), "{refusal}");
assert!(refusal.contains("could not read"), "{refusal}");
}
#[test]
fn an_unreadable_local_ref_is_undetermined() {
let refs = FakeRefs {
local: RefCell::new(vec![Err(
"`git rev-parse` failed: not a repository".to_string()
)]),
remote: Ok("new222".to_string()),
fetch: Ok(()),
fetched: RefCell::new(false),
};
let verdict = check(&refs, "main", RefreshMode::FetchOnDrift);
assert!(
matches!(verdict, BaseFreshness::Undetermined { .. }),
"{verdict:?}"
);
let refusal = verdict.refusal().expect("an unreadable base must refuse");
assert!(refusal.contains("not a repository"), "{refusal}");
}
#[test]
fn an_unreadable_local_ref_after_the_fetch_is_undetermined() {
let refs = FakeRefs {
local: RefCell::new(vec![
Ok(Some("old111".to_string())),
Err("`git rev-parse` failed: broken object store".to_string()),
]),
remote: Ok("new222".to_string()),
fetch: Ok(()),
fetched: RefCell::new(false),
};
let verdict = check(&refs, "main", RefreshMode::FetchOnDrift);
assert!(
matches!(verdict, BaseFreshness::Undetermined { .. }),
"{verdict:?}"
);
assert!(
verdict
.refusal()
.is_some_and(|r| r.contains("broken object store")),
"the cause must survive into the refusal: {verdict:?}"
);
}
#[test]
fn a_non_repository_reports_undetermined_not_absent() {
let tmp = tempfile::tempdir().expect("tempdir");
let refs = super::RealBaseRefs::at(tmp.path());
let answer = refs.local("main");
assert!(
answer.is_err(),
"a directory that is not a repository is unreadable, not ref-less: {answer:?}"
);
}
#[test]
fn compare_only_reports_stale_without_fetching() {
let refs = FakeRefs::new("old111", "new222", "new222");
let verdict = check(&refs, "main", RefreshMode::CompareOnly);
assert_eq!(
verdict,
BaseFreshness::Stale {
local: "old111".to_string(),
remote: "new222".to_string()
}
);
assert!(
!*refs.fetched.borrow(),
"a compare-only run must issue no fetch"
);
}
#[test]
fn an_absent_local_ref_is_named_in_the_refusal() {
let refs = FakeRefs {
local: RefCell::new(vec![Ok(None), Ok(None)]),
remote: Ok("new222".to_string()),
fetch: Ok(()),
fetched: RefCell::new(false),
};
let refusal = check(&refs, "main", RefreshMode::FetchOnDrift)
.refusal()
.expect("no local base ref is not a diffable base");
assert!(refusal.contains("absent"), "{refusal}");
}