Skip to main content

spate_test/
feed.rs

1//! Scripted feed for testing feed-reading sources.
2//!
3//! [`scripted_feed`] pairs a [`Feed`] implementation with a [`FeedScript`]
4//! handle. The test scripts deliveries, redeliveries, duplicates,
5//! dead-lettering, publish outcomes, refusals of `receive`, `renew`,
6//! `acknowledge` and `fail`, and depth reads, the source under test reads
7//! through the trait, and the script observes every acknowledgement,
8//! failure, renewal, publish, depth read and stop call. There is no queue and
9//! no clock, and behavior is deterministic.
10
11use spate_core::feed::{
12    Confirmation, Confirmer, Delivery, Depth, DepthOf, DepthReading, Feed, FeedError,
13    FeedErrorKind, FeedWaker, Receipt, Unit, confirmation, depth_reading,
14};
15use spate_core::hash::{FastMap, FastSet};
16use std::collections::{BTreeMap, VecDeque};
17use std::sync::{Arc, Mutex, MutexGuard};
18use std::time::Duration;
19
20/// A reader call [`FeedScript`] can script an error answer for.
21#[derive(Clone, Copy, Debug)]
22enum Call {
23    Receive,
24    Renew,
25    Acknowledge,
26    Fail,
27}
28
29impl Call {
30    fn name(self) -> &'static str {
31        match self {
32            Call::Receive => "receive",
33            Call::Renew => "renew",
34            Call::Acknowledge => "acknowledge",
35            Call::Fail => "fail",
36        }
37    }
38}
39
40#[derive(Default)]
41struct State {
42    /// Scripted deliveries not yet received.
43    queued: VecDeque<Delivery>,
44    /// Deliveries received and not yet acknowledged, failed or lapsed.
45    held: BTreeMap<Receipt, Delivery>,
46    /// Deliveries so far per unit id that `deliver` scripted. `deliver`
47    /// raises it, and so does `redeliver` of a receipt in `scripted`, so
48    /// duplicates and their redeliveries share a count.
49    counts: FastMap<String, u32>,
50    /// Receipts of deliveries `deliver` scripted, and of their redeliveries.
51    scripted: FastSet<Receipt>,
52    next_receipt: u64,
53    delivered: Vec<Delivery>,
54    acknowledged: Vec<Receipt>,
55    failed: Vec<(Receipt, String)>,
56    renewed: Vec<Receipt>,
57    gone: Vec<Receipt>,
58    published: Vec<Unit>,
59    dead_lettered: Vec<Unit>,
60    publish_outcomes: VecDeque<FeedErrorKind>,
61    /// Scripted error answers per [`Call`], indexed by `Call as usize`.
62    answers: [VecDeque<FeedErrorKind>; 4],
63    hold_publishes: bool,
64    held_publishes: Vec<(Unit, Confirmer)>,
65    /// Scripted answers to depth reads of the feed, served first.
66    feed_depths: VecDeque<Result<Depth, FeedError>>,
67    /// Scripted answers to depth reads of the dead-letter destination.
68    dead_letter_depths: VecDeque<Result<Depth, FeedError>>,
69    feed_depth_reads: usize,
70    dead_letter_depth_reads: usize,
71    /// The answer to `hands_back`.
72    hands_back: bool,
73    stopped_receiving: usize,
74    handed_back: Vec<Receipt>,
75    closes: Vec<Duration>,
76    waker: Option<FeedWaker>,
77}
78
79impl State {
80    fn queue(&mut self, unit: Unit, deliveries: u32) -> Receipt {
81        self.next_receipt += 1;
82        let receipt = Receipt(self.next_receipt);
83        let delivery = Delivery::new(unit, receipt, deliveries);
84        self.queued.push_back(delivery);
85        receipt
86    }
87
88    /// Take the delivery `receipt` names out of the queue or the held set.
89    fn lapse(&mut self, receipt: Receipt) -> Option<Delivery> {
90        if let Some(delivery) = self.held.remove(&receipt) {
91            return Some(delivery);
92        }
93        let at = self.queued.iter().position(|d| d.receipt == receipt)?;
94        self.queued.remove(at)
95    }
96
97    /// Answer a call on `receipt`: run `f` on the held delivery, or record
98    /// and return Gone.
99    fn on_held(
100        &mut self,
101        receipt: Receipt,
102        f: impl FnOnce(&mut State) -> Result<(), FeedError>,
103    ) -> Result<(), FeedError> {
104        if self.held.contains_key(&receipt) {
105            f(self)
106        } else {
107            self.gone.push(receipt);
108            Err(FeedError::new(
109                FeedErrorKind::Gone,
110                format!("{receipt} is not held by the scripted feed"),
111            ))
112        }
113    }
114
115    /// Serve the next scripted answer to `call`, if any. The caller returns
116    /// it before touching any other state.
117    fn answer(&mut self, call: Call) -> Result<(), FeedError> {
118        match self.answers[call as usize].pop_front() {
119            Some(kind) => Err(FeedError::new(kind, "refused by the feed script")),
120            None => Ok(()),
121        }
122    }
123
124    fn wake(&self) {
125        if let Some(waker) = &self.waker {
126            waker.wake();
127        }
128    }
129}
130
131impl std::fmt::Debug for State {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        f.debug_struct("State")
134            .field("queued", &self.queued.len())
135            .field("held", &self.held.len())
136            .field("acknowledged", &self.acknowledged.len())
137            .finish_non_exhaustive()
138    }
139}
140
141/// A [`Feed`] whose deliveries, publish outcomes and call refusals are
142/// scripted by the paired [`FeedScript`]. Build both with [`scripted_feed`].
143///
144/// `receive` hands out scripted deliveries in the order they were scripted.
145/// `fail` records the report and lets go of the delivery, and delivers
146/// nothing again on its own: the script decides what the feed does next. A
147/// call with a receipt the feed no longer holds returns
148/// [`Gone`](FeedErrorKind::Gone) and is recorded by
149/// [`FeedScript::gone`]. A scripted refusal of that call is served before
150/// this check. Its [`backend`](Feed::backend) key is `scripted`,
151/// so a pipeline it is passed to names `scripted` in its `feed:` section.
152#[derive(Debug)]
153pub struct ScriptedFeed {
154    state: Arc<Mutex<State>>,
155}
156
157/// Scripting and observation handle for a [`ScriptedFeed`].
158///
159/// Every scripting call wakes the reader, so a source parked on the feed
160/// picks the change up at once.
161#[derive(Clone, Debug)]
162pub struct FeedScript {
163    state: Arc<Mutex<State>>,
164}
165
166/// A scripted feed and its handle.
167///
168/// # Examples
169///
170/// ```
171/// use spate_core::feed::{Feed, FeedErrorKind, Unit};
172///
173/// let (feed, script) = spate_test::scripted_feed();
174/// let first = script.deliver(Unit::new("day-1", b"2026-10-01".to_vec()));
175/// let held = feed.receive(8).unwrap();
176/// assert_eq!(held[0].receipt, first);
177///
178/// // The queue gives the unit to the reader again under a new receipt.
179/// let second = script.redeliver(first);
180/// let again = feed.receive(8).unwrap();
181/// assert_eq!((again[0].receipt, again[0].deliveries), (second, 2));
182///
183/// // The first receipt has lapsed; the second acknowledges the unit.
184/// let lapsed = feed.acknowledge(first).unwrap_err();
185/// assert_eq!(lapsed.kind, FeedErrorKind::Gone);
186/// feed.acknowledge(second).unwrap();
187/// assert_eq!(script.acknowledged(), vec![second]);
188/// assert_eq!(script.gone(), vec![first]);
189/// ```
190#[must_use]
191pub fn scripted_feed() -> (ScriptedFeed, FeedScript) {
192    let state = Arc::new(Mutex::new(State::default()));
193    (
194        ScriptedFeed {
195            state: Arc::clone(&state),
196        },
197        FeedScript { state },
198    )
199}
200
201impl FeedScript {
202    /// Queue a delivery of `unit` for the next `receive`, and return its
203    /// receipt. Delivering a unit whose id this method delivered before is a
204    /// duplicate. [`Delivery::deliveries`] counts the deliveries this method
205    /// scripted for an id and their redeliveries together; a unit published
206    /// through the feed keeps its own count, whatever its id.
207    pub fn deliver(&self, unit: Unit) -> Receipt {
208        let mut state = self.lock();
209        let count = state.counts.entry(unit.id.clone()).or_default();
210        *count += 1;
211        let deliveries = *count;
212        let receipt = state.queue(unit, deliveries);
213        state.scripted.insert(receipt);
214        state.wake();
215        receipt
216    }
217
218    /// Deliver the unit `receipt` names again under a new receipt, as a queue
219    /// does when a lease lapses. The old receipt lapses: every later call
220    /// with it returns [`Gone`](FeedErrorKind::Gone).
221    ///
222    /// # Panics
223    ///
224    /// If `receipt` names no delivery that is queued or held.
225    pub fn redeliver(&self, receipt: Receipt) -> Receipt {
226        let mut state = self.lock();
227        let Some(delivery) = state.lapse(receipt) else {
228            panic!("redeliver: {receipt} is neither queued nor held");
229        };
230        // A unit `deliver` scripted shares one count with its duplicates. A
231        // published unit keeps its own count, whatever its id.
232        let scripted = state.scripted.remove(&receipt);
233        let deliveries = match state.counts.get_mut(&delivery.unit.id) {
234            Some(count) if scripted => {
235                *count += 1;
236                *count
237            }
238            _ => delivery.deliveries + 1,
239        };
240        let receipt = state.queue(delivery.unit, deliveries);
241        if scripted {
242            state.scripted.insert(receipt);
243        }
244        state.wake();
245        receipt
246    }
247
248    /// Move the unit `receipt` names to the dead-letter destination. The
249    /// receipt lapses as for [`redeliver`](Self::redeliver).
250    ///
251    /// # Panics
252    ///
253    /// If `receipt` names no delivery that is queued or held.
254    pub fn dead_letter(&self, receipt: Receipt) {
255        let mut state = self.lock();
256        let Some(delivery) = state.lapse(receipt) else {
257            panic!("dead_letter: {receipt} is neither queued nor held");
258        };
259        state.dead_lettered.push(delivery.unit);
260        state.wake();
261    }
262
263    /// Refuse the next unscripted `publish` with `kind` (repeat to script a
264    /// sequence). A refused unit is never delivered.
265    pub fn refuse_next_publish(&self, kind: FeedErrorKind) {
266        let mut state = self.lock();
267        state.publish_outcomes.push_back(kind);
268        state.wake();
269    }
270
271    /// Answer the next `receive` with `kind` (repeat to script a sequence).
272    ///
273    /// The call changes nothing in the feed: it takes no delivery, so
274    /// [`held`](Self::held) and [`delivered`](Self::delivered) are as before
275    /// it, and it sends no signal. Every kind but `Gone` is accepted,
276    /// including ones the trait does not list for `receive`, to reach a
277    /// reader's arm for them.
278    ///
279    /// # Panics
280    ///
281    /// If `kind` is [`Gone`](FeedErrorKind::Gone), which has no meaning for
282    /// `receive`.
283    pub fn refuse_next_receive(&self, kind: FeedErrorKind) {
284        self.refuse_next(Call::Receive, kind);
285    }
286
287    /// Answer the next `renew` with `kind` (repeat to script a sequence).
288    ///
289    /// The call changes nothing in the feed: [`held`](Self::held) and
290    /// [`renewed`](Self::renewed) are as before it. The answer goes to the
291    /// next `renew` whatever its receipt, a lapsed one included. Every kind
292    /// but `Gone` is accepted, including ones the trait does not list for
293    /// `renew`, to reach a reader's arm for them.
294    ///
295    /// # Panics
296    ///
297    /// If `kind` is [`Gone`](FeedErrorKind::Gone): the feed answers `Gone`
298    /// itself for a receipt it does not hold, and
299    /// [`redeliver`](Self::redeliver) or [`dead_letter`](Self::dead_letter)
300    /// lapses one.
301    pub fn refuse_next_renew(&self, kind: FeedErrorKind) {
302        self.refuse_next(Call::Renew, kind);
303    }
304
305    /// Answer the next `acknowledge` with `kind` (repeat to script a
306    /// sequence).
307    ///
308    /// The call changes nothing in the feed: [`held`](Self::held) and
309    /// [`acknowledged`](Self::acknowledged) are as before it, so the
310    /// delivery stays held for the reader to acknowledge again. The answer
311    /// goes to the next `acknowledge` whatever its receipt. Every kind but
312    /// `Gone` is accepted, including ones the trait does not list for
313    /// `acknowledge`, to reach a reader's arm for them.
314    ///
315    /// # Panics
316    ///
317    /// If `kind` is [`Gone`](FeedErrorKind::Gone): the feed answers `Gone`
318    /// itself for a receipt it does not hold, and
319    /// [`redeliver`](Self::redeliver) or [`dead_letter`](Self::dead_letter)
320    /// lapses one.
321    ///
322    /// # Examples
323    ///
324    /// ```
325    /// use spate_core::feed::{Feed, FeedErrorKind, Unit};
326    ///
327    /// let (feed, script) = spate_test::scripted_feed();
328    /// let receipt = script.deliver(Unit::new("day-1", b"2026-10-01".to_vec()));
329    /// feed.receive(8).unwrap();
330    /// script.refuse_next_acknowledge(FeedErrorKind::Retryable);
331    ///
332    /// let refused = feed.acknowledge(receipt).unwrap_err();
333    /// assert_eq!(refused.kind, FeedErrorKind::Retryable);
334    /// assert_eq!(script.held(), vec![receipt]);
335    /// assert_eq!(script.refusals_left(), 0);
336    ///
337    /// feed.acknowledge(receipt).unwrap();
338    /// assert_eq!(script.acknowledged(), vec![receipt]);
339    /// ```
340    pub fn refuse_next_acknowledge(&self, kind: FeedErrorKind) {
341        self.refuse_next(Call::Acknowledge, kind);
342    }
343
344    /// Answer the next `fail` with `kind` (repeat to script a sequence).
345    ///
346    /// The call changes nothing in the feed: [`held`](Self::held) and
347    /// [`failed`](Self::failed) are as before it, so the delivery stays held
348    /// for the reader to fail again. The answer goes to the next `fail`
349    /// whatever its receipt. Every kind but `Gone` is accepted, including
350    /// ones the trait does not list for `fail`, to reach a reader's arm for
351    /// them.
352    ///
353    /// # Panics
354    ///
355    /// If `kind` is [`Gone`](FeedErrorKind::Gone): the feed answers `Gone`
356    /// itself for a receipt it does not hold, and
357    /// [`redeliver`](Self::redeliver) or [`dead_letter`](Self::dead_letter)
358    /// lapses one.
359    pub fn refuse_next_fail(&self, kind: FeedErrorKind) {
360        self.refuse_next(Call::Fail, kind);
361    }
362
363    /// How many scripted refusals of `publish`, `receive`, `renew`,
364    /// `acknowledge` and `fail` are not yet served.
365    #[must_use]
366    pub fn refusals_left(&self) -> usize {
367        let state = self.lock();
368        state.publish_outcomes.len() + state.answers.iter().map(VecDeque::len).sum::<usize>()
369    }
370
371    fn refuse_next(&self, call: Call, kind: FeedErrorKind) {
372        let why = match call {
373            Call::Receive => "Gone has no meaning for receive",
374            _ => {
375                "the feed answers Gone itself for a receipt it does not hold; \
376                 lapse one with redeliver or dead_letter"
377            }
378        };
379        assert!(
380            kind != FeedErrorKind::Gone,
381            "refuse_next_{}: {why}",
382            call.name()
383        );
384        let mut state = self.lock();
385        state.answers[call as usize].push_back(kind);
386        state.wake();
387    }
388
389    /// Leave every later `publish` unsettled until
390    /// [`confirm_held_publishes`](Self::confirm_held_publishes).
391    pub fn hold_publishes(&self) {
392        let mut state = self.lock();
393        state.hold_publishes = true;
394        state.wake();
395    }
396
397    /// Confirm every held publish, queue a delivery of each unit in publish
398    /// order, and stop holding publishes.
399    pub fn confirm_held_publishes(&self) {
400        let mut state = self.lock();
401        state.hold_publishes = false;
402        for (unit, confirmer) in std::mem::take(&mut state.held_publishes) {
403            confirmer.confirm();
404            state.queue(unit, 1);
405        }
406        state.wake();
407    }
408
409    /// Answer the next unscripted depth read of `of` with `answer`, ahead
410    /// of the depth the feed's own state gives (repeat to script a
411    /// sequence). A read with no scripted answer left reports the units
412    /// queued as visible and the units held as in flight, or, for
413    /// [`DeadLetters`](DepthOf::DeadLetters), the units
414    /// [`dead_letter`](Self::dead_letter) moved as visible, all exact.
415    ///
416    /// A running pipeline makes dead-letter reads of its own, at
417    /// startup and then every
418    /// [`dead_letter_read_interval`](spate_core::pipeline::RuntimeOptions::dead_letter_read_interval).
419    /// A test that scripts or counts the source's dead-letter reads sets that
420    /// option to `None`.
421    ///
422    /// # Examples
423    ///
424    /// ```
425    /// use spate_core::feed::{Depth, DepthOf, Feed, FeedError, FeedErrorKind, Unit};
426    ///
427    /// let (feed, script) = spate_test::scripted_feed();
428    /// let mut peer_held = Depth::new("the scripted feed", true);
429    /// peer_held.in_flight = 1;
430    /// script.answer_depth(DepthOf::Feed, Ok(peer_held.clone()));
431    /// script.answer_depth(
432    ///     DepthOf::Feed,
433    ///     Err(FeedError::new(FeedErrorKind::Retryable, "throttled")),
434    /// );
435    ///
436    /// let read = |of| feed.read_depth(of).unwrap().try_outcome().unwrap();
437    /// assert_eq!(read(DepthOf::Feed).unwrap(), peer_held);
438    /// assert_eq!(read(DepthOf::Feed).unwrap_err().kind, FeedErrorKind::Retryable);
439    ///
440    /// // With the script spent, the feed reports its own state.
441    /// let receipt = script.deliver(Unit::new("a", b"a".to_vec()));
442    /// script.dead_letter(receipt);
443    /// assert!(read(DepthOf::Feed).unwrap().is_empty());
444    /// assert_eq!(read(DepthOf::DeadLetters).unwrap().visible, 1);
445    /// assert_eq!(script.depth_reads(DepthOf::Feed), 3);
446    /// ```
447    pub fn answer_depth(&self, of: DepthOf, answer: Result<Depth, FeedError>) {
448        let mut state = self.lock();
449        match of {
450            DepthOf::DeadLetters => state.dead_letter_depths.push_back(answer),
451            _ => state.feed_depths.push_back(answer),
452        }
453        state.wake();
454    }
455
456    /// Make the feed answer [`hands_back`](Feed::hands_back) with `true`, as a
457    /// backend does whose stopping reader reads to the stop reserve and hands
458    /// back the rest. A scripted feed answers `false` until this is called.
459    ///
460    /// # Examples
461    ///
462    /// ```
463    /// use spate_core::feed::{Feed, Unit};
464    /// use std::time::Duration;
465    ///
466    /// let (feed, script) = spate_test::scripted_feed();
467    /// assert!(!feed.hands_back());
468    /// script.hand_back_on_stop();
469    /// assert!(feed.hands_back());
470    ///
471    /// let receipt = script.deliver(Unit::new("day-1", b"2026-10-01".to_vec()));
472    /// feed.receive(8).unwrap();
473    /// feed.stop_receiving();
474    /// feed.hand_back(receipt).unwrap();
475    /// feed.close(Duration::from_secs(1));
476    /// assert_eq!(script.stopped_receiving(), 1);
477    /// assert_eq!(script.handed_back(), vec![receipt]);
478    /// assert!(script.held().is_empty());
479    /// assert_eq!(script.closes(), vec![Duration::from_secs(1)]);
480    /// ```
481    pub fn hand_back_on_stop(&self) {
482        let mut state = self.lock();
483        state.hands_back = true;
484        state.wake();
485    }
486
487    /// How many times the feed was told to [`stop_receiving`](Feed::stop_receiving).
488    /// The feed keeps answering `receive` after it, so a test sees whether
489    /// the reader stopped asking.
490    #[must_use]
491    pub fn stopped_receiving(&self) -> usize {
492        self.lock().stopped_receiving
493    }
494
495    /// Every receipt handed back, in order. A handed-back delivery is no
496    /// longer held.
497    #[must_use]
498    pub fn handed_back(&self) -> Vec<Receipt> {
499        self.lock().handed_back.clone()
500    }
501
502    /// The bound of every [`close`](Feed::close), in order. A close settles
503    /// at once and lets go of nothing, so [`held`](Self::held) still shows
504    /// what the reader held.
505    #[must_use]
506    pub fn closes(&self) -> Vec<Duration> {
507        self.lock().closes.clone()
508    }
509
510    /// How many depth reads of `of` the feed has answered.
511    #[must_use]
512    pub fn depth_reads(&self, of: DepthOf) -> usize {
513        let state = self.lock();
514        match of {
515            DepthOf::DeadLetters => state.dead_letter_depth_reads,
516            _ => state.feed_depth_reads,
517        }
518    }
519
520    /// Every delivery `receive` handed out, in order.
521    #[must_use]
522    pub fn delivered(&self) -> Vec<Delivery> {
523        self.lock().delivered.clone()
524    }
525
526    /// The receipts received and not yet acknowledged, failed or lapsed, in
527    /// receipt order.
528    #[must_use]
529    pub fn held(&self) -> Vec<Receipt> {
530        self.lock().held.keys().copied().collect()
531    }
532
533    /// Every receipt acknowledged, in order.
534    #[must_use]
535    pub fn acknowledged(&self) -> Vec<Receipt> {
536        self.lock().acknowledged.clone()
537    }
538
539    /// Every receipt failed, with its reason, in order.
540    #[must_use]
541    pub fn failed(&self) -> Vec<(Receipt, String)> {
542        self.lock().failed.clone()
543    }
544
545    /// Every receipt renewed, in order, one entry per call.
546    #[must_use]
547    pub fn renewed(&self) -> Vec<Receipt> {
548        self.lock().renewed.clone()
549    }
550
551    /// Every receipt a call was answered [`Gone`](FeedErrorKind::Gone) for,
552    /// in order.
553    #[must_use]
554    pub fn gone(&self) -> Vec<Receipt> {
555        self.lock().gone.clone()
556    }
557
558    /// Every unit passed to `publish`, in order, whatever its outcome.
559    #[must_use]
560    pub fn published(&self) -> Vec<Unit> {
561        self.lock().published.clone()
562    }
563
564    /// Every unit moved to the dead-letter destination by
565    /// [`dead_letter`](Self::dead_letter), in order.
566    #[must_use]
567    pub fn dead_lettered(&self) -> Vec<Unit> {
568        self.lock().dead_lettered.clone()
569    }
570
571    fn lock(&self) -> MutexGuard<'_, State> {
572        self.state.lock().expect("feed script poisoned")
573    }
574}
575
576impl ScriptedFeed {
577    fn lock(&self) -> MutexGuard<'_, State> {
578        self.state.lock().expect("feed script poisoned")
579    }
580}
581
582impl Feed for ScriptedFeed {
583    fn backend(&self) -> &str {
584        "scripted"
585    }
586
587    fn set_waker(&self, waker: FeedWaker) {
588        let mut state = self.lock();
589        if !state.queued.is_empty() {
590            waker.wake();
591        }
592        state.waker = Some(waker);
593    }
594
595    fn receive(&self, max: usize) -> Result<Vec<Delivery>, FeedError> {
596        let mut state = self.lock();
597        state.answer(Call::Receive)?;
598        let take = max.min(state.queued.len());
599        let out: Vec<Delivery> = state.queued.drain(..take).collect();
600        for delivery in &out {
601            state.held.insert(delivery.receipt, delivery.clone());
602            state.delivered.push(delivery.clone());
603        }
604        // A receive that took nothing sends no signal, or a reader at
605        // capacity calling `receive(0)` would never park.
606        if take > 0 && !state.queued.is_empty() {
607            state.wake();
608        }
609        Ok(out)
610    }
611
612    fn renew(&self, receipt: Receipt) -> Result<(), FeedError> {
613        let mut state = self.lock();
614        state.answer(Call::Renew)?;
615        state.on_held(receipt, |state| {
616            state.renewed.push(receipt);
617            Ok(())
618        })
619    }
620
621    fn acknowledge(&self, receipt: Receipt) -> Result<(), FeedError> {
622        let mut state = self.lock();
623        state.answer(Call::Acknowledge)?;
624        state.on_held(receipt, |state| {
625            state.held.remove(&receipt);
626            state.acknowledged.push(receipt);
627            Ok(())
628        })
629    }
630
631    fn fail(&self, receipt: Receipt, reason: &str) -> Result<(), FeedError> {
632        let mut state = self.lock();
633        state.answer(Call::Fail)?;
634        state.on_held(receipt, |state| {
635            state.held.remove(&receipt);
636            state.failed.push((receipt, reason.to_owned()));
637            Ok(())
638        })
639    }
640
641    fn publish(&self, unit: Unit) -> Confirmation {
642        let mut state = self.lock();
643        state.published.push(unit.clone());
644        let (confirmer, confirmation) = confirmation();
645        if let Some(kind) = state.publish_outcomes.pop_front() {
646            confirmer.reject(FeedError::new(kind, "refused by the feed script"));
647        } else if state.hold_publishes {
648            state.held_publishes.push((unit, confirmer));
649        } else {
650            confirmer.confirm();
651            state.queue(unit, 1);
652            state.wake();
653        }
654        confirmation
655    }
656
657    fn read_depth(&self, of: DepthOf) -> Option<DepthReading> {
658        let mut state = self.lock();
659        let answer = match of {
660            DepthOf::DeadLetters => {
661                state.dead_letter_depth_reads += 1;
662                state.dead_letter_depths.pop_front().unwrap_or_else(|| {
663                    let mut depth = Depth::new("the scripted dead-letter destination", false);
664                    depth.visible = state.dead_lettered.len() as u64;
665                    Ok(depth)
666                })
667            }
668            _ => {
669                state.feed_depth_reads += 1;
670                state.feed_depths.pop_front().unwrap_or_else(|| {
671                    let mut depth = Depth::new("the scripted feed", false);
672                    depth.visible = state.queued.len() as u64;
673                    depth.in_flight = state.held.len() as u64;
674                    Ok(depth)
675                })
676            }
677        };
678        let (reporter, reading) = depth_reading();
679        reporter.report(answer);
680        Some(reading)
681    }
682
683    fn hands_back(&self) -> bool {
684        self.lock().hands_back
685    }
686
687    fn stop_receiving(&self) {
688        self.lock().stopped_receiving += 1;
689    }
690
691    fn hand_back(&self, receipt: Receipt) -> Result<(), FeedError> {
692        self.lock().on_held(receipt, |state| {
693            state.held.remove(&receipt);
694            state.handed_back.push(receipt);
695            Ok(())
696        })
697    }
698
699    fn close(&self, within: Duration) -> Option<Confirmation> {
700        self.lock().closes.push(within);
701        let (confirmer, confirmation) = confirmation();
702        confirmer.confirm();
703        Some(confirmation)
704    }
705}
706
707#[cfg(test)]
708mod tests {
709    use super::*;
710    use spate_core::feed::wake_channel;
711    use std::time::Duration;
712
713    fn unit(id: &str) -> Unit {
714        Unit::new(id, id.as_bytes().to_vec())
715    }
716
717    fn kind(result: Result<(), FeedError>) -> FeedErrorKind {
718        result.unwrap_err().kind
719    }
720
721    /// Pins that with no scripted answer the feed reports its own queue, held
722    /// set and dead letters, exactly, each count told apart by a different
723    /// number.
724    #[test]
725    fn a_scripted_feed_reports_depth_from_its_own_state() {
726        let (feed, script) = scripted_feed();
727        let read = |of| {
728            feed.read_depth(of)
729                .expect("the scripted feed reports depth")
730                .try_outcome()
731                .expect("settled at once")
732                .expect("read")
733        };
734        assert!(read(DepthOf::Feed).is_empty());
735        let held = script.deliver(unit("held"));
736        let dead: Vec<Receipt> = ["dead-1", "dead-2", "dead-3"]
737            .into_iter()
738            .map(|id| script.deliver(unit(id)))
739            .collect();
740        script.deliver(unit("queued-1"));
741        script.deliver(unit("queued-2"));
742        let got = feed.receive(1).unwrap();
743        assert_eq!(got[0].receipt, held);
744        for receipt in dead {
745            script.dead_letter(receipt);
746        }
747        let depth = read(DepthOf::Feed);
748        assert_eq!(
749            (depth.visible, depth.in_flight, depth.delayed, depth.unsent),
750            (2, 1, 0, 0)
751        );
752        assert!(!depth.approximate);
753        let dead = read(DepthOf::DeadLetters);
754        assert_eq!((dead.visible, dead.in_flight), (3, 0));
755        assert_eq!(script.depth_reads(DepthOf::Feed), 2);
756        assert_eq!(script.depth_reads(DepthOf::DeadLetters), 1);
757    }
758
759    /// Pins that scripted depth answers are served in order per destination
760    /// before the feed's own state, and that scripting one wakes the reader.
761    #[test]
762    fn a_scripted_depth_answer_sequence_is_served_in_order() {
763        let (feed, script) = scripted_feed();
764        let (waker, park) = wake_channel();
765        feed.set_waker(waker);
766        let mut first = Depth::new("q", true);
767        first.delayed = 3;
768        script.answer_depth(DepthOf::Feed, Ok(first.clone()));
769        assert!(
770            park.park(Duration::ZERO),
771            "scripting did not wake the reader"
772        );
773        script.answer_depth(
774            DepthOf::Feed,
775            Err(FeedError::new(FeedErrorKind::Fatal, "gone")),
776        );
777        let mut dead = Depth::new("dlq", true);
778        dead.visible = 2;
779        script.answer_depth(DepthOf::DeadLetters, Ok(dead.clone()));
780
781        let read = |of| feed.read_depth(of).unwrap().try_outcome().unwrap();
782        assert_eq!(read(DepthOf::DeadLetters).unwrap(), dead);
783        assert_eq!(read(DepthOf::Feed).unwrap(), first);
784        assert_eq!(read(DepthOf::Feed).unwrap_err().kind, FeedErrorKind::Fatal);
785        assert!(read(DepthOf::Feed).unwrap().is_empty());
786        assert!(read(DepthOf::DeadLetters).unwrap().is_empty());
787    }
788
789    /// Pins that a redelivery lapses the old receipt, counts the delivery,
790    /// and records every call on the lapsed receipt as Gone.
791    #[test]
792    fn a_redelivery_lapses_the_old_receipt() {
793        let (feed, script) = scripted_feed();
794        let first = script.deliver(unit("a"));
795        assert_eq!(feed.receive(8).unwrap()[0].receipt, first);
796        assert_eq!(script.held(), vec![first]);
797
798        let second = script.redeliver(first);
799        assert!(script.held().is_empty(), "the lapsed receipt is still held");
800        let again = feed.receive(8).unwrap();
801        assert_eq!(again.len(), 1);
802        assert_eq!((again[0].receipt, again[0].deliveries), (second, 2));
803
804        assert_eq!(kind(feed.renew(first)), FeedErrorKind::Gone);
805        assert_eq!(kind(feed.acknowledge(first)), FeedErrorKind::Gone);
806        assert_eq!(kind(feed.fail(first, "late")), FeedErrorKind::Gone);
807        assert_eq!(script.gone(), vec![first, first, first]);
808        feed.renew(second).unwrap();
809        feed.acknowledge(second).unwrap();
810        assert_eq!(script.renewed(), vec![second]);
811        assert_eq!(script.acknowledged(), vec![second]);
812        assert!(script.failed().is_empty());
813        assert_eq!(script.delivered().len(), 2);
814    }
815
816    /// Pins that a duplicate of a held unit is a second delivery under its
817    /// own receipt, and that both receipts stay valid.
818    #[test]
819    fn a_duplicate_is_held_beside_the_first_delivery() {
820        let (feed, script) = scripted_feed();
821        let first = script.deliver(unit("a"));
822        let _ = feed.receive(8).unwrap();
823        let second = script.deliver(unit("a"));
824        let dup = feed.receive(8).unwrap();
825        assert_eq!((dup[0].receipt, dup[0].deliveries), (second, 2));
826        assert_eq!(script.held(), vec![first, second]);
827        feed.acknowledge(first).unwrap();
828        feed.fail(second, "poison").unwrap();
829        assert_eq!(script.acknowledged(), vec![first]);
830        assert_eq!(script.failed(), vec![(second, "poison".to_owned())]);
831        assert!(script.held().is_empty());
832        assert!(feed.receive(8).unwrap().is_empty(), "fail redelivered");
833    }
834
835    /// Pins that a dead-lettered unit lapses its receipt, whether queued or
836    /// held, and is recorded.
837    #[test]
838    fn a_dead_lettered_unit_lapses_its_receipt() {
839        let (feed, script) = scripted_feed();
840        let queued = script.deliver(unit("q"));
841        script.dead_letter(queued);
842        assert!(feed.receive(8).unwrap().is_empty());
843
844        let held = script.deliver(unit("h"));
845        let _ = feed.receive(8).unwrap();
846        script.dead_letter(held);
847        assert_eq!(kind(feed.acknowledge(held)), FeedErrorKind::Gone);
848        assert_eq!(script.dead_lettered(), vec![unit("q"), unit("h")]);
849    }
850
851    /// Pins publish outcomes: an unscripted publish is confirmed and
852    /// delivered, a refused one is never delivered, and a held one settles
853    /// only when confirmed and is delivered then.
854    #[test]
855    fn publishes_are_confirmed_refused_or_held_as_scripted() {
856        let (feed, script) = scripted_feed();
857        let mut plain = feed.publish(unit("plain"));
858        assert!(matches!(plain.try_outcome(), Some(Ok(()))));
859
860        script.refuse_next_publish(FeedErrorKind::Refused);
861        let mut refused = feed.publish(unit("refused"));
862        assert_eq!(
863            refused.try_outcome().unwrap().unwrap_err().kind,
864            FeedErrorKind::Refused
865        );
866
867        script.hold_publishes();
868        let mut held = feed.publish(unit("held"));
869        assert!(held.try_outcome().is_none());
870        let ids = |d: Vec<Delivery>| d.into_iter().map(|d| d.unit.id).collect::<Vec<_>>();
871        assert_eq!(ids(feed.receive(8).unwrap()), vec!["plain"]);
872
873        script.confirm_held_publishes();
874        assert!(matches!(held.try_outcome(), Some(Ok(()))));
875        assert_eq!(ids(feed.receive(8).unwrap()), vec!["held"]);
876        assert_eq!(
877            script.published(),
878            vec![unit("plain"), unit("refused"), unit("held")]
879        );
880    }
881
882    /// Pins that scripting calls wake a parked reader, and that a waker set
883    /// after a delivery was queued is woken at once.
884    #[test]
885    fn scripting_wakes_the_reader() {
886        let (feed, script) = scripted_feed();
887        let early = script.deliver(unit("early"));
888        let (waker, park) = wake_channel();
889        feed.set_waker(waker);
890        assert!(
891            park.park(Duration::ZERO),
892            "set_waker missed a queued delivery"
893        );
894
895        let _ = feed.receive(8).unwrap();
896        assert!(!park.park(Duration::ZERO));
897        let again = script.redeliver(early);
898        assert!(park.park(Duration::ZERO), "redeliver did not wake");
899        script.dead_letter(again);
900        assert!(park.park(Duration::ZERO), "dead_letter did not wake");
901        script.hold_publishes();
902        assert!(park.park(Duration::ZERO), "hold_publishes did not wake");
903        let _held = feed.publish(unit("p"));
904        assert!(!park.park(Duration::ZERO), "a held publish woke the reader");
905        script.confirm_held_publishes();
906        assert!(
907            park.park(Duration::ZERO),
908            "confirm_held_publishes did not wake"
909        );
910    }
911
912    /// Pins that a receive which leaves deliveries queued leaves a wake
913    /// pending, and that one which takes nothing sends none.
914    #[test]
915    fn a_partial_receive_leaves_a_wake_pending() {
916        let (feed, script) = scripted_feed();
917        let (waker, park) = wake_channel();
918        feed.set_waker(waker);
919        script.deliver(unit("a"));
920        script.deliver(unit("b"));
921        assert!(park.park(Duration::ZERO));
922        assert_eq!(feed.receive(1).unwrap().len(), 1);
923        assert!(
924            park.park(Duration::ZERO),
925            "a delivery is queued but the reader's park was not signalled"
926        );
927        assert!(feed.receive(0).unwrap().is_empty());
928        assert!(
929            !park.park(Duration::ZERO),
930            "receive(0) at capacity re-signalled"
931        );
932    }
933
934    /// Pins that a confirmed publish wakes the reader.
935    #[test]
936    fn a_confirmed_publish_wakes_the_reader() {
937        let (feed, _script) = scripted_feed();
938        let (waker, park) = wake_channel();
939        feed.set_waker(waker);
940        assert!(!park.park(Duration::ZERO));
941        drop(feed.publish(unit("p")));
942        assert!(park.park(Duration::ZERO), "publish did not wake the reader");
943    }
944
945    /// Pins that scripting a publish refusal wakes the reader, as every
946    /// scripting call does.
947    #[test]
948    fn refusing_the_next_publish_wakes_the_reader() {
949        let (feed, script) = scripted_feed();
950        let (waker, park) = wake_channel();
951        feed.set_waker(waker);
952        assert!(!park.park(Duration::ZERO));
953        script.refuse_next_publish(FeedErrorKind::Refused);
954        assert!(
955            park.park(Duration::ZERO),
956            "refuse_next_publish did not wake the reader"
957        );
958    }
959
960    /// One reader call on `receipt`, as its scripted answer sees it.
961    fn call(feed: &ScriptedFeed, call: Call, receipt: Receipt) -> Result<(), FeedError> {
962        match call {
963            Call::Receive => feed.receive(0).map(drop),
964            Call::Renew => feed.renew(receipt),
965            Call::Acknowledge => feed.acknowledge(receipt),
966            Call::Fail => feed.fail(receipt, "poison"),
967        }
968    }
969
970    fn refuse(script: &FeedScript, call: Call, kind: FeedErrorKind) {
971        match call {
972            Call::Receive => script.refuse_next_receive(kind),
973            Call::Renew => script.refuse_next_renew(kind),
974            Call::Acknowledge => script.refuse_next_acknowledge(kind),
975            Call::Fail => script.refuse_next_fail(kind),
976        }
977    }
978
979    const CALLS: [Call; 4] = [Call::Receive, Call::Renew, Call::Acknowledge, Call::Fail];
980
981    /// Pins that each call's scripted answers are served in order, one per
982    /// call, and that the call answers as unscripted once they are spent.
983    #[test]
984    fn scripted_answers_are_served_in_order_then_spent() {
985        for c in CALLS {
986            let (feed, script) = scripted_feed();
987            let receipt = script.deliver(unit("a"));
988            let _ = feed.receive(8).unwrap();
989            refuse(&script, c, FeedErrorKind::Retryable);
990            refuse(&script, c, FeedErrorKind::Fatal);
991            assert_eq!(script.refusals_left(), 2);
992            assert_eq!(
993                kind(call(&feed, c, receipt)),
994                FeedErrorKind::Retryable,
995                "{c:?}"
996            );
997            assert_eq!(script.refusals_left(), 1);
998            assert_eq!(kind(call(&feed, c, receipt)), FeedErrorKind::Fatal, "{c:?}");
999            assert_eq!(script.refusals_left(), 0);
1000            call(&feed, c, receipt).unwrap_or_else(|e| panic!("{c:?} after the script: {e}"));
1001        }
1002    }
1003
1004    /// Pins that a scripted answer to `renew`, `acknowledge` or `fail`
1005    /// leaves the feed's records and held set as they were, and that the
1006    /// next unscripted call lands.
1007    #[test]
1008    fn a_scripted_error_changes_nothing() {
1009        let (feed, script) = scripted_feed();
1010        let receipt = script.deliver(unit("a"));
1011        script.deliver(unit("b"));
1012        let _ = feed.receive(1).unwrap();
1013        for c in [Call::Renew, Call::Acknowledge, Call::Fail] {
1014            refuse(&script, c, FeedErrorKind::Retryable);
1015            assert_eq!(kind(call(&feed, c, receipt)), FeedErrorKind::Retryable);
1016            assert_eq!(script.held(), vec![receipt], "{c:?} changed the held set");
1017            assert!(script.renewed().is_empty(), "{c:?} recorded a renewal");
1018            assert!(script.acknowledged().is_empty(), "{c:?} recorded an ack");
1019            assert!(script.failed().is_empty(), "{c:?} recorded a fail");
1020            assert!(script.gone().is_empty(), "{c:?} recorded Gone");
1021        }
1022        feed.renew(receipt).unwrap();
1023        feed.acknowledge(receipt).unwrap();
1024        assert_eq!(script.renewed(), vec![receipt]);
1025        assert_eq!(script.acknowledged(), vec![receipt]);
1026    }
1027
1028    /// Pins that a scripted answer to `receive` takes no delivery: nothing
1029    /// is held or recorded, and the next `receive` hands out what is queued.
1030    #[test]
1031    fn a_scripted_receive_error_takes_nothing() {
1032        let (feed, script) = scripted_feed();
1033        let queued = script.deliver(unit("a"));
1034        script.refuse_next_receive(FeedErrorKind::Retryable);
1035        assert_eq!(feed.receive(8).unwrap_err().kind, FeedErrorKind::Retryable);
1036        assert!(script.held().is_empty());
1037        assert!(script.delivered().is_empty());
1038        let got = feed.receive(8).unwrap();
1039        assert_eq!(got.len(), 1);
1040        assert_eq!(got[0].receipt, queued);
1041    }
1042
1043    /// Pins that a scripted answer goes only to its own call: the other
1044    /// calls answer as unscripted while it waits.
1045    #[test]
1046    fn a_scripted_answer_waits_for_its_own_call() {
1047        for scripted in CALLS {
1048            let (feed, script) = scripted_feed();
1049            for id in ["a", "b", "c", "d"] {
1050                script.deliver(unit(id));
1051            }
1052            let receipts: Vec<Receipt> =
1053                feed.receive(4).unwrap().iter().map(|d| d.receipt).collect();
1054            refuse(&script, scripted, FeedErrorKind::Fatal);
1055            // Each other call gets a receipt of its own, so an acknowledge
1056            // or fail does not lapse the receipt a later call uses.
1057            for (i, c) in CALLS.into_iter().enumerate() {
1058                if c as usize != scripted as usize {
1059                    call(&feed, c, receipts[i])
1060                        .unwrap_or_else(|e| panic!("{c:?} took {scripted:?}'s answer: {e}"));
1061                }
1062            }
1063            assert_eq!(script.refusals_left(), 1);
1064            let own = receipts[scripted as usize];
1065            assert_eq!(kind(call(&feed, scripted, own)), FeedErrorKind::Fatal);
1066        }
1067    }
1068
1069    /// Pins that a scripted answer to `renew`, `acknowledge` or `fail` is
1070    /// served before the held check, so a call with a lapsed receipt takes
1071    /// it and is not recorded as Gone, and the next call is.
1072    #[test]
1073    fn a_scripted_answer_goes_to_a_lapsed_receipt_first() {
1074        for c in [Call::Renew, Call::Acknowledge, Call::Fail] {
1075            let (feed, script) = scripted_feed();
1076            let old = script.deliver(unit("a"));
1077            let _ = feed.receive(8).unwrap();
1078            script.redeliver(old);
1079            refuse(&script, c, FeedErrorKind::Retryable);
1080            assert_eq!(kind(call(&feed, c, old)), FeedErrorKind::Retryable, "{c:?}");
1081            assert!(script.gone().is_empty(), "{c:?} recorded Gone");
1082            assert_eq!(script.refusals_left(), 0, "{c:?}");
1083            assert_eq!(kind(call(&feed, c, old)), FeedErrorKind::Gone, "{c:?}");
1084            assert_eq!(script.gone(), vec![old], "{c:?}");
1085        }
1086    }
1087
1088    /// Pins that a scripted answer to `receive` sends no signal.
1089    #[test]
1090    fn a_refused_receive_sends_no_signal() {
1091        let (feed, script) = scripted_feed();
1092        let (waker, park) = wake_channel();
1093        feed.set_waker(waker);
1094        script.deliver(unit("a"));
1095        script.refuse_next_receive(FeedErrorKind::Retryable);
1096        assert!(park.park(Duration::ZERO));
1097        assert_eq!(feed.receive(8).unwrap_err().kind, FeedErrorKind::Retryable);
1098        assert!(
1099            !park.park(Duration::ZERO),
1100            "a refused receive signalled the reader"
1101        );
1102    }
1103
1104    /// Pins that `refusals_left` counts the publish refusals not yet served.
1105    #[test]
1106    fn refusals_left_counts_unserved_publish_refusals() {
1107        let (feed, script) = scripted_feed();
1108        script.refuse_next_publish(FeedErrorKind::Refused);
1109        script.refuse_next_receive(FeedErrorKind::Retryable);
1110        assert_eq!(script.refusals_left(), 2);
1111        let mut refused = feed.publish(unit("a"));
1112        assert_eq!(
1113            refused.try_outcome().unwrap().unwrap_err().kind,
1114            FeedErrorKind::Refused
1115        );
1116        assert_eq!(script.refusals_left(), 1);
1117    }
1118
1119    #[test]
1120    #[should_panic(expected = "refuse_next_receive: Gone has no meaning for receive")]
1121    fn scripting_gone_panics_for_receive() {
1122        scripted_feed().1.refuse_next_receive(FeedErrorKind::Gone);
1123    }
1124
1125    #[test]
1126    #[should_panic(expected = "refuse_next_renew: the feed answers Gone")]
1127    fn scripting_gone_panics_for_renew() {
1128        scripted_feed().1.refuse_next_renew(FeedErrorKind::Gone);
1129    }
1130
1131    #[test]
1132    #[should_panic(expected = "refuse_next_acknowledge: the feed answers Gone")]
1133    fn scripting_gone_panics_for_acknowledge() {
1134        scripted_feed()
1135            .1
1136            .refuse_next_acknowledge(FeedErrorKind::Gone);
1137    }
1138
1139    #[test]
1140    #[should_panic(expected = "refuse_next_fail: the feed answers Gone")]
1141    fn scripting_gone_panics_for_fail() {
1142        scripted_feed().1.refuse_next_fail(FeedErrorKind::Gone);
1143    }
1144
1145    /// Pins that scripting a refusal of any reader call wakes the reader,
1146    /// as every scripting call does.
1147    #[test]
1148    fn refusing_the_next_call_wakes_the_reader() {
1149        for c in CALLS {
1150            let (feed, script) = scripted_feed();
1151            let (waker, park) = wake_channel();
1152            feed.set_waker(waker);
1153            assert!(!park.park(Duration::ZERO));
1154            refuse(&script, c, FeedErrorKind::Retryable);
1155            assert!(
1156                park.park(Duration::ZERO),
1157                "refusing the next {c:?} did not wake the reader"
1158            );
1159        }
1160    }
1161
1162    /// Pins that a later `set_waker` replaces the waker: a scripting call
1163    /// signals only the newest one.
1164    #[test]
1165    fn a_later_set_waker_replaces_the_waker() {
1166        let (feed, script) = scripted_feed();
1167        let (first, first_park) = wake_channel();
1168        feed.set_waker(first);
1169        let (second, second_park) = wake_channel();
1170        feed.set_waker(second);
1171        script.deliver(unit("a"));
1172        assert!(
1173            second_park.park(Duration::ZERO),
1174            "deliver signalled the replaced waker"
1175        );
1176        assert!(!first_park.park(Duration::ZERO));
1177    }
1178}