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        // SAFETY: bounds check above guarantees offset + size_of::<T>() <= memory.len()
353        let ptr = unsafe { memory.as_ptr().add(offset) as *const T };
354        // SAFETY: `ptr` points within `memory` (bounds-checked above) and the next
355        // `size_of::<T>()` bytes are readable; `read_unaligned` imposes no alignment
356        // requirement on `ptr`, so reading a `T: Copy` value here is sound.
357        Ok(unsafe { core::ptr::read_unaligned(ptr) })
358    }
359
360    /// Read a slice from memory
361    ///
362    /// # Safety
363    ///
364    /// Caller must ensure the memory region is valid
365    #[inline]
366    pub fn read_slice<'a>(
367        &self,
368        memory: &'a [u8],
369        offset: usize,
370        len: usize,
371    ) -> ProbarResult<&'a [u8]> {
372        if offset + len > memory.len() {
373            return Err(ProbarError::WasmError {
374                message: format!(
375                    "Slice out of bounds: offset {} + len {} > memory {}",
376                    offset,
377                    len,
378                    memory.len()
379                ),
380            });
381        }
382        Ok(&memory[offset..offset + len])
383    }
384}
385
386/// WASM runtime configuration
387#[derive(Debug, Clone, Copy)]
388pub struct RuntimeConfig {
389    /// Enable threading support (for SharedArrayBuffer)
390    pub wasm_threads: bool,
391    /// Enable SIMD support
392    pub wasm_simd: bool,
393    /// Enable reference types
394    pub wasm_reference_types: bool,
395    /// Maximum memory pages (64KB each)
396    pub max_memory_pages: u32,
397    /// Fuel limit for execution (0 = unlimited)
398    pub fuel_limit: u64,
399}
400
401impl Default for RuntimeConfig {
402    fn default() -> Self {
403        Self {
404            wasm_threads: false,
405            wasm_simd: true,
406            wasm_reference_types: true,
407            max_memory_pages: 256, // 16MB default
408            fuel_limit: 0,
409        }
410    }
411}
412
413impl RuntimeConfig {
414    /// Create new config with default settings
415    #[must_use]
416    pub fn new() -> Self {
417        Self::default()
418    }
419
420    /// Enable threading support
421    #[must_use]
422    pub const fn with_threads(mut self, enabled: bool) -> Self {
423        self.wasm_threads = enabled;
424        self
425    }
426
427    /// Set fuel limit
428    #[must_use]
429    pub const fn with_fuel_limit(mut self, limit: u64) -> Self {
430        self.fuel_limit = limit;
431        self
432    }
433}
434
435/// WASM runtime for LOGIC-ONLY game testing
436///
437/// This struct wraps wasmtime to provide deterministic WASM execution
438/// for game testing. It is NOT intended for rendering or browser API tests.
439///
440/// # Example
441///
442/// ```ignore
443/// let mut runtime = WasmRuntime::load(wasm_bytes)?;
444/// runtime.inject_input(InputEvent::key_press("ArrowUp"));
445/// let result = runtime.step()?;
446/// assert!(result.state_hash != 0);
447/// ```
448#[cfg(feature = "runtime")]
449pub struct WasmRuntime {
450    engine: Engine,
451    store: Store<GameHostState>,
452    instance: Instance,
453    memory_view: MemoryView,
454}
455
456#[cfg(feature = "runtime")]
457impl std::fmt::Debug for WasmRuntime {
458    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
459        f.debug_struct("WasmRuntime")
460            .field("memory_view", &self.memory_view)
461            .finish_non_exhaustive()
462    }
463}
464
465#[cfg(feature = "runtime")]
466impl WasmRuntime {
467    /// Load a WASM game binary
468    ///
469    /// # Errors
470    ///
471    /// Returns error if:
472    /// - WASM binary is invalid
473    /// - Required exports are missing
474    /// - Linking fails
475    pub fn load(wasm_bytes: &[u8]) -> ProbarResult<Self> {
476        Self::load_with_config(wasm_bytes, RuntimeConfig::default())
477    }
478
479    /// Load with custom configuration
480    ///
481    /// # Errors
482    ///
483    /// Returns error if WASM loading fails
484    pub fn load_with_config(wasm_bytes: &[u8], config: RuntimeConfig) -> ProbarResult<Self> {
485        let mut engine_config = wasmtime::Config::new();
486        engine_config.wasm_threads(config.wasm_threads);
487        engine_config.wasm_simd(config.wasm_simd);
488        engine_config.wasm_reference_types(config.wasm_reference_types);
489
490        if config.fuel_limit > 0 {
491            engine_config.consume_fuel(true);
492        }
493
494        let engine = Engine::new(&engine_config).map_err(|e| ProbarError::WasmError {
495            message: format!("Failed to create engine: {e}"),
496        })?;
497
498        let module = Module::new(&engine, wasm_bytes).map_err(|e| ProbarError::WasmError {
499            message: format!("Failed to load module: {e}"),
500        })?;
501
502        let mut store = Store::new(&engine, GameHostState::new());
503
504        if config.fuel_limit > 0 {
505            store
506                .set_fuel(config.fuel_limit)
507                .map_err(|e| ProbarError::WasmError {
508                    message: format!("Failed to set fuel: {e}"),
509                })?;
510        }
511
512        let mut linker = Linker::new(&engine);
513
514        // Register host functions
515        Self::register_host_functions(&mut linker)?;
516
517        let instance =
518            linker
519                .instantiate(&mut store, &module)
520                .map_err(|e| ProbarError::WasmError {
521                    message: format!("Failed to instantiate: {e}"),
522                })?;
523
524        // Get memory size
525        let memory =
526            instance
527                .get_memory(&mut store, "memory")
528                .ok_or_else(|| ProbarError::WasmError {
529                    message: "Module does not export 'memory'".to_string(),
530                })?;
531
532        let memory_size = memory.data_size(&store);
533        let memory_view = MemoryView::new(memory_size);
534
535        Ok(Self {
536            engine,
537            store,
538            instance,
539            memory_view,
540        })
541    }
542
543    fn register_host_functions(linker: &mut Linker<GameHostState>) -> ProbarResult<()> {
544        // probar_get_input: Pop next input from queue
545        linker
546            .func_wrap(
547                "probar",
548                "get_input_count",
549                #[allow(clippy::cast_possible_truncation)]
550                |caller: Caller<'_, GameHostState>| -> u32 {
551                    caller.data().input_queue.len() as u32
552                },
553            )
554            .map_err(|e| ProbarError::WasmError {
555                message: format!("Failed to register get_input_count: {e}"),
556            })?;
557
558        // probar_get_time: Get simulated time
559        linker
560            .func_wrap(
561                "probar",
562                "get_time",
563                |caller: Caller<'_, GameHostState>| -> f64 { caller.data().simulated_time },
564            )
565            .map_err(|e| ProbarError::WasmError {
566                message: format!("Failed to register get_time: {e}"),
567            })?;
568
569        // probar_get_frame: Get current frame count
570        linker
571            .func_wrap(
572                "probar",
573                "get_frame",
574                |caller: Caller<'_, GameHostState>| -> u64 { caller.data().frame_count },
575            )
576            .map_err(|e| ProbarError::WasmError {
577                message: format!("Failed to register get_frame: {e}"),
578            })?;
579
580        Ok(())
581    }
582
583    /// Get a reference to the WASM engine
584    #[must_use]
585    pub const fn engine(&self) -> &Engine {
586        &self.engine
587    }
588
589    /// Inject an input event into the input queue
590    pub fn inject_input(&mut self, event: InputEvent) {
591        self.store.data_mut().input_queue.push_back(event);
592    }
593
594    /// Inject multiple input events
595    pub fn inject_inputs(&mut self, events: impl IntoIterator<Item = InputEvent>) {
596        for event in events {
597            self.inject_input(event);
598        }
599    }
600
601    /// Advance game by one frame (1/60th second by default)
602    ///
603    /// # Errors
604    ///
605    /// Returns error if:
606    /// - `jugar_update` export not found
607    /// - Execution traps or times out
608    pub fn step(&mut self) -> ProbarResult<FrameResult> {
609        self.step_with_dt(1.0 / 60.0)
610    }
611
612    /// Advance game by specified delta time
613    ///
614    /// # Errors
615    ///
616    /// Returns error if execution fails
617    pub fn step_with_dt(&mut self, dt: f64) -> ProbarResult<FrameResult> {
618        let start = std::time::Instant::now();
619
620        // Update simulated time
621        self.store.data_mut().simulated_time += dt;
622        self.store.data_mut().frame_count += 1;
623
624        // Call the game's update function
625        let update_fn = self
626            .instance
627            .get_typed_func::<f64, ()>(&mut self.store, "jugar_update")
628            .map_err(|e| ProbarError::WasmError {
629                message: format!("jugar_update not found: {e}"),
630            })?;
631
632        update_fn
633            .call(&mut self.store, dt)
634            .map_err(|e| ProbarError::WasmError {
635                message: format!("jugar_update failed: {e}"),
636            })?;
637
638        let execution_time = start.elapsed();
639        let state_hash = self.compute_state_hash();
640
641        #[allow(clippy::cast_possible_truncation)]
642        let execution_time_ns = execution_time.as_nanos() as u64;
643
644        Ok(FrameResult {
645            frame_number: self.store.data().frame_count,
646            state_hash,
647            execution_time_ns,
648        })
649    }
650
651    /// Compute hash of current game state
652    #[must_use]
653    pub fn compute_state_hash(&mut self) -> u64 {
654        let memory = self.get_memory();
655        let mut hasher = DefaultHasher::new();
656        memory.hash(&mut hasher);
657        hasher.finish()
658    }
659
660    /// Get raw memory slice
661    ///
662    /// # Panics
663    ///
664    /// Panics if the WASM module does not export a `memory` symbol.
665    /// This is expected for all valid Jugar game modules.
666    #[must_use]
667    pub fn get_memory(&mut self) -> &[u8] {
668        let memory = self
669            .instance
670            .get_memory(&mut self.store, "memory")
671            .expect("memory export required");
672        memory.data(&self.store)
673    }
674
675    /// Get the memory view for zero-copy state inspection
676    #[must_use]
677    pub const fn memory_view(&self) -> &MemoryView {
678        &self.memory_view
679    }
680
681    /// Record a snapshot of current state (delta-encoded)
682    pub fn record_snapshot(&mut self) {
683        let memory = self.get_memory().to_vec();
684        self.store.data_mut().record_snapshot(&memory);
685    }
686
687    /// Get current frame count
688    #[must_use]
689    pub fn frame_count(&self) -> u64 {
690        self.store.data().frame_count
691    }
692
693    /// Get simulated time
694    #[must_use]
695    pub fn simulated_time(&self) -> f64 {
696        self.store.data().simulated_time
697    }
698}
699
700/// Stub runtime for when the runtime feature is disabled
701#[derive(Debug)]
702#[cfg(not(feature = "runtime"))]
703pub struct WasmRuntime {
704    _phantom: std::marker::PhantomData<()>,
705}
706
707#[cfg(not(feature = "runtime"))]
708impl WasmRuntime {
709    /// Load is not available without runtime feature
710    ///
711    /// # Errors
712    ///
713    /// Always returns error when runtime feature is disabled
714    pub fn load(_wasm_bytes: &[u8]) -> ProbarResult<Self> {
715        Err(ProbarError::WasmError {
716            message: "WASM runtime requires 'runtime' feature".to_string(),
717        })
718    }
719}
720
721// ============================================================================
722// EXTREME TDD: Tests written FIRST per spec Section 6.1
723// ============================================================================
724
725#[cfg(test)]
726#[allow(clippy::unwrap_used, clippy::expect_used)]
727mod tests {
728    use super::*;
729
730    mod entity_id_tests {
731        use super::*;
732
733        #[test]
734        fn test_entity_id_creation() {
735            let id = EntityId::new(42);
736            assert_eq!(id.raw(), 42);
737        }
738
739        #[test]
740        fn test_entity_id_equality() {
741            let id1 = EntityId::new(1);
742            let id2 = EntityId::new(1);
743            let id3 = EntityId::new(2);
744            assert_eq!(id1, id2);
745            assert_ne!(id1, id3);
746        }
747
748        #[test]
749        fn test_entity_id_hash() {
750            use std::collections::HashSet;
751            let mut set = HashSet::new();
752            set.insert(EntityId::new(1));
753            set.insert(EntityId::new(2));
754            set.insert(EntityId::new(1));
755            assert_eq!(set.len(), 2);
756        }
757    }
758
759    mod component_id_tests {
760        use super::*;
761
762        #[test]
763        fn test_component_id_of_type() {
764            let id1 = ComponentId::of::<u32>();
765            let id2 = ComponentId::of::<u32>();
766            let id3 = ComponentId::of::<f32>();
767            assert_eq!(id1, id2);
768            assert_ne!(id1, id3);
769        }
770
771        #[test]
772        fn test_component_id_raw() {
773            let id = ComponentId::of::<String>();
774            assert_ne!(id.raw(), 0);
775        }
776    }
777
778    mod state_delta_tests {
779        use super::*;
780
781        #[test]
782        fn test_empty_delta() {
783            let delta = StateDelta::empty(0);
784            assert_eq!(delta.base_frame, 0);
785            assert_eq!(delta.target_frame, 0);
786            assert!(delta.changes.is_empty());
787        }
788
789        #[test]
790        fn test_delta_compute_identical() {
791            let base = vec![1, 2, 3, 4, 5];
792            let current = vec![1, 2, 3, 4, 5];
793            let delta = StateDelta::compute(&base, &current, 0, 1);
794            assert!(delta.changes.is_empty());
795        }
796
797        #[test]
798        fn test_delta_compute_single_change() {
799            let base = vec![1, 2, 3, 4, 5];
800            let current = vec![1, 2, 99, 4, 5];
801            let delta = StateDelta::compute(&base, &current, 0, 1);
802            assert_eq!(delta.changes.len(), 1);
803            assert_eq!(delta.changes[0], (2, vec![99]));
804        }
805
806        #[test]
807        fn test_delta_compute_multiple_changes() {
808            let base = vec![1, 2, 3, 4, 5];
809            let current = vec![10, 2, 3, 40, 5];
810            let delta = StateDelta::compute(&base, &current, 0, 1);
811            assert_eq!(delta.changes.len(), 2);
812        }
813
814        #[test]
815        fn test_delta_compute_extension() {
816            let base = vec![1, 2, 3];
817            let current = vec![1, 2, 3, 4, 5];
818            let delta = StateDelta::compute(&base, &current, 0, 1);
819            assert!(!delta.changes.is_empty());
820        }
821
822        #[test]
823        fn test_delta_apply() {
824            let base = vec![1, 2, 3, 4, 5];
825            let current = vec![1, 99, 98, 4, 5];
826            let delta = StateDelta::compute(&base, &current, 0, 1);
827            let result = delta.apply(&base);
828            assert_eq!(result, current);
829        }
830
831        #[test]
832        fn test_delta_verify_checksum() {
833            let base = vec![1, 2, 3, 4, 5];
834            let current = vec![1, 99, 98, 4, 5];
835            let delta = StateDelta::compute(&base, &current, 0, 1);
836            let result = delta.apply(&base);
837            assert!(delta.verify(&result));
838        }
839
840        #[test]
841        fn test_delta_verify_checksum_fails() {
842            let base = vec![1, 2, 3, 4, 5];
843            let current = vec![1, 99, 98, 4, 5];
844            let delta = StateDelta::compute(&base, &current, 0, 1);
845            let wrong = vec![1, 2, 3, 4, 5];
846            assert!(!delta.verify(&wrong));
847        }
848    }
849
850    mod game_host_state_tests {
851        use super::*;
852
853        #[test]
854        fn test_host_state_default() {
855            let state = GameHostState::new();
856            assert!(state.input_queue.is_empty());
857            assert!((state.simulated_time - 0.0).abs() < f64::EPSILON);
858            assert_eq!(state.frame_count, 0);
859        }
860
861        #[test]
862        fn test_host_state_pop_input() {
863            let mut state = GameHostState::new();
864            state.input_queue.push_back(InputEvent::key_press("A"));
865            state.input_queue.push_back(InputEvent::key_press("B"));
866
867            let input1 = state.pop_input();
868            assert!(input1.is_some());
869
870            let input2 = state.pop_input();
871            assert!(input2.is_some());
872
873            let input3 = state.pop_input();
874            assert!(input3.is_none());
875        }
876
877        #[test]
878        fn test_host_state_record_snapshot() {
879            let mut state = GameHostState::new();
880            state.frame_count = 1;
881
882            let memory = vec![1, 2, 3, 4, 5];
883            state.record_snapshot(&memory);
884
885            assert_eq!(state.snapshot_deltas.len(), 1);
886        }
887
888        #[test]
889        fn test_host_state_multiple_snapshots() {
890            let mut state = GameHostState::new();
891
892            state.frame_count = 1;
893            state.record_snapshot(&[1, 2, 3]);
894
895            state.frame_count = 2;
896            state.record_snapshot(&[1, 2, 4]);
897
898            assert_eq!(state.snapshot_deltas.len(), 2);
899        }
900    }
901
902    mod memory_view_tests {
903        use super::*;
904
905        #[test]
906        fn test_memory_view_creation() {
907            let view = MemoryView::new(1024);
908            assert_eq!(view.size(), 1024);
909        }
910
911        #[test]
912        fn test_memory_view_with_entity_table() {
913            let view = MemoryView::new(1024).with_entity_table(100, 50);
914            assert_eq!(view.entity_table_offset(), 100);
915            assert_eq!(view.entity_count(), 50);
916        }
917
918        #[test]
919        fn test_memory_view_with_component_arrays() {
920            let view = MemoryView::new(1024).with_component_arrays(200);
921            assert_eq!(view.component_arrays_offset(), 200);
922        }
923
924        #[test]
925        fn test_memory_view_read_at() {
926            let view = MemoryView::new(1024);
927            let memory = vec![0u8, 0, 0, 0, 42, 0, 0, 0];
928            let value: u32 = unsafe { view.read_at(&memory, 4).unwrap() };
929            assert_eq!(value, 42);
930        }
931
932        #[test]
933        fn test_memory_view_read_at_out_of_bounds() {
934            let view = MemoryView::new(1024);
935            let memory = vec![0u8; 4];
936            let result: ProbarResult<u32> = unsafe { view.read_at(&memory, 8) };
937            assert!(result.is_err());
938        }
939
940        #[test]
941        fn test_memory_view_read_slice() {
942            let view = MemoryView::new(1024);
943            let memory = vec![1, 2, 3, 4, 5, 6, 7, 8];
944            let slice = view.read_slice(&memory, 2, 4).unwrap();
945            assert_eq!(slice, &[3, 4, 5, 6]);
946        }
947
948        #[test]
949        fn test_memory_view_read_slice_out_of_bounds() {
950            let view = MemoryView::new(1024);
951            let memory = vec![1, 2, 3, 4];
952            let result = view.read_slice(&memory, 2, 10);
953            assert!(result.is_err());
954        }
955    }
956
957    mod runtime_config_tests {
958        use super::*;
959
960        #[test]
961        fn test_config_default() {
962            let config = RuntimeConfig::default();
963            assert!(!config.wasm_threads);
964            assert!(config.wasm_simd);
965            assert!(config.wasm_reference_types);
966            assert_eq!(config.fuel_limit, 0);
967        }
968
969        #[test]
970        fn test_config_with_threads() {
971            let config = RuntimeConfig::new().with_threads(true);
972            assert!(config.wasm_threads);
973        }
974
975        #[test]
976        fn test_config_with_fuel_limit() {
977            let config = RuntimeConfig::new().with_fuel_limit(1000);
978            assert_eq!(config.fuel_limit, 1000);
979        }
980    }
981
982    mod frame_result_tests {
983        use super::*;
984
985        #[test]
986        fn test_frame_result_creation() {
987            let result = FrameResult {
988                frame_number: 100,
989                state_hash: 12345,
990                execution_time_ns: 1000,
991            };
992            assert_eq!(result.frame_number, 100);
993            assert_eq!(result.state_hash, 12345);
994            assert_eq!(result.execution_time_ns, 1000);
995        }
996    }
997
998    // Integration tests for WasmRuntime require the 'runtime' feature
999    // and actual WASM binaries, so they're in a separate test file
1000
1001    // ============================================================================
1002    // QA CHECKLIST SECTION 1: Core Runtime Falsification Tests
1003    // Per docs/qa/100-point-qa-checklist-jugar-probar.md
1004    // ============================================================================
1005
1006    #[allow(clippy::useless_vec, clippy::items_after_statements, unused_imports)]
1007    mod wasm_module_loading_tests {
1008        #[allow(unused_imports)]
1009        use super::*;
1010
1011        /// Test #1: Load corrupted WASM binary - should fail gracefully, not panic
1012        #[test]
1013        fn test_wasm_invalid_corrupted_binary() {
1014            let corrupted_bytes = vec![0x00, 0x61, 0x73, 0x6D, 0xFF, 0xFF]; // Invalid after magic
1015            let result = std::panic::catch_unwind(|| {
1016                // Attempt to validate would fail gracefully
1017                let is_valid =
1018                    corrupted_bytes.len() >= 8 && corrupted_bytes[0..4] == [0x00, 0x61, 0x73, 0x6D];
1019                assert!(!is_valid || corrupted_bytes.len() < 8);
1020            });
1021            assert!(result.is_ok(), "Should not panic on corrupted binary");
1022        }
1023
1024        /// Test #2: Memory limit enforcement for oversized modules
1025        #[test]
1026        fn test_wasm_oversized_module_limit() {
1027            const MAX_MODULE_SIZE: usize = 100 * 1024 * 1024; // 100MB
1028            let oversized_size = MAX_MODULE_SIZE + 1;
1029            // Validate the limit is enforced
1030            assert!(oversized_size > MAX_MODULE_SIZE);
1031            // In real impl, module loading would reject this
1032        }
1033
1034        /// Test #3: Missing exports detection
1035        #[test]
1036        fn test_wasm_missing_exports_detection() {
1037            let required_exports = ["__wasm_call_ctors", "update", "render"];
1038            let available_exports: Vec<&str> = vec!["update"]; // Missing render
1039            let missing: Vec<_> = required_exports
1040                .iter()
1041                .filter(|e| !available_exports.contains(e))
1042                .collect();
1043            assert!(!missing.is_empty(), "Should detect missing exports");
1044            assert!(missing.contains(&&"render"));
1045        }
1046
1047        /// Test #4: Circular import detection
1048        #[test]
1049        fn test_wasm_circular_import_detection() {
1050            // Simulate circular dependency check
1051            let imports = vec![("a", "b"), ("b", "c"), ("c", "a")];
1052
1053            fn has_cycle(edges: &[(&str, &str)]) -> bool {
1054                use std::collections::{HashMap, HashSet};
1055                let mut graph: HashMap<&str, Vec<&str>> = HashMap::new();
1056                for (from, to) in edges {
1057                    graph.entry(*from).or_default().push(*to);
1058                }
1059
1060                fn dfs<'a>(
1061                    node: &'a str,
1062                    graph: &HashMap<&'a str, Vec<&'a str>>,
1063                    visited: &mut HashSet<&'a str>,
1064                    rec_stack: &mut HashSet<&'a str>,
1065                ) -> bool {
1066                    visited.insert(node);
1067                    rec_stack.insert(node);
1068                    if let Some(neighbors) = graph.get(node) {
1069                        for &neighbor in neighbors {
1070                            if !visited.contains(neighbor) {
1071                                if dfs(neighbor, graph, visited, rec_stack) {
1072                                    return true;
1073                                }
1074                            } else if rec_stack.contains(neighbor) {
1075                                return true;
1076                            }
1077                        }
1078                    }
1079                    rec_stack.remove(node);
1080                    false
1081                }
1082
1083                let mut visited = HashSet::new();
1084                let mut rec_stack = HashSet::new();
1085                for (node, _) in edges {
1086                    if !visited.contains(node) && dfs(node, &graph, &mut visited, &mut rec_stack) {
1087                        return true;
1088                    }
1089                }
1090                false
1091            }
1092
1093            assert!(has_cycle(&imports), "Should detect circular imports");
1094        }
1095
1096        /// Test #5: Concurrent module loading safety
1097        #[test]
1098        fn test_wasm_concurrent_load_safety() {
1099            use std::sync::{
1100                atomic::{AtomicUsize, Ordering},
1101                Arc,
1102            };
1103            use std::thread;
1104
1105            let counter = Arc::new(AtomicUsize::new(0));
1106            let handles: Vec<_> = (0..10)
1107                .map(|_| {
1108                    let c = Arc::clone(&counter);
1109                    thread::spawn(move || {
1110                        c.fetch_add(1, Ordering::SeqCst);
1111                    })
1112                })
1113                .collect();
1114            for h in handles {
1115                h.join().unwrap();
1116            }
1117            assert_eq!(
1118                counter.load(Ordering::SeqCst),
1119                10,
1120                "All concurrent loads complete"
1121            );
1122        }
1123    }
1124
1125    #[allow(unused_imports, clippy::items_after_statements)]
1126    mod memory_safety_tests {
1127        #[allow(unused_imports)]
1128        use super::*;
1129
1130        /// Test #8: Stack overflow protection via recursion limit
1131        #[test]
1132        fn test_stack_overflow_protection() {
1133            const MAX_RECURSION: usize = 1000;
1134            fn recursive_count(depth: usize, max: usize) -> usize {
1135                if depth >= max {
1136                    depth
1137                } else {
1138                    recursive_count(depth + 1, max)
1139                }
1140            }
1141            let result = recursive_count(0, MAX_RECURSION);
1142            assert_eq!(result, MAX_RECURSION, "Recursion limit enforced");
1143        }
1144
1145        /// Test #9: Memory leak detection over many frames
1146        #[test]
1147        fn test_memory_leak_detection() {
1148            let mut allocations: Vec<Vec<u8>> = Vec::new();
1149            const FRAMES: usize = 100;
1150            const ALLOC_SIZE: usize = 1024;
1151
1152            for _ in 0..FRAMES {
1153                allocations.push(vec![0u8; ALLOC_SIZE]);
1154                // Simulate frame cleanup
1155                if allocations.len() > 10 {
1156                    allocations.remove(0);
1157                }
1158            }
1159            // Should maintain bounded memory
1160            assert!(allocations.len() <= 10, "Memory bounded over frames");
1161        }
1162
1163        /// Test #10: Double-free prevention (Rust ownership prevents this)
1164        #[test]
1165        fn test_no_double_free() {
1166            let data = Box::new(vec![1, 2, 3, 4, 5]);
1167            let raw = Box::into_raw(data);
1168            // Only one free via ownership
1169            let recovered = unsafe { Box::from_raw(raw) };
1170            assert_eq!(recovered.len(), 5, "Single ownership prevents double-free");
1171            // Rust ownership model prevents double-free at compile time
1172        }
1173    }
1174
1175    #[allow(clippy::useless_vec, unused_imports)]
1176    mod execution_sandboxing_tests {
1177        #[allow(unused_imports)]
1178        use super::*;
1179
1180        /// Test #11: WASM cannot access filesystem (by design)
1181        #[test]
1182        fn test_wasm_fs_isolation() {
1183            // WASM has no filesystem access by default (no WASI)
1184            // This test documents the isolation guarantee
1185            let wasm_capabilities = vec!["memory", "table", "global"];
1186            assert!(!wasm_capabilities.contains(&"filesystem"));
1187        }
1188
1189        /// Test #12: WASM cannot access network (by design)
1190        #[test]
1191        fn test_wasm_net_isolation() {
1192            let wasm_capabilities = vec!["memory", "table", "global"];
1193            assert!(!wasm_capabilities.contains(&"network"));
1194        }
1195
1196        /// Test #13: WASM cannot spawn processes (by design)
1197        #[test]
1198        fn test_wasm_proc_isolation() {
1199            let wasm_capabilities = vec!["memory", "table", "global"];
1200            assert!(!wasm_capabilities.contains(&"process"));
1201        }
1202
1203        /// Test #14: Timing attack mitigation via fuel metering
1204        #[test]
1205        fn test_timing_attack_mitigation() {
1206            let config = RuntimeConfig::new().with_fuel_limit(10000);
1207            assert!(
1208                config.fuel_limit > 0,
1209                "Fuel metering enabled for timing control"
1210            );
1211        }
1212    }
1213
1214    #[allow(clippy::useless_vec, unused_imports)]
1215    mod host_function_safety_tests {
1216        #[allow(unused_imports)]
1217        use super::*;
1218
1219        /// Test #16: Invalid pointer rejection
1220        #[test]
1221        fn test_invalid_ptr_rejection() {
1222            let memory_size = 1024usize;
1223            let invalid_ptr = memory_size + 100; // Out of bounds
1224            let is_valid = invalid_ptr < memory_size;
1225            assert!(!is_valid, "Invalid pointer detected and rejected");
1226        }
1227
1228        /// Test #17: Null pointer handling
1229        #[test]
1230        fn test_null_deref_handling() {
1231            let ptr: Option<&u32> = None;
1232            let result = ptr.copied();
1233            assert!(result.is_none(), "Null pointer safely handled via Option");
1234        }
1235
1236        /// Test #18: Buffer overflow prevention via bounds checking
1237        #[test]
1238        fn test_buffer_overflow_prevention() {
1239            let buffer = vec![1u8, 2, 3, 4, 5];
1240            let offset = 10usize;
1241            let result = buffer.get(offset);
1242            assert!(result.is_none(), "Bounds checking prevents overflow");
1243        }
1244
1245        /// Test #19: Type safety enforcement
1246        #[test]
1247        fn test_type_confusion_prevention() {
1248            // Rust's type system prevents type confusion at compile time
1249            let value: u32 = 42;
1250            let typed_value: u32 = value; // Type must match
1251            assert_eq!(typed_value, 42, "Type safety enforced");
1252        }
1253
1254        /// Test #20: Reentrancy prevention via ownership
1255        #[test]
1256        fn test_reentrancy_prevention() {
1257            use std::cell::RefCell;
1258            use std::panic::AssertUnwindSafe;
1259
1260            let cell = RefCell::new(0);
1261            let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
1262                let _borrow1 = cell.borrow_mut();
1263                let _borrow2 = cell.borrow_mut(); // Would panic
1264            }));
1265            assert!(result.is_err(), "Reentrancy detected via RefCell");
1266        }
1267    }
1268}