1use super::wasm_runtime::{MockMessage, MockWasmRuntime, MockableWorker};
13use std::fmt::Debug;
14
15#[derive(Debug, Clone)]
17pub struct TestStep {
18 pub message: MockMessage,
20 pub expected_state: String,
22 pub description: Option<String>,
24}
25
26impl TestStep {
27 #[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 #[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#[derive(Debug, Clone)]
47pub enum StateAssertion {
48 Equals(String),
50 Contains(String),
52 OneOf(Vec<String>),
54 Custom(String),
56}
57
58impl StateAssertion {
59 #[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, }
68 }
69
70 #[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
82pub struct WasmCallbackTestHarness<W: MockableWorker> {
100 pub worker: W,
102 pub runtime: MockWasmRuntime,
104 steps_executed: usize,
106 errors: Vec<String>,
108}
109
110impl<W: MockableWorker> WasmCallbackTestHarness<W> {
111 #[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 #[must_use]
126 pub fn state(&self) -> String {
127 self.worker.get_state()
128 }
129
130 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 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 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 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 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 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 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 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 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 #[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 #[must_use]
283 pub fn steps_executed(&self) -> usize {
284 self.steps_executed
285 }
286
287 #[must_use]
289 pub fn errors(&self) -> &[String] {
290 &self.errors
291 }
292
293 #[must_use]
295 pub fn has_errors(&self) -> bool {
296 !self.errors.is_empty()
297 }
298
299 pub fn drain(&mut self) {
301 self.runtime.drain();
302 }
303
304 #[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 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 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 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 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 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(); }
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 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 harness.worker_ready();
690 harness.assert_state("loading");
691
692 harness.model_loaded(39.0, 1500.0);
694 harness.assert_state("ready");
695
696 harness.send_message(MockMessage::start(48000));
698 harness.assert_state("recording");
699
700 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 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 for step in &steps {
736 assert!(step.description.is_some());
737 }
738 }
739}