swactor/crates/distribution/tests/t_diag_gossip_receipt.rs

137 lines
5.5 KiB
Rust
Raw Normal View History

2026-05-25 18:19:06 +00:00
//! Spec §10 (gossip-receipt event, gap 10).
//!
//! The bundle's authoritative source for "did node X ever hear about
//! name Y from peer Z" is the typed `GossipReceived` event. The
//! existing coarse `MessageReceived` counter stays for backward
//! compatibility but is not the source of truth.
//!
//! Acceptance: in any run where one node fails to learn about
//! another node's registered name, the bundle distinguishes
//! unambiguously whether the gossip was never received vs received
//! and ignored. With `GossipReceived` present, the former is
//! readable from the receiver's event stream (no events with
//! payload_kind = "name_registry" from that source) vs the latter
//! (events present, but no corresponding registry entry in the
//! receiver's `Tier2Registry`).
use distribution::diagnostics::Event;
use distribution::diagnostics::sink::InMemorySink;
use distribution::diagnostics::{Aggregator, Identity, Role};
use distribution::swim::node::SwimNode;
use distribution::swim::probe::SwimConfig;
use distribution::types::{MemberState, NodeId, NodeRecord};
#[test]
fn gossip_received_round_trips_through_serde_with_a_typed_discriminator() {
let ev = Event::GossipReceived {
source_peer: NodeId([0x33; 32]),
payload_kind: "swim_piggyback".into(),
payload_bytes: 256,
item_count: 7,
};
let json = serde_json::to_value(&ev).unwrap();
assert_eq!(json["type"], "GossipReceived");
assert_eq!(json["payload_kind"], "swim_piggyback");
assert_eq!(json["payload_bytes"], 256);
assert_eq!(json["item_count"], 7);
let back: Event = serde_json::from_value(json).unwrap();
match back {
Event::GossipReceived { payload_kind, item_count, .. } => {
assert_eq!(payload_kind, "swim_piggyback");
assert_eq!(item_count, 7);
}
_ => panic!("expected GossipReceived"),
}
}
#[test]
fn swim_piggyback_apply_emits_gossip_received_with_correct_source_and_item_count() {
// Spec §10 acceptance contract: the typed event must fire when a
// node receives a SWIM piggyback. The source_peer must match
// whichever node sent the piggyback; item_count must match the
// number of membership updates packed inside.
use distribution::swim::dissemination::{membership_update, DisseminationQueue};
let my_id = NodeId([0xaa; 32]);
let peer_id = NodeId([0xbb; 32]);
let other_id = NodeId([0xcc; 32]);
let mut node = SwimNode::new(my_id, SwimConfig::default());
// Wire a diagnostics aggregator so we can observe what SWIM emits.
let id = Identity::new(my_id, Role::stage(), "run-gossip");
let agg = std::sync::Arc::new(Aggregator::new(id, InMemorySink::new()));
let emitter: distribution::diagnostics::sink::DynEmitter = agg.clone()
as std::sync::Arc<dyn distribution::diagnostics::sink::EventEmitter + Send + Sync>;
node.set_diagnostics(emitter);
// Pack two membership updates into a piggyback as a real sender
// would, then deliver it via a ping from `peer_id`.
let mut queue = DisseminationQueue::new(4);
queue.enqueue(membership_update(other_id, MemberState::Alive, 0), 4);
queue.enqueue(membership_update(peer_id, MemberState::Alive, 0), 4);
let piggyback = queue.pack_piggyback(8);
let _ = node.handle_ping(peer_id, 1, &piggyback);
let records = agg.sink().records();
let gossip: Vec<_> = records
.iter()
.filter_map(|r| match &r.event {
Event::GossipReceived {
source_peer,
payload_kind,
payload_bytes,
item_count,
} => Some((*source_peer, payload_kind.clone(), *payload_bytes, *item_count)),
_ => None,
})
.collect();
assert_eq!(
gossip.len(),
1,
"exactly one GossipReceived per piggyback; got {gossip:?}",
);
let (src, kind, bytes, items) = &gossip[0];
assert_eq!(*src, peer_id, "source must be the SWIM sender (the from arg)");
assert_eq!(kind, "swim_piggyback");
assert!(*bytes > 0, "payload_bytes must reflect actual piggyback size");
assert!(*items >= 1, "item_count must include the packed updates");
}
#[test]
fn empty_piggyback_does_not_fabricate_a_gossip_event() {
// §10 honesty: an empty piggyback is not a content receipt.
// Spec talks about "payload through the gossip layer" — empty
// bytes are not a payload. The bundle reader looking at
// GossipReceived counts must see actual gossip, not heartbeat
// ping noise.
let my_id = NodeId([0x11; 32]);
let peer_id = NodeId([0x22; 32]);
let mut node = SwimNode::new(my_id, SwimConfig::default());
let id = Identity::new(my_id, Role::stage(), "run-empty");
let agg = std::sync::Arc::new(Aggregator::new(id, InMemorySink::new()));
let emitter: distribution::diagnostics::sink::DynEmitter = agg.clone()
as std::sync::Arc<dyn distribution::diagnostics::sink::EventEmitter + Send + Sync>;
node.set_diagnostics(emitter);
let _ = node.handle_ping(peer_id, 1, &[]);
let gossip_count = agg
.sink()
.records()
.iter()
.filter(|r| matches!(r.event, Event::GossipReceived { .. }))
.count();
assert_eq!(
gossip_count, 0,
"empty piggyback must not emit GossipReceived",
);
// Suppress unused-import warning when the assertion above is the
// only NodeRecord-related use in this test.
let _ = NodeRecord {
node_id: peer_id,
state: MemberState::Alive,
incarnation: 0,
};
}