1use crate::events::{Command, Event, EventKind, Frame, Meta, ZoneRange};
49use cumulus3d_core::io::ply::PointCloud;
50use std::collections::{HashMap, VecDeque};
51use std::panic::{catch_unwind, AssertUnwindSafe};
52use std::sync::atomic::{AtomicUsize, Ordering};
53use std::sync::mpsc::{channel, Receiver, Sender};
54use std::sync::{Arc, Condvar, Mutex};
55use std::thread::JoinHandle;
56use std::time::SystemTime;
57
58pub trait Reducer {
64 type Input;
66 fn step(&mut self, input: Self::Input) -> Vec<Event>;
68 fn prelude(&mut self, _input: &Self::Input) -> Vec<Event> {
70 Vec::new()
71 }
72 fn poll(&mut self) -> Vec<Event>;
74 fn command(&mut self, c: Command) -> Vec<Event>;
76 fn finish(&mut self) -> Vec<Event> {
79 self.poll()
80 }
81}
82
83#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
85pub enum QueuePolicy {
86 #[default]
88 KeepAll,
89 LatestPerKey,
92 LatestOnly,
94}
95
96#[derive(Clone, Debug, Default)]
98pub struct Summary {
99 pub events: usize,
101 pub by_kind: HashMap<EventKind, usize>,
103 pub hook_calls: usize,
105 pub hook_panics: usize,
107 pub superseded: usize,
109}
110
111type Hook = Box<dyn FnMut(&Event) + Send + 'static>;
112type HookList = Arc<Mutex<Vec<Hook>>>;
113
114type LaneKey = Option<EventKind>;
116
117struct Shared {
119 subscribers: Mutex<Vec<Sender<Event>>>,
120 hook_calls: AtomicUsize,
121 hook_panics: AtomicUsize,
122 superseded: AtomicUsize,
123}
124
125impl Shared {
126 fn broadcast(&self, ev: &Event) {
127 let mut subs = self.subscribers.lock().unwrap_or_else(|p| p.into_inner());
128 subs.retain(|tx| tx.send(ev.clone()).is_ok());
129 }
130
131 fn run_hooks(&self, hooks: &HookList, ev: &Event) {
133 let mut hooks = hooks.lock().unwrap_or_else(|p| p.into_inner());
134 for hook in hooks.iter_mut() {
135 self.hook_calls.fetch_add(1, Ordering::Relaxed);
136 let r = catch_unwind(AssertUnwindSafe(|| hook(ev)));
137 if let Err(payload) = r {
138 self.hook_panics.fetch_add(1, Ordering::Relaxed);
139 let what = if let Some(s) = payload.downcast_ref::<&str>() {
140 (*s).to_string()
141 } else if let Some(s) = payload.downcast_ref::<String>() {
142 s.clone()
143 } else {
144 "알 수 없는 패닉".to_string()
145 };
146 let err = Event::Error {
147 meta: Meta { version: ev.meta().version, at: SystemTime::now() },
148 message: format!("{:?} 훅 패닉: {what}", ev.kind()),
149 };
150 self.broadcast(&err);
151 }
152 }
153 }
154}
155
156struct LaneQueue {
157 items: VecDeque<Event>,
158 busy: bool,
159 closed: bool,
160}
161
162struct LaneState {
163 q: Mutex<LaneQueue>,
164 cv: Condvar,
165}
166
167struct Lane {
168 hooks: HookList,
169 state: Arc<LaneState>,
170 worker: Option<JoinHandle<()>>,
171}
172
173impl Lane {
174 fn new() -> Self {
175 Lane {
176 hooks: Arc::new(Mutex::new(Vec::new())),
177 state: Arc::new(LaneState {
178 q: Mutex::new(LaneQueue { items: VecDeque::new(), busy: false, closed: false }),
179 cv: Condvar::new(),
180 }),
181 worker: None,
182 }
183 }
184
185 fn ensure_worker(&mut self, name: String, shared: &Arc<Shared>) {
186 if self.worker.is_some() {
187 return;
188 }
189 let state = Arc::clone(&self.state);
190 let hooks = Arc::clone(&self.hooks);
191 let shared = Arc::clone(shared);
192 let h = std::thread::Builder::new()
193 .name(name)
194 .spawn(move || loop {
195 let ev = {
196 let mut q = state.q.lock().unwrap_or_else(|p| p.into_inner());
197 loop {
198 if let Some(ev) = q.items.pop_front() {
199 q.busy = true;
200 break Some(ev);
201 }
202 if q.closed {
203 break None;
204 }
205 q = state.cv.wait(q).unwrap_or_else(|p| p.into_inner());
206 }
207 };
208 let Some(ev) = ev else { return };
209 shared.run_hooks(&hooks, &ev);
210 let mut q = state.q.lock().unwrap_or_else(|p| p.into_inner());
211 q.busy = false;
212 state.cv.notify_all();
213 })
214 .expect("훅 워커 스레드 생성 실패");
215 self.worker = Some(h);
216 }
217
218 fn enqueue(&self, ev: Event, policy: QueuePolicy) -> usize {
220 let mut q = self.state.q.lock().unwrap_or_else(|p| p.into_inner());
221 let before = q.items.len();
222 match policy {
223 QueuePolicy::KeepAll => {}
224 QueuePolicy::LatestOnly => {
225 let k = ev.kind();
226 q.items.retain(|e| e.kind() != k);
227 }
228 QueuePolicy::LatestPerKey => {
229 let k = (ev.kind(), supersede_key(&ev));
230 q.items.retain(|e| (e.kind(), supersede_key(e)) != k);
231 }
232 }
233 let dropped = before - q.items.len();
234 q.items.push_back(ev);
235 self.state.cv.notify_all();
236 dropped
237 }
238
239 fn wait_idle(&self) {
241 if self.worker.is_none() {
242 return;
243 }
244 let mut q = self.state.q.lock().unwrap_or_else(|p| p.into_inner());
245 while !q.items.is_empty() || q.busy {
246 q = self.state.cv.wait(q).unwrap_or_else(|p| p.into_inner());
247 }
248 }
249
250 fn shutdown(&mut self) {
251 {
252 let mut q = self.state.q.lock().unwrap_or_else(|p| p.into_inner());
253 q.closed = true;
254 self.state.cv.notify_all();
255 }
256 if let Some(h) = self.worker.take() {
257 let _ = h.join();
258 }
259 }
260}
261
262fn supersede_key(ev: &Event) -> Option<usize> {
264 match ev {
265 Event::ZoneArrived { range, .. }
266 | Event::ZonePreview { range, .. }
267 | Event::ZoneRefinedPose { range, .. }
268 | Event::ZoneRefined { range, .. } => Some(range.zone),
269 Event::BaseAdopted { zone, .. }
270 | Event::Reanchored { zone, .. }
271 | Event::RefineStarted { zone, .. }
272 | Event::ZoneAdjusted { zone, .. }
273 | Event::ZoneInvalidated { zone, .. } => Some(*zone),
274 Event::Snapshot { index, .. } => Some(*index),
275 Event::PositionDone { position, .. } | Event::FrameIngested { position, .. } => Some(*position),
276 _ => None,
277 }
278}
279
280struct Lanes {
282 map: HashMap<LaneKey, Lane>,
283}
284
285impl Lanes {
286 fn shutdown(&mut self) {
287 for lane in self.map.values_mut() {
288 lane.shutdown();
289 }
290 }
291}
292
293impl Drop for Lanes {
294 fn drop(&mut self) {
295 self.shutdown();
296 }
297}
298
299type Deriver = Box<dyn FnMut(&Event) -> Vec<Event> + Send + 'static>;
301
302pub struct Pipeline<R: Reducer> {
304 reducer: R,
305 derivers: Vec<Deriver>,
306 lanes: Lanes,
307 policies: HashMap<EventKind, QueuePolicy>,
308 shared: Arc<Shared>,
309 sync: bool,
310 events: usize,
311 by_kind: HashMap<EventKind, usize>,
312}
313
314impl<R: Reducer> Pipeline<R> {
315 pub fn new(reducer: R) -> Self {
317 Pipeline {
318 reducer,
319 derivers: Vec::new(),
320 lanes: Lanes { map: HashMap::new() },
321 policies: HashMap::new(),
322 shared: Arc::new(Shared {
323 subscribers: Mutex::new(Vec::new()),
324 hook_calls: AtomicUsize::new(0),
325 hook_panics: AtomicUsize::new(0),
326 superseded: AtomicUsize::new(0),
327 }),
328 sync: false,
329 events: 0,
330 by_kind: HashMap::new(),
331 }
332 }
333
334 pub fn sync(mut self, on: bool) -> Self {
337 if on && !self.sync {
338 self.flush();
339 }
340 self.sync = on;
341 self
342 }
343
344 pub fn policy(mut self, kind: EventKind, p: QueuePolicy) -> Self {
346 self.policies.insert(kind, p);
347 self
348 }
349
350 pub fn on<F>(mut self, kind: EventKind, f: F) -> Self
352 where
353 F: FnMut(&Event) + Send + 'static,
354 {
355 self.add_hook(Some(kind), Box::new(f));
356 self
357 }
358
359 pub fn on_any<F>(mut self, f: F) -> Self
361 where
362 F: FnMut(&Event) + Send + 'static,
363 {
364 self.add_hook(None, Box::new(f));
365 self
366 }
367
368 pub fn on_frame<F>(self, mut f: F) -> Self
371 where
372 F: FnMut(&Arc<Frame>) + Send + 'static,
373 {
374 self.on(EventKind::FrameDecoded, move |e| {
375 if let Event::FrameDecoded { frame, .. } = e {
376 f(frame)
377 }
378 })
379 }
380
381 pub fn on_zone_preview<F>(self, mut f: F) -> Self
383 where
384 F: FnMut(ZoneRange, &Arc<PointCloud>) + Send + 'static,
385 {
386 self.on(EventKind::ZonePreview, move |e| {
387 if let Event::ZonePreview { range, cloud, .. } = e {
388 f(*range, cloud)
389 }
390 })
391 }
392
393 pub fn on_zone_refined<F>(self, mut f: F) -> Self
395 where
396 F: FnMut(ZoneRange, &Arc<PointCloud>) + Send + 'static,
397 {
398 self.on(EventKind::ZoneRefined, move |e| {
399 if let Event::ZoneRefined { range, cloud, .. } = e {
400 f(*range, cloud)
401 }
402 })
403 }
404
405 pub fn on_snapshot<F>(self, mut f: F) -> Self
407 where
408 F: FnMut(usize, &Arc<PointCloud>) + Send + 'static,
409 {
410 self.on(EventKind::Snapshot, move |e| {
411 if let Event::Snapshot { index, cloud, .. } = e {
412 f(*index, cloud)
413 }
414 })
415 }
416
417 pub fn on_position_done<F>(self, mut f: F) -> Self
419 where
420 F: FnMut(usize, usize, usize) + Send + 'static,
421 {
422 self.on(EventKind::PositionDone, move |e| {
423 if let Event::PositionDone { position, registered, expected, .. } = e {
424 f(*position, *registered, *expected)
425 }
426 })
427 }
428
429 pub fn on_message<F>(self, f: F) -> Self
431 where
432 F: FnMut(EventKind, &str) + Send + 'static,
433 {
434 let f = Arc::new(Mutex::new(f));
435 let g = Arc::clone(&f);
436 self.on(EventKind::Warning, move |e| {
437 if let Event::Warning { message, .. } = e {
438 (f.lock().unwrap_or_else(|p| p.into_inner()))(EventKind::Warning, message)
439 }
440 })
441 .on(EventKind::Error, move |e| {
442 if let Event::Error { message, .. } = e {
443 (g.lock().unwrap_or_else(|p| p.into_inner()))(EventKind::Error, message)
444 }
445 })
446 }
447
448 pub fn derive<F>(mut self, f: F) -> Self
454 where
455 F: FnMut(&Event) -> Vec<Event> + Send + 'static,
456 {
457 self.derivers.push(Box::new(f));
458 self
459 }
460
461 fn add_hook(&mut self, key: LaneKey, hook: Hook) {
462 let lane = self.lanes.map.entry(key).or_insert_with(Lane::new);
463 lane.hooks.lock().unwrap_or_else(|p| p.into_inner()).push(hook);
464 }
465
466 pub fn subscribe(&self) -> Receiver<Event> {
468 let (tx, rx) = channel();
469 self.shared.subscribers.lock().unwrap_or_else(|p| p.into_inner()).push(tx);
470 rx
471 }
472
473 pub fn push(&mut self, input: R::Input) -> usize {
476 let pre = self.reducer.prelude(&input);
477 let n = self.dispatch(pre);
478 let evs = self.reducer.step(input);
479 n + self.dispatch(evs)
480 }
481
482 pub fn poll(&mut self) -> usize {
484 let evs = self.reducer.poll();
485 self.dispatch(evs)
486 }
487
488 pub fn command(&mut self, c: Command) -> usize {
490 let evs = self.reducer.command(c);
491 self.dispatch(evs)
492 }
493
494 pub fn flush(&mut self) {
496 for lane in self.lanes.map.values() {
497 lane.wait_idle();
498 }
499 }
500
501 pub fn reducer(&self) -> &R {
503 &self.reducer
504 }
505
506 pub fn reducer_mut(&mut self) -> &mut R {
508 &mut self.reducer
509 }
510
511 pub fn finish(mut self) -> (R, Summary) {
514 let evs = self.reducer.finish();
515 self.dispatch(evs);
516 self.lanes.shutdown();
517 self.shared.subscribers.lock().unwrap_or_else(|p| p.into_inner()).clear();
518 let summary = Summary {
519 events: self.events,
520 by_kind: std::mem::take(&mut self.by_kind),
521 hook_calls: self.shared.hook_calls.load(Ordering::Relaxed),
522 hook_panics: self.shared.hook_panics.load(Ordering::Relaxed),
523 superseded: self.shared.superseded.load(Ordering::Relaxed),
524 };
525 let Pipeline { reducer, .. } = self;
526 (reducer, summary)
527 }
528
529 fn dispatch(&mut self, evs: Vec<Event>) -> usize {
530 let mut n = 0;
531 let mut queue: VecDeque<Event> = evs.into();
532 while let Some(ev) = queue.pop_front() {
533 n += 1;
534 if !self.derivers.is_empty() {
535 let mut derived = Vec::new();
536 for d in self.derivers.iter_mut() {
537 match catch_unwind(AssertUnwindSafe(|| d(&ev))) {
538 Ok(v) => derived.extend(v),
539 Err(_) => {
540 self.shared.hook_panics.fetch_add(1, Ordering::Relaxed);
541 let meta = ev.meta().clone();
542 self.shared.broadcast(&Event::Error { meta, message: "derive: panic".to_string() });
543 }
544 }
545 }
546 for d in derived.into_iter().rev() {
547 queue.push_front(d);
548 }
549 }
550 self.events += 1;
551 *self.by_kind.entry(ev.kind()).or_insert(0) += 1;
552 self.shared.broadcast(&ev);
553 let kind = ev.kind();
554 if self.sync {
555 for key in [Some(kind), None] {
556 if let Some(lane) = self.lanes.map.get(&key) {
557 self.shared.run_hooks(&lane.hooks, &ev);
558 }
559 }
560 continue;
561 }
562 let policy = self.policies.get(&kind).copied().unwrap_or_default();
563 let has_kind = self.lanes.map.contains_key(&Some(kind));
564 let has_any = self.lanes.map.contains_key(&None);
565 let mut pending = Some(ev);
566 for (key, present, last) in [(Some(kind), has_kind, !has_any), (None, has_any, true)] {
567 if !present {
568 continue;
569 }
570 let ev = if last { pending.take() } else { pending.clone() };
571 let Some(ev) = ev else { break };
572 let name = match key {
573 Some(k) => format!("hook-{k:?}"),
574 None => "hook-any".to_string(),
575 };
576 let lane = self.lanes.map.get_mut(&key).expect("레인 존재 확인됨");
577 lane.ensure_worker(name, &self.shared);
578 let dropped = lane.enqueue(ev, policy);
579 self.shared.superseded.fetch_add(dropped, Ordering::Relaxed);
580 }
581 }
582 n
583 }
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589 use cumulus3d_core::Reconstruction;
590 use std::sync::mpsc::RecvTimeoutError;
591 use std::time::Duration;
592
593 fn meta(v: u64) -> Meta {
594 Meta { version: v, at: SystemTime::now() }
595 }
596 fn range(z: usize) -> ZoneRange {
597 ZoneRange { zone: z, lo: z * 10, hi: z * 10 + 10 }
598 }
599 fn cloud() -> Arc<PointCloud> {
600 Arc::new(PointCloud::default())
601 }
602 fn preview(v: u64, z: usize) -> Event {
603 Event::ZonePreview {
604 meta: meta(v),
605 range: range(z),
606 cloud: cloud(),
607 model: Arc::new(Reconstruction::default()),
608 dense: true,
609 frame: "gps".to_string(),
610 }
611 }
612 fn refined(v: u64, z: usize) -> Event {
613 Event::ZoneRefined { meta: meta(v), range: range(z), cloud: cloud(), model: Arc::new(Reconstruction::default()), dense: true }
614 }
615
616 #[derive(Default)]
619 struct Fake {
620 v: u64,
621 zones: Vec<usize>,
622 commands: Vec<String>,
623 }
624
625 impl Reducer for Fake {
626 type Input = usize;
627 fn step(&mut self, n: usize) -> Vec<Event> {
628 self.v += 1;
629 let mut out = vec![Event::PositionDone { meta: meta(self.v), position: n, registered: n, expected: n }];
630 if n.is_multiple_of(2) {
631 out.push(preview(self.v, n / 2));
632 self.zones.push(n / 2);
633 }
634 out
635 }
636 fn poll(&mut self) -> Vec<Event> {
637 Vec::new()
638 }
639 fn command(&mut self, c: Command) -> Vec<Event> {
640 self.commands.push(format!("{c:?}"));
641 match c {
642 Command::InvalidateZone(z) => vec![Event::Warning { meta: meta(self.v), message: format!("invalidate {z}") }],
643 Command::ResetFrom(p) => vec![Event::Warning { meta: meta(self.v), message: format!("reset {p}") }],
644 }
645 }
646 fn finish(&mut self) -> Vec<Event> {
647 self.zones.drain(..).map(|z| refined(self.v, z)).collect()
648 }
649 }
650
651 fn log<T>() -> Arc<Mutex<Vec<T>>> {
652 Arc::new(Mutex::new(Vec::new()))
653 }
654
655 #[test]
656 fn pipeline_sync_hook_order() {
657 let l = log();
658 let (a, b, c) = (Arc::clone(&l), Arc::clone(&l), Arc::clone(&l));
659 let mut p = Pipeline::new(Fake::default())
660 .sync(true)
661 .on_any(move |e| a.lock().unwrap().push(format!("any:{:?}", e.kind())))
662 .on_position_done(move |pos, _, _| b.lock().unwrap().push(format!("pos:{pos}")))
663 .on_zone_preview(move |r, _| c.lock().unwrap().push(format!("prev:{}", r.zone)));
664 p.push(1);
665 p.push(2);
666 let (_, s) = p.finish();
667 let got = l.lock().unwrap().clone();
668 assert_eq!(got, vec!["pos:1", "any:PositionDone", "pos:2", "any:PositionDone", "prev:1", "any:ZonePreview", "any:ZoneRefined"]);
669 assert_eq!(s.events, 4);
670 assert_eq!(s.by_kind[&EventKind::PositionDone], 2);
671 }
672
673 #[test]
674 fn pipeline_async_order_within_kind() {
675 let l = log::<String>();
676 let any = log::<String>();
677 let (a, b) = (Arc::clone(&l), Arc::clone(&any));
678 let mut p = Pipeline::new(Fake::default())
679 .on_position_done(move |pos, _, _| {
680 if pos.is_multiple_of(7) {
681 std::thread::sleep(Duration::from_millis(1));
682 }
683 a.lock().unwrap().push(pos.to_string())
684 })
685 .on_any(move |e| b.lock().unwrap().push(format!("{:?}", e.kind())));
686 for i in 0..200 {
687 p.push(i);
688 }
689 let (_, s) = p.finish();
690 let got: Vec<usize> = l.lock().unwrap().iter().map(|s| s.parse().unwrap()).collect();
691 assert_eq!(got, (0..200).collect::<Vec<_>>());
692 assert_eq!(any.lock().unwrap().len(), s.events);
693 assert_eq!(s.superseded, 0);
694 }
695
696 #[test]
697 fn pipeline_latest_per_key_policy() {
698 let gate = Arc::new((Mutex::new(false), Condvar::new()));
700 let seen = log();
701 let (g, s2) = (Arc::clone(&gate), Arc::clone(&seen));
702
703 struct Previews(u64);
704 impl Reducer for Previews {
705 type Input = (usize, u64);
706 fn step(&mut self, (z, v): (usize, u64)) -> Vec<Event> {
707 self.0 += 1;
708 vec![preview(v, z)]
709 }
710 fn poll(&mut self) -> Vec<Event> {
711 Vec::new()
712 }
713 fn command(&mut self, _: Command) -> Vec<Event> {
714 Vec::new()
715 }
716 }
717
718 let mut p =
719 Pipeline::new(Previews(0)).policy(EventKind::ZonePreview, QueuePolicy::LatestPerKey).on(EventKind::ZonePreview, move |e| {
720 let (m, cv) = &*g;
721 let mut open = m.lock().unwrap();
722 while !*open {
723 open = cv.wait(open).unwrap();
724 }
725 if let Event::ZonePreview { meta, range, .. } = e {
726 s2.lock().unwrap().push(format!("{}@{}", range.zone, meta.version));
727 }
728 });
729 p.push((9, 0));
731 std::thread::sleep(Duration::from_millis(50));
732 for v in 1..=5 {
733 p.push((1, v));
734 p.push((2, v));
735 }
736 {
737 let (m, cv) = &*gate;
738 *m.lock().unwrap() = true;
739 cv.notify_all();
740 }
741 let (_, s) = p.finish();
742 assert_eq!(seen.lock().unwrap().clone(), vec!["9@0", "1@5", "2@5"]);
743 assert_eq!(s.superseded, 8);
744 assert_eq!(s.events, 11);
745 }
746
747 #[test]
748 fn pipeline_panic_isolated() {
749 let l = log();
750 let a = Arc::clone(&l);
751 let mut p = Pipeline::new(Fake::default())
752 .on_position_done(|pos, _, _| {
753 if pos == 1 {
754 panic!("boom");
755 }
756 })
757 .on_position_done(move |pos, _, _| a.lock().unwrap().push(pos));
758 let rx = p.subscribe();
759 for i in 0..4 {
760 p.push(i);
761 }
762 let (_, s) = p.finish();
763 assert_eq!(l.lock().unwrap().clone(), vec![0, 1, 2, 3]);
764 assert_eq!(s.hook_panics, 1);
765 let errs: Vec<String> = rx
766 .iter()
767 .filter_map(|e| match e {
768 Event::Error { message, meta } => {
769 assert_eq!(meta.version, 2);
770 Some(message)
771 }
772 _ => None,
773 })
774 .collect();
775 assert_eq!(errs.len(), 1);
776 assert!(errs[0].contains("boom"), "{}", errs[0]);
777 }
778
779 #[test]
780 fn pipeline_channel_receives_all() {
781 let mut p = Pipeline::new(Fake::default());
782 let rx = p.subscribe();
783 p.push(1);
784 p.push(2);
785 let kinds: Vec<EventKind> = rx.try_iter().map(|e| e.kind()).collect();
786 assert_eq!(kinds, vec![EventKind::PositionDone, EventKind::PositionDone, EventKind::ZonePreview]);
787 let dropped = p.subscribe();
788 drop(dropped);
789 p.push(3);
790 let (_, s) = p.finish();
791 let rest: Vec<EventKind> = rx.iter().map(|e| e.kind()).collect();
792 assert_eq!(rest, vec![EventKind::PositionDone, EventKind::ZoneRefined]);
793 assert_eq!(s.events, 5);
794 assert!(matches!(rx.recv_timeout(Duration::from_millis(1)), Err(RecvTimeoutError::Disconnected)));
795 }
796
797 #[test]
798 fn pipeline_finish_drains() {
799 let n = Arc::new(AtomicUsize::new(0));
800 let refined = log();
801 let (a, b) = (Arc::clone(&n), Arc::clone(&refined));
802 let mut p = Pipeline::new(Fake::default())
803 .on(EventKind::PositionDone, move |_| {
804 std::thread::sleep(Duration::from_micros(200));
805 a.fetch_add(1, Ordering::SeqCst);
806 })
807 .on_zone_refined(move |r, _| b.lock().unwrap().push(r.zone));
808 for i in 0..50 {
809 p.push(i);
810 }
811 let (r, s) = p.finish();
812 assert_eq!(n.load(Ordering::SeqCst), 50);
813 assert_eq!(refined.lock().unwrap().len(), 25);
814 assert!(r.zones.is_empty());
815 assert_eq!(s.hook_calls, 75);
816 }
817
818 #[test]
819 fn pipeline_command_forwarded() {
820 let msgs = log();
821 let a = Arc::clone(&msgs);
822 let mut p = Pipeline::new(Fake::default()).sync(true).on_message(move |k, m| a.lock().unwrap().push(format!("{k:?}:{m}")));
823 assert_eq!(p.command(Command::InvalidateZone(3)), 1);
824 p.command(Command::ResetFrom(7));
825 assert_eq!(p.reducer().commands, vec!["InvalidateZone(3)", "ResetFrom(7)"]);
826 let (_, _) = p.finish();
827 assert_eq!(msgs.lock().unwrap().clone(), vec!["Warning:invalidate 3", "Warning:reset 7"]);
828 }
829}