Skip to main content

tocat_api/
pipeline.rs

1//! Composition: chains of [`Plugin`] stages, and the registry that builds them.
2//!
3//! # Cost model
4//!
5//! A chunk is threaded through the stages of a segment by reference. A stage
6//! that only observes (`ctx.pass_through()`) does not cause a copy: the next
7//! stage (and ultimately the socket) is handed the original read buffer. Only
8//! a stage that rewrites bytes materialises them, and even then the pipeline
9//! ping-pongs between two buffers it owns rather than allocating.
10//!
11//! So a pipeline of N observers costs N virtual calls per chunk and zero
12//! copies, which is why running one is not much worse than not running one.
13//!
14//! # Framing
15//!
16//! A chunk off the wire is one unit: one call to the stage below, one write at
17//! the far end. A stage that calls [`Ctx::boundary`] says it wants what it
18//! emitted delivered as several, which is how a stage like `block` cuts a
19//! stream into fixed-size records rather than merely accumulating them.
20//!
21//! Units cost something, so nothing pays for them unless it asked. A stage
22//! that never calls `boundary` leaves the boundary list empty, which reads as
23//! "one unit" everywhere and allocates nothing. Below a stage that did, every
24//! stage is called once per unit, so unit counts multiply down a chain: this
25//! is the reason `boundary` exists as an explicit request rather than
26//! something inferred from a stage emitting more than once.
27//!
28//! Passing through is still free under a framing stage. A stage that hands
29//! every unit back untouched copies nothing and answers [`Emit::Passthrough`],
30//! exactly as it would on an unframed chunk; the copy starts at the first unit
31//! it rewrites, drops or reframes.
32//!
33//! # Ticks
34//!
35//! A stage may also ask to be called on a schedule. The pipeline owns the
36//! schedules (one deadline per ticking stage) and the host owns the timer
37//! that asks whether any of them have come due. Splitting it that way means
38//! the host wakes at one period (the shortest any stage asked for) rather than
39//! holding a timer per stage, and a pipeline with nothing ticking costs
40//! nothing at all: [`Pipeline::tick_interval`] returns `None` and no timer is
41//! created.
42
43use std::{
44    collections::BTreeMap,
45    fmt,
46    sync::Arc,
47    time::{Duration, Instant},
48};
49
50use crate::{
51    Direction, PluginSpec,
52    channel::HostBuilder,
53    error::{PluginError, Result},
54    normalize,
55    plugin::{
56        BuildCtx, Ctx, EffectSink, Emission, Emit, Execution, ExternalStage, PipelineMeta, Plugin,
57        PluginFactory, Stage, StageInfo,
58    },
59};
60
61const EMPTY: &[u8] = &[];
62
63/// The framing of an unframed emission: one unit, covering everything.
64const NO_BOUNDS: &[usize] = &[];
65
66/// Which buffer the live bytes are sitting in.
67#[derive(Clone, Copy, PartialEq, Eq)]
68enum Slot {
69    /// The caller's read buffer: nothing has been rewritten yet.
70    Input,
71    A,
72    B,
73}
74
75/// What came out of a pipeline, and how it is framed.
76///
77/// [`bytes`](Self::bytes) is the whole emission and [`units`](Self::units) is
78/// the pieces the segment asked for it to be delivered in. Unless a stage
79/// called [`Ctx::boundary`] there is exactly one unit and the two say the same
80/// thing, so a sink with no framing of its own (a byte stream, a file) can
81/// write `bytes` in one call and ignore units entirely.
82///
83/// Where units are observable they matter: on a datagram sink one unit is one
84/// message, and across a detached boundary one unit is one parcel and so one
85/// call to the segment below.
86#[derive(Debug, Clone, Copy)]
87pub struct Emitted<'p> {
88    bytes: &'p [u8],
89    /// One offset per unit, each the end of its unit.
90    ///
91    /// Empty means unframed. Otherwise the last offset is always `bytes.len()`
92    /// and the offsets ascend, so every byte belongs to exactly one unit and
93    /// none can be dropped by a sink that iterates units.
94    bounds: &'p [usize],
95}
96
97impl<'p> Emitted<'p> {
98    /// One unit covering all of `bytes`, which is what an unframed chunk is.
99    #[must_use]
100    pub const fn whole(bytes: &'p [u8]) -> Self {
101        Self {
102            bytes,
103            bounds: NO_BOUNDS,
104        }
105    }
106
107    /// Nothing reached the end of the pipeline.
108    #[must_use]
109    pub const fn empty() -> Self {
110        Self::whole(EMPTY)
111    }
112
113    /// Everything emitted, concatenated.
114    #[must_use]
115    pub const fn bytes(&self) -> &'p [u8] {
116        self.bytes
117    }
118
119    #[must_use]
120    pub const fn is_empty(&self) -> bool {
121        self.bytes.is_empty()
122    }
123
124    #[must_use]
125    pub const fn len(&self) -> usize {
126        self.bytes.len()
127    }
128
129    /// Each unit, in order. Exactly one unit when nothing declared any.
130    ///
131    /// Takes `self` by value so the iterator borrows the pipeline rather than
132    /// this handle, which is what lets a caller iterate units while the
133    /// emission is in flight.
134    pub fn units(self) -> impl Iterator<Item = &'p [u8]> {
135        let Self { bytes, bounds } = self;
136
137        // Nothing declared any framing, so the whole emission is the one unit.
138        // An empty emission has no units at all rather than one empty one.
139        let unframed = bounds.is_empty() && !bytes.is_empty();
140
141        units(bytes, bounds).chain(unframed.then_some(bytes))
142    }
143}
144
145/// What one stage reads and what it writes into, borrowed out of a
146/// [`Buffers`], along with the slot the writes land in.
147type Halves<'b> = (Slot, &'b [u8], &'b [usize], &'b mut Emission);
148
149/// The two halves a segment ping-pongs between.
150///
151/// Bytes and boundaries travel together, which is why each half is a whole
152/// [`Emission`] rather than a buffer: a stage that rewrites its input writes
153/// into the far half and declares the framing of what it wrote there, and a
154/// stage that passes through leaves both where they are.
155#[derive(Default)]
156struct Buffers {
157    a: Emission,
158    b: Emission,
159}
160
161impl Buffers {
162    /// What a stage reads and what it writes into, as disjoint borrows, plus
163    /// the slot it wrote into so the caller cannot pick a different one.
164    ///
165    /// The destination is always the half the live bytes are not in, so a
166    /// stage never reads and writes the same buffer. The first stage of a
167    /// chunk reads the caller's buffer, which leaves both halves free.
168    fn borrow<'b>(&'b mut self, live: Slot, input: &'b [u8]) -> Halves<'b> {
169        match live {
170            Slot::Input => (Slot::A, input, NO_BOUNDS, &mut self.a),
171            Slot::A => (Slot::B, self.a.bytes(), self.a.bounds(), &mut self.b),
172            Slot::B => (Slot::A, self.b.bytes(), self.b.bounds(), &mut self.a),
173        }
174    }
175
176    /// The bytes currently live, and their framing.
177    fn live<'b>(&'b self, live: Slot, input: &'b [u8]) -> (&'b [u8], &'b [usize]) {
178        match live {
179            Slot::Input => (input, NO_BOUNDS),
180            Slot::A => (self.a.bytes(), self.a.bounds()),
181            Slot::B => (self.b.bytes(), self.b.bounds()),
182        }
183    }
184}
185
186/// An ordered chain of stages that run inline on one task.
187pub struct Pipeline {
188    /// Pipeline metadata
189    meta: PipelineMeta,
190    /// Pipeline stages
191    stages: Vec<Box<dyn Plugin>>,
192    /// Display names, parallel to `stages`.
193    ///
194    /// Kept in their own vector rather than beside each plugin because
195    /// `self.stages[i]` and `self.names[i]` are then disjoint fields, which
196    /// the borrow checker will let the hot loop touch at the same time.
197    names: Vec<String>,
198    /// The ping-pong halves the stages rewrite into.
199    bufs: Buffers,
200    /// One entry per stage that asked to be ticked, in stage order.
201    ticks: Vec<Schedule>,
202}
203
204/// When a ticking stage is next owed a call.
205struct Schedule {
206    stage: usize,
207    period: Duration,
208    next: Instant,
209}
210
211impl Pipeline {
212    #[must_use]
213    pub fn new(meta: PipelineMeta, stages: Vec<Box<dyn Plugin>>) -> Self {
214        let names = stages.iter().map(|s| s.name().to_string()).collect();
215        Self::with_names(meta, stages, names)
216    }
217
218    /// As [`Pipeline::new`], but with display names that may differ from the
219    /// plugin names: aliases, or `#n` suffixes for repeated plugins.
220    #[must_use]
221    pub fn with_names(
222        meta: PipelineMeta,
223        stages: Vec<Box<dyn Plugin>>,
224        names: Vec<String>,
225    ) -> Self {
226        debug_assert_eq!(stages.len(), names.len());
227
228        // Asked once, here, so the per-chunk path never touches it. A zero
229        // period would spin the host's timer, so it reads as "no ticks" (the
230        // same answer as `None`), which is what a stage configured with an
231        // interval of zero means by it.
232        let start = Instant::now();
233        let ticks = stages
234            .iter()
235            .enumerate()
236            .filter_map(|(stage, plugin)| {
237                let period = plugin.tick_interval().filter(|p| !p.is_zero())?;
238
239                Some(Schedule {
240                    stage,
241                    period,
242                    next: start + period,
243                })
244            })
245            .collect();
246
247        Self {
248            meta,
249            stages,
250            names,
251            bufs: Buffers::default(),
252            ticks,
253        }
254    }
255
256    #[must_use]
257    pub fn meta(&self) -> &PipelineMeta {
258        &self.meta
259    }
260
261    #[must_use]
262    pub fn is_empty(&self) -> bool {
263        self.stages.is_empty()
264    }
265
266    #[must_use]
267    pub fn len(&self) -> usize {
268        self.stages.len()
269    }
270
271    pub fn stage_names(&self) -> impl Iterator<Item = &str> {
272        self.names.iter().map(String::as_str)
273    }
274
275    /// How often the host should ask this pipeline for ticks, or `None` when
276    /// no stage wants any.
277    ///
278    /// The shortest period any stage asked for. A stage that wanted a longer
279    /// one is simply not due on most of those wakeups, which is cheaper than a
280    /// timer each.
281    #[must_use]
282    pub fn tick_interval(&self) -> Option<Duration> {
283        self.ticks.iter().map(|schedule| schedule.period).min()
284    }
285
286    /// The next stage owed a tick at `now`, with its schedule advanced.
287    fn due(&mut self, now: Instant) -> Option<usize> {
288        let schedule = self
289            .ticks
290            .iter_mut()
291            .find(|schedule| schedule.next <= now)?;
292
293        schedule.next += schedule.period;
294
295        // Slept through several periods: resume from now rather than firing
296        // once for each one we missed. A stage that wants to know how long it
297        // was actually away measures it itself.
298        if schedule.next <= now {
299            schedule.next = now + schedule.period;
300        }
301
302        Some(schedule.stage)
303    }
304
305    /// Restart one stage's schedule, as [`Ctx::rearm`] asked.
306    ///
307    /// The next tick falls a full period from now rather than from wherever
308    /// the cadence had reached, which turns the interval a stage asked for
309    /// into a delay it can rely on: a stage that starts holding bytes says so,
310    /// and hears back an interval later rather than at the next wakeup that
311    /// happens to be due.
312    ///
313    /// The clock is read here and only here, so a pipeline in which nothing
314    /// ever asks never touches it on the per-chunk path. A stage that asked
315    /// for no ticks has no schedule and is silently ignored.
316    fn rearm(&mut self, stage: usize) {
317        if let Some(schedule) = self.ticks.iter_mut().find(|s| s.stage == stage) {
318            schedule.next = Instant::now() + schedule.period;
319        }
320    }
321
322    /// Give one due stage its tick, and return what reached the end of the
323    /// pipeline.
324    ///
325    /// `None` means nothing was due. Call it again until it says so: two
326    /// stages can come due on the same wakeup, and each one's output has to be
327    /// written before the next runs.
328    ///
329    /// Unlike [`process`](Pipeline::process) and [`finish`](Pipeline::finish)
330    /// this does not run every stage. A tick belongs to one of them, and what
331    /// it emits cascades through the stages *below* it only, the ones above
332    /// are upstream of a chunk that did not come from them.
333    pub fn tick<'p>(
334        &'p mut self,
335        now: Instant,
336        sink: &mut dyn EffectSink,
337    ) -> Result<Option<Emitted<'p>>> {
338        let Some(index) = self.due(now) else {
339            return Ok(None);
340        };
341
342        run_tick(
343            &mut self.stages[index],
344            &self.meta,
345            &self.names[index],
346            &mut self.bufs.a,
347            sink,
348        )?;
349
350        // A stage may restart its own schedule from a tick as readily as from
351        // a chunk, which is how one that has just given up waiting says so.
352        if self.bufs.a.rearm_requested() {
353            self.rearm(index);
354        }
355
356        // The overwhelming case: an observer that reports and forwards
357        // nothing. No stage below it needs to hear about that.
358        if self.bufs.a.bytes().is_empty() {
359            return Ok(Some(Emitted::empty()));
360        }
361
362        self.drive(EMPTY, index + 1, Slot::A, false, sink).map(Some)
363    }
364
365    /// The first stage that must not carry datagrams, if any.
366    #[must_use]
367    pub fn datagram_hazard(&self) -> Option<&str> {
368        self.stages
369            .iter()
370            .zip(&self.names)
371            .find(|(stage, _)| !stage.datagram_safe())
372            .map(|(_, name)| name.as_str())
373    }
374
375    /// Push one chunk through every stage.
376    ///
377    /// The result borrows `input` directly when every stage passed it through,
378    /// and carries the framing of whatever reframed it otherwise.
379    pub fn process<'p>(
380        &'p mut self,
381        input: &'p [u8],
382        sink: &mut dyn EffectSink,
383    ) -> Result<Emitted<'p>> {
384        self.drive(input, 0, Slot::Input, false, sink)
385    }
386
387    /// Signal EOF, cascading each stage's final bytes through the ones below.
388    pub fn finish<'p>(&'p mut self, sink: &mut dyn EffectSink) -> Result<Emitted<'p>> {
389        self.drive(EMPTY, 0, Slot::Input, true, sink)
390    }
391
392    /// Thread bytes through `self.stages[from..]`.
393    ///
394    /// `live` says where they start out: [`Slot::Input`] for the caller's
395    /// buffer, or a half of the segment's own, which is how a tick cascades
396    /// into the stages below the one that fired.
397    fn drive<'p>(
398        &'p mut self,
399        input: &'p [u8],
400        from: usize,
401        live: Slot,
402        eof: bool,
403        sink: &mut dyn EffectSink,
404    ) -> Result<Emitted<'p>> {
405        let mut live = live;
406
407        for index in from..self.stages.len() {
408            let (slot, src, src_bounds, dst) = self.bufs.borrow(live, input);
409
410            run(
411                &mut self.stages[index],
412                &self.meta,
413                &self.names[index],
414                src,
415                src_bounds,
416                dst,
417                sink,
418                eof,
419            )?;
420
421            let emitted = dst.emit();
422            let rearm = dst.rearm_requested();
423
424            if rearm {
425                self.rearm(index);
426            }
427
428            if emitted != Emit::Passthrough {
429                live = slot;
430            }
431
432            // A swallowed chunk cannot become bytes again further down. At end
433            // of stream there is no early exit, because every stage still has
434            // to hear about it.
435            if !eof && self.bufs.live(live, input).0.is_empty() {
436                return Ok(Emitted::empty());
437            }
438        }
439
440        let (bytes, bounds) = self.bufs.live(live, input);
441
442        Ok(Emitted { bytes, bounds })
443    }
444}
445
446/// Give one stage the bytes above it, and collect what it emitted into `dst`.
447///
448/// `in_bounds` frames `input` the way [`Emitted`] describes: empty is one
449/// unit, which is the shape of every chunk off the wire and of everything a
450/// stage that never calls [`Ctx::boundary`] produces. When it is not empty the
451/// stage is called once per unit, because a stage handed framed bytes must not
452/// see two units fused into one call.
453///
454/// Leaves [`Emit::Passthrough`] on `dst` only when nothing was copied, so the
455/// caller can go on handing the source buffer down the chain.
456fn run(
457    plugin: &mut Box<dyn Plugin>,
458    meta: &PipelineMeta,
459    stage: &str,
460    input: &[u8],
461    in_bounds: &[usize],
462    dst: &mut Emission,
463    sink: &mut dyn EffectSink,
464    eof: bool,
465) -> Result<()> {
466    dst.reset();
467
468    if in_bounds.is_empty() {
469        {
470            let mut ctx = Ctx::new(meta, stage, input, dst, sink);
471
472            if eof {
473                // Every stage but the first is handed whatever the one above
474                // it produced on its way out.
475                if !input.is_empty() {
476                    plugin.on_bytes(&mut ctx, input)?;
477                }
478
479                plugin.on_eof(&mut ctx)?;
480            } else {
481                plugin.on_bytes(&mut ctx, input)?;
482            }
483        }
484
485        // Only when the stage declared framing of its own. Closing an unframed
486        // emission would allocate on the hot path to say what empty says.
487        if !dst.bounds().is_empty() {
488            dst.close();
489        }
490
491        return Ok(());
492    }
493
494    let mut copied = false;
495
496    for (index, unit) in units(input, in_bounds).enumerate() {
497        dst.next_unit();
498
499        {
500            let mut ctx = Ctx::new(meta, stage, unit, dst, sink);
501            plugin.on_bytes(&mut ctx, unit)?;
502        }
503
504        if !copied {
505            if dst.emit() == Emit::Passthrough {
506                // Nothing but the source buffer's own bytes so far, so there
507                // is still nothing to copy.
508                continue;
509            }
510
511            // The first unit this stage did not hand back verbatim, so the
512            // ones before it have to become real bytes now.
513            materialise(input, in_bounds, index, dst);
514            copied = true;
515        }
516
517        if dst.emit() == Emit::Passthrough {
518            dst.out.extend_from_slice(unit);
519        }
520
521        dst.close();
522    }
523
524    if eof {
525        dst.next_unit();
526
527        {
528            let mut ctx = Ctx::new(meta, stage, EMPTY, dst, sink);
529            plugin.on_eof(&mut ctx)?;
530        }
531
532        // An epilogue is bytes of its own, so it forces the copy that the
533        // units before it avoided.
534        if !copied && !dst.bytes().is_empty() {
535            materialise(input, in_bounds, in_bounds.len(), dst);
536            copied = true;
537        }
538
539        if copied {
540            dst.close();
541        }
542    }
543
544    dst.emit = if copied {
545        Emit::Buffered
546    } else {
547        Emit::Passthrough
548    };
549
550    Ok(())
551}
552
553/// Execute a stage's tick. There is no input, so anything it wants downstream
554/// it has to write.
555fn run_tick(
556    plugin: &mut Box<dyn Plugin>,
557    meta: &PipelineMeta,
558    stage: &str,
559    dst: &mut Emission,
560    sink: &mut dyn EffectSink,
561) -> Result<()> {
562    dst.reset();
563
564    {
565        let mut ctx = Ctx::new(meta, stage, EMPTY, dst, sink);
566        plugin.on_tick(&mut ctx)?;
567    }
568
569    if !dst.bounds().is_empty() {
570        dst.close();
571    }
572
573    Ok(())
574}
575
576/// Walk a buffer's units, given the offsets that frame it.
577fn units<'a>(bytes: &'a [u8], bounds: &'a [usize]) -> impl Iterator<Item = &'a [u8]> {
578    let mut start = 0;
579
580    bounds.iter().map(move |&end| {
581        let unit = &bytes[start..end];
582        start = end;
583        unit
584    })
585}
586
587/// Copy the units a stage handed back verbatim in front of whatever it has
588/// already written for the unit that broke the run.
589///
590/// At most once per stage per call, and only for a stage that was both handed
591/// framed bytes and did something other than pass them through. A pipeline
592/// with no framing in it never reaches this, and neither does an observer
593/// sitting under one.
594fn materialise(input: &[u8], in_bounds: &[usize], done: usize, dst: &mut Emission) {
595    let prefix = if done == 0 { 0 } else { in_bounds[done - 1] };
596
597    if prefix == 0 {
598        return;
599    }
600
601    dst.out.splice(0..0, input[..prefix].iter().copied());
602
603    // What the stage wrote is now further along by the length of the prefix,
604    // and the prefix's own units come first.
605    for bound in dst.bounds.iter_mut() {
606        *bound += prefix;
607    }
608
609    dst.bounds.splice(0..0, in_bounds[..done].iter().copied());
610}
611
612impl fmt::Debug for Pipeline {
613    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
614        f.debug_struct("Pipeline")
615            .field("direction", &self.meta.direction)
616            .field("stages", &self.stage_names().collect::<Vec<_>>())
617            .finish()
618    }
619}
620
621/// Everything that runs on one direction, split into segments.
622///
623/// One segment is the common case and runs entirely on the reading task. A
624/// stage declared `Detached` starts a new segment, which the host runs on its
625/// own task behind a bounded channel; subsequent inline stages join that
626/// segment rather than spawning more.
627/// One link in a chain: either stages the host calls inline, or a subprocess
628/// it feeds and drains.
629#[derive(Debug)]
630pub enum Segment {
631    Inline(Pipeline),
632    Process(ExternalStage),
633}
634
635/// Data processing chain.
636#[derive(Debug)]
637pub struct Chain {
638    meta: PipelineMeta,
639    segments: Vec<Segment>,
640}
641
642impl Chain {
643    #[must_use]
644    pub fn new(meta: PipelineMeta, segments: Vec<Segment>) -> Self {
645        Self { meta, segments }
646    }
647
648    #[must_use]
649    pub fn meta(&self) -> &PipelineMeta {
650        &self.meta
651    }
652
653    /// No stages at all: the host should use its plain copy path.
654    #[must_use]
655    pub fn is_empty(&self) -> bool {
656        self.segments.is_empty()
657    }
658
659    #[must_use]
660    pub fn segments(&self) -> &[Segment] {
661        &self.segments
662    }
663
664    #[must_use]
665    pub fn into_segments(self) -> Vec<Segment> {
666        self.segments
667    }
668
669    /// The first stage on this chain that must not carry datagrams, if any.
670    ///
671    /// A subprocess never can: its stdin and stdout are byte streams, so
672    /// message boundaries are gone the moment bytes cross the pipe.
673    #[must_use]
674    pub fn datagram_hazard(&self) -> Option<&str> {
675        self.segments().iter().find_map(|segment| match segment {
676            Segment::Inline(pipeline) => pipeline.datagram_hazard(),
677            Segment::Process(external) => Some(external.name.as_str()),
678        })
679    }
680
681    #[must_use]
682    pub fn stage_names(&self) -> Vec<&str> {
683        self.segments
684            .iter()
685            .flat_map(|segment| match segment {
686                Segment::Inline(pipeline) => pipeline.stage_names().collect::<Vec<_>>(),
687                Segment::Process(external) => vec![external.name.as_str()],
688            })
689            .collect()
690    }
691}
692
693/// Every plugin this binary knows how to build.
694#[derive(Default)]
695pub struct Registry {
696    factories: BTreeMap<String, Arc<dyn PluginFactory>>,
697}
698
699impl Registry {
700    #[must_use]
701    pub fn new() -> Self {
702        Self::default()
703    }
704
705    pub fn register(&mut self, factory: impl PluginFactory) -> &mut Self {
706        self.register_arc(Arc::new(factory))
707    }
708
709    pub fn register_arc(&mut self, factory: Arc<dyn PluginFactory>) -> &mut Self {
710        self.factories.insert(normalize(factory.name()), factory);
711        self
712    }
713
714    #[must_use]
715    pub fn get(&self, name: &str) -> Option<&Arc<dyn PluginFactory>> {
716        self.factories.get(&normalize(name))
717    }
718
719    pub fn iter(&self) -> impl Iterator<Item = &Arc<dyn PluginFactory>> {
720        self.factories.values()
721    }
722
723    /// The names plugins call themselves, not the normalized keys they are
724    /// stored under: these are shown to people, in `--list-plugins` and in the
725    /// suggestions on an unknown plugin.
726    pub fn names(&self) -> impl Iterator<Item = &str> {
727        // Keys are normalized, but name() preserves the original
728        self.factories.values().map(|f| f.name())
729    }
730
731    /// Build the chain for one direction.
732    ///
733    /// Entries that do not apply to `meta.direction` are skipped. On the
734    /// sink-to-source path the survivors are mirrored, so a declaration
735    /// `[a, b]` nests as `a(b(payload))` in both directions.
736    pub fn build(
737        &self,
738        specs: &[PluginSpec],
739        meta: &PipelineMeta,
740        host: &mut dyn HostBuilder,
741    ) -> Result<Chain> {
742        let mut selected: Vec<&PluginSpec> = specs
743            .iter()
744            .filter(|spec| spec.direction.contains(meta.direction))
745            .collect();
746
747        if meta.direction == Direction::SinkToSource {
748            selected.reverse();
749        }
750
751        let display = display_names(&selected);
752
753        // Neighbours, as seen on this path: the upstream endpoint, every
754        // stage, then the downstream endpoint. Stage `i` sits at `i + 1`.
755        let mut labels = Vec::with_capacity(display.len() + 2);
756        labels.push(meta.upstream().to_string());
757        labels.extend(display.iter().cloned());
758        labels.push(meta.downstream().to_string());
759
760        let total = selected.len();
761        let mut segments: Vec<Segment> = Vec::new();
762        let mut draft: Option<SegmentDraft> = None;
763
764        for (index, spec) in selected.iter().enumerate() {
765            let factory = self
766                .get(&spec.name)
767                .ok_or_else(|| PluginError::unknown(&spec.name, self.names()))?
768                .clone();
769
770            let execution = match spec.detach {
771                Some(true) => Execution::Detached,
772                Some(false) => Execution::Inline,
773                None => factory.execution(),
774            };
775
776            let stage_info = StageInfo {
777                index,
778                total,
779                name: &display[index],
780                upstream: &labels[index],
781                downstream: &labels[index + 2],
782            };
783
784            let mut ctx = BuildCtx::new(&spec.name, &spec.config, meta, stage_info, host);
785
786            match factory.build(&mut ctx)? {
787                Stage::Filter(plugin) => {
788                    // A detached stage starts a new segment; so does the first
789                    // stage after a subprocess, since it cannot run inside one.
790                    if draft.is_none() || execution == Execution::Detached {
791                        if let Some(ready) = draft.take() {
792                            segments.push(Segment::Inline(ready.into_pipeline(meta.clone())));
793                        }
794                        draft = Some(SegmentDraft::default());
795                    }
796
797                    draft
798                        .as_mut()
799                        .expect("a draft was just ensured")
800                        .push(plugin, display[index].clone());
801                }
802                Stage::External(external) => {
803                    if spec.detach == Some(false) {
804                        return Err(PluginError::config(
805                            &spec.name,
806                            "runs as a subprocess and always has its own task; `detach = false` \
807                             cannot be honoured",
808                        ));
809                    }
810
811                    if let Some(ready) = draft.take() {
812                        segments.push(Segment::Inline(ready.into_pipeline(meta.clone())));
813                    }
814
815                    segments.push(Segment::Process(external));
816                }
817            }
818        }
819
820        if let Some(ready) = draft.take() {
821            segments.push(Segment::Inline(ready.into_pipeline(meta.clone())));
822        }
823
824        Ok(Chain::new(meta.clone(), segments))
825    }
826
827    /// Build both directions at once. Returns `(source_to_sink,
828    /// sink_to_source)`.
829    ///
830    /// Both share one `host`, which is how identical side-channel targets end
831    /// up sharing a single writer.
832    pub fn build_pair(
833        &self,
834        specs: &[PluginSpec],
835        source: &str,
836        sink: &str,
837        peer: Option<&str>,
838        host: &mut dyn HostBuilder,
839    ) -> Result<(Chain, Chain)> {
840        let forward = PipelineMeta::new(Direction::SourceToSink, source, sink).with_peer(peer);
841        let reverse = PipelineMeta {
842            direction: Direction::SinkToSource,
843            ..forward.clone()
844        };
845
846        Ok((
847            self.build(specs, &forward, host)?,
848            self.build(specs, &reverse, host)?,
849        ))
850    }
851}
852
853/// A segment being assembled: stages and their display names, kept together so
854/// the two vectors cannot drift out of step.
855#[derive(Default)]
856struct SegmentDraft {
857    stages: Vec<Box<dyn Plugin>>,
858    names: Vec<String>,
859}
860
861impl SegmentDraft {
862    fn push(&mut self, plugin: Box<dyn Plugin>, name: String) {
863        self.stages.push(plugin);
864        self.names.push(name);
865    }
866
867    fn into_pipeline(self, meta: PipelineMeta) -> Pipeline {
868        Pipeline::with_names(meta, self.stages, self.names)
869    }
870}
871
872/// Display name per stage: the `as` alias when given, else the plugin name,
873/// with `#n` appended when a name would otherwise appear twice on one path.
874fn display_names(specs: &[&PluginSpec]) -> Vec<String> {
875    let base: Vec<&str> = specs
876        .iter()
877        .map(|spec| spec.alias.as_deref().unwrap_or(spec.name.as_str()))
878        .collect();
879
880    let mut seen: BTreeMap<&str, usize> = BTreeMap::new();
881    for name in &base {
882        *seen.entry(name).or_insert(0) += 1;
883    }
884
885    let mut used: BTreeMap<&str, usize> = BTreeMap::new();
886    base.iter()
887        .map(|name| {
888            if seen.get(name).copied().unwrap_or(0) > 1 {
889                let n = used.entry(name).or_insert(0);
890                *n += 1;
891                format!("{name}#{n}")
892            } else {
893                (*name).to_string()
894            }
895        })
896        .collect()
897}
898
899impl fmt::Debug for Registry {
900    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
901        f.debug_struct("Registry")
902            .field("plugins", &self.names().collect::<Vec<_>>())
903            .finish()
904    }
905}
906
907#[cfg(test)]
908mod tests {
909    use super::*;
910    use crate::{ChannelId, DirectionSpec, plugin::LogLevel};
911
912    #[derive(Default)]
913    struct Recorder {
914        writes: Vec<(ChannelId, Vec<u8>)>,
915        logs: Vec<String>,
916    }
917
918    impl EffectSink for Recorder {
919        fn write(&mut self, channel: ChannelId, bytes: &[u8]) {
920            self.writes.push((channel, bytes.to_vec()));
921        }
922
923        fn log(&mut self, _level: LogLevel, stage: &str, message: &str) {
924            self.logs.push(format!("{stage}: {message}"));
925        }
926    }
927
928    /// Observes without touching the payload.
929    struct Observer;
930
931    impl Plugin for Observer {
932        fn name(&self) -> &str {
933            "observer"
934        }
935
936        fn on_bytes(&mut self, ctx: &mut Ctx<'_>, input: &[u8]) -> Result<()> {
937            ctx.side_write(ChannelId(0), input);
938            ctx.pass_through();
939            Ok(())
940        }
941    }
942
943    struct Upper;
944
945    impl Plugin for Upper {
946        fn name(&self) -> &str {
947            "upper"
948        }
949
950        fn on_bytes(&mut self, ctx: &mut Ctx<'_>, input: &[u8]) -> Result<()> {
951            let upper: Vec<u8> = input.iter().map(u8::to_ascii_uppercase).collect();
952            ctx.forward(&upper);
953            Ok(())
954        }
955    }
956
957    /// Buffers everything, emits it reversed at EOF.
958    #[derive(Default)]
959    struct Reverse(Vec<u8>);
960
961    impl Plugin for Reverse {
962        fn name(&self) -> &str {
963            "reverse"
964        }
965
966        fn on_bytes(&mut self, _ctx: &mut Ctx<'_>, input: &[u8]) -> Result<()> {
967            self.0.extend_from_slice(input);
968            Ok(())
969        }
970
971        fn on_eof(&mut self, ctx: &mut Ctx<'_>) -> Result<()> {
972            let mut buf = std::mem::take(&mut self.0);
973            buf.reverse();
974            ctx.forward(&buf);
975            Ok(())
976        }
977    }
978
979    /// Emits on every tick, the way a keepalive would.
980    struct Beacon(Duration);
981
982    impl Plugin for Beacon {
983        fn name(&self) -> &str {
984            "beacon"
985        }
986
987        fn on_bytes(&mut self, ctx: &mut Ctx<'_>, _input: &[u8]) -> Result<()> {
988            ctx.pass_through();
989            Ok(())
990        }
991
992        fn tick_interval(&self) -> Option<Duration> {
993            Some(self.0)
994        }
995
996        fn on_tick(&mut self, ctx: &mut Ctx<'_>) -> Result<()> {
997            ctx.forward(b"ping");
998            Ok(())
999        }
1000    }
1001
1002    /// Wants ticks but never emits on one. The shape almost every ticking
1003    /// stage actually has.
1004    struct Quiet(Duration);
1005
1006    impl Plugin for Quiet {
1007        fn name(&self) -> &str {
1008            "quiet"
1009        }
1010
1011        fn on_bytes(&mut self, ctx: &mut Ctx<'_>, _input: &[u8]) -> Result<()> {
1012            ctx.pass_through();
1013            Ok(())
1014        }
1015
1016        fn tick_interval(&self) -> Option<Duration> {
1017            Some(self.0)
1018        }
1019
1020        fn on_tick(&mut self, ctx: &mut Ctx<'_>) -> Result<()> {
1021            ctx.log(LogLevel::Info, "still here");
1022            Ok(())
1023        }
1024    }
1025
1026    /// Cuts what it is given into fixed-size units, the way `block` does.
1027    /// Anything left over is emitted at EOF.
1028    struct Chop {
1029        size: usize,
1030        held: Vec<u8>,
1031    }
1032
1033    impl Chop {
1034        fn new(size: usize) -> Self {
1035            Self {
1036                size,
1037                held: Vec::new(),
1038            }
1039        }
1040    }
1041
1042    impl Plugin for Chop {
1043        fn name(&self) -> &str {
1044            "chop"
1045        }
1046
1047        fn on_bytes(&mut self, ctx: &mut Ctx<'_>, input: &[u8]) -> Result<()> {
1048            self.held.extend_from_slice(input);
1049
1050            while self.held.len() >= self.size {
1051                let rest = self.held.split_off(self.size);
1052                ctx.forward(&self.held);
1053                ctx.boundary();
1054                self.held = rest;
1055            }
1056
1057            Ok(())
1058        }
1059
1060        fn on_eof(&mut self, ctx: &mut Ctx<'_>) -> Result<()> {
1061            if !self.held.is_empty() {
1062                let held = std::mem::take(&mut self.held);
1063                ctx.forward(&held);
1064                ctx.boundary();
1065            }
1066
1067            Ok(())
1068        }
1069    }
1070
1071    /// Drops every unit whose first byte is `skip`, and passes the rest
1072    /// through. Enough to break the all-passthrough run part way along.
1073    struct Sieve(u8);
1074
1075    impl Plugin for Sieve {
1076        fn name(&self) -> &str {
1077            "sieve"
1078        }
1079
1080        fn on_bytes(&mut self, ctx: &mut Ctx<'_>, input: &[u8]) -> Result<()> {
1081            if input.first() == Some(&self.0) {
1082                ctx.drop_chunk();
1083            } else {
1084                ctx.pass_through();
1085            }
1086
1087            Ok(())
1088        }
1089    }
1090
1091    /// Passes everything through and writes an epilogue at end of stream, the
1092    /// way a codec closing a frame does.
1093    struct Trailer(&'static [u8]);
1094
1095    impl Plugin for Trailer {
1096        fn name(&self) -> &str {
1097            "trailer"
1098        }
1099
1100        fn on_bytes(&mut self, ctx: &mut Ctx<'_>, _input: &[u8]) -> Result<()> {
1101            ctx.pass_through();
1102            Ok(())
1103        }
1104
1105        fn on_eof(&mut self, ctx: &mut Ctx<'_>) -> Result<()> {
1106            ctx.forward(self.0);
1107            Ok(())
1108        }
1109    }
1110
1111    /// Passes everything through and restarts its own schedule on the way,
1112    /// the way a stage that has just started holding bytes does.
1113    struct Restart(Option<Duration>);
1114
1115    impl Plugin for Restart {
1116        fn name(&self) -> &str {
1117            "restart"
1118        }
1119
1120        fn on_bytes(&mut self, ctx: &mut Ctx<'_>, _input: &[u8]) -> Result<()> {
1121            ctx.rearm();
1122            ctx.pass_through();
1123            Ok(())
1124        }
1125
1126        fn tick_interval(&self) -> Option<Duration> {
1127            self.0
1128        }
1129    }
1130
1131    fn meta() -> PipelineMeta {
1132        PipelineMeta::new(Direction::SourceToSink, "src", "sink")
1133    }
1134
1135    /// Comfortably past any schedule set at construction.
1136    fn later() -> Instant {
1137        Instant::now() + Duration::from_secs(3600)
1138    }
1139
1140    /// Every unit of an emission, for comparing against a literal.
1141    fn parts<'a>(emitted: &Emitted<'a>) -> Vec<&'a [u8]> {
1142        emitted.units().collect()
1143    }
1144
1145    #[test]
1146    fn empty_pipeline_returns_the_input_slice() {
1147        let mut p = Pipeline::new(meta(), Vec::new());
1148        let mut sink = Recorder::default();
1149        let input = b"hello";
1150
1151        let out = p.process(input, &mut sink).unwrap();
1152        assert!(std::ptr::eq(out.bytes().as_ptr(), input.as_ptr()));
1153    }
1154
1155    #[test]
1156    fn observers_never_copy_the_payload() {
1157        let mut p = Pipeline::new(meta(), vec![Box::new(Observer), Box::new(Observer)]);
1158        let mut sink = Recorder::default();
1159        let input = b"payload";
1160
1161        let out = p.process(input, &mut sink).unwrap();
1162
1163        assert!(
1164            std::ptr::eq(out.bytes().as_ptr(), input.as_ptr()),
1165            "a chain of observers must hand the original buffer downstream",
1166        );
1167        assert_eq!(sink.writes.len(), 2);
1168    }
1169
1170    #[test]
1171    fn stages_chain_in_order() {
1172        let mut p = Pipeline::new(meta(), vec![Box::new(Upper), Box::new(Reverse::default())]);
1173        let mut sink = Recorder::default();
1174
1175        assert!(p.process(b"ab", &mut sink).unwrap().is_empty());
1176        assert!(p.process(b"cd", &mut sink).unwrap().is_empty());
1177        assert_eq!(p.finish(&mut sink).unwrap().bytes(), b"DCBA");
1178    }
1179
1180    #[test]
1181    fn repeated_plugins_get_distinct_display_names() {
1182        let specs = [
1183            PluginSpec::new("tee", DirectionSpec::Both),
1184            PluginSpec::new("tee", DirectionSpec::Both).named("audit"),
1185            PluginSpec::new("tee", DirectionSpec::Both),
1186        ];
1187        let refs: Vec<&PluginSpec> = specs.iter().collect();
1188
1189        assert_eq!(display_names(&refs), ["tee#1", "audit", "tee#2"]);
1190    }
1191
1192    #[test]
1193    fn a_pipeline_with_nothing_ticking_has_no_schedule() {
1194        let mut p = Pipeline::new(meta(), vec![Box::new(Observer)]);
1195        let mut sink = Recorder::default();
1196
1197        assert_eq!(p.tick_interval(), None, "so the host builds no timer");
1198        assert!(p.tick(later(), &mut sink).unwrap().is_none());
1199    }
1200
1201    /// One timer for the segment, at the shortest period asked for; the stage
1202    /// that wanted the longer one is simply not due on most wakeups.
1203    #[test]
1204    fn the_schedule_is_the_shortest_period_asked_for() {
1205        let p = Pipeline::new(
1206            meta(),
1207            vec![
1208                Box::new(Quiet(Duration::from_secs(30))),
1209                Box::new(Beacon(Duration::from_secs(5))),
1210            ],
1211        );
1212
1213        assert_eq!(p.tick_interval(), Some(Duration::from_secs(5)));
1214    }
1215
1216    #[test]
1217    fn a_tick_cascades_through_the_stages_below_it() {
1218        let mut p = Pipeline::new(
1219            meta(),
1220            vec![Box::new(Beacon(Duration::from_secs(60))), Box::new(Upper)],
1221        );
1222        let mut sink = Recorder::default();
1223
1224        assert!(
1225            p.tick(Instant::now(), &mut sink).unwrap().is_none(),
1226            "not due yet",
1227        );
1228
1229        let now = later();
1230        assert_eq!(p.tick(now, &mut sink).unwrap().unwrap().bytes(), b"PING");
1231        assert!(
1232            p.tick(now, &mut sink).unwrap().is_none(),
1233            "one turn per stage per wakeup, however far behind the schedule is",
1234        );
1235    }
1236
1237    /// The common case has to stay free: a stage that only reports must not
1238    /// push an empty chunk at everything below it.
1239    #[test]
1240    fn a_silent_tick_does_not_disturb_the_stages_below() {
1241        let mut p = Pipeline::new(
1242            meta(),
1243            vec![Box::new(Quiet(Duration::from_secs(60))), Box::new(Observer)],
1244        );
1245        let mut sink = Recorder::default();
1246
1247        assert!(p.tick(later(), &mut sink).unwrap().unwrap().is_empty());
1248        assert!(sink.writes.is_empty(), "the observer below never ran");
1249        assert_eq!(sink.logs, ["quiet: still here"]);
1250    }
1251
1252    /// Ticking is orthogonal to the data path: a stage above the beacon must
1253    /// not see its output, and the payload must be unaffected.
1254    #[test]
1255    fn ticks_and_chunks_do_not_interfere() {
1256        let mut p = Pipeline::new(
1257            meta(),
1258            vec![
1259                Box::new(Observer),
1260                Box::new(Beacon(Duration::from_secs(60))),
1261            ],
1262        );
1263        let mut sink = Recorder::default();
1264
1265        assert_eq!(
1266            p.tick(later(), &mut sink).unwrap().unwrap().bytes(),
1267            b"ping"
1268        );
1269        assert!(
1270            sink.writes.is_empty(),
1271            "the observer sits above the beacon and saw nothing",
1272        );
1273
1274        assert_eq!(
1275            p.process(b"payload", &mut sink).unwrap().bytes(),
1276            b"payload"
1277        );
1278        assert_eq!(sink.writes, [(ChannelId(0), b"payload".to_vec())]);
1279    }
1280
1281    #[test]
1282    fn two_stages_due_at_once_each_get_a_turn() {
1283        let mut p = Pipeline::new(
1284            meta(),
1285            vec![
1286                Box::new(Beacon(Duration::from_secs(60))),
1287                Box::new(Quiet(Duration::from_secs(60))),
1288            ],
1289        );
1290        let mut sink = Recorder::default();
1291        let now = later();
1292
1293        assert_eq!(p.tick(now, &mut sink).unwrap().unwrap().bytes(), b"ping");
1294        assert!(p.tick(now, &mut sink).unwrap().unwrap().is_empty());
1295        assert!(p.tick(now, &mut sink).unwrap().is_none());
1296    }
1297
1298    #[test]
1299    fn transform_then_observe_keeps_the_transformed_bytes() {
1300        let mut p = Pipeline::new(meta(), vec![Box::new(Upper), Box::new(Observer)]);
1301        let mut sink = Recorder::default();
1302
1303        assert_eq!(p.process(b"hi", &mut sink).unwrap().bytes(), b"HI");
1304        assert_eq!(sink.writes[0].1, b"HI".to_vec());
1305    }
1306
1307    /// Nothing asked for framing, so there is one unit and it is everything.
1308    #[test]
1309    fn an_unframed_emission_is_one_unit() {
1310        let mut p = Pipeline::new(meta(), vec![Box::new(Upper)]);
1311        let mut sink = Recorder::default();
1312
1313        let out = p.process(b"hi", &mut sink).unwrap();
1314        assert_eq!(parts(&out), [b"HI".as_slice()]);
1315    }
1316
1317    #[test]
1318    fn an_empty_emission_has_no_units() {
1319        assert!(Emitted::empty().units().next().is_none());
1320    }
1321
1322    /// The whole point of `boundary`: several units out of one call, and they
1323    /// stay separate rather than fusing into the concatenation.
1324    #[test]
1325    fn a_stage_can_emit_several_units_from_one_chunk() {
1326        let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(2))]);
1327        let mut sink = Recorder::default();
1328
1329        let out = p.process(b"abcdef", &mut sink).unwrap();
1330
1331        assert_eq!(out.bytes(), b"abcdef", "the bytes are still the bytes");
1332        assert_eq!(parts(&out), [b"ab".as_slice(), b"cd", b"ef"]);
1333    }
1334
1335    /// A stage under a framing stage is called once per unit, not once per
1336    /// chunk, and its output stays framed the same way.
1337    #[test]
1338    fn framing_survives_a_stage_that_rewrites_it() {
1339        let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(2)), Box::new(Upper)]);
1340        let mut sink = Recorder::default();
1341
1342        let out = p.process(b"abcdef", &mut sink).unwrap();
1343        assert_eq!(parts(&out), [b"AB".as_slice(), b"CD", b"EF"]);
1344    }
1345
1346    /// The cost model has to hold under framing too: an observer below a
1347    /// framing stage sees each unit and still copies nothing.
1348    #[test]
1349    fn an_observer_under_a_framing_stage_still_copies_nothing() {
1350        let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(2)), Box::new(Observer)]);
1351        let mut sink = Recorder::default();
1352
1353        let out = p.process(b"abcdef", &mut sink).unwrap();
1354
1355        assert_eq!(parts(&out), [b"ab".as_slice(), b"cd", b"ef"]);
1356        assert_eq!(
1357            sink.writes.len(),
1358            3,
1359            "the observer was called once per unit, not once per chunk",
1360        );
1361    }
1362
1363    /// The copy starts at the first unit that is not handed back verbatim, and
1364    /// the units before it have to survive that.
1365    #[test]
1366    fn units_passed_through_before_a_drop_are_kept() {
1367        let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(2)), Box::new(Sieve(b'c'))]);
1368        let mut sink = Recorder::default();
1369
1370        let out = p.process(b"abcdef", &mut sink).unwrap();
1371
1372        assert_eq!(out.bytes(), b"abef");
1373        assert_eq!(parts(&out), [b"ab".as_slice(), b"ef"]);
1374    }
1375
1376    /// Same again with the drop first, which is the case where there is no
1377    /// prefix to keep.
1378    #[test]
1379    fn dropping_the_first_unit_keeps_the_rest() {
1380        let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(2)), Box::new(Sieve(b'a'))]);
1381        let mut sink = Recorder::default();
1382
1383        let out = p.process(b"abcdef", &mut sink).unwrap();
1384
1385        assert_eq!(parts(&out), [b"cd".as_slice(), b"ef"]);
1386    }
1387
1388    /// A stage that emits at EOF after passing every unit through has to force
1389    /// the copy it had been avoiding, or its epilogue would arrive alone.
1390    #[test]
1391    fn an_epilogue_after_a_run_of_passthroughs_keeps_both() {
1392        let mut p = Pipeline::new(
1393            meta(),
1394            vec![Box::new(Chop::new(4)), Box::new(Trailer(b"!"))],
1395        );
1396        let mut sink = Recorder::default();
1397
1398        let out = p.process(b"abcdef", &mut sink).unwrap();
1399        assert_eq!(parts(&out), [b"abcd".as_slice()]);
1400
1401        let out = p.finish(&mut sink).unwrap();
1402        assert_eq!(out.bytes(), b"ef!");
1403        assert_eq!(parts(&out), [b"ef".as_slice(), b"!"]);
1404    }
1405
1406    /// A short tail is held until EOF, and arrives as a unit of its own.
1407    #[test]
1408    fn a_short_final_unit_is_emitted_at_end_of_stream() {
1409        let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(4))]);
1410        let mut sink = Recorder::default();
1411
1412        let out = p.process(b"abcdef", &mut sink).unwrap();
1413        assert_eq!(parts(&out), [b"abcd".as_slice()]);
1414
1415        let out = p.finish(&mut sink).unwrap();
1416        assert_eq!(parts(&out), [b"ef".as_slice()]);
1417    }
1418
1419    /// Two framing stages compose: the second reframes what the first handed
1420    /// it, one unit at a time.
1421    #[test]
1422    fn framing_stages_compose() {
1423        let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(4)), Box::new(Chop::new(2))]);
1424        let mut sink = Recorder::default();
1425
1426        let out = p.process(b"abcdefgh", &mut sink).unwrap();
1427        assert_eq!(parts(&out), [b"ab".as_slice(), b"cd", b"ef", b"gh"]);
1428    }
1429
1430    /// Without a rearm the schedule is a cadence: it advances from wherever
1431    /// it had reached, which says nothing about how long the stage has
1432    /// actually been waiting. A rearm makes the interval mean "from now".
1433    #[test]
1434    fn a_stage_can_restart_its_own_schedule() {
1435        let period = Duration::from_secs(600);
1436        let mut p = Pipeline::new(meta(), vec![Box::new(Restart(Some(period)))]);
1437        let mut sink = Recorder::default();
1438        let start = Instant::now();
1439
1440        // Fires, which moves the cadence on to two periods from construction.
1441        assert!(
1442            p.tick(start + period + Duration::from_secs(100), &mut sink)
1443                .unwrap()
1444                .is_some(),
1445        );
1446        assert!(
1447            p.tick(start + period + Duration::from_secs(200), &mut sink)
1448                .unwrap()
1449                .is_none(),
1450            "the cadence has moved past this",
1451        );
1452
1453        p.process(b"payload", &mut sink).unwrap();
1454
1455        assert!(
1456            p.tick(start + period + Duration::from_secs(300), &mut sink)
1457                .unwrap()
1458                .is_some(),
1459            "the chunk restarted the schedule, so a period from now is due \
1460             again well before the cadence would have come round",
1461        );
1462    }
1463
1464    #[test]
1465    fn rearming_a_stage_that_asked_for_no_ticks_does_nothing() {
1466        let mut p = Pipeline::new(meta(), vec![Box::new(Restart(None))]);
1467        let mut sink = Recorder::default();
1468
1469        assert_eq!(
1470            p.process(b"payload", &mut sink).unwrap().bytes(),
1471            b"payload"
1472        );
1473        assert!(p.tick(later(), &mut sink).unwrap().is_none());
1474    }
1475
1476    /// Buffering below a framing stage still works: the stage sees each unit
1477    /// and answers whenever it has something to say.
1478    #[test]
1479    fn a_buffering_stage_under_a_framing_stage_holds_across_units() {
1480        let mut p = Pipeline::new(
1481            meta(),
1482            vec![Box::new(Chop::new(2)), Box::new(Reverse::default())],
1483        );
1484        let mut sink = Recorder::default();
1485
1486        assert!(p.process(b"abcd", &mut sink).unwrap().is_empty());
1487        assert_eq!(p.finish(&mut sink).unwrap().bytes(), b"dcba");
1488    }
1489}