use std::{
collections::BTreeMap,
fmt,
sync::Arc,
time::{Duration, Instant},
};
use crate::{
Direction, PluginSpec,
channel::HostBuilder,
error::{PluginError, Result},
normalize,
plugin::{
BuildCtx, Ctx, EffectSink, Emission, Emit, Execution, ExternalStage, PipelineMeta, Plugin,
PluginFactory, Stage, StageInfo,
},
};
const EMPTY: &[u8] = &[];
const NO_BOUNDS: &[usize] = &[];
#[derive(Clone, Copy, PartialEq, Eq)]
enum Slot {
Input,
A,
B,
}
#[derive(Debug, Clone, Copy)]
pub struct Emitted<'p> {
bytes: &'p [u8],
bounds: &'p [usize],
}
impl<'p> Emitted<'p> {
#[must_use]
pub const fn whole(bytes: &'p [u8]) -> Self {
Self {
bytes,
bounds: NO_BOUNDS,
}
}
#[must_use]
pub const fn empty() -> Self {
Self::whole(EMPTY)
}
#[must_use]
pub const fn bytes(&self) -> &'p [u8] {
self.bytes
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
#[must_use]
pub const fn len(&self) -> usize {
self.bytes.len()
}
pub fn units(self) -> impl Iterator<Item = &'p [u8]> {
let Self { bytes, bounds } = self;
let unframed = bounds.is_empty() && !bytes.is_empty();
units(bytes, bounds).chain(unframed.then_some(bytes))
}
}
type Halves<'b> = (Slot, &'b [u8], &'b [usize], &'b mut Emission);
#[derive(Default)]
struct Buffers {
a: Emission,
b: Emission,
}
impl Buffers {
fn borrow<'b>(&'b mut self, live: Slot, input: &'b [u8]) -> Halves<'b> {
match live {
Slot::Input => (Slot::A, input, NO_BOUNDS, &mut self.a),
Slot::A => (Slot::B, self.a.bytes(), self.a.bounds(), &mut self.b),
Slot::B => (Slot::A, self.b.bytes(), self.b.bounds(), &mut self.a),
}
}
fn live<'b>(&'b self, live: Slot, input: &'b [u8]) -> (&'b [u8], &'b [usize]) {
match live {
Slot::Input => (input, NO_BOUNDS),
Slot::A => (self.a.bytes(), self.a.bounds()),
Slot::B => (self.b.bytes(), self.b.bounds()),
}
}
}
pub struct Pipeline {
meta: PipelineMeta,
stages: Vec<Box<dyn Plugin>>,
names: Vec<String>,
bufs: Buffers,
ticks: Vec<Schedule>,
}
struct Schedule {
stage: usize,
period: Duration,
next: Instant,
}
impl Pipeline {
#[must_use]
pub fn new(meta: PipelineMeta, stages: Vec<Box<dyn Plugin>>) -> Self {
let names = stages.iter().map(|s| s.name().to_string()).collect();
Self::with_names(meta, stages, names)
}
#[must_use]
pub fn with_names(
meta: PipelineMeta,
stages: Vec<Box<dyn Plugin>>,
names: Vec<String>,
) -> Self {
debug_assert_eq!(stages.len(), names.len());
let start = Instant::now();
let ticks = stages
.iter()
.enumerate()
.filter_map(|(stage, plugin)| {
let period = plugin.tick_interval().filter(|p| !p.is_zero())?;
Some(Schedule {
stage,
period,
next: start + period,
})
})
.collect();
Self {
meta,
stages,
names,
bufs: Buffers::default(),
ticks,
}
}
#[must_use]
pub fn meta(&self) -> &PipelineMeta {
&self.meta
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.stages.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.stages.len()
}
pub fn stage_names(&self) -> impl Iterator<Item = &str> {
self.names.iter().map(String::as_str)
}
#[must_use]
pub fn tick_interval(&self) -> Option<Duration> {
self.ticks.iter().map(|schedule| schedule.period).min()
}
fn due(&mut self, now: Instant) -> Option<usize> {
let schedule = self
.ticks
.iter_mut()
.find(|schedule| schedule.next <= now)?;
schedule.next += schedule.period;
if schedule.next <= now {
schedule.next = now + schedule.period;
}
Some(schedule.stage)
}
fn rearm(&mut self, stage: usize) {
if let Some(schedule) = self.ticks.iter_mut().find(|s| s.stage == stage) {
schedule.next = Instant::now() + schedule.period;
}
}
pub fn tick<'p>(
&'p mut self,
now: Instant,
sink: &mut dyn EffectSink,
) -> Result<Option<Emitted<'p>>> {
let Some(index) = self.due(now) else {
return Ok(None);
};
run_tick(
&mut self.stages[index],
&self.meta,
&self.names[index],
&mut self.bufs.a,
sink,
)?;
if self.bufs.a.rearm_requested() {
self.rearm(index);
}
if self.bufs.a.bytes().is_empty() {
return Ok(Some(Emitted::empty()));
}
self.drive(EMPTY, index + 1, Slot::A, false, sink).map(Some)
}
#[must_use]
pub fn datagram_hazard(&self) -> Option<&str> {
self.stages
.iter()
.zip(&self.names)
.find(|(stage, _)| !stage.datagram_safe())
.map(|(_, name)| name.as_str())
}
pub fn process<'p>(
&'p mut self,
input: &'p [u8],
sink: &mut dyn EffectSink,
) -> Result<Emitted<'p>> {
self.drive(input, 0, Slot::Input, false, sink)
}
pub fn finish<'p>(&'p mut self, sink: &mut dyn EffectSink) -> Result<Emitted<'p>> {
self.drive(EMPTY, 0, Slot::Input, true, sink)
}
fn drive<'p>(
&'p mut self,
input: &'p [u8],
from: usize,
live: Slot,
eof: bool,
sink: &mut dyn EffectSink,
) -> Result<Emitted<'p>> {
let mut live = live;
for index in from..self.stages.len() {
let (slot, src, src_bounds, dst) = self.bufs.borrow(live, input);
run(
&mut self.stages[index],
&self.meta,
&self.names[index],
src,
src_bounds,
dst,
sink,
eof,
)?;
let emitted = dst.emit();
let rearm = dst.rearm_requested();
if rearm {
self.rearm(index);
}
if emitted != Emit::Passthrough {
live = slot;
}
if !eof && self.bufs.live(live, input).0.is_empty() {
return Ok(Emitted::empty());
}
}
let (bytes, bounds) = self.bufs.live(live, input);
Ok(Emitted { bytes, bounds })
}
}
fn run(
plugin: &mut Box<dyn Plugin>,
meta: &PipelineMeta,
stage: &str,
input: &[u8],
in_bounds: &[usize],
dst: &mut Emission,
sink: &mut dyn EffectSink,
eof: bool,
) -> Result<()> {
dst.reset();
if in_bounds.is_empty() {
{
let mut ctx = Ctx::new(meta, stage, input, dst, sink);
if eof {
if !input.is_empty() {
plugin.on_bytes(&mut ctx, input)?;
}
plugin.on_eof(&mut ctx)?;
} else {
plugin.on_bytes(&mut ctx, input)?;
}
}
if !dst.bounds().is_empty() {
dst.close();
}
return Ok(());
}
let mut copied = false;
for (index, unit) in units(input, in_bounds).enumerate() {
dst.next_unit();
{
let mut ctx = Ctx::new(meta, stage, unit, dst, sink);
plugin.on_bytes(&mut ctx, unit)?;
}
if !copied {
if dst.emit() == Emit::Passthrough {
continue;
}
materialise(input, in_bounds, index, dst);
copied = true;
}
if dst.emit() == Emit::Passthrough {
dst.out.extend_from_slice(unit);
}
dst.close();
}
if eof {
dst.next_unit();
{
let mut ctx = Ctx::new(meta, stage, EMPTY, dst, sink);
plugin.on_eof(&mut ctx)?;
}
if !copied && !dst.bytes().is_empty() {
materialise(input, in_bounds, in_bounds.len(), dst);
copied = true;
}
if copied {
dst.close();
}
}
dst.emit = if copied {
Emit::Buffered
} else {
Emit::Passthrough
};
Ok(())
}
fn run_tick(
plugin: &mut Box<dyn Plugin>,
meta: &PipelineMeta,
stage: &str,
dst: &mut Emission,
sink: &mut dyn EffectSink,
) -> Result<()> {
dst.reset();
{
let mut ctx = Ctx::new(meta, stage, EMPTY, dst, sink);
plugin.on_tick(&mut ctx)?;
}
if !dst.bounds().is_empty() {
dst.close();
}
Ok(())
}
fn units<'a>(bytes: &'a [u8], bounds: &'a [usize]) -> impl Iterator<Item = &'a [u8]> {
let mut start = 0;
bounds.iter().map(move |&end| {
let unit = &bytes[start..end];
start = end;
unit
})
}
fn materialise(input: &[u8], in_bounds: &[usize], done: usize, dst: &mut Emission) {
let prefix = if done == 0 { 0 } else { in_bounds[done - 1] };
if prefix == 0 {
return;
}
dst.out.splice(0..0, input[..prefix].iter().copied());
for bound in dst.bounds.iter_mut() {
*bound += prefix;
}
dst.bounds.splice(0..0, in_bounds[..done].iter().copied());
}
impl fmt::Debug for Pipeline {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Pipeline")
.field("direction", &self.meta.direction)
.field("stages", &self.stage_names().collect::<Vec<_>>())
.finish()
}
}
#[derive(Debug)]
pub enum Segment {
Inline(Pipeline),
Process(ExternalStage),
}
#[derive(Debug)]
pub struct Chain {
meta: PipelineMeta,
segments: Vec<Segment>,
}
impl Chain {
#[must_use]
pub fn new(meta: PipelineMeta, segments: Vec<Segment>) -> Self {
Self { meta, segments }
}
#[must_use]
pub fn meta(&self) -> &PipelineMeta {
&self.meta
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.segments.is_empty()
}
#[must_use]
pub fn segments(&self) -> &[Segment] {
&self.segments
}
#[must_use]
pub fn into_segments(self) -> Vec<Segment> {
self.segments
}
#[must_use]
pub fn datagram_hazard(&self) -> Option<&str> {
self.segments().iter().find_map(|segment| match segment {
Segment::Inline(pipeline) => pipeline.datagram_hazard(),
Segment::Process(external) => Some(external.name.as_str()),
})
}
#[must_use]
pub fn stage_names(&self) -> Vec<&str> {
self.segments
.iter()
.flat_map(|segment| match segment {
Segment::Inline(pipeline) => pipeline.stage_names().collect::<Vec<_>>(),
Segment::Process(external) => vec![external.name.as_str()],
})
.collect()
}
}
#[derive(Default)]
pub struct Registry {
factories: BTreeMap<String, Arc<dyn PluginFactory>>,
}
impl Registry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, factory: impl PluginFactory) -> &mut Self {
self.register_arc(Arc::new(factory))
}
pub fn register_arc(&mut self, factory: Arc<dyn PluginFactory>) -> &mut Self {
self.factories.insert(normalize(factory.name()), factory);
self
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&Arc<dyn PluginFactory>> {
self.factories.get(&normalize(name))
}
pub fn iter(&self) -> impl Iterator<Item = &Arc<dyn PluginFactory>> {
self.factories.values()
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.factories.values().map(|f| f.name())
}
pub fn build(
&self,
specs: &[PluginSpec],
meta: &PipelineMeta,
host: &mut dyn HostBuilder,
) -> Result<Chain> {
let mut selected: Vec<&PluginSpec> = specs
.iter()
.filter(|spec| spec.direction.contains(meta.direction))
.collect();
if meta.direction == Direction::SinkToSource {
selected.reverse();
}
let display = display_names(&selected);
let mut labels = Vec::with_capacity(display.len() + 2);
labels.push(meta.upstream().to_string());
labels.extend(display.iter().cloned());
labels.push(meta.downstream().to_string());
let total = selected.len();
let mut segments: Vec<Segment> = Vec::new();
let mut draft: Option<SegmentDraft> = None;
for (index, spec) in selected.iter().enumerate() {
let factory = self
.get(&spec.name)
.ok_or_else(|| PluginError::unknown(&spec.name, self.names()))?
.clone();
let execution = match spec.detach {
Some(true) => Execution::Detached,
Some(false) => Execution::Inline,
None => factory.execution(),
};
let stage_info = StageInfo {
index,
total,
name: &display[index],
upstream: &labels[index],
downstream: &labels[index + 2],
};
let mut ctx = BuildCtx::new(&spec.name, &spec.config, meta, stage_info, host);
match factory.build(&mut ctx)? {
Stage::Filter(plugin) => {
if draft.is_none() || execution == Execution::Detached {
if let Some(ready) = draft.take() {
segments.push(Segment::Inline(ready.into_pipeline(meta.clone())));
}
draft = Some(SegmentDraft::default());
}
draft
.as_mut()
.expect("a draft was just ensured")
.push(plugin, display[index].clone());
}
Stage::External(external) => {
if spec.detach == Some(false) {
return Err(PluginError::config(
&spec.name,
"runs as a subprocess and always has its own task; `detach = false` \
cannot be honoured",
));
}
if let Some(ready) = draft.take() {
segments.push(Segment::Inline(ready.into_pipeline(meta.clone())));
}
segments.push(Segment::Process(external));
}
}
}
if let Some(ready) = draft.take() {
segments.push(Segment::Inline(ready.into_pipeline(meta.clone())));
}
Ok(Chain::new(meta.clone(), segments))
}
pub fn build_pair(
&self,
specs: &[PluginSpec],
source: &str,
sink: &str,
peer: Option<&str>,
host: &mut dyn HostBuilder,
) -> Result<(Chain, Chain)> {
let forward = PipelineMeta::new(Direction::SourceToSink, source, sink).with_peer(peer);
let reverse = PipelineMeta {
direction: Direction::SinkToSource,
..forward.clone()
};
Ok((
self.build(specs, &forward, host)?,
self.build(specs, &reverse, host)?,
))
}
}
#[derive(Default)]
struct SegmentDraft {
stages: Vec<Box<dyn Plugin>>,
names: Vec<String>,
}
impl SegmentDraft {
fn push(&mut self, plugin: Box<dyn Plugin>, name: String) {
self.stages.push(plugin);
self.names.push(name);
}
fn into_pipeline(self, meta: PipelineMeta) -> Pipeline {
Pipeline::with_names(meta, self.stages, self.names)
}
}
fn display_names(specs: &[&PluginSpec]) -> Vec<String> {
let base: Vec<&str> = specs
.iter()
.map(|spec| spec.alias.as_deref().unwrap_or(spec.name.as_str()))
.collect();
let mut seen: BTreeMap<&str, usize> = BTreeMap::new();
for name in &base {
*seen.entry(name).or_insert(0) += 1;
}
let mut used: BTreeMap<&str, usize> = BTreeMap::new();
base.iter()
.map(|name| {
if seen.get(name).copied().unwrap_or(0) > 1 {
let n = used.entry(name).or_insert(0);
*n += 1;
format!("{name}#{n}")
} else {
(*name).to_string()
}
})
.collect()
}
impl fmt::Debug for Registry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Registry")
.field("plugins", &self.names().collect::<Vec<_>>())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ChannelId, DirectionSpec, plugin::LogLevel};
#[derive(Default)]
struct Recorder {
writes: Vec<(ChannelId, Vec<u8>)>,
logs: Vec<String>,
}
impl EffectSink for Recorder {
fn write(&mut self, channel: ChannelId, bytes: &[u8]) {
self.writes.push((channel, bytes.to_vec()));
}
fn log(&mut self, _level: LogLevel, stage: &str, message: &str) {
self.logs.push(format!("{stage}: {message}"));
}
}
struct Observer;
impl Plugin for Observer {
fn name(&self) -> &str {
"observer"
}
fn on_bytes(&mut self, ctx: &mut Ctx<'_>, input: &[u8]) -> Result<()> {
ctx.side_write(ChannelId(0), input);
ctx.pass_through();
Ok(())
}
}
struct Upper;
impl Plugin for Upper {
fn name(&self) -> &str {
"upper"
}
fn on_bytes(&mut self, ctx: &mut Ctx<'_>, input: &[u8]) -> Result<()> {
let upper: Vec<u8> = input.iter().map(u8::to_ascii_uppercase).collect();
ctx.forward(&upper);
Ok(())
}
}
#[derive(Default)]
struct Reverse(Vec<u8>);
impl Plugin for Reverse {
fn name(&self) -> &str {
"reverse"
}
fn on_bytes(&mut self, _ctx: &mut Ctx<'_>, input: &[u8]) -> Result<()> {
self.0.extend_from_slice(input);
Ok(())
}
fn on_eof(&mut self, ctx: &mut Ctx<'_>) -> Result<()> {
let mut buf = std::mem::take(&mut self.0);
buf.reverse();
ctx.forward(&buf);
Ok(())
}
}
struct Beacon(Duration);
impl Plugin for Beacon {
fn name(&self) -> &str {
"beacon"
}
fn on_bytes(&mut self, ctx: &mut Ctx<'_>, _input: &[u8]) -> Result<()> {
ctx.pass_through();
Ok(())
}
fn tick_interval(&self) -> Option<Duration> {
Some(self.0)
}
fn on_tick(&mut self, ctx: &mut Ctx<'_>) -> Result<()> {
ctx.forward(b"ping");
Ok(())
}
}
struct Quiet(Duration);
impl Plugin for Quiet {
fn name(&self) -> &str {
"quiet"
}
fn on_bytes(&mut self, ctx: &mut Ctx<'_>, _input: &[u8]) -> Result<()> {
ctx.pass_through();
Ok(())
}
fn tick_interval(&self) -> Option<Duration> {
Some(self.0)
}
fn on_tick(&mut self, ctx: &mut Ctx<'_>) -> Result<()> {
ctx.log(LogLevel::Info, "still here");
Ok(())
}
}
struct Chop {
size: usize,
held: Vec<u8>,
}
impl Chop {
fn new(size: usize) -> Self {
Self {
size,
held: Vec::new(),
}
}
}
impl Plugin for Chop {
fn name(&self) -> &str {
"chop"
}
fn on_bytes(&mut self, ctx: &mut Ctx<'_>, input: &[u8]) -> Result<()> {
self.held.extend_from_slice(input);
while self.held.len() >= self.size {
let rest = self.held.split_off(self.size);
ctx.forward(&self.held);
ctx.boundary();
self.held = rest;
}
Ok(())
}
fn on_eof(&mut self, ctx: &mut Ctx<'_>) -> Result<()> {
if !self.held.is_empty() {
let held = std::mem::take(&mut self.held);
ctx.forward(&held);
ctx.boundary();
}
Ok(())
}
}
struct Sieve(u8);
impl Plugin for Sieve {
fn name(&self) -> &str {
"sieve"
}
fn on_bytes(&mut self, ctx: &mut Ctx<'_>, input: &[u8]) -> Result<()> {
if input.first() == Some(&self.0) {
ctx.drop_chunk();
} else {
ctx.pass_through();
}
Ok(())
}
}
struct Trailer(&'static [u8]);
impl Plugin for Trailer {
fn name(&self) -> &str {
"trailer"
}
fn on_bytes(&mut self, ctx: &mut Ctx<'_>, _input: &[u8]) -> Result<()> {
ctx.pass_through();
Ok(())
}
fn on_eof(&mut self, ctx: &mut Ctx<'_>) -> Result<()> {
ctx.forward(self.0);
Ok(())
}
}
struct Restart(Option<Duration>);
impl Plugin for Restart {
fn name(&self) -> &str {
"restart"
}
fn on_bytes(&mut self, ctx: &mut Ctx<'_>, _input: &[u8]) -> Result<()> {
ctx.rearm();
ctx.pass_through();
Ok(())
}
fn tick_interval(&self) -> Option<Duration> {
self.0
}
}
fn meta() -> PipelineMeta {
PipelineMeta::new(Direction::SourceToSink, "src", "sink")
}
fn later() -> Instant {
Instant::now() + Duration::from_secs(3600)
}
fn parts<'a>(emitted: &Emitted<'a>) -> Vec<&'a [u8]> {
emitted.units().collect()
}
#[test]
fn empty_pipeline_returns_the_input_slice() {
let mut p = Pipeline::new(meta(), Vec::new());
let mut sink = Recorder::default();
let input = b"hello";
let out = p.process(input, &mut sink).unwrap();
assert!(std::ptr::eq(out.bytes().as_ptr(), input.as_ptr()));
}
#[test]
fn observers_never_copy_the_payload() {
let mut p = Pipeline::new(meta(), vec![Box::new(Observer), Box::new(Observer)]);
let mut sink = Recorder::default();
let input = b"payload";
let out = p.process(input, &mut sink).unwrap();
assert!(
std::ptr::eq(out.bytes().as_ptr(), input.as_ptr()),
"a chain of observers must hand the original buffer downstream",
);
assert_eq!(sink.writes.len(), 2);
}
#[test]
fn stages_chain_in_order() {
let mut p = Pipeline::new(meta(), vec![Box::new(Upper), Box::new(Reverse::default())]);
let mut sink = Recorder::default();
assert!(p.process(b"ab", &mut sink).unwrap().is_empty());
assert!(p.process(b"cd", &mut sink).unwrap().is_empty());
assert_eq!(p.finish(&mut sink).unwrap().bytes(), b"DCBA");
}
#[test]
fn repeated_plugins_get_distinct_display_names() {
let specs = [
PluginSpec::new("tee", DirectionSpec::Both),
PluginSpec::new("tee", DirectionSpec::Both).named("audit"),
PluginSpec::new("tee", DirectionSpec::Both),
];
let refs: Vec<&PluginSpec> = specs.iter().collect();
assert_eq!(display_names(&refs), ["tee#1", "audit", "tee#2"]);
}
#[test]
fn a_pipeline_with_nothing_ticking_has_no_schedule() {
let mut p = Pipeline::new(meta(), vec![Box::new(Observer)]);
let mut sink = Recorder::default();
assert_eq!(p.tick_interval(), None, "so the host builds no timer");
assert!(p.tick(later(), &mut sink).unwrap().is_none());
}
#[test]
fn the_schedule_is_the_shortest_period_asked_for() {
let p = Pipeline::new(
meta(),
vec![
Box::new(Quiet(Duration::from_secs(30))),
Box::new(Beacon(Duration::from_secs(5))),
],
);
assert_eq!(p.tick_interval(), Some(Duration::from_secs(5)));
}
#[test]
fn a_tick_cascades_through_the_stages_below_it() {
let mut p = Pipeline::new(
meta(),
vec![Box::new(Beacon(Duration::from_secs(60))), Box::new(Upper)],
);
let mut sink = Recorder::default();
assert!(
p.tick(Instant::now(), &mut sink).unwrap().is_none(),
"not due yet",
);
let now = later();
assert_eq!(p.tick(now, &mut sink).unwrap().unwrap().bytes(), b"PING");
assert!(
p.tick(now, &mut sink).unwrap().is_none(),
"one turn per stage per wakeup, however far behind the schedule is",
);
}
#[test]
fn a_silent_tick_does_not_disturb_the_stages_below() {
let mut p = Pipeline::new(
meta(),
vec![Box::new(Quiet(Duration::from_secs(60))), Box::new(Observer)],
);
let mut sink = Recorder::default();
assert!(p.tick(later(), &mut sink).unwrap().unwrap().is_empty());
assert!(sink.writes.is_empty(), "the observer below never ran");
assert_eq!(sink.logs, ["quiet: still here"]);
}
#[test]
fn ticks_and_chunks_do_not_interfere() {
let mut p = Pipeline::new(
meta(),
vec![
Box::new(Observer),
Box::new(Beacon(Duration::from_secs(60))),
],
);
let mut sink = Recorder::default();
assert_eq!(
p.tick(later(), &mut sink).unwrap().unwrap().bytes(),
b"ping"
);
assert!(
sink.writes.is_empty(),
"the observer sits above the beacon and saw nothing",
);
assert_eq!(
p.process(b"payload", &mut sink).unwrap().bytes(),
b"payload"
);
assert_eq!(sink.writes, [(ChannelId(0), b"payload".to_vec())]);
}
#[test]
fn two_stages_due_at_once_each_get_a_turn() {
let mut p = Pipeline::new(
meta(),
vec![
Box::new(Beacon(Duration::from_secs(60))),
Box::new(Quiet(Duration::from_secs(60))),
],
);
let mut sink = Recorder::default();
let now = later();
assert_eq!(p.tick(now, &mut sink).unwrap().unwrap().bytes(), b"ping");
assert!(p.tick(now, &mut sink).unwrap().unwrap().is_empty());
assert!(p.tick(now, &mut sink).unwrap().is_none());
}
#[test]
fn transform_then_observe_keeps_the_transformed_bytes() {
let mut p = Pipeline::new(meta(), vec![Box::new(Upper), Box::new(Observer)]);
let mut sink = Recorder::default();
assert_eq!(p.process(b"hi", &mut sink).unwrap().bytes(), b"HI");
assert_eq!(sink.writes[0].1, b"HI".to_vec());
}
#[test]
fn an_unframed_emission_is_one_unit() {
let mut p = Pipeline::new(meta(), vec![Box::new(Upper)]);
let mut sink = Recorder::default();
let out = p.process(b"hi", &mut sink).unwrap();
assert_eq!(parts(&out), [b"HI".as_slice()]);
}
#[test]
fn an_empty_emission_has_no_units() {
assert!(Emitted::empty().units().next().is_none());
}
#[test]
fn a_stage_can_emit_several_units_from_one_chunk() {
let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(2))]);
let mut sink = Recorder::default();
let out = p.process(b"abcdef", &mut sink).unwrap();
assert_eq!(out.bytes(), b"abcdef", "the bytes are still the bytes");
assert_eq!(parts(&out), [b"ab".as_slice(), b"cd", b"ef"]);
}
#[test]
fn framing_survives_a_stage_that_rewrites_it() {
let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(2)), Box::new(Upper)]);
let mut sink = Recorder::default();
let out = p.process(b"abcdef", &mut sink).unwrap();
assert_eq!(parts(&out), [b"AB".as_slice(), b"CD", b"EF"]);
}
#[test]
fn an_observer_under_a_framing_stage_still_copies_nothing() {
let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(2)), Box::new(Observer)]);
let mut sink = Recorder::default();
let out = p.process(b"abcdef", &mut sink).unwrap();
assert_eq!(parts(&out), [b"ab".as_slice(), b"cd", b"ef"]);
assert_eq!(
sink.writes.len(),
3,
"the observer was called once per unit, not once per chunk",
);
}
#[test]
fn units_passed_through_before_a_drop_are_kept() {
let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(2)), Box::new(Sieve(b'c'))]);
let mut sink = Recorder::default();
let out = p.process(b"abcdef", &mut sink).unwrap();
assert_eq!(out.bytes(), b"abef");
assert_eq!(parts(&out), [b"ab".as_slice(), b"ef"]);
}
#[test]
fn dropping_the_first_unit_keeps_the_rest() {
let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(2)), Box::new(Sieve(b'a'))]);
let mut sink = Recorder::default();
let out = p.process(b"abcdef", &mut sink).unwrap();
assert_eq!(parts(&out), [b"cd".as_slice(), b"ef"]);
}
#[test]
fn an_epilogue_after_a_run_of_passthroughs_keeps_both() {
let mut p = Pipeline::new(
meta(),
vec![Box::new(Chop::new(4)), Box::new(Trailer(b"!"))],
);
let mut sink = Recorder::default();
let out = p.process(b"abcdef", &mut sink).unwrap();
assert_eq!(parts(&out), [b"abcd".as_slice()]);
let out = p.finish(&mut sink).unwrap();
assert_eq!(out.bytes(), b"ef!");
assert_eq!(parts(&out), [b"ef".as_slice(), b"!"]);
}
#[test]
fn a_short_final_unit_is_emitted_at_end_of_stream() {
let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(4))]);
let mut sink = Recorder::default();
let out = p.process(b"abcdef", &mut sink).unwrap();
assert_eq!(parts(&out), [b"abcd".as_slice()]);
let out = p.finish(&mut sink).unwrap();
assert_eq!(parts(&out), [b"ef".as_slice()]);
}
#[test]
fn framing_stages_compose() {
let mut p = Pipeline::new(meta(), vec![Box::new(Chop::new(4)), Box::new(Chop::new(2))]);
let mut sink = Recorder::default();
let out = p.process(b"abcdefgh", &mut sink).unwrap();
assert_eq!(parts(&out), [b"ab".as_slice(), b"cd", b"ef", b"gh"]);
}
#[test]
fn a_stage_can_restart_its_own_schedule() {
let period = Duration::from_secs(600);
let mut p = Pipeline::new(meta(), vec![Box::new(Restart(Some(period)))]);
let mut sink = Recorder::default();
let start = Instant::now();
assert!(
p.tick(start + period + Duration::from_secs(100), &mut sink)
.unwrap()
.is_some(),
);
assert!(
p.tick(start + period + Duration::from_secs(200), &mut sink)
.unwrap()
.is_none(),
"the cadence has moved past this",
);
p.process(b"payload", &mut sink).unwrap();
assert!(
p.tick(start + period + Duration::from_secs(300), &mut sink)
.unwrap()
.is_some(),
"the chunk restarted the schedule, so a period from now is due \
again well before the cadence would have come round",
);
}
#[test]
fn rearming_a_stage_that_asked_for_no_ticks_does_nothing() {
let mut p = Pipeline::new(meta(), vec![Box::new(Restart(None))]);
let mut sink = Recorder::default();
assert_eq!(
p.process(b"payload", &mut sink).unwrap().bytes(),
b"payload"
);
assert!(p.tick(later(), &mut sink).unwrap().is_none());
}
#[test]
fn a_buffering_stage_under_a_framing_stage_holds_across_units() {
let mut p = Pipeline::new(
meta(),
vec![Box::new(Chop::new(2)), Box::new(Reverse::default())],
);
let mut sink = Recorder::default();
assert!(p.process(b"abcd", &mut sink).unwrap().is_empty());
assert_eq!(p.finish(&mut sink).unwrap().bytes(), b"dcba");
}
}