//! Shared support for the datastream tests (testing spec §2, §3). //! //! Two things live here, both deliberately separate from the system under //! test so a test never asserts the code against itself: //! //! * the **payload library** — realistic record shapes and log lines, the //! bytes the pipe will actually carry (testing spec §5: "never //! placeholder text"); and //! * the **reference model** — the spec's rules restated as small, total //! functions over sequences (testing spec §3). It is the trusted oracle: //! every test's `expected` is derived from it, never captured from a run. //! It is written naively on purpose (collect, sort, scan) so a reader can //! confirm it against the spec by eye, while the pipe computes the same //! answers the long way. //! //! This module is `#[path]`-included into more than one test binary, so //! some items are unused in some of them. #![allow(dead_code)] use distribution::datastream::catalog::{ self, IdentityRecord, LifecycleCost, MembershipTransition, ProcStream, Record, ResourceSample, Role, RuntimeStats, TransportInternals, }; use distribution::datastream::frame::{ChannelId, Frame, Position, StreamId}; use distribution::datastream::mux::Mux; use distribution::datastream::store::GapSpan; pub use distribution::datastream::transport::Delivery; /// An in-process node (testing spec §2 — the faked machine boundary): a /// *real* mux plus the producers that feed it. Producers push realistic /// bytes in at the producer seam; [`Node::sent`] takes the mux's ordered /// output stream — the value on the mux→transport seam. Only the machine /// boundary is faked; the mux is the production code. pub struct Node { stream: StreamId, mux: Mux, } impl Node { /// Stand up a node for a given stream (node identity + lifetime). pub fn new(stream: StreamId) -> Self { Node { mux: Mux::unbounded(stream.clone()), stream } } /// The stream this node produces. pub fn stream_id(&self) -> &StreamId { &self.stream } /// A producer emits a typed record on its own channel (spec §6.1). pub fn emit(&self, record: &R) -> Position { self.mux.submit(R::channel(), record.encode()) } /// A producer emits a line of raw process output (spec §6.2). pub fn emit_text(&self, label: &str, stream: ProcStream, line: &str) -> Position { self.mux.submit(catalog::process_output(label, stream), line.as_bytes().to_vec()) } /// A producer emits bytes on a channel the consumer may not know /// (spec §6.3) — opaque to everything until a view learns the channel. pub fn emit_opaque(&self, channel: &str, bytes: &[u8]) -> Position { self.mux.submit(ChannelId::new(channel), bytes.to_vec()) } /// Take the node's ordered output stream (drains the mux). pub fn sent(&self) -> Vec { self.mux.drain() } } /// Realistic payloads, drawn on by both the verified vectors (testing spec /// §5) and the deployment scenario (testing spec §8). Nothing here is /// placeholder text. pub mod payloads { use super::*; /// A boot/identity record for a node. pub fn identity(node: &str, life: u64) -> IdentityRecord { IdentityRecord { node: node.to_string(), role: Role::Worker, region: "us-east-1".to_string(), life, } } /// A plausible resource sample; `tick` nudges the values so a series is /// not constant. pub fn resource(tick: u64) -> ResourceSample { ResourceSample { cpu_pct: 12.5 + (tick % 7) as f32 * 3.0, mem_used_mb: 2048 + (tick % 5) as u32 * 128, mem_total_mb: 16384, gpu_pct: (tick % 4) as f32 * 25.0, disk_used_gb: 40 + (tick % 3) as u32, net_rx_kbps: 900 + (tick % 11) as u32 * 30, net_tx_kbps: 300 + (tick % 13) as u32 * 20, } } /// A transport-internals snapshot. pub fn transport(tick: u64) -> TransportInternals { TransportInternals { relay_connected: !tick.is_multiple_of(9), direct_peers: 2 + (tick % 3) as u32, relay_peers: 1, rtt_ms_p50: 18 + (tick % 5) as u32 * 4, } } /// A membership transition between two peers' states. pub fn membership(peer: &str, from: &str, to: &str) -> MembershipTransition { MembershipTransition { peer: peer.to_string(), from: from.to_string(), to: to.to_string(), reason: "probe timeout".to_string(), } } /// A runtime-stats record. pub fn runtime(tick: u64) -> RuntimeStats { RuntimeStats { actors_live: 30 + (tick % 6) as u32, mailbox_depth: (tick % 17) as u32, scheduled_tasks: 4 + (tick % 3) as u32, } } /// A provider/lifecycle/cost record. pub fn lifecycle(phase: &str, uptime_s: u64) -> LifecycleCost { LifecycleCost { phase: phase.to_string(), cost_usd_per_hr: 1.27, uptime_s } } /// A realistic line of process output (without trailing newline). pub fn log_line(label: &str, tick: u64) -> String { format!("[{label}] step {tick} loss=0.{:03} lr=3e-4", 250 - (tick % 200)) } } /// The reference model: the spec's rules as plain total functions over /// sequences. No transport, no storage, no concurrency, no time. pub mod reference { use super::*; /// The frames a consumer was delivered for one stream, in arrival /// order — the raw material reconstruction works over. pub fn delivered_frames(stream: &StreamId, deliveries: &[Delivery]) -> Vec { deliveries .iter() .filter(|d| &d.stream == stream) .map(|d| d.frame.clone()) .collect() } /// Reconstruction (spec §8.1, testing spec §3): "keep the frames that /// were delivered, in position order." Duplicates of a position /// collapse to one (the carrier may not fabricate content, spec §9, so /// a repeat carries identical bytes). pub fn reconstruct(delivered: &[Frame]) -> Vec { let mut frames: Vec = Vec::new(); for f in delivered { if !frames.iter().any(|seen| seen.position == f.position) { frames.push(f.clone()); } } frames.sort_by_key(|f| f.position); frames } /// The surfaced gaps as spans: the **interior** runs of positions missing /// between the first and last delivered position (spec §7.5, §8). A /// consumer can only detect gaps it has bracketing frames for; positions /// lost after the last delivered frame are invisible and manifest as the /// stream ending (spec §7.4 node death = truncation, not a gap). /// /// Walks the sorted delivered positions — O(frames), never the gap size — /// so the oracle agrees with the store on the cheap path even across a /// near-`u64::MAX` gap. pub fn gap_spans(delivered: &[Frame]) -> Vec { let recon = reconstruct(delivered); let mut spans = Vec::new(); let mut prev: Option = None; for f in &recon { let p = f.position.0; if let Some(q) = prev && p > q + 1 { spans.push(GapSpan { start: q + 1, end: p - 1 }); } prev = Some(p); } spans } /// One structural item on the merged timeline (testing spec §3: "all /// stored frames in position order, channels interleaved"). This is the /// *structure* of the merged log — order and surfaced gaps — decoupled /// from how each payload is rendered for display, which is a separate /// §9.3 concern the tests assert on its own. #[derive(Debug, Clone, PartialEq, Eq)] pub enum TimelineItem { Frame { position: u64, channel: String }, Gap { start: u64, end: u64 }, } /// The merged log oracle (spec §9.2): every delivered frame in position /// order, channels interleaved, with an interior gap surfaced wherever a /// position is missing. Written naively so it is obviously the spec. pub fn merged_log(delivered: &[Frame]) -> Vec { let frames = reconstruct(delivered); let mut out = Vec::new(); let mut prev: Option = None; for f in &frames { let pos = f.position.0; if let Some(p) = prev && pos > p + 1 { out.push(TimelineItem::Gap { start: p + 1, end: pos - 1 }); } out.push(TimelineItem::Frame { position: pos, channel: f.channel.to_string() }); prev = Some(pos); } out } } /// Convenience: build a frame on a typed channel from a record. pub fn typed_frame(record: &R, position: u64) -> Frame { Frame::new(R::channel(), Position(position), record.encode()) } /// Convenience: build a frame on a raw-text process-output channel. pub fn text_frame(label: &str, stream: ProcStream, line: &str, position: u64) -> Frame { Frame::new(catalog::process_output(label, stream), Position(position), line.as_bytes().to_vec()) } /// Convenience: a frame on a channel id the consumer does not know — an /// opaque channel (spec §6.3). pub fn opaque_frame(id: &str, payload: &[u8], position: u64) -> Frame { Frame::new(ChannelId::new(id), Position(position), payload.to_vec()) }