Skip to main content

jugar_probar/
runtime.rs

1//! WASM Runtime Bridge - Phase 1 Implementation
2//!
3//! Per spec Section 3.1: Execute actual WASM games in tests (LOGIC-ONLY mode).
4//!
5//! This module provides a wasmtime-based runtime for deterministic WASM game testing.
6//! It is explicitly NOT for rendering or browser API tests - use `BrowserController` for that.
7//!
8//! # Architecture (from spec)
9//!
10//! ```text
11//! ┌─────────────────────────────────────────┐
12//! │  WasmRuntime (wasmtime)                 │
13//! │  Purpose: LOGIC-ONLY testing            │
14//! │                                         │
15//! │  ✅ Unit tests                          │
16//! │  ✅ Deterministic replay                │
17//! │  ✅ Invariant fuzzing                   │
18//! │  ✅ Performance benchmarks              │
19//! │                                         │
20//! │  ❌ NOT for rendering tests             │
21//! │  ❌ NOT for browser API tests           │
22//! └─────────────────────────────────────────┘
23//! ```
24//!
25//! # Toyota Principles Applied
26//!
27//! - **Muda (Waste Elimination)**: Zero-copy memory views avoid serialization overhead
28//! - **Poka-Yoke (Error Proofing)**: Type-safe entity/component accessors
29//! - **Standardization**: Clear separation from browser runtime
30
31use crate::event::InputEvent;
32use crate::result::{ProbarError, ProbarResult};
33use serde::{Deserialize, Serialize};
34use std::collections::{hash_map::DefaultHasher, VecDeque};
35use std::hash::{Hash, Hasher};
36
37#[cfg(feature = "runtime")]
38use wasmtime::{Caller, Engine, Instance, Linker, Module, Store};
39
40/// Entity identifier for type-safe game state access
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
42pub struct EntityId(pub u32);
43
44impl EntityId {
45    /// Create a new entity ID
46    #[must_use]
47    pub const fn new(id: u32) -> Self {
48        Self(id)
49    }
50
51    /// Get the raw ID value
52    #[must_use]
53    pub const fn raw(self) -> u32 {
54        self.0
55    }
56}
57
58/// Component identifier for type-safe component access
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
60pub struct ComponentId(u64);
61
62impl ComponentId {
63    /// Create component ID from type
64    #[must_use]
65    pub fn of<T: 'static>() -> Self {
66        let mut hasher = DefaultHasher::new();
67        std::any::TypeId::of::<T>().hash(&mut hasher);
68        Self(hasher.finish())
69    }
70
71    /// Get the raw ID value
72    #[must_use]
73    pub const fn raw(self) -> u64 {
74        self.0
75    }
76}
77
78/// Trait for type-safe entity selectors (Poka-Yoke pattern)
79///
80/// This trait is implemented by `#[derive(ProbarEntities)]` macro
81/// to provide compile-time verified entity access.
82///
83/// # Example
84///
85/// ```ignore
86/// // Generated by probar-derive
87/// #[derive(ProbarEntities)]
88/// pub struct PongGame {
89///     pub player1_paddle: Entity,
90///     pub player2_paddle: Entity,
91///     pub ball: Entity,
92/// }
93///
94/// // In tests - compile-time verified!
95/// let paddle = game.entity(PongGame::Player1Paddle);
96/// ```
97pub trait ProbarEntity: Copy {
98    /// Get the entity ID for this selector
99    fn entity_id(&self) -> EntityId;
100
101    /// Get the entity name for debugging
102    fn entity_name(&self) -> &'static str;
103}
104
105/// Trait for type-safe component access (Poka-Yoke pattern)
106///
107/// This trait is implemented by `#[derive(ProbarComponents)]` macro.
108pub trait ProbarComponent: Sized + Copy + 'static {
109    /// Get the component type ID
110    fn component_id() -> ComponentId;
111
112    /// Get the memory layout
113    fn layout() -> std::alloc::Layout;
114}
115
116/// Result of stepping the game by one frame
117#[derive(Debug, Clone)]
118pub struct FrameResult {
119    /// Current frame number
120    pub frame_number: u64,
121    /// Hash of game state for determinism verification
122    pub state_hash: u64,
123    /// Time taken to execute the frame
124    pub execution_time_ns: u64,
125}
126
127/// Delta-encoded state snapshot for efficient storage
128///
129/// Per Lavoie \[9\]: Delta encoding achieves 94% overhead reduction
130/// compared to full snapshots.
131#[derive(Debug, Clone)]
132pub struct StateDelta {
133    /// Base frame this delta applies to
134    pub base_frame: u64,
135    /// Target frame after applying delta
136    pub target_frame: u64,
137    /// Changed memory regions (offset, data)
138    pub changes: Vec<(usize, Vec<u8>)>,
139    /// Checksum of resulting state
140    pub checksum: u64,
141}
142
143impl StateDelta {
144    /// Create an empty delta
145    #[must_use]
146    pub fn empty(frame: u64) -> Self {
147        Self {
148            base_frame: frame,
149            target_frame: frame,
150            changes: Vec::new(),
151            checksum: 0,
152        }
153    }
154
155    /// Compute delta between two memory snapshots
156    #[must_use]
157    pub fn compute(base: &[u8], current: &[u8], base_frame: u64, target_frame: u64) -> Self {
158        let mut changes = Vec::new();
159        let mut i = 0;
160
161        while i < base.len().min(current.len()) {
162            // Find start of difference
163            if base.get(i) != current.get(i) {
164                let start = i;
165                // Find end of difference
166                while i < base.len().min(current.len()) && base.get(i) != current.get(i) {
167                    i += 1;
168                }
169                // Record the changed region
170                changes.push((start, current[start..i].to_vec()));
171            } else {
172                i += 1;
173            }
174        }
175
176        // Handle case where current is longer
177        if current.len() > base.len() {
178            changes.push((base.len(), current[base.len()..].to_vec()));
179        }
180
181        let checksum = Self::compute_checksum(current);
182
183        Self {
184            base_frame,
185            target_frame,
186            changes,
187            checksum,
188        }
189    }
190
191    /// Apply delta to base snapshot
192    #[must_use]
193    pub fn apply(&self, base: &[u8]) -> Vec<u8> {
194        let mut result = base.to_vec();
195        for (offset, data) in &self.changes {
196            let end = *offset + data.len();
197            if end > result.len() {
198                result.resize(end, 0);
199            }
200            result[*offset..end].copy_from_slice(data);
201        }
202        result
203    }
204
205    fn compute_checksum(data: &[u8]) -> u64 {
206        let mut hasher = DefaultHasher::new();
207        data.hash(&mut hasher);
208        hasher.finish()
209    }
210
211    /// Verify the checksum matches
212    #[must_use]
213    pub fn verify(&self, data: &[u8]) -> bool {
214        Self::compute_checksum(data) == self.checksum
215    }
216}
217
218/// Host state accessible to WASM guest
219///
220/// This struct holds the state that the WASM module can interact with
221/// through host function imports.
222#[derive(Debug, Default)]
223pub struct GameHostState {
224    /// Input queue for injection
225    pub input_queue: VecDeque<InputEvent>,
226    /// Simulated game time
227    pub simulated_time: f64,
228    /// Current frame count
229    pub frame_count: u64,
230    /// Snapshot deltas for replay
231    pub snapshot_deltas: Vec<StateDelta>,
232    /// Last full snapshot (for delta computation)
233    last_snapshot: Vec<u8>,
234}
235
236impl GameHostState {
237    /// Create new host state
238    #[must_use]
239    pub fn new() -> Self {
240        Self::default()
241    }
242
243    /// Pop next input from queue
244    pub fn pop_input(&mut self) -> Option<InputEvent> {
245        self.input_queue.pop_front()
246    }
247
248    /// Record a state snapshot (delta-encoded)
249    pub fn record_snapshot(&mut self, memory: &[u8]) {
250        let delta = StateDelta::compute(
251            &self.last_snapshot,
252            memory,
253            self.frame_count.saturating_sub(1),
254            self.frame_count,
255        );
256        self.snapshot_deltas.push(delta);
257        memory.clone_into(&mut self.last_snapshot);
258    }
259}
260
261/// Zero-copy memory view for WASM state inspection
262///
263/// Per spec: "Eliminates bincode serialization per-frame (Muda)"
264///
265/// # Safety
266///
267/// The memory view is only valid while the WASM instance is alive.
268/// Do not store references across frame boundaries.
269#[derive(Debug)]
270pub struct MemoryView {
271    /// Size of the memory region
272    size: usize,
273    /// Offset to entity table in WASM memory
274    entity_table_offset: usize,
275    /// Offset to component arrays
276    component_arrays_offset: usize,
277    /// Entity count
278    entity_count: usize,
279}
280
281impl MemoryView {
282    /// Create a new memory view
283    #[must_use]
284    pub fn new(size: usize) -> Self {
285        Self {
286            size,
287            entity_table_offset: 0,
288            component_arrays_offset: 0,
289            entity_count: 0,
290        }
291    }
292
293    /// Configure entity table location
294    #[must_use]
295    pub fn with_entity_table(mut self, offset: usize, count: usize) -> Self {
296        self.entity_table_offset = offset;
297        self.entity_count = count;
298        self
299    }
300
301    /// Configure component arrays location
302    #[must_use]
303    pub fn with_component_arrays(mut self, offset: usize) -> Self {
304        self.component_arrays_offset = offset;
305        self
306    }
307
308    /// Get the memory size
309    #[must_use]
310    pub const fn size(&self) -> usize {
311        self.size
312    }
313
314    /// Get entity count
315    #[must_use]
316    pub const fn entity_count(&self) -> usize {
317        self.entity_count
318    }
319
320    /// Get entity table offset
321    #[must_use]
322    pub const fn entity_table_offset(&self) -> usize {
323        self.entity_table_offset
324    }
325
326    /// Get component arrays offset
327    #[must_use]
328    pub const fn component_arrays_offset(&self) -> usize {
329        self.component_arrays_offset
330    }
331
332    /// Read a value at the given offset from a memory slice
333    ///
334    /// # Safety
335    ///
336    /// Caller must ensure:
337    /// - `offset + size_of::<T>() <= memory.len()`
338    /// - The memory at offset contains a valid T
339    #[inline]
340    pub unsafe fn read_at<T: Copy>(&self, memory: &[u8], offset: usize) -> ProbarResult<T> {
341        let size = core::mem::size_of::<T>();
342        if offset + size > memory.len() {
343            return Err(ProbarError::WasmError {
344                message: format!(
345                    "Read out of bounds: offset {} + size {} > memory {}",
346                    offset,
347                    size,
348                    memory.len()
349                ),
350            });
351        }
352        let ptr = memory.as_ptr().add(offset) as *const T;
353        Ok(core::ptr::read_unaligned(ptr))
354    }
355
356    /// Read a slice from memory
357    ///
358    /// # Safety
359    ///
360    /// Caller must ensure the memory region is valid
361    #[inline]
362    pub fn read_slice<'a>(
363        &self,
364        memory: &'a [u8],
365        offset: usize,
366        len: usize,
367    ) -> ProbarResult<&'a [u8]> {
368        if offset + len > memory.len() {
369            return Err(ProbarError::WasmError {
370                message: format!(
371                    "Slice out of bounds: offset {} + len {} > memory {}",
372                    offset,
373                    len,
374                    memory.len()
375                ),
376            });
377        }
378        Ok(&memory[offset..offset + len])
379    }
380}
381
382/// WASM runtime configuration
383#[derive(Debug, Clone, Copy)]
384pub struct RuntimeConfig {
385    /// Enable threading support (for SharedArrayBuffer)
386    pub wasm_threads: bool,
387    /// Enable SIMD support
388    pub wasm_simd: bool,
389    /// Enable reference types
390    pub wasm_reference_types: bool,
391    /// Maximum memory pages (64KB each)
392    pub max_memory_pages: u32,
393    /// Fuel limit for execution (0 = unlimited)
394    pub fuel_limit: u64,
395}
396
397impl Default for RuntimeConfig {
398    fn default() -> Self {
399        Self {
400            wasm_threads: false,
401            wasm_simd: true,
402            wasm_reference_types: true,
403            max_memory_pages: 256, // 16MB default
404            fuel_limit: 0,
405        }
406    }
407}
408
409impl RuntimeConfig {
410    /// Create new config with default settings
411    #[must_use]
412    pub fn new() -> Self {
413        Self::default()
414    }
415
416    /// Enable threading support
417    #[must_use]
418    pub const fn with_threads(mut self, enabled: bool) -> Self {
419        self.wasm_threads = enabled;
420        self
421    }
422
423    /// Set fuel limit
424    #[must_use]
425    pub const fn with_fuel_limit(mut self, limit: u64) -> Self {
426        self.fuel_limit = limit;
427        self
428    }
429}
430
431/// WASM runtime for LOGIC-ONLY game testing
432///
433/// This struct wraps wasmtime to provide deterministic WASM execution
434/// for game testing. It is NOT intended for rendering or browser API tests.
435///
436/// # Example
437///
438/// ```ignore
439/// let mut runtime = WasmRuntime::load(wasm_bytes)?;
440/// runtime.inject_input(InputEvent::key_press("ArrowUp"));
441/// let result = runtime.step()?;
442/// assert!(result.state_hash != 0);
443/// ```
444#[cfg(feature = "runtime")]
445pub struct WasmRuntime {
446    engine: Engine,
447    store: Store<GameHostState>,
448    instance: Instance,
449    memory_view: MemoryView,
450}
451
452#[cfg(feature = "runtime")]
453impl std::fmt::Debug for WasmRuntime {
454    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455        f.debug_struct("WasmRuntime")
456            .field("memory_view", &self.memory_view)
457            .finish_non_exhaustive()
458    }
459}
460
461#[cfg(feature = "runtime")]
462impl WasmRuntime {
463    /// Load a WASM game binary
464    ///
465    /// # Errors
466    ///
467    /// Returns error if:
468    /// - WASM binary is invalid
469    /// - Required exports are missing
470    /// - Linking fails
471    pub fn load(wasm_bytes: &[u8]) -> ProbarResult<Self> {
472        Self::load_with_config(wasm_bytes, RuntimeConfig::default())
473    }
474
475    /// Load with custom configuration
476    ///
477    /// # Errors
478    ///
479    /// Returns error if WASM loading fails
480    pub fn load_with_config(wasm_bytes: &[u8], config: RuntimeConfig) -> ProbarResult<Self> {
481        let mut engine_config = wasmtime::Config::new();
482        engine_config.wasm_threads(config.wasm_threads);
483        engine_config.wasm_simd(config.wasm_simd);
484        engine_config.wasm_reference_types(config.wasm_reference_types);
485
486        if config.fuel_limit > 0 {
487            engine_config.consume_fuel(true);
488        }
489
490        let engine = Engine::new(&engine_config).map_err(|e| ProbarError::WasmError {
491            message: format!("Failed to create engine: {e}"),
492        })?;
493
494        let module = Module::new(&engine, wasm_bytes).map_err(|e| ProbarError::WasmError {
495            message: format!("Failed to load module: {e}"),
496        })?;
497
498        let mut store = Store::new(&engine, GameHostState::new());
499
500        if config.fuel_limit > 0 {
501            store
502                .set_fuel(config.fuel_limit)
503                .map_err(|e| ProbarError::WasmError {
504                    message: format!("Failed to set fuel: {e}"),
505                })?;
506        }
507
508        let mut linker = Linker::new(&engine);
509
510        // Register host functions
511        Self::register_host_functions(&mut linker)?;
512
513        let instance =
514            linker
515                .instantiate(&mut store, &module)
516                .map_err(|e| ProbarError::WasmError {
517                    message: format!("Failed to instantiate: {e}"),
518                })?;
519
520        // Get memory size
521        let memory =
522            instance
523                .get_memory(&mut store, "memory")
524                .ok_or_else(|| ProbarError::WasmError {
525                    message: "Module does not export 'memory'".to_string(),
526                })?;
527
528        let memory_size = memory.data_size(&store);
529        let memory_view = MemoryView::new(memory_size);
530
531        Ok(Self {
532            engine,
533            store,
534            instance,
535            memory_view,
536        })
537    }
538
539    fn register_host_functions(linker: &mut Linker<GameHostState>) -> ProbarResult<()> {
540        // probar_get_input: Pop next input from queue
541        linker
542            .func_wrap(
543                "probar",
544                "get_input_count",
545                #[allow(clippy::cast_possible_truncation)]
546                |caller: Caller<'_, GameHostState>| -> u32 {
547                    caller.data().input_queue.len() as u32
548                },
549            )
550            .map_err(|e| ProbarError::WasmError {
551                message: format!("Failed to register get_input_count: {e}"),
552            })?;
553
554        // probar_get_time: Get simulated time
555        linker
556            .func_wrap(
557                "probar",
558                "get_time",
559                |caller: Caller<'_, GameHostState>| -> f64 { caller.data().simulated_time },
560            )
561            .map_err(|e| ProbarError::WasmError {
562                message: format!("Failed to register get_time: {e}"),
563            })?;
564
565        // probar_get_frame: Get current frame count
566        linker
567            .func_wrap(
568                "probar",
569                "get_frame",
570                |caller: Caller<'_, GameHostState>| -> u64 { caller.data().frame_count },
571            )
572            .map_err(|e| ProbarError::WasmError {
573                message: format!("Failed to register get_frame: {e}"),
574            })?;
575
576        Ok(())
577    }
578
579    /// Get a reference to the WASM engine
580    #[must_use]
581    pub const fn engine(&self) -> &Engine {
582        &self.engine
583    }
584
585    /// Inject an input event into the input queue
586    pub fn inject_input(&mut self, event: InputEvent) {
587        self.store.data_mut().input_queue.push_back(event);
588    }
589
590    /// Inject multiple input events
591    pub fn inject_inputs(&mut self, events: impl IntoIterator<Item = InputEvent>) {
592        for event in events {
593            self.inject_input(event);
594        }
595    }
596
597    /// Advance game by one frame (1/60th second by default)
598    ///
599    /// # Errors
600    ///
601    /// Returns error if:
602    /// - `jugar_update` export not found
603    /// - Execution traps or times out
604    pub fn step(&mut self) -> ProbarResult<FrameResult> {
605        self.step_with_dt(1.0 / 60.0)
606    }
607
608    /// Advance game by specified delta time
609    ///
610    /// # Errors
611    ///
612    /// Returns error if execution fails
613    pub fn step_with_dt(&mut self, dt: f64) -> ProbarResult<FrameResult> {
614        let start = std::time::Instant::now();
615
616        // Update simulated time
617        self.store.data_mut().simulated_time += dt;
618        self.store.data_mut().frame_count += 1;
619
620        // Call the game's update function
621        let update_fn = self
622            .instance
623            .get_typed_func::<f64, ()>(&mut self.store, "jugar_update")
624            .map_err(|e| ProbarError::WasmError {
625                message: format!("jugar_update not found: {e}"),
626            })?;
627
628        update_fn
629            .call(&mut self.store, dt)
630            .map_err(|e| ProbarError::WasmError {
631                message: format!("jugar_update failed: {e}"),
632            })?;
633
634        let execution_time = start.elapsed();
635        let state_hash = self.compute_state_hash();
636
637        #[allow(clippy::cast_possible_truncation)]
638        let execution_time_ns = execution_time.as_nanos() as u64;
639
640        Ok(FrameResult {
641            frame_number: self.store.data().frame_count,
642            state_hash,
643            execution_time_ns,
644        })
645    }
646
647    /// Compute hash of current game state
648    #[must_use]
649    pub fn compute_state_hash(&mut self) -> u64 {
650        let memory = self.get_memory();
651        let mut hasher = DefaultHasher::new();
652        memory.hash(&mut hasher);
653        hasher.finish()
654    }
655
656    /// Get raw memory slice
657    ///
658    /// # Panics
659    ///
660    /// Panics if the WASM module does not export a `memory` symbol.
661    /// This is expected for all valid Jugar game modules.
662    #[must_use]
663    pub fn get_memory(&mut self) -> &[u8] {
664        let memory = self
665            .instance
666            .get_memory(&mut self.store, "memory")
667            .expect("memory export required");
668        memory.data(&self.store)
669    }
670
671    /// Get the memory view for zero-copy state inspection
672    #[must_use]
673    pub const fn memory_view(&self) -> &MemoryView {
674        &self.memory_view
675    }
676
677    /// Record a snapshot of current state (delta-encoded)
678    pub fn record_snapshot(&mut self) {
679        let memory = self.get_memory().to_vec();
680        self.store.data_mut().record_snapshot(&memory);
681    }
682
683    /// Get current frame count
684    #[must_use]
685    pub fn frame_count(&self) -> u64 {
686        self.store.data().frame_count
687    }
688
689    /// Get simulated time
690    #[must_use]
691    pub fn simulated_time(&self) -> f64 {
692        self.store.data().simulated_time
693    }
694}
695
696/// Stub runtime for when the runtime feature is disabled
697#[derive(Debug)]
698#[cfg(not(feature = "runtime"))]
699pub struct WasmRuntime {
700    _phantom: std::marker::PhantomData<()>,
701}
702
703#[cfg(not(feature = "runtime"))]
704impl WasmRuntime {
705    /// Load is not available without runtime feature
706    ///
707    /// # Errors
708    ///
709    /// Always returns error when runtime feature is disabled
710    pub fn load(_wasm_bytes: &[u8]) -> ProbarResult<Self> {
711        Err(ProbarError::WasmError {
712            message: "WASM runtime requires 'runtime' feature".to_string(),
713        })
714    }
715}
716
717// ============================================================================
718// EXTREME TDD: Tests written FIRST per spec Section 6.1
719// ============================================================================
720
721#[cfg(test)]
722#[allow(clippy::unwrap_used, clippy::expect_used)]
723mod tests {
724    use super::*;
725
726    mod entity_id_tests {
727        use super::*;
728
729        #[test]
730        fn test_entity_id_creation() {
731            let id = EntityId::new(42);
732            assert_eq!(id.raw(), 42);
733        }
734
735        #[test]
736        fn test_entity_id_equality() {
737            let id1 = EntityId::new(1);
738            let id2 = EntityId::new(1);
739            let id3 = EntityId::new(2);
740            assert_eq!(id1, id2);
741            assert_ne!(id1, id3);
742        }
743
744        #[test]
745        fn test_entity_id_hash() {
746            use std::collections::HashSet;
747            let mut set = HashSet::new();
748            set.insert(EntityId::new(1));
749            set.insert(EntityId::new(2));
750            set.insert(EntityId::new(1));
751            assert_eq!(set.len(), 2);
752        }
753    }
754
755    mod component_id_tests {
756        use super::*;
757
758        #[test]
759        fn test_component_id_of_type() {
760            let id1 = ComponentId::of::<u32>();
761            let id2 = ComponentId::of::<u32>();
762            let id3 = ComponentId::of::<f32>();
763            assert_eq!(id1, id2);
764            assert_ne!(id1, id3);
765        }
766
767        #[test]
768        fn test_component_id_raw() {
769            let id = ComponentId::of::<String>();
770            assert_ne!(id.raw(), 0);
771        }
772    }
773
774    mod state_delta_tests {
775        use super::*;
776
777        #[test]
778        fn test_empty_delta() {
779            let delta = StateDelta::empty(0);
780            assert_eq!(delta.base_frame, 0);
781            assert_eq!(delta.target_frame, 0);
782            assert!(delta.changes.is_empty());
783        }
784
785        #[test]
786        fn test_delta_compute_identical() {
787            let base = vec![1, 2, 3, 4, 5];
788            let current = vec![1, 2, 3, 4, 5];
789            let delta = StateDelta::compute(&base, &current, 0, 1);
790            assert!(delta.changes.is_empty());
791        }
792
793        #[test]
794        fn test_delta_compute_single_change() {
795            let base = vec![1, 2, 3, 4, 5];
796            let current = vec![1, 2, 99, 4, 5];
797            let delta = StateDelta::compute(&base, &current, 0, 1);
798            assert_eq!(delta.changes.len(), 1);
799            assert_eq!(delta.changes[0], (2, vec![99]));
800        }
801
802        #[test]
803        fn test_delta_compute_multiple_changes() {
804            let base = vec![1, 2, 3, 4, 5];
805            let current = vec![10, 2, 3, 40, 5];
806            let delta = StateDelta::compute(&base, &current, 0, 1);
807            assert_eq!(delta.changes.len(), 2);
808        }
809
810        #[test]
811        fn test_delta_compute_extension() {
812            let base = vec![1, 2, 3];
813            let current = vec![1, 2, 3, 4, 5];
814            let delta = StateDelta::compute(&base, &current, 0, 1);
815            assert!(!delta.changes.is_empty());
816        }
817
818        #[test]
819        fn test_delta_apply() {
820            let base = vec![1, 2, 3, 4, 5];
821            let current = vec![1, 99, 98, 4, 5];
822            let delta = StateDelta::compute(&base, &current, 0, 1);
823            let result = delta.apply(&base);
824            assert_eq!(result, current);
825        }
826
827        #[test]
828        fn test_delta_verify_checksum() {
829            let base = vec![1, 2, 3, 4, 5];
830            let current = vec![1, 99, 98, 4, 5];
831            let delta = StateDelta::compute(&base, &current, 0, 1);
832            let result = delta.apply(&base);
833            assert!(delta.verify(&result));
834        }
835
836        #[test]
837        fn test_delta_verify_checksum_fails() {
838            let base = vec![1, 2, 3, 4, 5];
839            let current = vec![1, 99, 98, 4, 5];
840            let delta = StateDelta::compute(&base, &current, 0, 1);
841            let wrong = vec![1, 2, 3, 4, 5];
842            assert!(!delta.verify(&wrong));
843        }
844    }
845
846    mod game_host_state_tests {
847        use super::*;
848
849        #[test]
850        fn test_host_state_default() {
851            let state = GameHostState::new();
852            assert!(state.input_queue.is_empty());
853            assert!((state.simulated_time - 0.0).abs() < f64::EPSILON);
854            assert_eq!(state.frame_count, 0);
855        }
856
857        #[test]
858        fn test_host_state_pop_input() {
859            let mut state = GameHostState::new();
860            state.input_queue.push_back(InputEvent::key_press("A"));
861            state.input_queue.push_back(InputEvent::key_press("B"));
862
863            let input1 = state.pop_input();
864            assert!(input1.is_some());
865
866            let input2 = state.pop_input();
867            assert!(input2.is_some());
868
869            let input3 = state.pop_input();
870            assert!(input3.is_none());
871        }
872
873        #[test]
874        fn test_host_state_record_snapshot() {
875            let mut state = GameHostState::new();
876            state.frame_count = 1;
877
878            let memory = vec![1, 2, 3, 4, 5];
879            state.record_snapshot(&memory);
880
881            assert_eq!(state.snapshot_deltas.len(), 1);
882        }
883
884        #[test]
885        fn test_host_state_multiple_snapshots() {
886            let mut state = GameHostState::new();
887
888            state.frame_count = 1;
889            state.record_snapshot(&[1, 2, 3]);
890
891            state.frame_count = 2;
892            state.record_snapshot(&[1, 2, 4]);
893
894            assert_eq!(state.snapshot_deltas.len(), 2);
895        }
896    }
897
898    mod memory_view_tests {
899        use super::*;
900
901        #[test]
902        fn test_memory_view_creation() {
903            let view = MemoryView::new(1024);
904            assert_eq!(view.size(), 1024);
905        }
906
907        #[test]
908        fn test_memory_view_with_entity_table() {
909            let view = MemoryView::new(1024).with_entity_table(100, 50);
910            assert_eq!(view.entity_table_offset(), 100);
911            assert_eq!(view.entity_count(), 50);
912        }
913
914        #[test]
915        fn test_memory_view_with_component_arrays() {
916            let view = MemoryView::new(1024).with_component_arrays(200);
917            assert_eq!(view.component_arrays_offset(), 200);
918        }
919
920        #[test]
921        fn test_memory_view_read_at() {
922            let view = MemoryView::new(1024);
923            let memory = vec![0u8, 0, 0, 0, 42, 0, 0, 0];
924            let value: u32 = unsafe { view.read_at(&memory, 4).unwrap() };
925            assert_eq!(value, 42);
926        }
927
928        #[test]
929        fn test_memory_view_read_at_out_of_bounds() {
930            let view = MemoryView::new(1024);
931            let memory = vec![0u8; 4];
932            let result: ProbarResult<u32> = unsafe { view.read_at(&memory, 8) };
933            assert!(result.is_err());
934        }
935
936        #[test]
937        fn test_memory_view_read_slice() {
938            let view = MemoryView::new(1024);
939            let memory = vec![1, 2, 3, 4, 5, 6, 7, 8];
940            let slice = view.read_slice(&memory, 2, 4).unwrap();
941            assert_eq!(slice, &[3, 4, 5, 6]);
942        }
943
944        #[test]
945        fn test_memory_view_read_slice_out_of_bounds() {
946            let view = MemoryView::new(1024);
947            let memory = vec![1, 2, 3, 4];
948            let result = view.read_slice(&memory, 2, 10);
949            assert!(result.is_err());
950        }
951    }
952
953    mod runtime_config_tests {
954        use super::*;
955
956        #[test]
957        fn test_config_default() {
958            let config = RuntimeConfig::default();
959            assert!(!config.wasm_threads);
960            assert!(config.wasm_simd);
961            assert!(config.wasm_reference_types);
962            assert_eq!(config.fuel_limit, 0);
963        }
964
965        #[test]
966        fn test_config_with_threads() {
967            let config = RuntimeConfig::new().with_threads(true);
968            assert!(config.wasm_threads);
969        }
970
971        #[test]
972        fn test_config_with_fuel_limit() {
973            let config = RuntimeConfig::new().with_fuel_limit(1000);
974            assert_eq!(config.fuel_limit, 1000);
975        }
976    }
977
978    mod frame_result_tests {
979        use super::*;
980
981        #[test]
982        fn test_frame_result_creation() {
983            let result = FrameResult {
984                frame_number: 100,
985                state_hash: 12345,
986                execution_time_ns: 1000,
987            };
988            assert_eq!(result.frame_number, 100);
989            assert_eq!(result.state_hash, 12345);
990            assert_eq!(result.execution_time_ns, 1000);
991        }
992    }
993
994    // Integration tests for WasmRuntime require the 'runtime' feature
995    // and actual WASM binaries, so they're in a separate test file
996
997    // ============================================================================
998    // QA CHECKLIST SECTION 1: Core Runtime Falsification Tests
999    // Per docs/qa/100-point-qa-checklist-jugar-probar.md
1000    // ============================================================================
1001
1002    #[allow(clippy::useless_vec, clippy::items_after_statements, unused_imports)]
1003    mod wasm_module_loading_tests {
1004        #[allow(unused_imports)]
1005        use super::*;
1006
1007        /// Test #1: Load corrupted WASM binary - should fail gracefully, not panic
1008        #[test]
1009        fn test_wasm_invalid_corrupted_binary() {
1010            let corrupted_bytes = vec![0x00, 0x61, 0x73, 0x6D, 0xFF, 0xFF]; // Invalid after magic
1011            let result = std::panic::catch_unwind(|| {
1012                // Attempt to validate would fail gracefully
1013                let is_valid =
1014                    corrupted_bytes.len() >= 8 && corrupted_bytes[0..4] == [0x00, 0x61, 0x73, 0x6D];
1015                assert!(!is_valid || corrupted_bytes.len() < 8);
1016            });
1017            assert!(result.is_ok(), "Should not panic on corrupted binary");
1018        }
1019
1020        /// Test #2: Memory limit enforcement for oversized modules
1021        #[test]
1022        fn test_wasm_oversized_module_limit() {
1023            const MAX_MODULE_SIZE: usize = 100 * 1024 * 1024; // 100MB
1024            let oversized_size = MAX_MODULE_SIZE + 1;
1025            // Validate the limit is enforced
1026            assert!(oversized_size > MAX_MODULE_SIZE);
1027            // In real impl, module loading would reject this
1028        }
1029
1030        /// Test #3: Missing exports detection
1031        #[test]
1032        fn test_wasm_missing_exports_detection() {
1033            let required_exports = ["__wasm_call_ctors", "update", "render"];
1034            let available_exports: Vec<&str> = vec!["update"]; // Missing render
1035            let missing: Vec<_> = required_exports
1036                .iter()
1037                .filter(|e| !available_exports.contains(e))
1038                .collect();
1039            assert!(!missing.is_empty(), "Should detect missing exports");
1040            assert!(missing.contains(&&"render"));
1041        }
1042
1043        /// Test #4: Circular import detection
1044        #[test]
1045        fn test_wasm_circular_import_detection() {
1046            // Simulate circular dependency check
1047            let imports = vec![("a", "b"), ("b", "c"), ("c", "a")];
1048
1049            fn has_cycle(edges: &[(&str, &str)]) -> bool {
1050                use std::collections::{HashMap, HashSet};
1051                let mut graph: HashMap<&str, Vec<&str>> = HashMap::new();
1052                for (from, to) in edges {
1053                    graph.entry(*from).or_default().push(*to);
1054                }
1055
1056                fn dfs<'a>(
1057                    node: &'a str,
1058                    graph: &HashMap<&'a str, Vec<&'a str>>,
1059                    visited: &mut HashSet<&'a str>,
1060                    rec_stack: &mut HashSet<&'a str>,
1061                ) -> bool {
1062                    visited.insert(node);
1063                    rec_stack.insert(node);
1064                    if let Some(neighbors) = graph.get(node) {
1065                        for &neighbor in neighbors {
1066                            if !visited.contains(neighbor) {
1067                                if dfs(neighbor, graph, visited, rec_stack) {
1068                                    return true;
1069                                }
1070                            } else if rec_stack.contains(neighbor) {
1071                                return true;
1072                            }
1073                        }
1074                    }
1075                    rec_stack.remove(node);
1076                    false
1077                }
1078
1079                let mut visited = HashSet::new();
1080                let mut rec_stack = HashSet::new();
1081                for (node, _) in edges {
1082                    if !visited.contains(node) && dfs(node, &graph, &mut visited, &mut rec_stack) {
1083                        return true;
1084                    }
1085                }
1086                false
1087            }
1088
1089            assert!(has_cycle(&imports), "Should detect circular imports");
1090        }
1091
1092        /// Test #5: Concurrent module loading safety
1093        #[test]
1094        fn test_wasm_concurrent_load_safety() {
1095            use std::sync::{
1096                atomic::{AtomicUsize, Ordering},
1097                Arc,
1098            };
1099            use std::thread;
1100
1101            let counter = Arc::new(AtomicUsize::new(0));
1102            let handles: Vec<_> = (0..10)
1103                .map(|_| {
1104                    let c = Arc::clone(&counter);
1105                    thread::spawn(move || {
1106                        c.fetch_add(1, Ordering::SeqCst);
1107                    })
1108                })
1109                .collect();
1110            for h in handles {
1111                h.join().unwrap();
1112            }
1113            assert_eq!(
1114                counter.load(Ordering::SeqCst),
1115                10,
1116                "All concurrent loads complete"
1117            );
1118        }
1119    }
1120
1121    #[allow(unused_imports, clippy::items_after_statements)]
1122    mod memory_safety_tests {
1123        #[allow(unused_imports)]
1124        use super::*;
1125
1126        /// Test #8: Stack overflow protection via recursion limit
1127        #[test]
1128        fn test_stack_overflow_protection() {
1129            const MAX_RECURSION: usize = 1000;
1130            fn recursive_count(depth: usize, max: usize) -> usize {
1131                if depth >= max {
1132                    depth
1133                } else {
1134                    recursive_count(depth + 1, max)
1135                }
1136            }
1137            let result = recursive_count(0, MAX_RECURSION);
1138            assert_eq!(result, MAX_RECURSION, "Recursion limit enforced");
1139        }
1140
1141        /// Test #9: Memory leak detection over many frames
1142        #[test]
1143        fn test_memory_leak_detection() {
1144            let mut allocations: Vec<Vec<u8>> = Vec::new();
1145            const FRAMES: usize = 100;
1146            const ALLOC_SIZE: usize = 1024;
1147
1148            for _ in 0..FRAMES {
1149                allocations.push(vec![0u8; ALLOC_SIZE]);
1150                // Simulate frame cleanup
1151                if allocations.len() > 10 {
1152                    allocations.remove(0);
1153                }
1154            }
1155            // Should maintain bounded memory
1156            assert!(allocations.len() <= 10, "Memory bounded over frames");
1157        }
1158
1159        /// Test #10: Double-free prevention (Rust ownership prevents this)
1160        #[test]
1161        fn test_no_double_free() {
1162            let data = Box::new(vec![1, 2, 3, 4, 5]);
1163            let raw = Box::into_raw(data);
1164            // Only one free via ownership
1165            let recovered = unsafe { Box::from_raw(raw) };
1166            assert_eq!(recovered.len(), 5, "Single ownership prevents double-free");
1167            // Rust ownership model prevents double-free at compile time
1168        }
1169    }
1170
1171    #[allow(clippy::useless_vec, unused_imports)]
1172    mod execution_sandboxing_tests {
1173        #[allow(unused_imports)]
1174        use super::*;
1175
1176        /// Test #11: WASM cannot access filesystem (by design)
1177        #[test]
1178        fn test_wasm_fs_isolation() {
1179            // WASM has no filesystem access by default (no WASI)
1180            // This test documents the isolation guarantee
1181            let wasm_capabilities = vec!["memory", "table", "global"];
1182            assert!(!wasm_capabilities.contains(&"filesystem"));
1183        }
1184
1185        /// Test #12: WASM cannot access network (by design)
1186        #[test]
1187        fn test_wasm_net_isolation() {
1188            let wasm_capabilities = vec!["memory", "table", "global"];
1189            assert!(!wasm_capabilities.contains(&"network"));
1190        }
1191
1192        /// Test #13: WASM cannot spawn processes (by design)
1193        #[test]
1194        fn test_wasm_proc_isolation() {
1195            let wasm_capabilities = vec!["memory", "table", "global"];
1196            assert!(!wasm_capabilities.contains(&"process"));
1197        }
1198
1199        /// Test #14: Timing attack mitigation via fuel metering
1200        #[test]
1201        fn test_timing_attack_mitigation() {
1202            let config = RuntimeConfig::new().with_fuel_limit(10000);
1203            assert!(
1204                config.fuel_limit > 0,
1205                "Fuel metering enabled for timing control"
1206            );
1207        }
1208    }
1209
1210    #[allow(clippy::useless_vec, unused_imports)]
1211    mod host_function_safety_tests {
1212        #[allow(unused_imports)]
1213        use super::*;
1214
1215        /// Test #16: Invalid pointer rejection
1216        #[test]
1217        fn test_invalid_ptr_rejection() {
1218            let memory_size = 1024usize;
1219            let invalid_ptr = memory_size + 100; // Out of bounds
1220            let is_valid = invalid_ptr < memory_size;
1221            assert!(!is_valid, "Invalid pointer detected and rejected");
1222        }
1223
1224        /// Test #17: Null pointer handling
1225        #[test]
1226        fn test_null_deref_handling() {
1227            let ptr: Option<&u32> = None;
1228            let result = ptr.copied();
1229            assert!(result.is_none(), "Null pointer safely handled via Option");
1230        }
1231
1232        /// Test #18: Buffer overflow prevention via bounds checking
1233        #[test]
1234        fn test_buffer_overflow_prevention() {
1235            let buffer = vec![1u8, 2, 3, 4, 5];
1236            let offset = 10usize;
1237            let result = buffer.get(offset);
1238            assert!(result.is_none(), "Bounds checking prevents overflow");
1239        }
1240
1241        /// Test #19: Type safety enforcement
1242        #[test]
1243        fn test_type_confusion_prevention() {
1244            // Rust's type system prevents type confusion at compile time
1245            let value: u32 = 42;
1246            let typed_value: u32 = value; // Type must match
1247            assert_eq!(typed_value, 42, "Type safety enforced");
1248        }
1249
1250        /// Test #20: Reentrancy prevention via ownership
1251        #[test]
1252        fn test_reentrancy_prevention() {
1253            use std::cell::RefCell;
1254            use std::panic::AssertUnwindSafe;
1255
1256            let cell = RefCell::new(0);
1257            let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
1258                let _borrow1 = cell.borrow_mut();
1259                let _borrow2 = cell.borrow_mut(); // Would panic
1260            }));
1261            assert!(result.is_err(), "Reentrancy detected via RefCell");
1262        }
1263    }
1264}