Skip to main content

jugar_probar/mock/
test_harness.rs

1//! WASM Callback Test Harness
2//!
3//! Per `PROBAR-SPEC-WASM-001` Section 2.2, this provides a test harness
4//! for `WorkerManager`-style components that use callbacks.
5//!
6//! ## Iron Lotus Philosophy
7//!
8//! This harness tests ACTUAL code, not models. It would have caught
9//! the WAPR-QA-REGRESSION-005 state sync bug because it verifies that
10//! `get_state()` returns the correct value after callback processing.
11
12use super::wasm_runtime::{MockMessage, MockWasmRuntime, MockableWorker};
13use std::fmt::Debug;
14
15/// A single test step with expected state
16#[derive(Debug, Clone)]
17pub struct TestStep {
18    /// Message to send
19    pub message: MockMessage,
20    /// Expected state after processing
21    pub expected_state: String,
22    /// Optional description
23    pub description: Option<String>,
24}
25
26impl TestStep {
27    /// Create a new test step
28    #[must_use]
29    pub fn new(message: MockMessage, expected_state: &str) -> Self {
30        Self {
31            message,
32            expected_state: expected_state.to_string(),
33            description: None,
34        }
35    }
36
37    /// Add a description to this step
38    #[must_use]
39    pub fn with_description(mut self, desc: &str) -> Self {
40        self.description = Some(desc.to_string());
41        self
42    }
43}
44
45/// Assertion about component state
46#[derive(Debug, Clone)]
47pub enum StateAssertion {
48    /// State equals expected value
49    Equals(String),
50    /// State contains substring
51    Contains(String),
52    /// State matches one of several values
53    OneOf(Vec<String>),
54    /// Custom predicate (as description)
55    Custom(String),
56}
57
58impl StateAssertion {
59    /// Check if state satisfies the assertion
60    #[must_use]
61    pub fn check(&self, actual: &str) -> bool {
62        match self {
63            Self::Equals(expected) => actual == expected,
64            Self::Contains(substring) => actual.contains(substring),
65            Self::OneOf(options) => options.iter().any(|o| actual == o),
66            Self::Custom(_) => true, // Custom predicates need external evaluation
67        }
68    }
69
70    /// Get a human-readable description of the assertion
71    #[must_use]
72    pub fn describe(&self) -> String {
73        match self {
74            Self::Equals(expected) => format!("state == \"{expected}\""),
75            Self::Contains(substring) => format!("state contains \"{substring}\""),
76            Self::OneOf(options) => format!("state in {:?}", options),
77            Self::Custom(desc) => desc.clone(),
78        }
79    }
80}
81
82/// Test harness for WASM callback components
83///
84/// Wraps a component with mock runtime and provides testing utilities.
85///
86/// # Example
87///
88/// ```rust,ignore
89/// let harness = WasmCallbackTestHarness::<MyWorker>::new();
90///
91/// // Spawn and verify initial state
92/// harness.worker.spawn("model.apr").unwrap();
93/// harness.assert_state("spawning");
94///
95/// // Simulate worker ready
96/// harness.worker_ready();
97/// harness.assert_state("loading");  // Would FAIL with state sync bug!
98/// ```
99pub struct WasmCallbackTestHarness<W: MockableWorker> {
100    /// The worker component under test
101    pub worker: W,
102    /// The mock runtime (shared with worker)
103    pub runtime: MockWasmRuntime,
104    /// Test steps executed
105    steps_executed: usize,
106    /// Errors encountered
107    errors: Vec<String>,
108}
109
110impl<W: MockableWorker> WasmCallbackTestHarness<W> {
111    /// Create a new test harness
112    #[must_use]
113    pub fn new() -> Self {
114        let runtime = MockWasmRuntime::new();
115        let worker = W::with_mock_runtime(runtime.clone());
116        Self {
117            worker,
118            runtime,
119            steps_executed: 0,
120            errors: Vec::new(),
121        }
122    }
123
124    /// Get the current state
125    #[must_use]
126    pub fn state(&self) -> String {
127        self.worker.get_state()
128    }
129
130    /// Assert that state equals expected value
131    ///
132    /// # Panics
133    ///
134    /// Panics if state doesn't match expected.
135    pub fn assert_state(&self, expected: &str) {
136        let actual = self.worker.get_state();
137        assert_eq!(
138            actual, expected,
139            "State mismatch: expected '{}', got '{}'",
140            expected, actual
141        );
142    }
143
144    /// Assert that state satisfies a predicate
145    ///
146    /// # Panics
147    ///
148    /// Panics if assertion fails.
149    pub fn assert(&self, assertion: &StateAssertion) {
150        let actual = self.worker.get_state();
151        assert!(
152            assertion.check(&actual),
153            "Assertion failed: {} (actual: '{}')",
154            assertion.describe(),
155            actual
156        );
157    }
158
159    /// Check for state synchronization (catches WAPR-QA-REGRESSION-005 type bugs)
160    ///
161    /// # Panics
162    ///
163    /// Panics if internal state differs from reported state.
164    pub fn assert_state_synced(&self) {
165        let reported = self.worker.get_state();
166        let internal = self.worker.debug_internal_state();
167        assert_eq!(
168            reported, internal,
169            "STATE DESYNC DETECTED! Reported: '{}', Internal: '{}'\n\
170             This indicates a bug like WAPR-QA-REGRESSION-005 where closure \
171             updates a different variable than state checks use.",
172            reported, internal
173        );
174    }
175
176    /// Simulate worker becoming ready
177    pub fn worker_ready(&mut self) {
178        self.runtime.receive_message(MockMessage::Ready);
179        self.runtime.tick();
180        self.steps_executed += 1;
181    }
182
183    /// Simulate model loaded
184    pub fn model_loaded(&mut self, size_mb: f64, load_time_ms: f64) {
185        self.runtime.receive_message(MockMessage::ModelLoaded {
186            size_mb,
187            load_time_ms,
188        });
189        self.runtime.tick();
190        self.steps_executed += 1;
191    }
192
193    /// Simulate an error
194    pub fn worker_error(&mut self, message: &str) {
195        self.runtime.receive_message(MockMessage::Error {
196            message: message.to_string(),
197        });
198        self.runtime.tick();
199        self.steps_executed += 1;
200    }
201
202    /// Send a custom message and tick
203    pub fn send_message(&mut self, msg: MockMessage) {
204        self.runtime.receive_message(msg);
205        self.runtime.tick();
206        self.steps_executed += 1;
207    }
208
209    /// Execute a sequence of test steps
210    ///
211    /// # Errors
212    ///
213    /// Returns error if any step's expected state doesn't match.
214    pub fn execute_steps(&mut self, steps: &[TestStep]) -> Result<(), String> {
215        for (i, step) in steps.iter().enumerate() {
216            self.runtime.receive_message(step.message.clone());
217            self.runtime.tick();
218            self.steps_executed += 1;
219
220            let actual = self.worker.get_state();
221            if actual != step.expected_state {
222                let desc = step
223                    .description
224                    .as_ref()
225                    .map(|d| format!(" ({})", d))
226                    .unwrap_or_default();
227                return Err(format!(
228                    "Step {}{}: expected state '{}', got '{}'",
229                    i + 1,
230                    desc,
231                    step.expected_state,
232                    actual
233                ));
234            }
235        }
236        Ok(())
237    }
238
239    /// Execute steps and collect all errors (don't fail fast)
240    pub fn execute_steps_all(&mut self, steps: &[TestStep]) -> Vec<String> {
241        let mut errors = Vec::new();
242
243        for (i, step) in steps.iter().enumerate() {
244            self.runtime.receive_message(step.message.clone());
245            self.runtime.tick();
246            self.steps_executed += 1;
247
248            let actual = self.worker.get_state();
249            if actual != step.expected_state {
250                let desc = step
251                    .description
252                    .as_ref()
253                    .map(|d| format!(" ({})", d))
254                    .unwrap_or_default();
255                errors.push(format!(
256                    "Step {}{}: expected state '{}', got '{}'",
257                    i + 1,
258                    desc,
259                    step.expected_state,
260                    actual
261                ));
262            }
263        }
264
265        errors
266    }
267
268    /// Get the happy path test steps for a typical worker lifecycle
269    #[must_use]
270    pub fn happy_path_steps() -> Vec<TestStep> {
271        vec![
272            TestStep::new(MockMessage::Ready, "loading").with_description("Worker ready"),
273            TestStep::new(MockMessage::model_loaded(39.0, 1500.0), "ready")
274                .with_description("Model loaded"),
275            TestStep::new(MockMessage::start(48000), "recording")
276                .with_description("Recording started"),
277            TestStep::new(MockMessage::Stop, "ready").with_description("Recording stopped"),
278        ]
279    }
280
281    /// Get steps executed count
282    #[must_use]
283    pub fn steps_executed(&self) -> usize {
284        self.steps_executed
285    }
286
287    /// Get recorded errors
288    #[must_use]
289    pub fn errors(&self) -> &[String] {
290        &self.errors
291    }
292
293    /// Check if harness has errors
294    #[must_use]
295    pub fn has_errors(&self) -> bool {
296        !self.errors.is_empty()
297    }
298
299    /// Process all pending messages
300    pub fn drain(&mut self) {
301        self.runtime.drain();
302    }
303
304    /// Get pending message count
305    #[must_use]
306    pub fn pending_count(&self) -> usize {
307        self.runtime.pending_count()
308    }
309}
310
311impl<W: MockableWorker> Default for WasmCallbackTestHarness<W> {
312    fn default() -> Self {
313        Self::new()
314    }
315}
316
317impl<W: MockableWorker> std::fmt::Debug for WasmCallbackTestHarness<W> {
318    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319        f.debug_struct("WasmCallbackTestHarness")
320            .field("worker_state", &self.worker.get_state())
321            .field("runtime", &self.runtime)
322            .field("steps_executed", &self.steps_executed)
323            .field("errors_count", &self.errors.len())
324            .finish()
325    }
326}
327
328#[cfg(test)]
329#[allow(clippy::unwrap_used, clippy::expect_used)]
330mod tests {
331    use super::*;
332
333    // Simple mock worker for testing the harness itself
334    struct SimpleWorker {
335        state: String,
336        #[allow(dead_code)]
337        runtime: MockWasmRuntime,
338    }
339
340    impl MockableWorker for SimpleWorker {
341        fn with_mock_runtime(mut runtime: MockWasmRuntime) -> Self {
342            let worker = Self {
343                state: "uninitialized".to_string(),
344                runtime: runtime.clone(),
345            };
346
347            // Set up message handler that updates state
348            let state_ptr = std::rc::Rc::new(std::cell::RefCell::new("uninitialized".to_string()));
349            let state_clone = std::rc::Rc::clone(&state_ptr);
350
351            runtime.on_message(move |msg| {
352                let new_state = match msg {
353                    MockMessage::Ready => "loading",
354                    MockMessage::ModelLoaded { .. } => "ready",
355                    MockMessage::Start { .. } => "recording",
356                    MockMessage::Stop => "ready",
357                    MockMessage::Error { .. } => "error",
358                    MockMessage::Shutdown => "shutdown",
359                    _ => return,
360                };
361                *state_clone.borrow_mut() = new_state.to_string();
362            });
363
364            // HACK: This is a simplified test implementation
365            // In real code, the state would be properly shared
366            worker
367        }
368
369        fn get_state(&self) -> String {
370            self.state.clone()
371        }
372    }
373
374    #[test]
375    fn test_test_step_creation() {
376        let step = TestStep::new(MockMessage::Ready, "loading").with_description("Worker ready");
377
378        assert!(matches!(step.message, MockMessage::Ready));
379        assert_eq!(step.expected_state, "loading");
380        assert_eq!(step.description, Some("Worker ready".to_string()));
381    }
382
383    #[test]
384    fn test_state_assertion_equals() {
385        let assertion = StateAssertion::Equals("ready".to_string());
386        assert!(assertion.check("ready"));
387        assert!(!assertion.check("loading"));
388    }
389
390    #[test]
391    fn test_state_assertion_contains() {
392        let assertion = StateAssertion::Contains("load".to_string());
393        assert!(assertion.check("loading"));
394        assert!(assertion.check("loaded"));
395        assert!(!assertion.check("ready"));
396    }
397
398    #[test]
399    fn test_state_assertion_one_of() {
400        let assertion = StateAssertion::OneOf(vec!["ready".to_string(), "loading".to_string()]);
401        assert!(assertion.check("ready"));
402        assert!(assertion.check("loading"));
403        assert!(!assertion.check("error"));
404    }
405
406    #[test]
407    fn test_state_assertion_describe() {
408        assert_eq!(
409            StateAssertion::Equals("ready".to_string()).describe(),
410            r#"state == "ready""#
411        );
412        assert_eq!(
413            StateAssertion::Contains("load".to_string()).describe(),
414            r#"state contains "load""#
415        );
416    }
417
418    #[test]
419    fn test_harness_happy_path_steps() {
420        let steps = WasmCallbackTestHarness::<SimpleWorker>::happy_path_steps();
421        assert!(!steps.is_empty());
422        assert!(matches!(steps[0].message, MockMessage::Ready));
423    }
424
425    #[test]
426    fn test_state_assertion_custom() {
427        let assertion = StateAssertion::Custom("custom check".to_string());
428        // Custom assertions always return true (need external evaluation)
429        assert!(assertion.check("anything"));
430        assert_eq!(assertion.describe(), "custom check");
431    }
432
433    #[test]
434    fn test_state_assertion_one_of_describe() {
435        let assertion = StateAssertion::OneOf(vec!["ready".to_string(), "loading".to_string()]);
436        let desc = assertion.describe();
437        assert!(desc.contains("ready"));
438        assert!(desc.contains("loading"));
439    }
440
441    // Stateful mock worker that actually updates state
442    struct StatefulWorker {
443        state: std::rc::Rc<std::cell::RefCell<String>>,
444        #[allow(dead_code)]
445        runtime: MockWasmRuntime,
446    }
447
448    impl MockableWorker for StatefulWorker {
449        fn with_mock_runtime(mut runtime: MockWasmRuntime) -> Self {
450            let state_ptr = std::rc::Rc::new(std::cell::RefCell::new("uninitialized".to_string()));
451            let state_clone = std::rc::Rc::clone(&state_ptr);
452
453            runtime.on_message(move |msg| {
454                let new_state = match msg {
455                    MockMessage::Ready => "loading",
456                    MockMessage::ModelLoaded { .. } => "ready",
457                    MockMessage::Start { .. } => "recording",
458                    MockMessage::Stop => "ready",
459                    MockMessage::Error { .. } => "error",
460                    MockMessage::Shutdown => "shutdown",
461                    _ => return,
462                };
463                *state_clone.borrow_mut() = new_state.to_string();
464            });
465
466            Self {
467                state: state_ptr,
468                runtime,
469            }
470        }
471
472        fn get_state(&self) -> String {
473            self.state.borrow().clone()
474        }
475
476        fn debug_internal_state(&self) -> String {
477            self.state.borrow().clone()
478        }
479    }
480
481    #[test]
482    fn test_harness_new() {
483        let harness = WasmCallbackTestHarness::<StatefulWorker>::new();
484        assert_eq!(harness.steps_executed(), 0);
485        assert!(!harness.has_errors());
486        assert!(harness.errors().is_empty());
487    }
488
489    #[test]
490    fn test_harness_worker_ready() {
491        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
492        harness.worker_ready();
493        assert_eq!(harness.steps_executed(), 1);
494        assert_eq!(harness.worker.get_state(), "loading");
495    }
496
497    #[test]
498    fn test_harness_model_loaded() {
499        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
500        harness.worker_ready();
501        harness.model_loaded(39.0, 1500.0);
502        assert_eq!(harness.steps_executed(), 2);
503        assert_eq!(harness.worker.get_state(), "ready");
504    }
505
506    #[test]
507    fn test_harness_worker_error() {
508        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
509        harness.worker_ready();
510        harness.worker_error("test error");
511        assert_eq!(harness.worker.get_state(), "error");
512    }
513
514    #[test]
515    fn test_harness_send_message() {
516        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
517        harness.send_message(MockMessage::Shutdown);
518        assert_eq!(harness.worker.get_state(), "shutdown");
519    }
520
521    #[test]
522    fn test_harness_assert_state() {
523        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
524        harness.worker_ready();
525        harness.assert_state("loading");
526    }
527
528    #[test]
529    fn test_harness_assert_predicate() {
530        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
531        harness.worker_ready();
532        harness.assert(&StateAssertion::Equals("loading".to_string()));
533        harness.assert(&StateAssertion::Contains("load".to_string()));
534    }
535
536    #[test]
537    fn test_harness_assert_state_synced() {
538        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
539        harness.worker_ready();
540        harness.assert_state_synced(); // Should not panic
541    }
542
543    #[test]
544    fn test_harness_execute_steps_success() {
545        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
546        let steps = vec![
547            TestStep::new(MockMessage::Ready, "loading"),
548            TestStep::new(MockMessage::model_loaded(39.0, 1500.0), "ready"),
549        ];
550        let result = harness.execute_steps(&steps);
551        assert!(result.is_ok());
552        assert_eq!(harness.steps_executed(), 2);
553    }
554
555    #[test]
556    fn test_harness_execute_steps_failure() {
557        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
558        let steps = vec![TestStep::new(MockMessage::Ready, "wrong_state")];
559        let result = harness.execute_steps(&steps);
560        assert!(result.is_err());
561    }
562
563    #[test]
564    fn test_harness_execute_steps_failure_with_description() {
565        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
566        let steps =
567            vec![TestStep::new(MockMessage::Ready, "wrong_state").with_description("Worker ready")];
568        let result = harness.execute_steps(&steps);
569        assert!(result.is_err());
570        assert!(result.unwrap_err().contains("Worker ready"));
571    }
572
573    #[test]
574    fn test_harness_execute_steps_all() {
575        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
576        let steps = vec![
577            TestStep::new(MockMessage::Ready, "wrong1"),
578            TestStep::new(MockMessage::model_loaded(39.0, 1500.0), "wrong2"),
579        ];
580        let errors = harness.execute_steps_all(&steps);
581        assert_eq!(errors.len(), 2);
582    }
583
584    #[test]
585    fn test_harness_execute_steps_all_with_description() {
586        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
587        let steps = vec![TestStep::new(MockMessage::Ready, "wrong").with_description("Test step")];
588        let errors = harness.execute_steps_all(&steps);
589        assert!(!errors.is_empty());
590        assert!(errors[0].contains("Test step"));
591    }
592
593    #[test]
594    fn test_harness_default() {
595        let harness: WasmCallbackTestHarness<StatefulWorker> = WasmCallbackTestHarness::default();
596        assert_eq!(harness.steps_executed(), 0);
597        assert!(!harness.has_errors());
598    }
599
600    #[test]
601    fn test_harness_debug() {
602        let harness = WasmCallbackTestHarness::<StatefulWorker>::new();
603        let debug_str = format!("{:?}", harness);
604        assert!(debug_str.contains("WasmCallbackTestHarness"));
605        assert!(debug_str.contains("steps_executed"));
606    }
607
608    #[test]
609    fn test_harness_state() {
610        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
611        assert_eq!(harness.state(), "uninitialized");
612        harness.worker_ready();
613        assert_eq!(harness.state(), "loading");
614    }
615
616    #[test]
617    fn test_harness_drain() {
618        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
619        harness.runtime.receive_message(MockMessage::Ready);
620        harness
621            .runtime
622            .receive_message(MockMessage::model_loaded(39.0, 1500.0));
623        assert_eq!(harness.pending_count(), 2);
624        harness.drain();
625        assert_eq!(harness.pending_count(), 0);
626    }
627
628    #[test]
629    fn test_harness_pending_count() {
630        let harness = WasmCallbackTestHarness::<StatefulWorker>::new();
631        assert_eq!(harness.pending_count(), 0);
632        harness.runtime.receive_message(MockMessage::Ready);
633        assert_eq!(harness.pending_count(), 1);
634    }
635
636    #[test]
637    fn test_test_step_without_description() {
638        let step = TestStep::new(MockMessage::Ready, "loading");
639        assert!(step.description.is_none());
640    }
641
642    #[test]
643    fn test_execute_steps_success_no_description() {
644        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
645        let steps = vec![
646            TestStep::new(MockMessage::Ready, "loading"),
647            TestStep::new(MockMessage::model_loaded(39.0, 1500.0), "ready"),
648        ];
649        let result = harness.execute_steps(&steps);
650        assert!(result.is_ok());
651    }
652
653    #[test]
654    fn test_execute_steps_all_success() {
655        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
656        let steps = vec![TestStep::new(MockMessage::Ready, "loading")];
657        let errors = harness.execute_steps_all(&steps);
658        assert!(errors.is_empty());
659    }
660
661    #[test]
662    fn test_execute_steps_all_no_description() {
663        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
664        let steps = vec![TestStep::new(MockMessage::Ready, "wrong_state")];
665        let errors = harness.execute_steps_all(&steps);
666        assert!(!errors.is_empty());
667        // Should contain step number but no description
668        assert!(errors[0].contains("Step 1:"));
669    }
670
671    #[test]
672    fn test_state_assertion_one_of_empty() {
673        let assertion = StateAssertion::OneOf(vec![]);
674        assert!(!assertion.check("any"));
675    }
676
677    #[test]
678    fn test_harness_errors_initially_empty() {
679        let harness = WasmCallbackTestHarness::<StatefulWorker>::new();
680        assert!(harness.errors().is_empty());
681        assert!(!harness.has_errors());
682    }
683
684    #[test]
685    fn test_harness_full_lifecycle() {
686        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
687
688        // Worker ready
689        harness.worker_ready();
690        harness.assert_state("loading");
691
692        // Model loaded
693        harness.model_loaded(39.0, 1500.0);
694        harness.assert_state("ready");
695
696        // Start recording
697        harness.send_message(MockMessage::start(48000));
698        harness.assert_state("recording");
699
700        // Stop recording
701        harness.send_message(MockMessage::Stop);
702        harness.assert_state("ready");
703
704        assert_eq!(harness.steps_executed(), 4);
705    }
706
707    #[test]
708    fn test_harness_shutdown() {
709        let mut harness = WasmCallbackTestHarness::<StatefulWorker>::new();
710        harness.send_message(MockMessage::Shutdown);
711        assert_eq!(harness.state(), "shutdown");
712    }
713
714    #[test]
715    fn test_state_assertion_equals_empty() {
716        let assertion = StateAssertion::Equals(String::new());
717        assert!(assertion.check(""));
718        assert!(!assertion.check("something"));
719    }
720
721    #[test]
722    fn test_state_assertion_contains_empty() {
723        let assertion = StateAssertion::Contains(String::new());
724        // Empty string is contained in any string
725        assert!(assertion.check("anything"));
726        assert!(assertion.check(""));
727    }
728
729    #[test]
730    fn test_happy_path_steps_structure() {
731        let steps = WasmCallbackTestHarness::<StatefulWorker>::happy_path_steps();
732        assert_eq!(steps.len(), 4);
733
734        // Check all steps have descriptions
735        for step in &steps {
736            assert!(step.description.is_some());
737        }
738    }
739}