//! One event through every flow: the ops to queue and the issue's next //! snapshot. No I/O, so the whole decision is testable. use std::collections::BTreeSet; use swarm_wire::router_todo::Op; use crate::event::{Delivery, Snapshot, fields, is_assign}; use swarm_wire::notify_flows::Flow; use crate::flow::{Action, evaluate}; /// What one event comes to. #[derive(Debug, PartialEq, Eq)] pub struct Routed { /// The ops to queue, one per agent that exists. pub actions: Vec, /// Recipients that are not agents of this swarm, so got nothing. pub skipped: Vec, pub next: Snapshot, } /// Run `flows` over one delivery of `event_type` on an issue whose snapshot is /// `snapshot`. `is_agent` says whether a recipient is an agent of this swarm. pub fn route( event_type: &str, delivery: &Delivery, snapshot: &Snapshot, flows: &[Flow], is_agent: impl Fn(&str) -> bool, ) -> Routed { let fields = fields(event_type, delivery, snapshot); let mut actions = Vec::new(); let mut skipped = Vec::new(); for action in flows.iter().flat_map(|f| evaluate(f, event_type, &fields)) { if is_agent(&action.agent) { actions.push(action); } else if !skipped.contains(&action.agent) { skipped.push(action.agent); } } let mut next = snapshot.clone(); if is_assign(event_type) { // A skipped assignee is left out of the snapshot, so the next assign // event counts it as added again: an agent assigned before it is in // the roster gets its todo once it is. let mut assignees = delivery.assignees.clone(); assignees.retain(|a| !skipped.contains(a)); next.assignees = Some(assignees); } let mut cleared = BTreeSet::new(); for action in &actions { match action.op.op { Op::Upsert => { next.holders.insert(action.agent.clone()); } Op::Clear => { cleared.insert(action.agent.clone()); } } } next.holders.retain(|h| !cleared.contains(h)); Routed { actions, skipped, next, } } #[cfg(test)] mod tests { use super::*; use swarm_wire::notify_flows::builtins; fn delivery(action: &str, assignees: &[&str]) -> Delivery { Delivery { action: action.to_owned(), repo: "o/r".to_owned(), repo_id: 1, number: 9, title: "t".to_owned(), url: "u".to_owned(), sender: "mara".to_owned(), is_pull: false, assignees: assignees.iter().map(|a| (*a).to_owned()).collect(), } } fn agents(a: &Routed) -> Vec<(&str, Op)> { a.actions .iter() .map(|x| (x.agent.as_str(), x.op.op)) .collect() } /// Assign, reassign, close: the three built-ins in the order an issue /// meets them, carrying the snapshot from one event to the next. #[test] fn an_issue_assigned_reassigned_and_closed_queues_and_clears_its_todos() { let flows = builtins(); let is_agent = |a: &str| a != "mara"; let one = route( "issue_assign", &delivery("assigned", &["atlas", "mara"]), &Snapshot::default(), &flows, is_agent, ); assert_eq!(agents(&one), [("atlas", Op::Upsert)]); assert_eq!(one.skipped, ["mara"]); assert_eq!(one.next.holders, BTreeSet::from(["atlas".to_owned()])); let two = route( "issue_assign", &delivery("unassigned", &["mara"]), &one.next, &flows, is_agent, ); assert_eq!(agents(&two), [("atlas", Op::Upsert)]); assert!(two.actions[0].op.summary.contains("not assigned")); let three = route( "issue_assign", &delivery("assigned", &["mara", "iris"]), &two.next, &flows, is_agent, ); assert_eq!(agents(&three), [("iris", Op::Upsert)]); let closed = route( "issues", &delivery("closed", &["mara", "iris"]), &three.next, &flows, is_agent, ); // Per agent the clear comes before the closed todo that its prefix would take. let mut got = agents(&closed); got.sort_by_key(|(a, _)| *a); assert_eq!( got, [ ("atlas", Op::Clear), ("atlas", Op::Upsert), ("iris", Op::Clear), ("iris", Op::Upsert) ] ); assert!(closed.next.holders.is_empty()); // A close leaves the assignee snapshot as the last assign set it. assert_eq!(closed.next.assignees, three.next.assignees); } /// An agent assigned before it reports status gets nothing then, and its /// todo on the next assign event, once it is in the roster. #[test] fn an_assignee_not_yet_in_the_roster_gets_its_todo_on_the_next_assign() { let flows = builtins(); let early = route( "issue_assign", &delivery("assigned", &["nova"]), &Snapshot::default(), &flows, |_| false, ); assert!(early.actions.is_empty()); assert_eq!(early.skipped, ["nova"]); assert_eq!(early.next.assignees, Some(BTreeSet::new())); let later = route( "issue_assign", &delivery("assigned", &["nova", "atlas"]), &early.next, &flows, |_| true, ); let mut got = agents(&later); got.sort_unstable_by_key(|(a, _)| *a); assert_eq!(got, [("atlas", Op::Upsert), ("nova", Op::Upsert)]); assert_eq!( later.next.assignees, Some(BTreeSet::from(["atlas".to_owned(), "nova".to_owned()])) ); } #[test] fn an_event_no_flow_matches_changes_nothing() { let before = Snapshot { assignees: Some(BTreeSet::from(["atlas".to_owned()])), holders: BTreeSet::from(["atlas".to_owned()]), }; let r = route( "issue_comment", &delivery("created", &[]), &before, &builtins(), |_| true, ); assert!(r.actions.is_empty()); assert_eq!(r.next, before); } }