use std::collections::HashMap;
use flowscope::extract::FiveTupleKey;
use flowscope::pcap::{
Pulse, datagram_messages, datagram_pulses, session_messages, session_pulses,
};
use flowscope::{DatagramParser, FlowSide, SessionParser, Timestamp};
const HTTP_SESSION_PATH: &str = "tests/data/http_session.pcap";
const DNS_QUERIES_PATH: &str = "tests/data/dns_queries.pcap";
#[derive(Default, Clone)]
struct ByteCounter;
impl SessionParser for ByteCounter {
type Message = usize;
fn feed_initiator(&mut self, b: &[u8], _ts: Timestamp, out: &mut Vec<usize>) {
out.push(b.len());
}
fn feed_responder(&mut self, b: &[u8], _ts: Timestamp, out: &mut Vec<usize>) {
out.push(b.len());
}
}
#[derive(Default, Clone)]
struct DgramSizer;
impl DatagramParser for DgramSizer {
type Message = usize;
fn parse(&mut self, payload: &[u8], _side: FlowSide, _ts: Timestamp, out: &mut Vec<usize>) {
out.push(payload.len());
}
}
fn assert_ordering<M>(pulses: &[Pulse<FiveTupleKey, M>]) {
let mut started: HashMap<FiveTupleKey, usize> = HashMap::new();
let mut last_msg: HashMap<FiveTupleKey, usize> = HashMap::new();
let mut ended: HashMap<FiveTupleKey, usize> = HashMap::new();
for (i, p) in pulses.iter().enumerate() {
match p {
Pulse::Started { key, .. } => {
started.entry(*key).or_insert(i);
}
Pulse::Message(m) => {
last_msg.insert(m.key, i);
}
Pulse::Ended { key, .. } => {
ended.insert(*key, i);
}
_ => {}
}
}
for (key, &msg_idx) in &last_msg {
let s = started.get(key).copied();
assert!(
s.is_some_and(|s| s < msg_idx),
"message on {key:?} arrived before its Started",
);
if let Some(&e) = ended.get(key) {
assert!(
msg_idx < e,
"message on {key:?} arrived after its Ended (trailing-drain footgun)",
);
}
}
}
#[test]
fn session_pulses_interleaves_lifecycle_and_messages() {
let pulses: Vec<_> = session_pulses::<ByteCounter>(HTTP_SESSION_PATH)
.expect("open pcap")
.collect();
let n_started = pulses
.iter()
.filter(|p| matches!(p, Pulse::Started { .. }))
.count();
let n_msg = pulses
.iter()
.filter(|p| matches!(p, Pulse::Message(_)))
.count();
let n_ended = pulses
.iter()
.filter(|p| matches!(p, Pulse::Ended { .. }))
.count();
assert!(n_started >= 1, "at least one flow started");
assert!(n_msg >= 1, "the session carried payload");
assert!(n_ended >= 1, "the flow ended (flushed at end-of-pcap)");
let msg_only = session_messages::<ByteCounter>(HTTP_SESSION_PATH)
.expect("open pcap")
.count();
assert_eq!(
n_msg, msg_only,
"pulse Message count == session_messages count"
);
assert_ordering(&pulses);
}
#[test]
fn datagram_pulses_interleaves_lifecycle_and_messages() {
let pulses: Vec<_> = datagram_pulses::<DgramSizer>(DNS_QUERIES_PATH)
.expect("open pcap")
.collect();
let n_msg = pulses
.iter()
.filter(|p| matches!(p, Pulse::Message(_)))
.count();
assert!(
pulses.iter().any(|p| matches!(p, Pulse::Started { .. })),
"at least one datagram flow started",
);
assert!(n_msg >= 1, "DNS datagrams produced messages");
let msg_only = datagram_messages::<DgramSizer>(DNS_QUERIES_PATH)
.expect("open pcap")
.count();
assert_eq!(
n_msg, msg_only,
"pulse Message count == datagram_messages count"
);
assert_ordering(&pulses);
}