use super::*;
use crate::negotiation::Protocol;
use crate::task::{NewTaskOpts, Task};
use std::os::unix::process::ExitStatusExt;
use std::process::ExitStatus;
use tempfile::tempdir;
fn out(success: bool, stderr: &str) -> Output {
Output {
status: ExitStatus::from_raw(if success { 0 } else { 1 << 8 }),
stdout: Vec::new(),
stderr: stderr.as_bytes().to_vec(),
}
}
#[test]
fn classify_success() {
assert_eq!(classify_push_output(&out(true, "")), AttemptClass::Ok);
}
#[test]
fn classify_non_ff_rejected() {
let s = "! [rejected] balls/tasks -> balls/tasks (non-fast-forward)";
assert_eq!(classify_push_output(&out(false, s)), AttemptClass::Conflict);
}
#[test]
fn classify_fetch_first_rejected() {
let s = "! [rejected] (fetch first)";
assert_eq!(classify_push_output(&out(false, s)), AttemptClass::Conflict);
}
#[test]
fn classify_bracket_rejected_alone() {
let s = "warning: failed; rejected with [rejected] tag";
assert_eq!(classify_push_output(&out(false, s)), AttemptClass::Conflict);
}
#[test]
fn classify_unreachable_dns() {
let s = "fatal: Could not resolve hostname github.com";
assert!(matches!(
classify_push_output(&out(false, s)),
AttemptClass::Unreachable(_)
));
}
#[test]
fn classify_unreachable_repo_not_found() {
let s = "ERROR: Repository not found.";
assert!(matches!(
classify_push_output(&out(false, s)),
AttemptClass::Unreachable(_)
));
}
#[test]
fn classify_other_falls_through() {
let s = "fatal: weird unexpected error from git";
assert!(matches!(
classify_push_output(&out(false, s)),
AttemptClass::Other(_)
));
}
#[test]
fn is_unreachable_recognises_common_markers() {
assert!(is_unreachable("connection refused"));
assert!(is_unreachable("permission denied"));
assert!(is_unreachable("network is unreachable"));
assert!(!is_unreachable("totally fine"));
}
fn stealth_store_with_task(claimer: &str) -> (tempfile::TempDir, Store, String) {
let td = tempdir().unwrap();
let tasks_dir = td.path().join("tasks");
let store = Store::init(
td.path(),
true,
Some(tasks_dir.to_string_lossy().into_owned()),
)
.unwrap();
let mut task = Task::new(NewTaskOpts { title: "t".into(), ..Default::default() }, "bl-7e57".into());
task.claimed_by = Some(claimer.into());
store.save_task(&task).unwrap();
(td, store, "bl-7e57".into())
}
#[test]
fn post_merge_returns_none_when_we_still_own_claim() {
let (_td, store, id) = stealth_store_with_task("alice");
let mut p = GitRemoteClaimProtocol::new(Event::Claim, &store, &id, "alice");
assert!(p.post_merge().unwrap().is_none());
}
#[test]
fn post_merge_returns_lost_with_unknown_winner_when_claim_cleared() {
let (_td, store, id) = stealth_store_with_task("");
let mut task = store.load_task(&id).unwrap();
task.claimed_by = None;
store.save_task(&task).unwrap();
let mut p = GitRemoteClaimProtocol::new(Event::Claim, &store, &id, "alice");
let outcome = p.post_merge().unwrap().unwrap();
assert_eq!(outcome, SyncedClaimResult::Lost { winner: "(unknown)".into() });
}
#[test]
fn post_merge_skips_ownership_check_for_review_and_close() {
let (_td, store, id) = stealth_store_with_task("");
let mut task = store.load_task(&id).unwrap();
task.claimed_by = None;
store.save_task(&task).unwrap();
for ev in [Event::Review, Event::Close] {
let mut p = GitRemoteClaimProtocol::new(ev, &store, &id, "alice");
assert!(
p.post_merge().unwrap().is_none(),
"{ev:?} post_merge should never short-circuit"
);
}
}
use crate::participant::{Event, Participant, Projection};
#[test]
fn participant_advertises_git_remote_name_and_full_projection() {
let p = GitRemoteParticipant::for_claim();
assert_eq!(p.name(), "git-remote");
assert_eq!(p.subscriptions(), &[Event::Claim]);
assert_eq!(*p.projection(), Projection::full());
}
#[test]
fn participant_default_matches_for_claim() {
let a = GitRemoteParticipant::default();
let b = GitRemoteParticipant::for_claim();
assert_eq!(a.subscriptions(), b.subscriptions());
assert_eq!(*a.projection(), *b.projection());
}
#[test]
fn participant_failure_policy_required_for_subscribed_events() {
let p = GitRemoteParticipant::for_claim();
assert_eq!(p.failure_policy(Event::Claim), FailurePolicy::Required);
for e in [Event::Review, Event::Close, Event::Update, Event::Sync] {
assert_eq!(p.failure_policy(e), FailurePolicy::BestEffort);
}
let p = GitRemoteParticipant::for_lifecycle(&[Event::Review, Event::Close]);
assert_eq!(p.failure_policy(Event::Review), FailurePolicy::Required);
assert_eq!(p.failure_policy(Event::Close), FailurePolicy::Required);
assert_eq!(p.failure_policy(Event::Claim), FailurePolicy::BestEffort);
}
#[test]
fn participant_protocol_is_some_for_lifecycle_events() {
let (_td, store, id) = stealth_store_with_task("alice");
let p = GitRemoteParticipant::for_claim();
let mk = |event| crate::participant::EventCtx {
event,
store: &store,
task_id: &id,
identity: "alice",
};
assert!(p.protocol(Event::Claim, mk(Event::Claim)).is_some());
for e in [Event::Review, Event::Close] {
assert!(p.protocol(e, mk(e)).is_some(), "{e:?}");
}
for e in [Event::Update, Event::Sync] {
assert!(p.protocol(e, mk(e)).is_none(), "{e:?}");
}
}