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