183 lines
6.9 KiB
Rust
183 lines
6.9 KiB
Rust
//! Running a flows document over past events without delivering anything.
|
|
//!
|
|
//! Each event goes through the walk the router delivers with ([`flow::reached`]
|
|
//! and [`route::route`]), counting the nodes it reaches. The per-issue
|
|
//! snapshots start empty and carry from one event to the next, as the router's
|
|
//! do, so an assign event's `assignees_added` is the difference to the
|
|
//! previous replayed one. Every recipient counts as an agent.
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use swarm_wire::notify_flows::Flow;
|
|
use swarm_wire::router_api::{DryRun, FlowHits, NodeHits};
|
|
|
|
use crate::event::{Delivery, Snapshot, fields};
|
|
use crate::{flow, route};
|
|
|
|
/// `flows` over `events`, oldest first, each a forge event type and body.
|
|
/// An event that names no issue or PR is not counted.
|
|
pub fn run(flows: &[Flow], events: &[(String, Vec<u8>)]) -> DryRun {
|
|
let mut hits: Vec<BTreeMap<&str, u64>> = flows
|
|
.iter()
|
|
.map(|f| f.nodes.iter().map(|n| (n.id.as_str(), 0)).collect())
|
|
.collect();
|
|
let mut snapshots: BTreeMap<String, Snapshot> = BTreeMap::new();
|
|
let mut run = 0;
|
|
for (event_type, body) in events {
|
|
let Some(delivery) = Delivery::parse(body) else {
|
|
continue;
|
|
};
|
|
run += 1;
|
|
let key = delivery.state_key();
|
|
let before = snapshots.remove(&key).unwrap_or_default();
|
|
let event_fields = fields(event_type, &delivery, &before);
|
|
for (flow, counts) in flows.iter().zip(&mut hits) {
|
|
for id in flow::reached(flow, event_type, &event_fields) {
|
|
if let Some(count) = counts.get_mut(id) {
|
|
*count += 1;
|
|
}
|
|
}
|
|
}
|
|
let routed = route::route(event_type, &delivery, &before, flows, |_| true);
|
|
snapshots.insert(key, routed.next);
|
|
}
|
|
DryRun {
|
|
events: run,
|
|
flows: flows
|
|
.iter()
|
|
.zip(hits)
|
|
.map(|(flow, counts)| FlowHits {
|
|
flow: flow.id.clone(),
|
|
nodes: flow
|
|
.nodes
|
|
.iter()
|
|
.map(|n| NodeHits {
|
|
node: n.id.clone(),
|
|
hits: counts.get(n.id.as_str()).copied().unwrap_or(0),
|
|
})
|
|
.collect(),
|
|
})
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use swarm_wire::notify_flows::{Document, builtins};
|
|
|
|
use super::*;
|
|
|
|
fn body(action: &str, number: i64, assignees: &[&str]) -> Vec<u8> {
|
|
let people: Vec<_> = assignees
|
|
.iter()
|
|
.map(|a| serde_json::json!({ "login": a }))
|
|
.collect();
|
|
serde_json::to_vec(&serde_json::json!({
|
|
"action": action,
|
|
"repository": { "full_name": "o/r", "id": 1 },
|
|
"issue": { "number": number, "title": "t", "html_url": "u", "assignees": people },
|
|
"sender": { "login": "mara" },
|
|
}))
|
|
.expect("encodes")
|
|
}
|
|
|
|
fn count(r: &DryRun, flow: &str, node: &str) -> u64 {
|
|
r.flows
|
|
.iter()
|
|
.find(|f| f.flow == flow)
|
|
.and_then(|f| f.nodes.iter().find(|n| n.node == node))
|
|
.unwrap_or_else(|| panic!("no node {flow}/{node}"))
|
|
.hits
|
|
}
|
|
|
|
/// The built-ins over an assign, a second assign that adds nobody, an
|
|
/// unassign and a close: each node counts the events that reached it.
|
|
#[test]
|
|
fn each_node_counts_the_events_that_reached_it() {
|
|
let (flows, _) = Document::default().active();
|
|
let events = vec![
|
|
("issue_assign".to_owned(), body("assigned", 5, &["atlas"])),
|
|
("issue_assign".to_owned(), body("assigned", 5, &["atlas"])),
|
|
("issue_assign".to_owned(), body("unassigned", 5, &[])),
|
|
("issues".to_owned(), body("closed", 5, &[])),
|
|
("issues".to_owned(), b"not json".to_vec()),
|
|
];
|
|
let r = run(&flows, &events);
|
|
assert_eq!(r.events, 4);
|
|
// The source sees all three assign events, the match and the sink both
|
|
// assigned ones, though the second adds nobody.
|
|
assert_eq!(count(&r, "assigned", "source"), 3);
|
|
assert_eq!(count(&r, "assigned", "match"), 2);
|
|
assert_eq!(count(&r, "assigned", "sink"), 2);
|
|
// Only the unassign reaches the unassigned flow's sink.
|
|
assert_eq!(count(&r, "unassigned", "source"), 3);
|
|
assert_eq!(count(&r, "unassigned", "sink"), 1);
|
|
// The close is an `issues` event, which these flows start from only
|
|
// in `closed`.
|
|
assert_eq!(count(&r, "closed", "source"), 1);
|
|
assert_eq!(count(&r, "closed", "sink"), 1);
|
|
// No pull request push: the classifier flow counts zero.
|
|
assert_eq!(count(&r, "head-classifier", "source"), 0);
|
|
assert_eq!(count(&r, "head-classifier", "request"), 0);
|
|
}
|
|
|
|
/// A condition on a field that depends on the previous event of the
|
|
/// issue (`assignees_added`) holds for the first assign event and not for
|
|
/// an identical second one.
|
|
#[test]
|
|
fn the_issues_snapshot_carries_from_one_replayed_event_to_the_next() {
|
|
use swarm_wire::notify_flows::{Condition, Edge, Node, NodeKind};
|
|
|
|
let node = |id: &str, kind| Node {
|
|
id: id.to_owned(),
|
|
kind,
|
|
};
|
|
let edge = |from: &str, to: &str| Edge {
|
|
from: from.to_owned(),
|
|
to: to.to_owned(),
|
|
};
|
|
let flow = Flow {
|
|
id: "added".to_owned(),
|
|
name: "added".to_owned(),
|
|
enabled: true,
|
|
nodes: vec![
|
|
node(
|
|
"source",
|
|
NodeKind::Source {
|
|
events: vec!["issue_assign".to_owned()],
|
|
},
|
|
),
|
|
node(
|
|
"atlas",
|
|
NodeKind::Match {
|
|
when: vec![Condition {
|
|
field: "assignees_added".to_owned(),
|
|
equals: "atlas".to_owned(),
|
|
}],
|
|
},
|
|
),
|
|
],
|
|
edges: vec![edge("source", "atlas")],
|
|
};
|
|
let assign = ("issue_assign".to_owned(), body("assigned", 5, &["atlas"]));
|
|
let r = run(&[flow], &[assign.clone(), assign]);
|
|
assert_eq!(count(&r, "added", "source"), 2);
|
|
assert_eq!(count(&r, "added", "atlas"), 1);
|
|
}
|
|
|
|
/// Only the flows the document turns on are run.
|
|
#[test]
|
|
fn a_switched_off_built_in_is_not_listed() {
|
|
let doc = Document {
|
|
disabled_builtins: vec!["closed".to_owned()],
|
|
..Document::default()
|
|
};
|
|
let (flows, _) = doc.active();
|
|
let r = run(&flows, &[]);
|
|
assert_eq!(r.events, 0);
|
|
let listed: Vec<&str> = r.flows.iter().map(|f| f.flow.as_str()).collect();
|
|
assert!(!listed.contains(&"closed"));
|
|
assert!(listed.contains(&"assigned"));
|
|
assert_eq!(listed.len(), builtins().len() - 1);
|
|
}
|
|
}
|