1use serde::{Deserialize, Serialize};
16use std::cell::RefCell;
17use std::collections::VecDeque;
18use std::rc::Rc;
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub enum MockMessage {
32 Bootstrap {
34 base_url: String,
36 },
37 Init {
39 model_url: String,
41 },
42 Ready,
44 ModelLoaded {
46 size_mb: f64,
48 load_time_ms: f64,
50 },
51 Start {
53 sample_rate: u32,
55 },
56 Stop,
58 Partial {
60 text: String,
62 is_final: bool,
64 },
65 Error {
67 message: String,
69 },
70 Shutdown,
72 Custom {
74 msg_type: String,
76 payload: String,
78 },
79}
80
81impl MockMessage {
82 #[must_use]
84 pub fn bootstrap(base_url: &str) -> Self {
85 Self::Bootstrap {
86 base_url: base_url.to_string(),
87 }
88 }
89
90 #[must_use]
92 pub fn init(model_url: &str) -> Self {
93 Self::Init {
94 model_url: model_url.to_string(),
95 }
96 }
97
98 #[must_use]
100 pub fn model_loaded(size_mb: f64, load_time_ms: f64) -> Self {
101 Self::ModelLoaded {
102 size_mb,
103 load_time_ms,
104 }
105 }
106
107 #[must_use]
109 pub fn start(sample_rate: u32) -> Self {
110 Self::Start { sample_rate }
111 }
112
113 #[must_use]
115 pub fn error(message: &str) -> Self {
116 Self::Error {
117 message: message.to_string(),
118 }
119 }
120
121 #[must_use]
123 pub fn partial(text: &str, is_final: bool) -> Self {
124 Self::Partial {
125 text: text.to_string(),
126 is_final,
127 }
128 }
129}
130
131pub struct MockWasmRuntime {
136 incoming: Rc<RefCell<VecDeque<MockMessage>>>,
138 outgoing: Rc<RefCell<VecDeque<MockMessage>>>,
140 handlers: Rc<RefCell<Vec<Box<dyn Fn(&MockMessage)>>>>,
142 started: bool,
144 messages_processed: usize,
146}
147
148impl Default for MockWasmRuntime {
149 fn default() -> Self {
150 Self::new()
151 }
152}
153
154impl std::fmt::Debug for MockWasmRuntime {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 f.debug_struct("MockWasmRuntime")
157 .field("incoming_count", &self.incoming.borrow().len())
158 .field("outgoing_count", &self.outgoing.borrow().len())
159 .field("handlers_count", &self.handlers.borrow().len())
160 .field("started", &self.started)
161 .field("messages_processed", &self.messages_processed)
162 .finish()
163 }
164}
165
166impl Clone for MockWasmRuntime {
167 fn clone(&self) -> Self {
168 Self {
169 incoming: Rc::clone(&self.incoming),
170 outgoing: Rc::clone(&self.outgoing),
171 handlers: Rc::clone(&self.handlers),
172 started: self.started,
173 messages_processed: self.messages_processed,
174 }
175 }
176}
177
178impl MockWasmRuntime {
179 #[must_use]
181 pub fn new() -> Self {
182 Self {
183 incoming: Rc::new(RefCell::new(VecDeque::new())),
184 outgoing: Rc::new(RefCell::new(VecDeque::new())),
185 handlers: Rc::new(RefCell::new(Vec::new())),
186 started: false,
187 messages_processed: 0,
188 }
189 }
190
191 pub fn on_message<F>(&mut self, handler: F)
193 where
194 F: Fn(&MockMessage) + 'static,
195 {
196 self.handlers.borrow_mut().push(Box::new(handler));
197 }
198
199 #[allow(clippy::expect_used)] pub fn post_message(&self, msg: MockMessage) {
214 let serialized = bincode::serialize(&msg)
216 .expect("MockMessage serialization failed - this would fail in browser postMessage");
217 let cloned: MockMessage = bincode::deserialize(&serialized)
218 .expect("MockMessage deserialization failed - corrupted message");
219
220 self.outgoing.borrow_mut().push_back(cloned);
221 }
222
223 #[allow(clippy::expect_used)] pub fn receive_message(&self, msg: MockMessage) {
241 let serialized = bincode::serialize(&msg)
243 .expect("MockMessage serialization failed - this would fail in browser postMessage");
244 let cloned: MockMessage = bincode::deserialize(&serialized)
245 .expect("MockMessage deserialization failed - corrupted message");
246
247 self.incoming.borrow_mut().push_back(cloned);
248 }
249
250 #[doc(hidden)]
255 pub fn receive_message_unchecked(&self, msg: MockMessage) {
256 self.incoming.borrow_mut().push_back(msg);
257 }
258
259 pub fn tick(&mut self) -> bool {
269 let msg = self.incoming.borrow_mut().pop_front();
271
272 if let Some(msg) = msg {
273 struct HandlersGuard {
275 handlers_ref: Rc<RefCell<Vec<Box<dyn Fn(&MockMessage)>>>>,
276 handlers_to_run: Vec<Box<dyn Fn(&MockMessage)>>,
277 }
278
279 impl Drop for HandlersGuard {
280 fn drop(&mut self) {
281 let mut handlers = self.handlers_ref.borrow_mut();
282 let new_handlers = std::mem::take(&mut *handlers);
284 *handlers = std::mem::take(&mut self.handlers_to_run);
285 handlers.extend(new_handlers);
286 }
287 }
288
289 let handlers_guard = HandlersGuard {
291 handlers_ref: Rc::clone(&self.handlers),
292 handlers_to_run: {
293 let mut h = self.handlers.borrow_mut();
294 std::mem::take(&mut *h)
295 },
296 };
297
298 for handler in &handlers_guard.handlers_to_run {
300 handler(&msg);
301 }
302
303 self.messages_processed += 1;
306 true
307 } else {
308 false
309 }
310 }
311
312 pub fn drain(&mut self) {
320 self.drain_bounded(10_000);
321 }
322
323 pub fn drain_bounded(&mut self, max_messages: usize) -> usize {
327 let mut processed = 0;
328 while processed < max_messages && self.tick() {
329 processed += 1;
330 }
331 processed
332 }
333
334 pub fn tick_n(&mut self, n: usize) -> usize {
336 let mut processed = 0;
337 for _ in 0..n {
338 if self.tick() {
339 processed += 1;
340 } else {
341 break;
342 }
343 }
344 processed
345 }
346
347 #[must_use]
349 pub fn pending_count(&self) -> usize {
350 self.incoming.borrow().len()
351 }
352
353 #[must_use]
355 pub fn take_outgoing(&self) -> Vec<MockMessage> {
356 self.outgoing.borrow_mut().drain(..).collect()
357 }
358
359 #[must_use]
361 pub fn peek_outgoing(&self) -> Vec<MockMessage> {
362 self.outgoing.borrow().iter().cloned().collect()
363 }
364
365 #[must_use]
367 pub fn has_outgoing(&self) -> bool {
368 !self.outgoing.borrow().is_empty()
369 }
370
371 #[must_use]
373 pub fn total_processed(&self) -> usize {
374 self.messages_processed
375 }
376
377 pub fn reset(&mut self) {
379 self.incoming.borrow_mut().clear();
380 self.outgoing.borrow_mut().clear();
381 self.handlers.borrow_mut().clear();
382 self.messages_processed = 0;
383 }
384
385 pub fn start(&mut self) {
387 self.started = true;
388 }
389
390 #[must_use]
392 pub fn is_started(&self) -> bool {
393 self.started
394 }
395}
396
397pub trait MockableWorker: Sized {
401 fn with_mock_runtime(runtime: MockWasmRuntime) -> Self;
403
404 fn get_state(&self) -> String;
406
407 fn debug_internal_state(&self) -> String {
411 self.get_state() }
413
414 fn is_state_synced(&self) -> bool {
418 self.get_state() == self.debug_internal_state()
419 }
420}
421
422#[cfg(test)]
423#[allow(clippy::unwrap_used, clippy::expect_used)]
424mod tests {
425 use super::*;
426
427 #[test]
428 fn test_mock_message_constructors() {
429 let bootstrap = MockMessage::bootstrap("http://localhost:8080");
430 assert!(matches!(
431 bootstrap,
432 MockMessage::Bootstrap { base_url } if base_url == "http://localhost:8080"
433 ));
434
435 let init = MockMessage::init("/models/whisper-tiny.apr");
436 assert!(
437 matches!(init, MockMessage::Init { model_url } if model_url == "/models/whisper-tiny.apr")
438 );
439
440 let loaded = MockMessage::model_loaded(39.0, 1500.0);
441 assert!(matches!(
442 loaded,
443 MockMessage::ModelLoaded { size_mb, load_time_ms }
444 if (size_mb - 39.0).abs() < f64::EPSILON && (load_time_ms - 1500.0).abs() < f64::EPSILON
445 ));
446
447 let start = MockMessage::start(48000);
448 assert!(matches!(start, MockMessage::Start { sample_rate } if sample_rate == 48000));
449
450 let error = MockMessage::error("Test error");
451 assert!(matches!(error, MockMessage::Error { message } if message == "Test error"));
452
453 let partial = MockMessage::partial("Hello", false);
454 assert!(
455 matches!(partial, MockMessage::Partial { text, is_final } if text == "Hello" && !is_final)
456 );
457 }
458
459 #[test]
460 fn test_mock_runtime_message_flow() {
461 let mut runtime = MockWasmRuntime::new();
462 let received = Rc::new(RefCell::new(Vec::new()));
463 let received_clone = Rc::clone(&received);
464
465 runtime.on_message(move |msg| {
466 received_clone.borrow_mut().push(msg.clone());
467 });
468
469 runtime.receive_message(MockMessage::Ready);
471 runtime.receive_message(MockMessage::model_loaded(39.0, 1500.0));
472
473 assert_eq!(runtime.pending_count(), 2);
474
475 assert!(runtime.tick());
477 assert_eq!(received.borrow().len(), 1);
478 assert!(matches!(&received.borrow()[0], MockMessage::Ready));
479
480 runtime.drain();
482 assert_eq!(received.borrow().len(), 2);
483 assert_eq!(runtime.total_processed(), 2);
484 }
485
486 #[test]
487 fn test_mock_runtime_outgoing() {
488 let runtime = MockWasmRuntime::new();
489
490 runtime.post_message(MockMessage::start(48000));
491 runtime.post_message(MockMessage::Stop);
492
493 assert!(runtime.has_outgoing());
494 assert_eq!(runtime.peek_outgoing().len(), 2);
495
496 let outgoing = runtime.take_outgoing();
497 assert_eq!(outgoing.len(), 2);
498 assert!(!runtime.has_outgoing());
499 }
500
501 #[test]
502 fn test_mock_runtime_clone() {
503 let runtime1 = MockWasmRuntime::new();
504 runtime1.receive_message(MockMessage::Ready);
505
506 let runtime2 = runtime1;
507
508 assert_eq!(runtime2.pending_count(), 1);
510 }
511
512 #[test]
513 fn test_mock_runtime_tick_n() {
514 let mut runtime = MockWasmRuntime::new();
515 let count = Rc::new(RefCell::new(0));
516 let count_clone = Rc::clone(&count);
517
518 runtime.on_message(move |_| {
519 *count_clone.borrow_mut() += 1;
520 });
521
522 for _ in 0..10 {
523 runtime.receive_message(MockMessage::Ready);
524 }
525
526 let processed = runtime.tick_n(5);
528 assert_eq!(processed, 5);
529 assert_eq!(*count.borrow(), 5);
530 assert_eq!(runtime.pending_count(), 5);
531 }
532
533 #[test]
534 fn test_mock_runtime_reset() {
535 let mut runtime = MockWasmRuntime::new();
536
537 runtime.receive_message(MockMessage::Ready);
538 runtime.post_message(MockMessage::Stop);
539 runtime.on_message(|_| {});
540 runtime.tick();
541
542 assert!(runtime.total_processed() > 0);
543
544 runtime.reset();
545
546 assert_eq!(runtime.pending_count(), 0);
547 assert!(!runtime.has_outgoing());
548 assert_eq!(runtime.total_processed(), 0);
549 }
550
551 #[test]
552 fn test_mock_message_equality() {
553 let msg1 = MockMessage::model_loaded(39.0, 1500.0);
554 let msg2 = MockMessage::model_loaded(39.0, 1500.0);
555 let msg3 = MockMessage::model_loaded(40.0, 1500.0);
556
557 assert_eq!(msg1, msg2);
558 assert_ne!(msg1, msg3);
559 }
560
561 #[test]
562 fn test_mock_runtime_default() {
563 let runtime = MockWasmRuntime::default();
564 assert!(!runtime.is_started());
565 assert_eq!(runtime.pending_count(), 0);
566 assert_eq!(runtime.total_processed(), 0);
567 }
568
569 #[test]
570 fn test_mock_runtime_debug() {
571 let runtime = MockWasmRuntime::new();
572 let debug_str = format!("{:?}", runtime);
573 assert!(debug_str.contains("MockWasmRuntime"));
574 assert!(debug_str.contains("incoming_count"));
575 assert!(debug_str.contains("started"));
576 }
577
578 #[test]
579 fn test_mock_runtime_start() {
580 let mut runtime = MockWasmRuntime::new();
581 assert!(!runtime.is_started());
582 runtime.start();
583 assert!(runtime.is_started());
584 }
585
586 #[test]
587 fn test_mock_runtime_receive_message_unchecked() {
588 let runtime = MockWasmRuntime::new();
589 runtime.receive_message_unchecked(MockMessage::Ready);
590 assert_eq!(runtime.pending_count(), 1);
591 }
592
593 #[test]
594 fn test_mock_runtime_tick_empty() {
595 let mut runtime = MockWasmRuntime::new();
596 assert!(!runtime.tick());
597 assert_eq!(runtime.total_processed(), 0);
598 }
599
600 #[test]
601 fn test_mock_runtime_drain_bounded() {
602 let mut runtime = MockWasmRuntime::new();
603 let counter = Rc::new(RefCell::new(0));
604 let counter_clone = Rc::clone(&counter);
605
606 runtime.on_message(move |_| {
607 *counter_clone.borrow_mut() += 1;
608 });
609
610 for _ in 0..20 {
611 runtime.receive_message(MockMessage::Ready);
612 }
613
614 let processed = runtime.drain_bounded(5);
616 assert_eq!(processed, 5);
617 assert_eq!(*counter.borrow(), 5);
618 assert_eq!(runtime.pending_count(), 15);
619 }
620
621 #[test]
622 fn test_mock_runtime_drain_all() {
623 let mut runtime = MockWasmRuntime::new();
624 for _ in 0..10 {
625 runtime.receive_message(MockMessage::Ready);
626 }
627
628 runtime.drain();
629 assert_eq!(runtime.pending_count(), 0);
630 }
631
632 #[test]
633 fn test_mock_runtime_clone_shared_state() {
634 let runtime1 = MockWasmRuntime::new();
635 let runtime2 = runtime1.clone();
636
637 runtime1.receive_message(MockMessage::Ready);
638 assert_eq!(runtime1.pending_count(), 1);
640 assert_eq!(runtime2.pending_count(), 1);
641
642 runtime2.post_message(MockMessage::Stop);
643 assert!(runtime1.has_outgoing());
644 assert!(runtime2.has_outgoing());
645 }
646
647 #[test]
648 fn test_mock_runtime_peek_outgoing() {
649 let runtime = MockWasmRuntime::new();
650 runtime.post_message(MockMessage::start(48000));
651 runtime.post_message(MockMessage::Stop);
652
653 let peeked = runtime.peek_outgoing();
654 assert_eq!(peeked.len(), 2);
655
656 let peeked_again = runtime.peek_outgoing();
658 assert_eq!(peeked_again.len(), 2);
659 }
660
661 #[test]
662 fn test_mock_runtime_take_outgoing_consumes() {
663 let runtime = MockWasmRuntime::new();
664 runtime.post_message(MockMessage::Ready);
665
666 let taken = runtime.take_outgoing();
667 assert_eq!(taken.len(), 1);
668
669 assert!(!runtime.has_outgoing());
671 let taken_again = runtime.take_outgoing();
672 assert!(taken_again.is_empty());
673 }
674
675 #[test]
676 fn test_mock_message_custom() {
677 let msg = MockMessage::Custom {
678 msg_type: "test".to_string(),
679 payload: r#"{"key": "value"}"#.to_string(),
680 };
681
682 match msg {
683 MockMessage::Custom { msg_type, payload } => {
684 assert_eq!(msg_type, "test");
685 assert!(payload.contains("key"));
686 }
687 _ => panic!("Expected Custom message"),
688 }
689 }
690
691 #[test]
692 fn test_mock_message_partial() {
693 let msg = MockMessage::partial("Hello world", true);
694 match msg {
695 MockMessage::Partial { text, is_final } => {
696 assert_eq!(text, "Hello world");
697 assert!(is_final);
698 }
699 _ => panic!("Expected Partial message"),
700 }
701
702 let msg2 = MockMessage::partial("Partial", false);
703 match msg2 {
704 MockMessage::Partial { is_final, .. } => {
705 assert!(!is_final);
706 }
707 _ => panic!("Expected Partial message"),
708 }
709 }
710
711 #[test]
712 fn test_mock_message_serialization() {
713 let messages = vec![
715 MockMessage::bootstrap("http://localhost"),
716 MockMessage::init("/model.apr"),
717 MockMessage::Ready,
718 MockMessage::model_loaded(100.0, 2000.0),
719 MockMessage::start(44100),
720 MockMessage::Stop,
721 MockMessage::partial("text", true),
722 MockMessage::error("oops"),
723 MockMessage::Shutdown,
724 MockMessage::Custom {
725 msg_type: "t".into(),
726 payload: "{}".into(),
727 },
728 ];
729
730 for msg in messages {
731 let serialized = bincode::serialize(&msg).expect("Should serialize");
732 let deserialized: MockMessage =
733 bincode::deserialize(&serialized).expect("Should deserialize");
734 assert_eq!(msg, deserialized);
735 }
736 }
737
738 #[test]
739 fn test_mockable_worker_is_state_synced() {
740 struct TestWorker {
741 reported: String,
742 internal: String,
743 }
744
745 impl MockableWorker for TestWorker {
746 fn with_mock_runtime(_: MockWasmRuntime) -> Self {
747 Self {
748 reported: "same".into(),
749 internal: "same".into(),
750 }
751 }
752
753 fn get_state(&self) -> String {
754 self.reported.clone()
755 }
756
757 fn debug_internal_state(&self) -> String {
758 self.internal.clone()
759 }
760 }
761
762 let worker = TestWorker {
763 reported: "state".into(),
764 internal: "state".into(),
765 };
766 assert!(worker.is_state_synced());
767
768 let desynced = TestWorker {
769 reported: "one".into(),
770 internal: "two".into(),
771 };
772 assert!(!desynced.is_state_synced());
773 }
774
775 #[test]
776 fn test_mock_runtime_multiple_handlers() {
777 let mut runtime = MockWasmRuntime::new();
778 let counter1 = Rc::new(RefCell::new(0));
779 let counter2 = Rc::new(RefCell::new(0));
780
781 let c1 = Rc::clone(&counter1);
782 runtime.on_message(move |_| {
783 *c1.borrow_mut() += 1;
784 });
785
786 let c2 = Rc::clone(&counter2);
787 runtime.on_message(move |_| {
788 *c2.borrow_mut() += 10;
789 });
790
791 runtime.receive_message(MockMessage::Ready);
792 runtime.tick();
793
794 assert_eq!(*counter1.borrow(), 1);
796 assert_eq!(*counter2.borrow(), 10);
797 }
798
799 #[test]
800 fn test_mock_runtime_handler_adds_new_handler() {
801 let mut runtime = MockWasmRuntime::new();
802 let counter = Rc::new(RefCell::new(0));
803 let counter_clone = Rc::clone(&counter);
804
805 runtime.on_message(move |_| {
807 *counter_clone.borrow_mut() += 1;
808 });
809
810 runtime.receive_message(MockMessage::Ready);
812 runtime.tick();
813 assert_eq!(*counter.borrow(), 1);
814
815 runtime.receive_message(MockMessage::Stop);
817 runtime.tick();
818 assert_eq!(*counter.borrow(), 2);
819 }
820
821 #[test]
822 fn test_mock_runtime_tick_n_partial() {
823 let mut runtime = MockWasmRuntime::new();
824
825 runtime.receive_message(MockMessage::Ready);
827 runtime.receive_message(MockMessage::Stop);
828 runtime.receive_message(MockMessage::Shutdown);
829
830 let processed = runtime.tick_n(2);
832 assert_eq!(processed, 2);
833 assert_eq!(runtime.pending_count(), 1);
834 }
835
836 #[test]
837 fn test_mock_runtime_tick_n_more_than_available() {
838 let mut runtime = MockWasmRuntime::new();
839 runtime.receive_message(MockMessage::Ready);
840
841 let processed = runtime.tick_n(100);
843 assert_eq!(processed, 1);
844 assert_eq!(runtime.pending_count(), 0);
845 }
846
847 #[test]
848 fn test_mock_runtime_tick_n_zero() {
849 let mut runtime = MockWasmRuntime::new();
850 runtime.receive_message(MockMessage::Ready);
851
852 let processed = runtime.tick_n(0);
853 assert_eq!(processed, 0);
854 assert_eq!(runtime.pending_count(), 1);
855 }
856}