#![cfg(feature = "std")]
use core::time::Duration;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use proptest::prelude::*;
use pipe_io::sink::VecSink;
use pipe_io::{
Batch, BatchPolicy, Clock, ErrorPolicy, Pipeline, StageFailure, Window, WindowPolicy,
};
fn arb_small_vec<T>(elem: impl Strategy<Value = T>) -> impl Strategy<Value = Vec<T>>
where
T: Clone + std::fmt::Debug + 'static,
{
prop::collection::vec(elem, 0..32)
}
#[derive(Clone)]
struct ScriptedClock {
times: Arc<Mutex<std::vec::IntoIter<Instant>>>,
fallback: Arc<Mutex<Instant>>,
}
impl ScriptedClock {
fn new(times: Vec<Instant>) -> Self {
let last = *times.last().expect("at least one time");
Self {
times: Arc::new(Mutex::new(times.into_iter())),
fallback: Arc::new(Mutex::new(last)),
}
}
}
impl Clock for ScriptedClock {
fn now(&self) -> Instant {
let mut iter = self.times.lock().unwrap();
if let Some(t) = iter.next() {
*self.fallback.lock().unwrap() = t;
t
} else {
*self.fallback.lock().unwrap()
}
}
}
proptest! {
#[test]
fn map_preserves_length(items in arb_small_vec(any::<i32>())) {
let n = items.len();
let sink = VecSink::<i64>::new();
let handle = sink.handle();
Pipeline::from_iter(items)
.map(|x: i32| i64::from(x).wrapping_mul(2))
.sink(sink)
.run()
.unwrap();
prop_assert_eq!(handle.take().len(), n);
}
#[test]
fn filter_admits_only_satisfying_items(items in arb_small_vec(any::<i32>())) {
let expected: Vec<i32> = items.iter().copied().filter(|n| *n >= 0).collect();
let sink = VecSink::<i32>::new();
let handle = sink.handle();
Pipeline::from_iter(items)
.filter(|n: &i32| *n >= 0)
.sink(sink)
.run()
.unwrap();
prop_assert_eq!(handle.take(), expected);
}
#[test]
fn pipeline_preserves_order(items in arb_small_vec(any::<i32>())) {
let sink = VecSink::<i32>::new();
let handle = sink.handle();
Pipeline::from_iter(items.clone())
.sink(sink)
.run()
.unwrap();
prop_assert_eq!(handle.take(), items);
}
#[test]
fn batching_is_lossless(items in arb_small_vec(any::<u32>()), batch_size in 1usize..16) {
let sink = VecSink::<Vec<u32>>::new();
let handle = sink.handle();
Pipeline::from_iter(items.clone())
.batch(BatchPolicy::new().max_items(batch_size))
.map(|b: Batch<u32>| b.into_inner())
.sink(sink)
.run()
.unwrap();
let flattened: Vec<u32> = handle.take().into_iter().flatten().collect();
prop_assert_eq!(flattened, items);
}
#[test]
fn batches_respect_max_items(items in arb_small_vec(any::<u32>()), batch_size in 1usize..16) {
let sink = VecSink::<Vec<u32>>::new();
let handle = sink.handle();
Pipeline::from_iter(items)
.batch(BatchPolicy::new().max_items(batch_size))
.map(|b: Batch<u32>| b.into_inner())
.sink(sink)
.run()
.unwrap();
for batch in handle.take() {
prop_assert!(batch.len() <= batch_size);
prop_assert!(!batch.is_empty());
}
}
#[test]
fn tumbling_windowing_is_lossless(items in arb_small_vec(any::<u32>())) {
if items.is_empty() {
return Ok(());
}
let t0 = Instant::now();
let mut times: Vec<Instant> = (0..items.len())
.map(|i| t0 + Duration::from_millis(i as u64 * 100))
.collect();
times.push(t0 + Duration::from_secs(10));
let clock = ScriptedClock::new(times);
let sink = VecSink::<Vec<u32>>::new();
let handle = sink.handle();
Pipeline::from_iter(items.clone())
.window_with(WindowPolicy::Tumbling { size: Duration::from_secs(1) }, clock)
.map(|w: Window<u32>| w.into_inner())
.sink(sink)
.run()
.unwrap();
let flat: Vec<u32> = handle.take().into_iter().flatten().collect();
prop_assert_eq!(flat, items);
}
#[test]
fn tumbling_window_times_are_monotonic(items in arb_small_vec(any::<u32>())) {
if items.is_empty() {
return Ok(());
}
let t0 = Instant::now();
let mut times: Vec<Instant> = (0..items.len())
.map(|i| t0 + Duration::from_millis(i as u64 * 100))
.collect();
times.push(t0 + Duration::from_secs(10));
let clock = ScriptedClock::new(times);
let sink = VecSink::<(Instant, Instant)>::new();
let handle = sink.handle();
Pipeline::from_iter(items)
.window_with(WindowPolicy::Tumbling { size: Duration::from_secs(1) }, clock)
.map(|w: Window<u32>| (w.start(), w.end()))
.sink(sink)
.run()
.unwrap();
for (start, end) in handle.take() {
prop_assert!(start <= end);
}
}
#[test]
fn continue_policy_swallows_errors(items in arb_small_vec(any::<i32>())) {
let sink = VecSink::<i32>::new();
let handle = sink.handle();
let result = Pipeline::from_iter(items.clone())
.on_error(ErrorPolicy::Continue)
.try_map(|n: i32| -> Result<i32, &'static str> {
if n.is_negative() { Err("no negatives") } else { Ok(n) }
})
.sink(sink)
.run();
prop_assert!(result.is_ok());
let expected: Vec<i32> = items.into_iter().filter(|n| !n.is_negative()).collect();
prop_assert_eq!(handle.take(), expected);
}
#[test]
fn dead_letter_partitions_items(items in arb_small_vec(any::<i32>())) {
let main = VecSink::<i32>::new();
let main_h = main.handle();
let dlq = VecSink::<StageFailure>::new();
let dlq_h = dlq.handle();
Pipeline::from_iter(items.clone())
.on_error(ErrorPolicy::DeadLetter)
.try_map(|n: i32| -> Result<i32, &'static str> {
if n.is_negative() { Err("no negatives") } else { Ok(n) }
})
.dead_letter(dlq)
.sink(main)
.run()
.unwrap();
let successes = main_h.take();
let failures = dlq_h.take();
prop_assert_eq!(successes.len() + failures.len(), items.len());
let expected_ok: Vec<i32> = items.iter().copied().filter(|n| !n.is_negative()).collect();
prop_assert_eq!(successes, expected_ok);
for f in &failures {
prop_assert_eq!(f.stage.as_str(), "try_map");
}
}
#[test]
fn run_and_run_with_sync_agree(items in arb_small_vec(any::<i32>())) {
let sink1 = VecSink::<i32>::new();
let h1 = sink1.handle();
Pipeline::from_iter(items.clone())
.map(|n: i32| n.wrapping_mul(2))
.sink(sink1)
.run()
.unwrap();
let sink2 = VecSink::<i32>::new();
let h2 = sink2.handle();
Pipeline::from_iter(items)
.map(|n: i32| n.wrapping_mul(2))
.sink(sink2)
.run_with(pipe_io::driver::SyncDriver::new())
.unwrap();
prop_assert_eq!(h1.take(), h2.take());
}
#[test]
fn run_threaded_agrees_with_sync(items in arb_small_vec(any::<i32>())) {
let sink1 = VecSink::<i32>::new();
let h1 = sink1.handle();
Pipeline::from_iter(items.clone())
.filter(|n: &i32| *n >= 0)
.map(|n: i32| n.wrapping_add(1))
.sink(sink1)
.run()
.unwrap();
let sink2 = VecSink::<i32>::new();
let h2 = sink2.handle();
Pipeline::from_iter(items)
.filter(|n: &i32| *n >= 0)
.map(|n: i32| n.wrapping_add(1))
.sink(sink2)
.run_threaded()
.unwrap();
prop_assert_eq!(h1.take(), h2.take());
}
}