1use std::{
11 any::{Any, TypeId, type_name},
12 collections::{HashMap, HashSet, VecDeque},
13 fmt,
14 marker::PhantomData,
15 sync::{
16 Arc, Mutex, OnceLock,
17 atomic::{AtomicUsize, Ordering},
18 },
19 thread,
20};
21
22#[cfg(test)]
23use std::sync::mpsc;
24
25use crate::{
26 actor::{Actor, ActorProcessingErr, ActorRef},
27 stream::{OverflowStrategy, StreamError, StreamResult},
28};
29
30type DatumValue = Box<dyn DatumElement>;
31type StageMapFn = dyn Fn(DatumValue) -> StreamResult<DatumValue> + Send + Sync;
32type StageTypedMapFn = dyn Any + Send + Sync;
33type StageZipFn = dyn Fn(DatumValue, DatumValue) -> StreamResult<DatumValue> + Send + Sync;
34type StageUnzipFn = dyn Fn(DatumValue) -> (DatumValue, DatumValue) + Send + Sync;
35type StageTypedUnzipFn = dyn Any + Send + Sync;
38type StageCompareFn = dyn Fn(&DatumValue, &DatumValue) -> std::cmp::Ordering + Send + Sync;
39type StageSequenceFn = dyn Fn(&DatumValue) -> u64 + Send + Sync;
40type StageTypedSequenceFn = dyn Any + Send + Sync;
43type StageSnapshotFn = dyn Fn(&[&DatumValue]) -> DatumValue + Send + Sync;
44type StageTypedSnapshotFn = dyn Any + Send + Sync;
47type StagePartitionFn = dyn Fn(&DatumValue) -> usize + Send + Sync;
48type StageTypedPartitionFn = dyn Any + Send + Sync;
51
52#[derive(Clone)]
53struct StageMapFns {
54 erased: Arc<StageMapFn>,
55 typed: Arc<StageTypedMapFn>,
56 scalar_i64: Option<Arc<StageTypedMapFn>>,
57 scalar_finish: Option<Arc<StageTypedMapFn>>,
58}
59
60#[derive(Clone)]
68pub(super) enum TypedCyclicOp {
69 BufferPassthrough,
72 TakeWhile(Arc<dyn Any + Send + Sync>),
76}
77
78impl fmt::Debug for TypedCyclicOp {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 Self::BufferPassthrough => f.write_str("BufferPassthrough"),
82 Self::TakeWhile(_) => f.write_str("TakeWhile(..)"),
83 }
84 }
85}
86
87pub(crate) trait DatumElement: Any + Send {
88 fn clone_box(&self) -> DatumValue;
89 fn into_any(self: Box<Self>) -> Box<dyn Any + Send>;
90 fn as_any_ref(&self) -> &dyn Any;
91}
92
93impl<T> DatumElement for T
94where
95 T: Any + Clone + Send,
96{
97 fn clone_box(&self) -> DatumValue {
98 Box::new(self.clone())
99 }
100
101 fn into_any(self: Box<Self>) -> Box<dyn Any + Send> {
102 self
103 }
104
105 fn as_any_ref(&self) -> &dyn Any {
106 self
107 }
108}
109
110fn datum<T>(value: T) -> DatumValue
111where
112 T: Clone + Send + 'static,
113{
114 Box::new(value)
115}
116
117fn downcast_datum<T, S>(
118 value: DatumValue,
119 operation: &'static str,
120 port: impl FnOnce() -> S,
121) -> StreamResult<T>
122where
123 T: Send + 'static,
124 S: Into<String>,
125{
126 value
131 .into_any()
132 .downcast::<T>()
133 .map(|value| *value)
134 .map_err(|_| StreamError::InvalidPortOperation {
135 operation,
136 port: port().into(),
137 reason: format!("element type did not match {}", type_name::<T>()),
138 })
139}
140
141static NEXT_PORT_ID: AtomicUsize = AtomicUsize::new(1);
142
143fn next_port_id() -> PortId {
144 PortId(NEXT_PORT_ID.fetch_add(1, Ordering::Relaxed))
145}
146
147fn next_port_id_block(count: usize) -> PortId {
148 debug_assert!(count > 0);
149 PortId(NEXT_PORT_ID.fetch_add(count, Ordering::Relaxed))
150}
151
152#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
156pub struct PortId(usize);
157
158impl PortId {
159 #[must_use]
160 pub const fn as_usize(self) -> usize {
161 self.0
162 }
163
164 const fn offset(self, offset: usize) -> Self {
165 Self(self.0 + offset)
166 }
167}
168
169#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
171pub enum PortKind {
172 Inlet,
173 Outlet,
174}
175
176macro_rules! shared_name {
177 ($fn_name:ident, $value:literal) => {
178 fn $fn_name() -> Arc<str> {
179 static NAME: OnceLock<Arc<str>> = OnceLock::new();
180 Arc::clone(NAME.get_or_init(|| Arc::from($value)))
181 }
182 };
183}
184
185shared_name!(identity_stage_name, "Identity");
186shared_name!(identity_inlet_name, "Identity.in");
187shared_name!(identity_outlet_name, "Identity.out");
188shared_name!(map_stage_name, "Map");
189shared_name!(map_inlet_name, "Map.in");
190shared_name!(map_outlet_name, "Map.out");
191shared_name!(broadcast_stage_name, "Broadcast");
192shared_name!(broadcast_inlet_name, "Broadcast.in");
193shared_name!(balance_stage_name, "Balance");
194shared_name!(balance_inlet_name, "Balance.in");
195shared_name!(merge_stage_name, "Merge");
196shared_name!(merge_outlet_name, "Merge.out");
197shared_name!(merge_preferred_stage_name, "MergePreferred");
198shared_name!(merge_preferred_preferred_name, "MergePreferred.preferred");
199shared_name!(merge_preferred_outlet_name, "MergePreferred.out");
200shared_name!(merge_prioritized_stage_name, "MergePrioritized");
201shared_name!(merge_prioritized_outlet_name, "MergePrioritized.out");
202shared_name!(concat_stage_name, "Concat");
203shared_name!(concat_outlet_name, "Concat.out");
204shared_name!(or_else_stage_name, "OrElse");
205shared_name!(or_else_primary_name, "OrElse.primary");
206shared_name!(or_else_secondary_name, "OrElse.secondary");
207shared_name!(or_else_outlet_name, "OrElse.out");
208shared_name!(interleave_stage_name, "Interleave");
209shared_name!(interleave_outlet_name, "Interleave.out");
210shared_name!(zip_stage_name, "Zip");
211shared_name!(zip_in0_name, "Zip.in0");
212shared_name!(zip_in1_name, "Zip.in1");
213shared_name!(zip_outlet_name, "Zip.out");
214shared_name!(async_boundary_stage_name, "AsyncBoundary");
215shared_name!(async_boundary_inlet_name, "AsyncBoundary.in");
216shared_name!(async_boundary_outlet_name, "AsyncBoundary.out");
217
218mod builder;
219mod executor;
220mod junctions;
221mod metrics;
222mod ports;
223mod shapes;
224mod stage;
225mod typed_builder;
226mod wire;
227
228#[cfg(test)]
229use self::executor::BoundaryCountExecutor;
230use self::{
231 builder::StageRecord,
232 stage::{StageKind, StageTimerMailbox, StageTimerRuntime},
233};
234
235pub use self::{
236 builder::{
237 AsyncBoundaryExecutionConfig, FusedExecutionConfig, FusedExecutionReport,
238 FusedExecutorTier, FusedNodeAttributes, FusedPlanReport, FusedSegment, FusedTerminalReport,
239 FusedTierChange, FusedTierChangeReason, Graph, GraphBlueprint, GraphBuilder, GraphDsl,
240 ImportedGraph, PartialGraph,
241 },
242 junctions::{
243 AsyncBoundary, Balance, Broadcast, Buffer, Concat, Identity, Interleave, MapStage, Merge,
244 MergeLatest, MergePreferred, MergePrioritized, MergeSequence, MergeSorted, OrElse,
245 Partition, TakeWhile, Unzip, UnzipWith, Zip,
246 },
247 metrics::{FusedNodeMetrics, metrics_disabled_path_probe, metrics_no_site_probe},
248 ports::{AnyInlet, AnyOutlet, Inlet, Outlet, PortRef},
249 shapes::{
250 BidiShape, FanInShape, FanOutShape, FanOutShape2, FlowShape, MergePreferredShape,
251 PortAllocator, Shape, SinkShape, SourceShape, ZipShape,
252 },
253 stage::{
254 AsyncCallback, GraphStage, GraphStageLogic, InHandler, OutHandler, StageSpec, TimerHandler,
255 },
256 typed_builder::{OpenInlet, OpenOutlet, TypedGraphBuilder},
257 wire::{
258 AutoInletEndpoint, AutoOutletEndpoint, InletCursor, OutletCursor, WireDsl, WirePair,
259 WireSpec,
260 },
261};
262
263#[cfg(test)]
264mod tests {
265 use super::executor::ExecutorMode;
266 use super::*;
267 use crate::{Attribute, AttributeError, Attributes, Fusion, KernelBackend, MetricsLevel};
268
269 fn segment_indices<S: Shape>(graph: &GraphBlueprint<S>) -> Vec<Vec<usize>> {
270 graph
271 .segments()
272 .iter()
273 .map(|segment| segment.stage_indices().to_vec())
274 .collect()
275 }
276
277 fn assert_same_flow_blueprint_and_typed_tier<In, Out>(
278 current: &GraphBlueprint<FlowShape<In, Out>>,
279 typed: &GraphBlueprint<FlowShape<In, Out>>,
280 input: Vec<In>,
281 ) where
282 In: Clone + Send + 'static,
283 Out: Send + PartialEq + fmt::Debug + 'static,
284 {
285 assert_eq!(current.stage_count(), typed.stage_count());
286 assert_eq!(current.edge_count(), typed.edge_count());
287 assert_eq!(segment_indices(current), segment_indices(typed));
288
289 let config = FusedExecutionConfig {
290 event_limit: 5_000_000,
291 };
292 let current_auto = current
293 .run_with_input_report(input.clone(), config)
294 .unwrap();
295 let typed_auto = typed.run_with_input_report(input.clone(), config).unwrap();
296 assert_eq!(current_auto, typed_auto);
297
298 let current_typed = current
299 .run_with_input_report_mode(input.clone(), config, ExecutorMode::TypedOnly)
300 .unwrap();
301 let typed_typed = typed
302 .run_with_input_report_mode(input, config, ExecutorMode::TypedOnly)
303 .unwrap();
304 assert_eq!(current_typed, typed_typed);
305 assert_eq!(current_auto, current_typed);
306 assert_eq!(typed_auto, typed_typed);
307 }
308
309 #[test]
310 fn graph_dsl_builds_broadcast_zip_flow() {
311 let graph = GraphDsl::try_create(|builder| {
312 let broadcast = builder.add(Broadcast::<i32>::new(2));
313 let zip = builder.add(Zip::<i32, i32>::new());
314
315 builder.connect(broadcast.outlet(0)?, zip.in0())?;
316 builder.connect(broadcast.outlet(1)?, zip.in1())?;
317
318 Ok(FlowShape::new(broadcast.inlet(), zip.outlet()))
319 })
320 .unwrap();
321
322 assert_eq!(graph.stage_count(), 2);
323 assert_eq!(graph.edge_count(), 2);
324 assert_eq!(graph.shape().inlets().len(), 1);
325 assert_eq!(graph.shape().outlets().len(), 1);
326 assert_eq!(
327 graph.run_with_input([1, 2, 3]).unwrap(),
328 vec![(1, 1), (2, 2), (3, 3)]
329 );
330 }
331
332 #[test]
333 fn consuming_builder_matches_current_linear_blueprint_and_tier() {
334 let current = GraphDsl::try_create(|builder| {
335 let first = builder.add(Identity::<u64>::new());
336 let second = builder.add(MapStage::new(|item: u64| item + 1));
337
338 builder.connect(first.outlet(), second.inlet())?;
339
340 Ok(FlowShape::new(first.inlet(), second.outlet()))
341 })
342 .unwrap();
343
344 let typed = GraphDsl::typed_try_create(|builder| {
345 let (builder, input, first_out) = builder.add_flow(Identity::<u64>::new());
346 let (builder, second_in, output) =
347 builder.add_flow(MapStage::new(|item: u64| item + 1));
348
349 let builder = builder.connect(first_out, second_in)?;
350
351 Ok((
352 builder,
353 FlowShape::new(input.into_port(), output.into_port()),
354 ))
355 })
356 .unwrap();
357
358 assert_same_flow_blueprint_and_typed_tier(¤t, &typed, (0_u64..16).collect());
359 }
360
361 #[test]
362 fn consuming_builder_matches_current_broadcast_zip_blueprint_and_tier() {
363 let current = GraphDsl::try_create(|builder| {
364 let broadcast = builder.add(Broadcast::<u64>::new(2));
365 let zip = builder.add(Zip::<u64, u64>::new());
366
367 builder.connect(broadcast.outlet(0)?, zip.in0())?;
368 builder.connect(broadcast.outlet(1)?, zip.in1())?;
369
370 Ok(FlowShape::new(broadcast.inlet(), zip.outlet()))
371 })
372 .unwrap();
373
374 let typed = GraphDsl::typed_try_create(|builder| {
375 let (builder, input, [left_out, right_out]) = builder.add_broadcast2::<u64>();
376 let (builder, left_in, right_in, output) = builder.add_zip::<u64, u64>();
377
378 let builder = builder.connect(left_out, left_in)?;
379 let builder = builder.connect(right_out, right_in)?;
380
381 Ok((
382 builder,
383 FlowShape::new(input.into_port(), output.into_port()),
384 ))
385 })
386 .unwrap();
387
388 assert_same_flow_blueprint_and_typed_tier(¤t, &typed, (0_u64..16).collect());
389 }
390
391 #[test]
392 fn consuming_builder_matches_current_balance_merge_blueprint_and_tier() {
393 let current = GraphDsl::try_create(|builder| {
394 let balance = builder.add(Balance::<u64>::new(2));
395 let merge = builder.add(Merge::<u64>::new(2));
396
397 builder.connect(balance.outlet(0)?, merge.inlet(0)?)?;
398 builder.connect(balance.outlet(1)?, merge.inlet(1)?)?;
399
400 Ok(FlowShape::new(balance.inlet(), merge.outlet()))
401 })
402 .unwrap();
403
404 let typed = GraphDsl::typed_try_create(|builder| {
405 let (builder, input, [left_out, right_out]) = builder.add_balance2::<u64>();
406 let (builder, [left_in, right_in], output) = builder.add_merge2::<u64>();
407
408 let builder = builder.connect(left_out, left_in)?;
409 let builder = builder.connect(right_out, right_in)?;
410
411 Ok((
412 builder,
413 FlowShape::new(input.into_port(), output.into_port()),
414 ))
415 })
416 .unwrap();
417
418 assert_same_flow_blueprint_and_typed_tier(¤t, &typed, (0_u64..16).collect());
419 }
420
421 #[test]
422 fn graph_dsl_zip_slots_follow_inlet_ids() {
423 let graph = GraphDsl::try_create(|builder| {
424 let broadcast = builder.add(Broadcast::<i32>::new(2));
425 let identity = builder.add(Identity::<i32>::new());
426 let zip = builder.add(Zip::<i32, i32>::new());
427
428 builder.connect(broadcast.outlet(0)?, identity.inlet())?;
429 builder.connect(identity.outlet(), zip.in1())?;
430 builder.connect(broadcast.outlet(1)?, zip.in0())?;
431
432 Ok(FlowShape::new(broadcast.inlet(), zip.outlet()))
433 })
434 .unwrap();
435
436 assert_eq!(
437 graph.run_with_input([1, 2, 3]).unwrap(),
438 vec![(1, 1), (2, 2), (3, 3)]
439 );
440 }
441
442 #[test]
443 fn graph_dsl_zip_buffers_skewed_inlet_arrivals() {
444 let graph = GraphDsl::try_create(|builder| {
460 let fan = builder.add(Broadcast::<i32>::new(2));
461 let doubler = builder.add(Broadcast::<i32>::new(2));
462 let merge = builder.add(Merge::<i32>::new(2));
463 let zip = builder.add(Zip::<i32, i32>::new());
464
465 builder.connect(fan.outlet(0)?, doubler.inlet())?;
466 builder.connect(doubler.outlet(0)?, merge.inlet(0)?)?;
467 builder.connect(doubler.outlet(1)?, merge.inlet(1)?)?;
468 builder.connect(merge.outlet(), zip.in0())?;
469 builder.connect(fan.outlet(1)?, zip.in1())?;
470
471 Ok(FlowShape::new(fan.inlet(), zip.outlet()))
472 })
473 .unwrap();
474
475 assert_eq!(
479 graph.run_with_input([10, 20]).unwrap(),
480 vec![(10, 10), (10, 20)]
481 );
482 }
483
484 #[test]
485 fn graph_dsl_runs_merge_preferred_buffered_feedback_cycle() {
486 let graph = GraphDsl::try_create(|builder| {
487 let merge = builder.add(MergePreferred::<i32>::new(1));
488 let broadcast = builder.add(Broadcast::<i32>::new(2));
489 let buffer = builder.add(Buffer::<i32>::new(8, OverflowStrategy::Backpressure));
490 let positive = builder.add(TakeWhile::<i32>::new(|item| *item > 0));
491 let decrement = builder.add(MapStage::new(|item: i32| item - 1));
492
493 builder.connect(merge.outlet(), broadcast.inlet())?;
494 builder.connect(broadcast.outlet(1)?, buffer.inlet())?;
495 builder.connect(buffer.outlet(), positive.inlet())?;
496 builder.connect(positive.outlet(), decrement.inlet())?;
497 builder.connect(decrement.outlet(), merge.preferred())?;
498
499 Ok(FlowShape::new(merge.secondary(0)?, broadcast.outlet(0)?))
500 })
501 .unwrap();
502
503 assert_eq!(graph.run_with_input([3]).unwrap(), vec![3, 2, 1, 0]);
504 }
505
506 #[test]
507 fn graph_dsl_runs_buffered_merge_feedback_cycle() {
508 let graph = GraphDsl::try_create(|builder| {
509 let merge = builder.add(Merge::<i32>::new(2));
510 let broadcast = builder.add(Broadcast::<i32>::new(2));
511 let buffer = builder.add(Buffer::<i32>::new(8, OverflowStrategy::Backpressure));
512 let positive = builder.add(TakeWhile::<i32>::new(|item| *item > 0));
513 let decrement = builder.add(MapStage::new(|item: i32| item - 1));
514
515 builder.connect(merge.outlet(), broadcast.inlet())?;
516 builder.connect(broadcast.outlet(1)?, buffer.inlet())?;
517 builder.connect(buffer.outlet(), positive.inlet())?;
518 builder.connect(positive.outlet(), decrement.inlet())?;
519 builder.connect(decrement.outlet(), merge.inlet(1)?)?;
520
521 Ok(FlowShape::new(merge.inlet(0)?, broadcast.outlet(0)?))
522 })
523 .unwrap();
524
525 assert_eq!(graph.run_with_input([3]).unwrap(), vec![3, 2, 1, 0]);
526 }
527
528 #[test]
529 fn graph_dsl_unbuffered_merge_cycle_surfaces_event_limit() {
530 let graph = GraphDsl::try_create(|builder| {
531 let merge = builder.add(Merge::<i32>::new(2));
532 let broadcast = builder.add(Broadcast::<i32>::new(2));
533
534 builder.connect(merge.outlet(), broadcast.inlet())?;
535 builder.connect(broadcast.outlet(1)?, merge.inlet(1)?)?;
536
537 Ok(FlowShape::new(merge.inlet(0)?, broadcast.outlet(0)?))
538 })
539 .unwrap();
540
541 let result = graph.run_with_input_report([1], FusedExecutionConfig { event_limit: 256 });
542
543 assert_eq!(result, Err(StreamError::EventLimitExceeded { limit: 256 }));
544 }
545
546 #[test]
547 fn partial_graph_can_be_imported_with_its_shape() {
548 let partial = GraphDsl::partial(|builder| {
549 let first = builder.add(Identity::<i32>::new());
550 let second = builder.add_named(Identity::<i32>::new(), "partial.tail");
551 builder.connect(first.outlet(), second.inlet())?;
552 Ok(FlowShape::new(first.inlet(), second.outlet()))
553 })
554 .named("partial.identity");
555
556 let graph = GraphDsl::try_create(|builder| {
557 let imported = builder.import(&partial)?;
558 let after = builder.add(Identity::<i32>::new());
559 builder.connect(imported.outlet(), after.inlet())?;
560 Ok(FlowShape::new(imported.inlet(), after.outlet()))
561 })
562 .unwrap()
563 .named("outer.graph");
564
565 assert_eq!(graph.run_with_input([1, 2, 3]).unwrap(), vec![1, 2, 3]);
566 assert_eq!(graph.attributes().name(), Some("outer.graph"));
567 }
568
569 #[test]
570 fn graph_attributes_follow_innermost_wins_order() {
571 let graph = GraphDsl::create(|builder| {
572 builder.add_with_attributes(
573 Identity::<i32>::new(),
574 Attributes::named("stage-outer").and(Attributes::named("stage-inner")),
575 )
576 })
577 .unwrap()
578 .add_attributes(Attributes::dispatcher("graph-outer"))
579 .add_attributes(Attributes::dispatcher("graph-inner"));
580
581 assert_eq!(graph.attributes().dispatcher_hint(), Some("graph-inner"));
582 assert_eq!(
583 graph.stages[0].spec.attributes().name(),
584 Some("stage-inner")
585 );
586 }
587
588 #[test]
589 fn effective_attributes_are_closest_wins_with_partial_stage_override() {
590 let graph = GraphDsl::try_create(|builder| {
591 let first =
592 builder.add_with_attributes(Identity::<i32>::new(), Attributes::batch_size(64));
593 let second = builder.add(Identity::<i32>::new());
594 builder.connect(first.outlet(), second.inlet())?;
595 Ok(FlowShape::new(first.inlet(), second.outlet()))
596 })
597 .unwrap()
598 .add_attributes(Attributes::named("graph-node"))
599 .add_attributes(Attributes::metrics_level(MetricsLevel::Counts))
600 .add_attributes(Attributes::batch_size(16));
601
602 let report = graph
603 .run_with_input_report([1, 2], FusedExecutionConfig::default())
604 .unwrap();
605
606 let first = &report.plan.stage_attributes[0].attributes;
607 assert_eq!(first.name(), Some("graph-node"));
608 assert_eq!(first.metrics_level_hint(), Some(MetricsLevel::Counts));
609 assert_eq!(first.batch_size_hint(), Some(64));
610
611 let second = &report.plan.stage_attributes[1].attributes;
612 assert_eq!(second.name(), Some("graph-node"));
613 assert_eq!(second.metrics_level_hint(), Some(MetricsLevel::Counts));
614 assert_eq!(second.batch_size_hint(), Some(16));
615 }
616
617 #[test]
618 fn execution_report_exposes_full_attribute_vocabulary() {
619 let full_bag = Attributes::named("full-node")
620 .and(Attributes::input_buffer(2, 8))
621 .and(Attributes::dispatcher("graph-dispatcher"))
622 .and(Attributes::batch_size(128))
623 .and(Attributes::parallelism(4))
624 .and(Attributes::fusion(Fusion::Auto))
625 .and(Attributes::metrics_level(MetricsLevel::Timing))
626 .and(Attributes::kernel_backend(KernelBackend::Python));
627
628 let graph = GraphDsl::create(|builder| {
629 builder.add_with_attributes(Identity::<i32>::new(), full_bag)
630 })
631 .unwrap();
632
633 let report = graph
634 .run_with_input_report([7], FusedExecutionConfig::default())
635 .unwrap();
636 let attrs = &report.plan.stage_attributes[0].attributes;
637
638 assert_eq!(report.plan.selected_tier, FusedExecutorTier::TypedLinear);
639 assert_eq!(attrs.name(), Some("full-node"));
640 assert_eq!(attrs.input_buffer_hint(), Some((2, 8)));
641 assert_eq!(attrs.dispatcher_hint(), Some("graph-dispatcher"));
642 assert_eq!(attrs.batch_size_hint(), Some(128));
643 assert_eq!(attrs.parallelism_hint(), Some(4));
644 assert_eq!(attrs.fusion_hint(), Some(Fusion::Auto));
645 assert_eq!(attrs.metrics_level_hint(), Some(MetricsLevel::Timing));
646 assert_eq!(attrs.kernel_backend_hint(), Some(KernelBackend::Python));
647 assert_eq!(attrs.attribute_list().len(), 8);
648 }
649
650 #[test]
651 fn attribute_validation_rejects_zero_buffer_and_parallelism() {
652 assert_eq!(
653 Attributes::try_input_buffer(0, 8),
654 Err(AttributeError::ZeroInputBuffer)
655 );
656 assert_eq!(
657 Attributes::try_parallelism(0),
658 Err(AttributeError::ZeroParallelism)
659 );
660 }
661
662 #[test]
663 fn typed_graph_builder_accepts_stage_attributes() {
664 let graph = GraphDsl::typed_try_create(|builder| {
665 let (builder, inlet, outlet) = builder
666 .add_flow_with_attributes(Identity::<i32>::new(), Attributes::batch_size(32));
667 Ok((
668 builder,
669 FlowShape::new(inlet.into_port(), outlet.into_port()),
670 ))
671 })
672 .unwrap()
673 .add_attributes(Attributes::metrics_level(MetricsLevel::Counts));
674
675 let report = graph
676 .run_with_input_report([1], FusedExecutionConfig::default())
677 .unwrap();
678 let attrs = &report.plan.stage_attributes[0].attributes;
679
680 assert_eq!(attrs.batch_size_hint(), Some(32));
681 assert_eq!(attrs.metrics_level_hint(), Some(MetricsLevel::Counts));
682 }
683
684 #[test]
685 fn metrics_off_reports_an_empty_snapshot() {
686 let graph = GraphDsl::create(|builder| builder.add(Identity::<i32>::new())).unwrap();
687 let report = graph
688 .run_with_input_report([1, 2, 3], FusedExecutionConfig::default())
689 .unwrap();
690
691 assert!(report.node_metrics.is_empty());
692 assert!(report.metrics_snapshot().is_empty());
693 }
694
695 #[test]
696 fn linear_metrics_are_exact_and_levels_are_monotonic() {
697 fn run(level: MetricsLevel) -> Vec<FusedNodeMetrics> {
698 let graph = GraphDsl::try_create(|builder| {
699 let first = builder.add(Identity::<i32>::new());
700 let second = builder.add(MapStage::new(|item: i32| item + 1));
701 builder.connect(first.outlet(), second.inlet())?;
702 Ok(FlowShape::new(first.inlet(), second.outlet()))
703 })
704 .unwrap()
705 .add_attributes(Attributes::metrics_level(level));
706
707 graph
708 .run_with_input_report([1, 2, 3, 4], FusedExecutionConfig::default())
709 .unwrap()
710 .node_metrics
711 }
712
713 let counts = run(MetricsLevel::Counts);
714 let timing = run(MetricsLevel::Timing);
715 let stalls = run(MetricsLevel::Stalls);
716 for snapshot in [&counts, &timing, &stalls] {
717 assert_eq!(snapshot.len(), 2);
718 assert!(
719 snapshot
720 .iter()
721 .all(|node| node.elements_in == 4 && node.elements_out == 4)
722 );
723 assert!(snapshot.iter().all(|node| {
724 node.batches_in == 0
725 && node.batches_out == 0
726 && node.rows_in == 0
727 && node.rows_out == 0
728 }));
729 }
730 assert!(counts.iter().all(|node| node.processing_time.is_zero()));
731 assert!(timing.iter().all(|node| !node.processing_time.is_zero()));
732 assert!(stalls.iter().all(|node| !node.processing_time.is_zero()));
733 assert!(
734 timing
735 .iter()
736 .all(|node| node.backpressure_stall_time.is_zero())
737 );
738 }
739
740 #[test]
741 fn junction_metrics_count_each_stage_boundary_exactly() {
742 let graph = GraphDsl::try_create(|builder| {
743 let broadcast = builder.add(Broadcast::<i32>::new(2));
744 let zip = builder.add(Zip::<i32, i32>::new());
745 builder.connect(broadcast.outlet(0)?, zip.in0())?;
746 builder.connect(broadcast.outlet(1)?, zip.in1())?;
747 Ok(FlowShape::new(broadcast.inlet(), zip.outlet()))
748 })
749 .unwrap()
750 .add_attributes(Attributes::metrics_level(MetricsLevel::Counts));
751
752 let report = graph
753 .run_with_input_report([1, 2, 3], FusedExecutionConfig::default())
754 .unwrap();
755 assert_eq!(
756 report.plan.selected_tier,
757 FusedExecutorTier::TypedAcyclicJunction
758 );
759 assert_eq!(report.node_metrics.len(), 2);
760 assert_eq!(report.node_metrics[0].elements_in, 3);
761 assert_eq!(report.node_metrics[0].elements_out, 6);
762 assert_eq!(report.node_metrics[1].elements_in, 6);
763 assert_eq!(report.node_metrics[1].elements_out, 3);
764 }
765
766 #[test]
767 fn stalls_level_observes_bounded_zip_wait_and_queue_depth() {
768 let graph = GraphDsl::create(|builder| builder.add(Zip::<i32, i32>::new()))
769 .unwrap()
770 .add_attributes(Attributes::metrics_level(MetricsLevel::Stalls));
771
772 let report = graph
773 .run_zip_report(vec![1], vec![2], FusedExecutionConfig::default())
774 .unwrap();
775 assert_eq!(report.node_metrics.len(), 1);
776 let metrics = &report.node_metrics[0];
777 assert_eq!(metrics.elements_in, 2);
778 assert_eq!(metrics.elements_out, 1);
779 assert!(!metrics.processing_time.is_zero());
780 assert!(!metrics.backpressure_stall_time.is_zero());
781 assert_eq!(metrics.queue_depth, Some(1));
782 }
783
784 #[test]
785 fn typed_and_mutable_builders_report_equivalent_metrics() {
786 let current = GraphDsl::create(|builder| builder.add(Identity::<i32>::new()))
787 .unwrap()
788 .add_attributes(Attributes::metrics_level(MetricsLevel::Counts));
789 let typed = GraphDsl::typed_try_create(|builder| {
790 let (builder, inlet, outlet) = builder.add_flow(Identity::<i32>::new());
791 Ok((
792 builder,
793 FlowShape::new(inlet.into_port(), outlet.into_port()),
794 ))
795 })
796 .unwrap()
797 .add_attributes(Attributes::metrics_level(MetricsLevel::Counts));
798
799 let current = current
800 .run_with_input_report([1, 2], FusedExecutionConfig::default())
801 .unwrap();
802 let typed = typed
803 .run_with_input_report([1, 2], FusedExecutionConfig::default())
804 .unwrap();
805 assert_eq!(current.node_metrics, typed.node_metrics);
806 }
807
808 #[test]
809 fn fusion_erased_only_forces_erased_tier_and_reports_reason() {
810 let graph = GraphDsl::create(|builder| builder.add(Identity::<i32>::new()))
811 .unwrap()
812 .add_attributes(Attributes::fusion(Fusion::ErasedOnly));
813
814 let report = graph
815 .run_with_input_report([1, 2], FusedExecutionConfig::default())
816 .unwrap();
817
818 assert_eq!(report.output, vec![1, 2]);
819 assert_eq!(report.plan.selected_tier, FusedExecutorTier::Erased);
820 assert_eq!(report.plan.tier_changes.len(), 1);
821 let change = &report.plan.tier_changes[0];
822 assert_eq!(
823 change.requested_attribute,
824 Some(Attribute::Fusion(Fusion::ErasedOnly))
825 );
826 assert_eq!(
827 change.reason,
828 FusedTierChangeReason::FusionErasedOnlyRequested
829 );
830 assert_eq!(change.selected_tier, FusedExecutorTier::Erased);
831 assert!(!change.affected_nodes.is_empty());
832 }
833
834 #[test]
835 fn graph_dsl_rejects_invalid_and_incomplete_wiring() {
836 let graph = GraphDsl::try_create(|builder| {
837 let first = builder.add(Identity::<i32>::new());
838 let second = builder.add(Identity::<i32>::new());
839 let third = builder.add(Identity::<i32>::new());
840
841 builder.connect(first.outlet(), second.inlet())?;
842 let duplicate = builder.connect(first.outlet(), third.inlet());
843 assert!(matches!(duplicate, Err(StreamError::GraphValidation(_))));
844
845 Ok(FlowShape::new(first.inlet(), second.outlet()))
846 });
847
848 assert!(matches!(graph, Err(StreamError::GraphValidation(_))));
849
850 let graph = GraphDsl::create(|builder| {
851 let broadcast = builder.add(Broadcast::<i32>::new(2));
852 SourceShape::new(broadcast.outlet(0).unwrap())
853 });
854 assert!(matches!(graph, Err(StreamError::GraphValidation(_))));
855 }
856
857 #[test]
858 fn graph_dsl_rejects_erased_type_mismatch() {
859 let graph = GraphDsl::try_create(|builder| {
860 let left = builder.add(Identity::<i32>::new());
861 let right = builder.add(Identity::<u64>::new());
862 let mismatch = builder.connect_any(left.outlet().erase(), right.inlet().erase());
863 assert!(matches!(mismatch, Err(StreamError::GraphValidation(_))));
864 Ok(FlowShape::new(left.inlet(), right.outlet()))
865 });
866
867 assert!(matches!(graph, Err(StreamError::GraphValidation(_))));
868 }
869
870 #[test]
871 fn graph_dsl_rejects_result_ports_with_spoofed_metadata() {
872 let graph = GraphDsl::create(|builder| {
873 let shape = builder.add(Identity::<i32>::new());
874 FlowShape::new(
875 Inlet::<u64>::with_id(shape.inlet().id(), "Identity.in"),
876 shape.outlet(),
877 )
878 });
879
880 assert!(matches!(graph, Err(StreamError::GraphValidation(_))));
881 }
882
883 #[test]
884 fn allocated_ports_do_not_collide_with_manual_ports() {
885 let manual = Inlet::<i32>::new("manual");
886 let graph = GraphDsl::try_create(|builder| {
887 let shape = builder.add(Identity::<i32>::new());
888 assert_ne!(manual.id(), shape.inlet().id());
889 Ok(shape)
890 });
891
892 assert!(graph.is_ok());
893 }
894
895 #[test]
896 fn identity_ports_keep_static_names_and_unique_ids() {
897 let graph = GraphDsl::try_create(|builder| {
898 let first = builder.add(Identity::<i32>::new());
899 let second = builder.add(Identity::<i32>::new());
900
901 assert_eq!(first.inlet().name(), "Identity.in");
902 assert_eq!(first.outlet().name(), "Identity.out");
903 assert_eq!(second.inlet().name(), "Identity.in");
904 assert_eq!(second.outlet().name(), "Identity.out");
905 assert_ne!(first.inlet().id(), first.outlet().id());
906 assert_ne!(first.outlet().id(), second.inlet().id());
907 assert_ne!(second.inlet().id(), second.outlet().id());
908
909 builder.connect(first.outlet(), second.inlet())?;
910 Ok(FlowShape::new(first.inlet(), second.outlet()))
911 });
912
913 assert!(graph.is_ok());
914 }
915
916 #[test]
917 fn graph_stage_logic_checks_port_operations() {
918 let shape = FlowShape::new(Inlet::<i32>::new("in"), Outlet::<i32>::new("out"));
919 let inlet = shape.inlet();
920 let outlet = shape.outlet();
921 let mut logic = GraphStageLogic::new(&shape);
922
923 assert!(matches!(
924 logic.grab(&inlet),
925 Err(StreamError::InvalidPortOperation { .. })
926 ));
927 logic.pull(&inlet).unwrap();
928 assert!(logic.has_been_pulled(&inlet));
929 assert!(matches!(
930 logic.pull(&inlet),
931 Err(StreamError::InvalidPortOperation { .. })
932 ));
933 logic.offer(&inlet, 41).unwrap();
934 assert!(!logic.has_been_pulled(&inlet));
935 assert_eq!(logic.grab(&inlet).unwrap(), 41);
936
937 assert!(matches!(
938 logic.push(&outlet, 1),
939 Err(StreamError::InvalidPortOperation { .. })
940 ));
941 logic.request(&outlet).unwrap();
942 assert!(logic.is_available(&outlet));
943 logic.push(&outlet, 42).unwrap();
944 assert!(!logic.is_available(&outlet));
945 logic.complete(&outlet).unwrap();
946 assert!(logic.is_closed(&outlet));
947 assert!(matches!(
948 logic.request(&outlet),
949 Err(StreamError::InvalidPortOperation { .. })
950 ));
951 }
952
953 #[test]
954 fn fused_execution_enforces_event_limit() {
955 let graph = GraphDsl::create(|builder| builder.add(Identity::<i32>::new())).unwrap();
956
957 let result = graph.run_with_input_report([1, 2], FusedExecutionConfig { event_limit: 1 });
958
959 assert_eq!(result, Err(StreamError::EventLimitExceeded { limit: 1 }));
960 }
961
962 #[test]
963 fn async_boundary_splits_fused_segments() {
964 let graph = GraphDsl::try_create(|builder| {
965 let first = builder.add(Identity::<i32>::new());
966 let boundary = builder.add(AsyncBoundary::<i32>::new());
967 let second = builder.add(MapStage::new(|item: i32| item + 1));
968
969 builder.connect(first.outlet(), boundary.inlet())?;
970 builder.connect(boundary.outlet(), second.inlet())?;
971
972 Ok(FlowShape::new(first.inlet(), second.outlet()))
973 })
974 .unwrap();
975
976 assert_eq!(graph.segments().len(), 3);
977 let report = graph
978 .run_with_input_report([1, 2], FusedExecutionConfig::default())
979 .unwrap();
980 assert_eq!(report.output, vec![2, 3]);
981 assert_eq!(report.async_boundary_crossings, 2);
982 }
983
984 #[test]
985 fn async_boundary_count_path_uses_ractor_handoff_segments() {
986 let graph = GraphDsl::try_create(|builder| {
987 let first = builder.add(MapStage::new(|item: i32| item + 1));
988 let boundary = builder.add(AsyncBoundary::<i32>::new());
989 let second = builder.add(MapStage::new(|item: i32| item * 2));
990
991 builder.connect(first.outlet(), boundary.inlet())?;
992 builder.connect(boundary.outlet(), second.inlet())?;
993
994 Ok(FlowShape::new(first.inlet(), second.outlet()))
995 })
996 .unwrap();
997
998 let report = graph
999 .run_async_boundary_count_with_input_report(
1000 [1, 2, 3],
1001 AsyncBoundaryExecutionConfig {
1002 fused: FusedExecutionConfig::default(),
1003 buffer_size: 2,
1004 },
1005 )
1006 .unwrap();
1007
1008 assert_eq!(report.result, 3);
1009 assert_eq!(report.async_boundary_crossings, 3);
1010 assert_eq!(report.events, 18);
1011 }
1012
1013 #[test]
1014 fn threaded_async_boundary_baseline_matches_ractor_count_path() {
1015 let graph = GraphDsl::try_create(|builder| {
1016 let first = builder.add(MapStage::new(|item: i32| item + 1));
1017 let boundary = builder.add(AsyncBoundary::<i32>::new());
1018 let second = builder.add(MapStage::new(|item: i32| item * 2));
1019
1020 builder.connect(first.outlet(), boundary.inlet())?;
1021 builder.connect(boundary.outlet(), second.inlet())?;
1022
1023 Ok(FlowShape::new(first.inlet(), second.outlet()))
1024 })
1025 .unwrap();
1026
1027 let config = AsyncBoundaryExecutionConfig {
1028 fused: FusedExecutionConfig::default(),
1029 buffer_size: 2,
1030 };
1031 let ractor_report = graph
1032 .run_async_boundary_count_with_input_report([1, 2, 3], config)
1033 .unwrap();
1034 let threaded_report = BoundaryCountExecutor::Threaded
1035 .run_count(
1036 [1, 2, 3],
1037 graph.typed_linear_async_segments().unwrap(),
1038 config,
1039 )
1040 .unwrap();
1041
1042 assert_eq!(threaded_report.result, ractor_report.result);
1043 assert_eq!(threaded_report.events, ractor_report.events);
1044 assert_eq!(
1045 threaded_report.async_boundary_crossings,
1046 ractor_report.async_boundary_crossings
1047 );
1048 }
1049
1050 #[test]
1051 fn ractor_async_boundary_rejects_zero_buffer_size() {
1052 let graph = GraphDsl::try_create(|builder| {
1053 let first = builder.add(MapStage::new(|item: i32| item + 1));
1054 let boundary = builder.add(AsyncBoundary::<i32>::new());
1055 let second = builder.add(MapStage::new(|item: i32| item * 2));
1056
1057 builder.connect(first.outlet(), boundary.inlet())?;
1058 builder.connect(boundary.outlet(), second.inlet())?;
1059
1060 Ok(FlowShape::new(first.inlet(), second.outlet()))
1061 })
1062 .unwrap();
1063
1064 let result = BoundaryCountExecutor::Ractor.run_count(
1065 [1, 2, 3],
1066 graph.typed_linear_async_segments().unwrap(),
1067 AsyncBoundaryExecutionConfig {
1068 fused: FusedExecutionConfig::default(),
1069 buffer_size: 0,
1070 },
1071 );
1072
1073 assert!(matches!(
1074 result,
1075 Err(StreamError::GraphValidation(message)) if message.contains("buffer_size")
1076 ));
1077 }
1078
1079 #[test]
1080 fn ractor_async_boundary_streams_input_without_eager_collection() {
1081 let graph = GraphDsl::try_create(|builder| {
1082 let first = builder.add(MapStage::new(|item: i32| item + 1));
1083 let boundary = builder.add(AsyncBoundary::<i32>::new());
1084 let second = builder.add(MapStage::new(|item: i32| item * 2));
1085
1086 builder.connect(first.outlet(), boundary.inlet())?;
1087 builder.connect(boundary.outlet(), second.inlet())?;
1088
1089 Ok(FlowShape::new(first.inlet(), second.outlet()))
1090 })
1091 .unwrap();
1092
1093 let result = BoundaryCountExecutor::Ractor.run_count(
1094 std::iter::repeat(1_i32),
1095 graph.typed_linear_async_segments().unwrap(),
1096 AsyncBoundaryExecutionConfig {
1097 fused: FusedExecutionConfig { event_limit: 4 },
1098 buffer_size: 1,
1099 },
1100 );
1101
1102 assert_eq!(result, Err(StreamError::EventLimitExceeded { limit: 4 }));
1103 }
1104
1105 #[test]
1106 fn ractor_async_boundary_runs_inside_existing_tokio_runtime() {
1107 let graph = GraphDsl::try_create(|builder| {
1108 let first = builder.add(MapStage::new(|item: i32| item + 1));
1109 let boundary = builder.add(AsyncBoundary::<i32>::new());
1110 let second = builder.add(MapStage::new(|item: i32| item * 2));
1111
1112 builder.connect(first.outlet(), boundary.inlet())?;
1113 builder.connect(boundary.outlet(), second.inlet())?;
1114
1115 Ok(FlowShape::new(first.inlet(), second.outlet()))
1116 })
1117 .unwrap();
1118
1119 let runtime = tokio::runtime::Builder::new_current_thread()
1120 .build()
1121 .unwrap();
1122 let report = runtime
1123 .block_on(async {
1124 BoundaryCountExecutor::Ractor.run_count(
1125 [1, 2, 3],
1126 graph.typed_linear_async_segments().unwrap(),
1127 AsyncBoundaryExecutionConfig {
1128 fused: FusedExecutionConfig::default(),
1129 buffer_size: 2,
1130 },
1131 )
1132 })
1133 .unwrap();
1134
1135 assert_eq!(report.result, 3);
1136 assert_eq!(report.async_boundary_crossings, 3);
1137 assert_eq!(report.events, 18);
1138 }
1139
1140 #[test]
1141 fn balance_merge_round_robins_through_junctions() {
1142 let graph = GraphDsl::try_create(|builder| {
1143 let balance = builder.add(Balance::<i32>::new(2));
1144 let merge = builder.add(Merge::<i32>::new(2));
1145
1146 builder.connect(balance.outlet(0)?, merge.inlet(0)?)?;
1147 builder.connect(balance.outlet(1)?, merge.inlet(1)?)?;
1148
1149 Ok(FlowShape::new(balance.inlet(), merge.outlet()))
1150 })
1151 .unwrap();
1152
1153 assert_eq!(graph.run_with_input(0..6).unwrap(), vec![0, 1, 2, 3, 4, 5]);
1154 }
1155
1156 #[test]
1157 fn prioritized_merge_uses_weighted_schedule() {
1158 let graph =
1159 GraphDsl::create(|builder| builder.add(MergePrioritized::<i32>::new(vec![2, 1])))
1160 .unwrap();
1161
1162 assert_eq!(
1163 graph
1164 .run_fan_in(vec![vec![1, 2, 3, 4], vec![100, 101]])
1165 .unwrap(),
1166 vec![1, 2, 100, 3, 4, 101]
1167 );
1168 }
1169
1170 #[test]
1171 fn fused_execution_supports_count_and_fold_sinks() {
1172 let graph = GraphDsl::try_create(|builder| {
1173 let first = builder.add(MapStage::new(|item: u64| item + 1));
1174 let second = builder.add(MapStage::new(|item: u64| item * 2));
1175
1176 builder.connect(first.outlet(), second.inlet())?;
1177
1178 Ok(FlowShape::new(first.inlet(), second.outlet()))
1179 })
1180 .unwrap();
1181
1182 assert_eq!(graph.run_count_with_input(0..4).unwrap(), 4);
1183 assert_eq!(
1184 graph
1185 .run_fold_with_input(0..4, 0, |acc, item| acc + item)
1186 .unwrap(),
1187 20
1188 );
1189 }
1190
1191 #[test]
1192 fn typed_linear_fast_path_runs_same_type_chains() {
1193 let graph = GraphDsl::try_create(|builder| {
1194 let first = builder.add(MapStage::new(|item: u64| item + 1));
1195 let second = builder.add(MapStage::new(|item: u64| item * 2));
1196
1197 builder.connect(first.outlet(), second.inlet())?;
1198
1199 Ok(FlowShape::new(first.inlet(), second.outlet()))
1200 })
1201 .unwrap();
1202
1203 let report = graph
1204 .run_typed_linear_with_input_report([1, 2, 3], FusedExecutionConfig::default())
1205 .unwrap();
1206 assert_eq!(report.output, vec![4, 6, 8]);
1207 assert_eq!(report.events, 12);
1208
1209 assert_eq!(graph.run_typed_linear_count_with_input(0..4).unwrap(), 4);
1210 assert_eq!(
1211 graph
1212 .run_typed_linear_fold_with_input(0..4, 0, |acc, item| acc + item)
1213 .unwrap(),
1214 20
1215 );
1216 }
1217
1218 #[test]
1219 fn typed_linear_fast_path_rejects_junction_graphs() {
1220 let graph = GraphDsl::try_create(|builder| {
1221 let balance = builder.add(Balance::<i32>::new(2));
1222 let merge = builder.add(Merge::<i32>::new(2));
1223
1224 builder.connect(balance.outlet(0)?, merge.inlet(0)?)?;
1225 builder.connect(balance.outlet(1)?, merge.inlet(1)?)?;
1226
1227 Ok(FlowShape::new(balance.inlet(), merge.outlet()))
1228 })
1229 .unwrap();
1230
1231 assert!(matches!(
1232 graph.run_typed_linear_count_with_input([1, 2, 3]),
1233 Err(StreamError::GraphValidation(_))
1234 ));
1235 }
1236
1237 #[test]
1238 fn merge_preferred_drains_preferred_before_secondaries() {
1239 let graph = GraphDsl::create(|builder| builder.add(MergePreferred::<i32>::new(2))).unwrap();
1240
1241 assert_eq!(
1242 graph
1243 .run_merge_preferred(vec![1, 2, 3], vec![vec![100, 101], vec![200]])
1244 .unwrap(),
1245 vec![1, 2, 3, 100, 200, 101]
1246 );
1247 }
1248
1249 #[test]
1250 fn merge_waits_for_all_inputs_to_complete() {
1251 let graph = GraphDsl::create(|builder| builder.add(Merge::<i32>::new(2))).unwrap();
1252
1253 assert_eq!(
1254 graph.run_fan_in(vec![vec![], vec![10, 20]]).unwrap(),
1255 vec![10, 20]
1256 );
1257 assert_eq!(graph.run_fan_in(vec![vec![1], vec![]]).unwrap(), vec![1]);
1258 }
1259
1260 #[test]
1261 fn merge_sorted_drains_remaining_input_after_peer_completes() {
1262 let graph = GraphDsl::try_create(|builder| {
1263 let unzip = builder.add(Unzip::<i32, i32>::new());
1264 let merge = builder.add(MergeSorted::<i32>::new());
1265
1266 builder.connect(unzip.out0(), merge.inlet(0)?)?;
1267 builder.connect(unzip.out1(), merge.inlet(1)?)?;
1268
1269 Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1270 })
1271 .unwrap();
1272
1273 assert_eq!(
1274 graph.run_with_input([(1, 2), (4, 3), (6, 5)]).unwrap(),
1275 vec![1, 2, 3, 4, 5, 6]
1276 );
1277 }
1278
1279 #[test]
1280 fn merge_sequence_reorders_adversarial_arrivals_by_sequence_number() {
1281 let graph = GraphDsl::try_create(|builder| {
1282 let unzip = builder.add(Unzip::<u64, u64>::new());
1283 let merge = builder.add(MergeSequence::<u64>::new(2, |item| *item));
1284
1285 builder.connect(unzip.out0(), merge.inlet(0)?)?;
1286 builder.connect(unzip.out1(), merge.inlet(1)?)?;
1287
1288 Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1289 })
1290 .unwrap();
1291
1292 assert_eq!(
1293 graph.run_with_input([(0, 1), (2, 3), (4, 5)]).unwrap(),
1294 vec![0, 1, 2, 3, 4, 5]
1295 );
1296 }
1297
1298 #[test]
1299 fn merge_latest_emits_with_last_seen_peer_and_honors_eager_complete() {
1300 let graph = GraphDsl::try_create(|builder| {
1301 let unzip = builder.add(Unzip::<i32, i32>::new());
1302 let merge = builder.add(MergeLatest::<i32>::new(2, false));
1303
1304 builder.connect(unzip.out0(), merge.inlet(0)?)?;
1305 builder.connect(unzip.out1(), merge.inlet(1)?)?;
1306
1307 Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1308 })
1309 .unwrap();
1310
1311 assert_eq!(
1312 graph.run_with_input([(1, 10), (2, 11)]).unwrap(),
1313 vec![vec![1, 10], vec![2, 10], vec![2, 11]]
1314 );
1315 }
1316
1317 #[test]
1318 fn zip_completes_when_any_input_completes() {
1319 let graph = GraphDsl::create(|builder| builder.add(Zip::<i32, i32>::new())).unwrap();
1320
1321 assert_eq!(
1322 graph.run_zip(vec![1, 2, 3], vec![10]).unwrap(),
1323 vec![(1, 10)]
1324 );
1325 assert_eq!(graph.run_zip(vec![1], vec![10, 20]).unwrap(), vec![(1, 10)]);
1326 assert_eq!(
1327 graph.run_zip(vec![], vec![10, 20]).unwrap(),
1328 Vec::<(i32, i32)>::new()
1329 );
1330 }
1331
1332 #[test]
1333 fn concat_drains_inputs_in_declared_order() {
1334 let graph = GraphDsl::create(|builder| builder.add(Concat::<i32>::new(3))).unwrap();
1335
1336 assert_eq!(
1337 graph
1338 .run_concat(vec![vec![1, 2], vec![], vec![3, 4]])
1339 .unwrap(),
1340 vec![1, 2, 3, 4]
1341 );
1342 }
1343
1344 #[test]
1345 fn or_else_switches_only_if_primary_is_empty() {
1346 let graph = GraphDsl::create(|builder| builder.add(OrElse::<i32>::new())).unwrap();
1347
1348 assert_eq!(
1349 graph.run_or_else(vec![], vec![10, 20]).unwrap(),
1350 vec![10, 20]
1351 );
1352 assert_eq!(
1353 graph.run_or_else(vec![1, 2], vec![10, 20]).unwrap(),
1354 vec![1, 2]
1355 );
1356 }
1357
1358 #[test]
1359 fn or_else_secondary_first_dropped_when_primary_emits() {
1360 let graph = GraphDsl::create(|builder| builder.add(OrElse::<i32>::new())).unwrap();
1361
1362 assert_eq!(
1363 graph
1364 .run_or_else_secondary_first(vec![1, 2], vec![10, 20])
1365 .unwrap(),
1366 vec![1, 2]
1367 );
1368 }
1369
1370 #[test]
1371 fn or_else_secondary_first_flushed_when_primary_empty() {
1372 let graph = GraphDsl::create(|builder| builder.add(OrElse::<i32>::new())).unwrap();
1373
1374 assert_eq!(
1375 graph
1376 .run_or_else_secondary_first(vec![], vec![10, 20])
1377 .unwrap(),
1378 vec![10, 20]
1379 );
1380 }
1381
1382 #[test]
1383 fn or_else_secondary_closed_then_primary_empty_drains_buffer() {
1384 let graph = GraphDsl::create(|builder| builder.add(OrElse::<i32>::new())).unwrap();
1385
1386 assert_eq!(
1387 graph
1388 .run_or_else_secondary_closed_first(vec![10, 20])
1389 .unwrap(),
1390 vec![10, 20]
1391 );
1392 }
1393
1394 #[test]
1395 fn interleave_cycles_segment_sized_chunks() {
1396 let graph = GraphDsl::create(|builder| builder.add(Interleave::<i32>::new(3, 2))).unwrap();
1397
1398 assert_eq!(
1399 graph
1400 .run_interleave(vec![vec![1, 2, 3], vec![10, 11, 12], vec![20]], 2, false)
1401 .unwrap(),
1402 vec![1, 2, 10, 11, 20, 3, 12]
1403 );
1404 }
1405
1406 #[test]
1407 fn interleave_eager_close_stops_when_any_input_completes() {
1408 let graph = GraphDsl::create(|builder| {
1409 builder.add(Interleave::<i32>::new_with_eager_close(2, 1, true))
1410 })
1411 .unwrap();
1412
1413 assert_eq!(
1414 graph
1415 .run_interleave(vec![vec![1, 2], vec![]], 1, true)
1416 .unwrap(),
1417 Vec::<i32>::new()
1418 );
1419 assert_eq!(
1420 graph
1421 .run_interleave(vec![vec![1], vec![10, 11]], 1, true)
1422 .unwrap(),
1423 vec![1, 10]
1424 );
1425 }
1426
1427 #[test]
1428 fn partition_routes_only_live_outlets_after_peer_cancels() {
1429 let graph = GraphDsl::try_create(|builder| {
1430 let partition = builder.add(Partition::<i32>::new(2, |item| (*item % 2) as usize));
1431 let merge = builder.add(Merge::<i32>::new(2));
1432
1433 builder.connect(partition.outlet(0)?, merge.inlet(0)?)?;
1434 builder.connect(partition.outlet(1)?, merge.inlet(1)?)?;
1435
1436 Ok(FlowShape::new(partition.inlet(), merge.outlet()))
1437 })
1438 .unwrap();
1439
1440 assert_eq!(
1441 graph.run_with_input([0, 1, 2, 3]).unwrap(),
1442 vec![0, 1, 2, 3]
1443 );
1444 }
1445
1446 #[test]
1447 fn unzip_with_keeps_live_outlet_running_after_peer_finishes() {
1448 let graph = GraphDsl::try_create(|builder| {
1449 let unzip = builder.add(UnzipWith::<i32, i32, i32>::new(|item| (item, item * 10)));
1450 let zip = builder.add(Zip::<i32, i32>::new());
1451
1452 builder.connect(unzip.out0(), zip.in0())?;
1453 builder.connect(unzip.out1(), zip.in1())?;
1454
1455 Ok(FlowShape::new(unzip.inlet(), zip.outlet()))
1456 })
1457 .unwrap();
1458
1459 assert_eq!(
1460 graph.run_with_input([1, 2, 3]).unwrap(),
1461 vec![(1, 10), (2, 20), (3, 30)]
1462 );
1463 }
1464
1465 #[test]
1471 fn unzip_merge_sorted_swapped_inlets_still_sorted() {
1472 let graph_normal = GraphDsl::try_create(|builder| {
1473 let unzip = builder.add(Unzip::<i32, i32>::new());
1474 let merge = builder.add(MergeSorted::<i32>::new());
1475 builder.connect(unzip.out0(), merge.inlet(0)?)?;
1476 builder.connect(unzip.out1(), merge.inlet(1)?)?;
1477 Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1478 })
1479 .unwrap();
1480
1481 let graph_swapped = GraphDsl::try_create(|builder| {
1483 let unzip = builder.add(Unzip::<i32, i32>::new());
1484 let merge = builder.add(MergeSorted::<i32>::new());
1485 builder.connect(unzip.out0(), merge.inlet(1)?)?;
1486 builder.connect(unzip.out1(), merge.inlet(0)?)?;
1487 Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1488 })
1489 .unwrap();
1490
1491 let input = vec![(1, 2), (4, 3), (6, 5)];
1492 let expected = vec![1, 2, 3, 4, 5, 6];
1493 assert_eq!(
1494 graph_normal.run_with_input(input.clone()).unwrap(),
1495 expected
1496 );
1497 assert_eq!(graph_swapped.run_with_input(input).unwrap(), expected);
1498 }
1499
1500 #[test]
1504 fn unzip_merge_latest_swapped_inlets_correct_snapshot_order() {
1505 let graph_normal = GraphDsl::try_create(|builder| {
1508 let unzip = builder.add(Unzip::<i32, i32>::new());
1509 let merge = builder.add(MergeLatest::<i32>::new(2, false));
1510 builder.connect(unzip.out0(), merge.inlet(0)?)?;
1511 builder.connect(unzip.out1(), merge.inlet(1)?)?;
1512 Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1513 })
1514 .unwrap();
1515
1516 let graph_swapped = GraphDsl::try_create(|builder| {
1520 let unzip = builder.add(Unzip::<i32, i32>::new());
1521 let merge = builder.add(MergeLatest::<i32>::new(2, false));
1522 builder.connect(unzip.out0(), merge.inlet(1)?)?;
1523 builder.connect(unzip.out1(), merge.inlet(0)?)?;
1524 Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1525 })
1526 .unwrap();
1527
1528 assert_eq!(
1529 graph_normal.run_with_input([(1, 10)]).unwrap(),
1530 vec![vec![1, 10]]
1531 );
1532 assert_eq!(
1534 graph_swapped.run_with_input([(1, 10)]).unwrap(),
1535 vec![vec![10, 1]]
1536 );
1537 }
1538
1539 #[test]
1543 fn merge_sequence_fails_on_gap_at_completion() {
1544 let graph = GraphDsl::try_create(|builder| {
1548 let unzip = builder.add(Unzip::<u64, u64>::new());
1549 let merge = builder.add(MergeSequence::<u64>::new(2, |item| *item));
1550 builder.connect(unzip.out0(), merge.inlet(0)?)?;
1551 builder.connect(unzip.out1(), merge.inlet(1)?)?;
1552 Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1553 })
1554 .unwrap();
1555
1556 let result = graph.run_with_input([(1u64, 2u64)]);
1557 assert!(
1558 matches!(result, Err(StreamError::Failed(ref msg)) if msg.contains("expected sequence")),
1559 "expected a sequence-gap error, got: {result:?}"
1560 );
1561 }
1562
1563 #[test]
1566 fn merge_latest_eager_complete_closes_on_first_inlet_done() {
1567 let graph_eager = GraphDsl::try_create(|builder| {
1572 let unzip = builder.add(Unzip::<i32, i32>::new());
1573 let merge = builder.add(MergeLatest::<i32>::new(2, true));
1574 builder.connect(unzip.out0(), merge.inlet(0)?)?;
1575 builder.connect(unzip.out1(), merge.inlet(1)?)?;
1576 Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1577 })
1578 .unwrap();
1579
1580 let graph_non_eager = GraphDsl::try_create(|builder| {
1581 let unzip = builder.add(Unzip::<i32, i32>::new());
1582 let merge = builder.add(MergeLatest::<i32>::new(2, false));
1583 builder.connect(unzip.out0(), merge.inlet(0)?)?;
1584 builder.connect(unzip.out1(), merge.inlet(1)?)?;
1585 Ok(FlowShape::new(unzip.inlet(), merge.outlet()))
1586 })
1587 .unwrap();
1588
1589 let result_eager = graph_eager.run_with_input([(1i32, 10i32)]).unwrap();
1593 let result_non_eager = graph_non_eager.run_with_input([(1i32, 10i32)]).unwrap();
1594
1595 assert!(!result_eager.is_empty(), "eager graph produced no output");
1597 assert!(
1598 !result_non_eager.is_empty(),
1599 "non-eager graph produced no output"
1600 );
1601
1602 assert_eq!(result_eager, result_non_eager);
1605 }
1606
1607 #[test]
1608 fn scalar_builtin_graph_matches_erased_oracle_and_uses_typed_linear() {
1609 let graph = GraphDsl::try_create(|builder| {
1610 let add = builder.add(MapStage::map_add(7));
1611 let subtract = builder.add(MapStage::map_subtract(3));
1612 builder.connect(add.outlet(), subtract.inlet())?;
1613 Ok(FlowShape::new(add.inlet(), subtract.outlet()))
1614 })
1615 .unwrap();
1616 let input = -600_i64..600;
1617 let auto = graph
1618 .run_with_input_report_mode(
1619 input.clone(),
1620 FusedExecutionConfig::default(),
1621 super::executor::ExecutorMode::Auto,
1622 )
1623 .unwrap();
1624 let erased = graph
1625 .run_with_input_report_mode(
1626 input,
1627 FusedExecutionConfig::default(),
1628 super::executor::ExecutorMode::ErasedOnly,
1629 )
1630 .unwrap();
1631 assert_eq!(auto.output, erased.output);
1632 assert_eq!(auto.plan.selected_tier, FusedExecutorTier::TypedLinear);
1633 }
1634
1635 #[test]
1636 fn scalar_builtin_graph_replays_whole_segment_at_first_failure() {
1637 let graph = GraphDsl::try_create(|builder| {
1638 let add = builder.add(MapStage::map_add(1));
1639 let subtract = builder.add(MapStage::map_subtract(10));
1640 builder.connect(add.outlet(), subtract.inlet())?;
1641 Ok(FlowShape::new(add.inlet(), subtract.outlet()))
1642 })
1643 .unwrap();
1644 let observed = Arc::new(Mutex::new(Vec::new()));
1645 let folded = Arc::clone(&observed);
1646 let result = graph.run_fold_with_input_report(
1647 [0, i64::MIN + 5, i64::MAX],
1648 (),
1649 move |(), value| {
1650 folded.lock().expect("fold output poisoned").push(value);
1651 },
1652 FusedExecutionConfig::default(),
1653 );
1654 assert_eq!(
1655 result,
1656 Err(StreamError::Failed(format!(
1657 "integer overflow in map_subtract: {} - 10",
1658 i64::MIN + 6
1659 )))
1660 );
1661 assert_eq!(*observed.lock().unwrap(), vec![-9]);
1662
1663 let erased = graph.run_with_input_report_mode(
1664 [0, i64::MIN + 5, i64::MAX],
1665 FusedExecutionConfig::default(),
1666 super::executor::ExecutorMode::ErasedOnly,
1667 );
1668 assert_eq!(
1669 erased.map(|report| report.output),
1670 result.map(|_| Vec::new())
1671 );
1672 }
1673
1674 #[test]
1675 fn scalar_builtin_graph_multiply_stays_checked_scalar() {
1676 let graph = GraphDsl::create(|builder| builder.add(MapStage::map_multiply(2))).unwrap();
1677 assert_eq!(
1678 graph.run_with_input([2_i64, i64::MAX]),
1679 Err(StreamError::Failed(format!(
1680 "integer overflow in map_multiply: {} * 2",
1681 i64::MAX
1682 )))
1683 );
1684 }
1685}