use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use super::*;
const HEAD_SHA: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const OTHER_SHA: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
fn worktree() -> &'static Path {
Path::new("/nonexistent/worktree-7267")
}
fn root() -> &'static Path {
Path::new("/nonexistent/root-7267")
}
pub(crate) struct FakeProbe {
heads: BTreeMap<String, BranchPrState>,
head_commit: Result<String, String>,
search: Result<Vec<MergedPrHead>, String>,
ancestors: BTreeSet<(String, String)>,
ancestry_error: Option<String>,
calls: RefCell<Vec<String>>,
}
impl Default for FakeProbe {
fn default() -> Self {
Self {
heads: BTreeMap::new(),
head_commit: Ok(HEAD_SHA.to_string()),
search: Ok(Vec::new()),
ancestors: BTreeSet::new(),
ancestry_error: None,
calls: RefCell::new(Vec::new()),
}
}
}
impl FakeProbe {
pub(crate) fn with_head(mut self, branch: &str, state: BranchPrState) -> Self {
self.heads.insert(branch.to_string(), state);
self
}
pub(crate) fn with_search(mut self, rows: Vec<MergedPrHead>) -> Self {
self.search = Ok(rows);
self
}
pub(crate) fn with_search_error(mut self, reason: &str) -> Self {
self.search = Err(reason.to_string());
self
}
pub(crate) fn with_head_commit(mut self, answer: Result<String, String>) -> Self {
self.head_commit = answer;
self
}
pub(crate) fn with_ancestry_error(mut self, reason: &str) -> Self {
self.ancestry_error = Some(reason.to_string());
self
}
pub(crate) fn with_ancestor(mut self, ancestor: &str, descendant: &str) -> Self {
self.ancestors
.insert((ancestor.to_string(), descendant.to_string()));
self
}
pub(crate) fn calls(&self) -> Vec<String> {
self.calls.borrow().clone()
}
}
impl LandingProbe for FakeProbe {
fn state_for_head(&self, _registry_root: &Path, branch: &str) -> BranchPrState {
self.calls.borrow_mut().push(format!("head:{branch}"));
self.heads
.get(branch)
.cloned()
.unwrap_or(BranchPrState::NoPr)
}
fn head_commit(&self, _worktree: &Path) -> Result<String, String> {
self.calls.borrow_mut().push("head_commit".to_string());
self.head_commit.clone()
}
fn merged_prs_containing(
&self,
_registry_root: &Path,
sha: &str,
) -> Result<Vec<MergedPrHead>, String> {
self.calls.borrow_mut().push(format!("search:{sha}"));
self.search.clone()
}
fn is_ancestor(
&self,
_worktree: &Path,
ancestor: &str,
descendant: &str,
) -> Result<bool, String> {
self.calls
.borrow_mut()
.push(format!("ancestor:{ancestor}:{descendant}"));
if let Some(reason) = &self.ancestry_error {
return Err(reason.clone());
}
Ok(self
.ancestors
.contains(&(ancestor.to_string(), descendant.to_string())))
}
}
fn row(number: u64, head_ref_oid: &str) -> MergedPrHead {
MergedPrHead {
number,
head_ref_oid: head_ref_oid.to_string(),
is_cross_repository: false,
}
}
#[test]
fn a_round_sibling_with_a_merged_pr_is_reclaimable() {
let probe = FakeProbe::default().with_head("fix/7267-thing", BranchPrState::Merged { pr: 91 });
let out = resolve_landing(
worktree(),
root(),
Some("fix/7267-thing-r2"),
BranchPrState::NoPr,
&probe,
);
assert_eq!(out, BranchPrState::Merged { pr: 91 });
assert_eq!(probe.calls(), vec!["head:fix/7267-thing".to_string()]);
}
#[test]
fn a_round_sibling_whose_stem_pr_is_open_is_not_widened() {
let probe = FakeProbe::default().with_head("fix/7267-thing", BranchPrState::Open { pr: 92 });
let out = resolve_landing(
worktree(),
root(),
Some("fix/7267-thing-r2"),
BranchPrState::NoPr,
&probe,
);
assert_eq!(out, BranchPrState::NoPr, "an open sibling grants nothing");
}
#[test]
fn a_branch_without_a_round_suffix_asks_for_no_sibling() {
let probe = FakeProbe::default();
let _ = resolve_landing(
worktree(),
root(),
Some("fix/7267-thing"),
BranchPrState::NoPr,
&probe,
);
assert!(
!probe.calls().iter().any(|c| c.starts_with("head:")),
"no stem to ask about: {:?}",
probe.calls()
);
}
#[test]
fn a_renamed_branch_matches_the_pr_opened_from_its_head_commit() {
let probe = FakeProbe::default().with_search(vec![row(93, HEAD_SHA)]);
let out = resolve_landing(
worktree(),
root(),
Some("fix/7267-recut"),
BranchPrState::NoPr,
&probe,
);
assert_eq!(out, BranchPrState::Merged { pr: 93 });
}
#[test]
fn a_pr_opened_from_a_descendant_of_this_head_also_vouches() {
let probe = FakeProbe::default()
.with_search(vec![row(94, OTHER_SHA)])
.with_ancestor(HEAD_SHA, OTHER_SHA);
let out = resolve_landing(
worktree(),
root(),
Some("fix/7267-recut"),
BranchPrState::NoPr,
&probe,
);
assert_eq!(out, BranchPrState::Merged { pr: 94 });
}
#[test]
fn a_detached_worktree_can_still_match_by_head_commit() {
let probe = FakeProbe::default().with_search(vec![row(95, HEAD_SHA)]);
let out = resolve_landing(worktree(), root(), None, BranchPrState::Unknown, &probe);
assert_eq!(out, BranchPrState::Merged { pr: 95 });
}
#[test]
fn an_unrelated_merged_pr_containing_no_ancestor_is_not_a_match() {
let probe = FakeProbe::default().with_search(vec![row(96, OTHER_SHA)]);
let out = resolve_landing(
worktree(),
root(),
Some("fix/7267-recut"),
BranchPrState::NoPr,
&probe,
);
assert_eq!(out, BranchPrState::NoPr);
}
#[test]
fn a_fork_pull_request_containing_the_commit_is_ignored() {
let mut fork = row(97, HEAD_SHA);
fork.is_cross_repository = true;
let probe = FakeProbe::default().with_search(vec![fork]);
let out = resolve_landing(
worktree(),
root(),
Some("fix/7267-recut"),
BranchPrState::NoPr,
&probe,
);
assert_eq!(out, BranchPrState::NoPr);
}
#[test]
fn no_merged_pr_under_any_stem_is_still_refused() {
let probe = FakeProbe::default();
for (branch, exact) in [
(Some("fix/7267-thing-r2"), BranchPrState::NoPr),
(Some("fix/7267-thing"), BranchPrState::NoPr),
(None, BranchPrState::Unknown),
] {
let out = resolve_landing(worktree(), root(), branch, exact.clone(), &probe);
assert_eq!(out, exact, "branch {branch:?} must stay refused");
}
}
#[test]
fn an_open_pr_on_the_branch_is_never_widened() {
let probe = FakeProbe::default()
.with_head("fix/7267-thing", BranchPrState::Merged { pr: 98 })
.with_search(vec![row(99, HEAD_SHA)]);
for exact in [
BranchPrState::Open { pr: 1 },
BranchPrState::ClosedUnmerged { pr: 2 },
BranchPrState::LookupFailed {
reason: "`gh` exited 4".to_string(),
},
] {
let out = resolve_landing(
worktree(),
root(),
Some("fix/7267-thing-r2"),
exact.clone(),
&probe,
);
assert_eq!(out, exact, "a settled answer is the answer");
}
assert!(probe.calls().is_empty(), "and costs no widening call");
}
#[test]
fn a_failed_head_commit_read_leaves_the_refusal_standing() {
let probe = FakeProbe::default()
.with_search(vec![row(100, HEAD_SHA)])
.with_head_commit(Err("`git rev-parse HEAD` failed (128)".to_string()));
let out = resolve_landing(
worktree(),
root(),
Some("fix/7267-recut"),
BranchPrState::NoPr,
&probe,
);
assert_eq!(out, BranchPrState::NoPr);
assert!(
!probe.calls().iter().any(|c| c.starts_with("search:")),
"and never searches on a HEAD it could not read: {:?}",
probe.calls()
);
}
#[test]
fn a_failed_commit_search_leaves_the_refusal_standing() {
let probe = FakeProbe::default().with_search_error("`gh` exited 4: gh auth login");
let out = resolve_landing(
worktree(),
root(),
Some("fix/7267-recut"),
BranchPrState::NoPr,
&probe,
);
assert_eq!(out, BranchPrState::NoPr);
}
#[test]
fn a_failed_ancestry_probe_leaves_the_refusal_standing() {
let probe = FakeProbe::default()
.with_search(vec![row(101, OTHER_SHA)])
.with_ancestry_error("bad object bbbbbbb");
let out = resolve_landing(
worktree(),
root(),
Some("fix/7267-recut"),
BranchPrState::NoPr,
&probe,
);
assert_eq!(out, BranchPrState::NoPr);
}
#[test]
fn an_empty_head_commit_reaches_no_search() {
let probe = FakeProbe::default().with_head_commit(Ok(" ".to_string()));
let out = resolve_landing(
worktree(),
root(),
Some("fix/7267-recut"),
BranchPrState::NoPr,
&probe,
);
assert_eq!(out, BranchPrState::NoPr);
assert_eq!(probe.calls(), vec!["head_commit".to_string()]);
}
fn index(branch: &str, pr: u64, state: &str) -> PrIndex {
PrIndex::from_json(
&format!(r#"[{{"number": {pr}, "headRefName": "{branch}", "state": "{state}"}}]"#),
400,
)
}
#[test]
fn resolve_with_index_without_fallback_never_calls_the_probe() {
let probe = FakeProbe::default().with_head("fix/7267-thing", BranchPrState::Merged { pr: 102 });
let out = resolve_with_index(
worktree(),
root(),
Some("fix/7267-unrelated"),
&index("fix/7267-thing", 102, "MERGED"),
false,
&probe,
);
assert_eq!(out, BranchPrState::NoPr);
assert!(probe.calls().is_empty(), "{:?}", probe.calls());
}
#[test]
fn resolve_with_index_retries_a_truncated_index_per_branch() {
let truncated = PrIndex::from_json(
r#"[{"number": 103, "headRefName": "someone/else", "state": "MERGED"}]"#,
1,
);
let probe = FakeProbe::default().with_head("fix/7267-mine", BranchPrState::Merged { pr: 104 });
let out = resolve_with_index(
worktree(),
root(),
Some("fix/7267-mine"),
&truncated,
true,
&probe,
);
assert_eq!(out, BranchPrState::Merged { pr: 104 });
assert_eq!(probe.calls(), vec!["head:fix/7267-mine".to_string()]);
}
#[test]
fn resolve_with_index_widens_a_complete_index_no_pr_answer() {
let probe = FakeProbe::default().with_search(vec![row(105, HEAD_SHA)]);
let out = resolve_with_index(
worktree(),
root(),
Some("fix/7267-recut"),
&index("someone/else", 103, "MERGED"),
true,
&probe,
);
assert_eq!(out, BranchPrState::Merged { pr: 105 });
}