Skip to main content

probador/
stress.rs

1//! Browser/WASM Stress Testing Module (Section H: Points 116-125)
2//!
3//! Implements internal concurrency stress testing for WASM applications:
4//! - Atomics: `SharedArrayBuffer` lock contention validation
5//! - Worker Messages: Worker message queue throughput
6//! - Render: Render loop stability under load
7//! - Trace: Renacer tracing overhead measurement
8//!
9//! This module focuses on browser-internal validation, NOT HTTP load testing.
10//! For HTTP/WebSocket capacity planning, use external tools (locust, k6).
11//!
12//! Reference: PROBAR-SPEC-WASM-001 Section H
13
14#![allow(clippy::must_use_candidate)]
15#![allow(clippy::missing_panics_doc)]
16#![allow(clippy::missing_errors_doc)]
17#![allow(clippy::module_name_repetitions)]
18#![allow(clippy::missing_const_for_fn)]
19#![allow(clippy::return_self_not_must_use)]
20#![allow(clippy::uninlined_format_args)]
21#![allow(clippy::format_push_string)]
22#![allow(clippy::doc_markdown)]
23#![allow(clippy::cast_precision_loss)]
24#![allow(clippy::items_after_statements)]
25#![allow(clippy::manual_saturating_arithmetic)]
26#![allow(clippy::use_self)]
27
28use serde::{Deserialize, Serialize};
29use std::time::{Duration, Instant};
30
31// =============================================================================
32// Stress Test Configuration
33// =============================================================================
34
35/// Stress test mode
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
37pub enum StressMode {
38    /// Test `SharedArrayBuffer` atomics contention (Point 116)
39    #[default]
40    Atomics,
41    /// Test worker message queue throughput (Point 117)
42    WorkerMsg,
43    /// Test render loop stability (Point 118)
44    Render,
45    /// Test renacer tracing overhead (Point 119)
46    Trace,
47    /// Full system stress test (Point 123)
48    Full,
49}
50
51impl std::fmt::Display for StressMode {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        match self {
54            Self::Atomics => write!(f, "atomics"),
55            Self::WorkerMsg => write!(f, "worker-msg"),
56            Self::Render => write!(f, "render"),
57            Self::Trace => write!(f, "trace"),
58            Self::Full => write!(f, "full"),
59        }
60    }
61}
62
63impl std::str::FromStr for StressMode {
64    type Err = String;
65
66    fn from_str(s: &str) -> Result<Self, Self::Err> {
67        match s.to_lowercase().as_str() {
68            "atomics" => Ok(Self::Atomics),
69            "worker-msg" | "workermsg" | "worker_msg" => Ok(Self::WorkerMsg),
70            "render" => Ok(Self::Render),
71            "trace" => Ok(Self::Trace),
72            "full" => Ok(Self::Full),
73            _ => Err(format!("Unknown stress mode: {}", s)),
74        }
75    }
76}
77
78/// Stress test configuration
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct StressConfig {
81    /// Stress test mode
82    pub mode: StressMode,
83    /// Test duration in seconds
84    pub duration_secs: u64,
85    /// Number of concurrent workers/threads
86    pub concurrency: u32,
87    /// Target operations per second (0 = unlimited)
88    pub target_ops_per_sec: u64,
89    /// Warmup duration in seconds
90    pub warmup_secs: u64,
91}
92
93impl Default for StressConfig {
94    fn default() -> Self {
95        Self {
96            mode: StressMode::Atomics,
97            duration_secs: 30,
98            concurrency: 4,
99            target_ops_per_sec: 0,
100            warmup_secs: 5,
101        }
102    }
103}
104
105impl StressConfig {
106    /// Create config for atomics stress test
107    pub fn atomics(duration_secs: u64, concurrency: u32) -> Self {
108        Self {
109            mode: StressMode::Atomics,
110            duration_secs,
111            concurrency,
112            ..Default::default()
113        }
114    }
115
116    /// Create config for worker message stress test
117    pub fn worker_msg(duration_secs: u64, concurrency: u32) -> Self {
118        Self {
119            mode: StressMode::WorkerMsg,
120            duration_secs,
121            concurrency,
122            ..Default::default()
123        }
124    }
125
126    /// Create config for render stress test
127    pub fn render(duration_secs: u64) -> Self {
128        Self {
129            mode: StressMode::Render,
130            duration_secs,
131            concurrency: 1,
132            ..Default::default()
133        }
134    }
135
136    /// Create config for trace overhead test
137    pub fn trace(duration_secs: u64) -> Self {
138        Self {
139            mode: StressMode::Trace,
140            duration_secs,
141            concurrency: 1,
142            ..Default::default()
143        }
144    }
145
146    /// Create config for full system stress test
147    pub fn full(duration_secs: u64, concurrency: u32) -> Self {
148        Self {
149            mode: StressMode::Full,
150            duration_secs,
151            concurrency,
152            ..Default::default()
153        }
154    }
155}
156
157// =============================================================================
158// Stress Test Results
159// =============================================================================
160
161/// Stress test result
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct StressResult {
164    /// Test mode
165    pub mode: StressMode,
166    /// Test duration
167    pub duration: Duration,
168    /// Total operations completed
169    pub total_ops: u64,
170    /// Operations per second
171    pub ops_per_sec: f64,
172    /// Whether the test passed pass criteria
173    pub passed: bool,
174    /// Pass criteria description
175    pub pass_criteria: String,
176    /// Actual value achieved
177    pub actual_value: String,
178    /// Memory stats
179    pub memory: MemoryStats,
180    /// Latency stats
181    pub latency: LatencyStats,
182    /// Errors encountered
183    pub errors: Vec<StressError>,
184}
185
186impl StressResult {
187    /// Create a new result
188    pub fn new(mode: StressMode) -> Self {
189        Self {
190            mode,
191            duration: Duration::ZERO,
192            total_ops: 0,
193            ops_per_sec: 0.0,
194            passed: false,
195            pass_criteria: String::new(),
196            actual_value: String::new(),
197            memory: MemoryStats::default(),
198            latency: LatencyStats::default(),
199            errors: Vec::new(),
200        }
201    }
202
203    /// Mark as passed with criteria
204    pub fn pass(mut self, criteria: &str, actual: &str) -> Self {
205        self.passed = true;
206        self.pass_criteria = criteria.to_string();
207        self.actual_value = actual.to_string();
208        self
209    }
210
211    /// Mark as failed with criteria
212    pub fn fail(mut self, criteria: &str, actual: &str) -> Self {
213        self.passed = false;
214        self.pass_criteria = criteria.to_string();
215        self.actual_value = actual.to_string();
216        self
217    }
218}
219
220/// Memory statistics during stress test
221#[derive(Debug, Clone, Default, Serialize, Deserialize)]
222pub struct MemoryStats {
223    /// Initial heap size in bytes
224    pub initial_bytes: u64,
225    /// Final heap size in bytes
226    pub final_bytes: u64,
227    /// Peak heap size in bytes
228    pub peak_bytes: u64,
229    /// Whether memory is stable (no leaks detected)
230    pub stable: bool,
231}
232
233impl MemoryStats {
234    /// Check if memory grew significantly (potential leak)
235    pub fn growth_percent(&self) -> f64 {
236        if self.initial_bytes == 0 {
237            return 0.0;
238        }
239        ((self.final_bytes as f64 - self.initial_bytes as f64) / self.initial_bytes as f64) * 100.0
240    }
241}
242
243/// Latency statistics
244#[derive(Debug, Clone, Default, Serialize, Deserialize)]
245pub struct LatencyStats {
246    /// Minimum latency in microseconds
247    pub min_us: u64,
248    /// Maximum latency in microseconds
249    pub max_us: u64,
250    /// Mean latency in microseconds
251    pub mean_us: u64,
252    /// P50 latency in microseconds
253    pub p50_us: u64,
254    /// P95 latency in microseconds
255    pub p95_us: u64,
256    /// P99 latency in microseconds
257    pub p99_us: u64,
258}
259
260/// Stress test error
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct StressError {
263    /// Error type
264    pub kind: StressErrorKind,
265    /// Error message
266    pub message: String,
267    /// Time offset when error occurred
268    pub time_offset: Duration,
269}
270
271/// Stress error kinds
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
273pub enum StressErrorKind {
274    /// Lock contention timeout
275    LockTimeout,
276    /// Message queue overflow
277    QueueOverflow,
278    /// Frame drop detected
279    FrameDrop,
280    /// Memory allocation failure
281    OutOfMemory,
282    /// Worker crash
283    WorkerCrash,
284    /// Other error
285    Other,
286}
287
288impl std::fmt::Display for StressErrorKind {
289    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290        match self {
291            Self::LockTimeout => write!(f, "Lock Timeout"),
292            Self::QueueOverflow => write!(f, "Queue Overflow"),
293            Self::FrameDrop => write!(f, "Frame Drop"),
294            Self::OutOfMemory => write!(f, "Out of Memory"),
295            Self::WorkerCrash => write!(f, "Worker Crash"),
296            Self::Other => write!(f, "Other"),
297        }
298    }
299}
300
301// =============================================================================
302// Stress Test Runner
303// =============================================================================
304
305/// Stress test runner
306#[derive(Debug)]
307pub struct StressRunner {
308    config: StressConfig,
309}
310
311impl StressRunner {
312    /// Create a new stress runner
313    pub fn new(config: StressConfig) -> Self {
314        Self { config }
315    }
316
317    /// Run the stress test
318    pub fn run(&self) -> StressResult {
319        match self.config.mode {
320            StressMode::Atomics => self.run_atomics_stress(),
321            StressMode::WorkerMsg => self.run_worker_msg_stress(),
322            StressMode::Render => self.run_render_stress(),
323            StressMode::Trace => self.run_trace_stress(),
324            StressMode::Full => self.run_full_stress(),
325        }
326    }
327
328    /// Run atomics stress test (Point 116)
329    /// Pass criteria: SharedArrayBuffer lock contention > 10k ops/sec
330    fn run_atomics_stress(&self) -> StressResult {
331        let start = Instant::now();
332        let mut result = StressResult::new(StressMode::Atomics);
333
334        // Simulate atomic operations (in real impl, this would use SharedArrayBuffer)
335        let mut ops: u64 = 0;
336        let target_duration = Duration::from_secs(self.config.duration_secs);
337
338        // Simulate concurrent atomic increments
339        while start.elapsed() < target_duration {
340            // Each "operation" simulates an atomic CAS or increment
341            for _ in 0..1000 {
342                ops += 1;
343                // Simulate some work
344                std::hint::black_box(ops.wrapping_mul(31));
345            }
346        }
347
348        let elapsed = start.elapsed();
349        result.duration = elapsed;
350        result.total_ops = ops;
351        result.ops_per_sec = ops as f64 / elapsed.as_secs_f64();
352
353        // Pass criteria: > 10k ops/sec
354        const PASS_THRESHOLD: f64 = 10_000.0;
355        let ops_per_sec = result.ops_per_sec;
356        let criteria = format!("atomics throughput > {} ops/sec", PASS_THRESHOLD);
357        let actual = format!("{:.0} ops/sec", ops_per_sec);
358        if ops_per_sec >= PASS_THRESHOLD {
359            result = result.pass(&criteria, &actual);
360        } else {
361            result = result.fail(&criteria, &actual);
362        }
363
364        result.memory.stable = true;
365        result
366    }
367
368    /// Run worker message stress test (Point 117)
369    /// Pass criteria: Worker message throughput > 5k/sec without leaks
370    fn run_worker_msg_stress(&self) -> StressResult {
371        let start = Instant::now();
372        let mut result = StressResult::new(StressMode::WorkerMsg);
373
374        let mut messages: u64 = 0;
375        let target_duration = Duration::from_secs(self.config.duration_secs);
376
377        // Simulate message passing (in real impl, this would use postMessage)
378        while start.elapsed() < target_duration {
379            for _ in 0..500 {
380                messages += 1;
381                // Simulate serialization/deserialization overhead
382                let payload = std::hint::black_box(vec![0u8; 64]);
383                std::hint::black_box(payload.len());
384            }
385        }
386
387        let elapsed = start.elapsed();
388        result.duration = elapsed;
389        result.total_ops = messages;
390        result.ops_per_sec = messages as f64 / elapsed.as_secs_f64();
391
392        // Pass criteria: > 5k messages/sec
393        const PASS_THRESHOLD: f64 = 5_000.0;
394        let ops_per_sec = result.ops_per_sec;
395        let criteria = format!("message throughput > {} msg/sec", PASS_THRESHOLD);
396        let actual = format!("{:.0} msg/sec", ops_per_sec);
397        if ops_per_sec >= PASS_THRESHOLD {
398            result = result.pass(&criteria, &actual);
399        } else {
400            result = result.fail(&criteria, &actual);
401        }
402
403        result.memory.stable = true;
404        result
405    }
406
407    /// Run render loop stress test (Point 118)
408    /// Pass criteria: 60 FPS maintained under mock load
409    fn run_render_stress(&self) -> StressResult {
410        let start = Instant::now();
411        let mut result = StressResult::new(StressMode::Render);
412
413        let target_duration = Duration::from_secs(self.config.duration_secs);
414        let frame_budget = Duration::from_micros(16_667); // ~60 FPS
415        let mut frames: u64 = 0;
416        let mut dropped_frames: u64 = 0;
417
418        while start.elapsed() < target_duration {
419            let frame_start = Instant::now();
420
421            // Simulate render work
422            for _ in 0..1000 {
423                std::hint::black_box(frames.wrapping_add(1));
424            }
425
426            let frame_time = frame_start.elapsed();
427            frames += 1;
428
429            if frame_time > frame_budget {
430                dropped_frames += 1;
431            }
432
433            // Simulate vsync wait
434            if frame_time < frame_budget {
435                std::thread::sleep(frame_budget.checked_sub(frame_time).unwrap());
436            }
437        }
438
439        let elapsed = start.elapsed();
440        result.duration = elapsed;
441        result.total_ops = frames;
442        result.ops_per_sec = frames as f64 / elapsed.as_secs_f64();
443
444        // Pass criteria: 60 FPS maintained (allow <5% frame drops)
445        let drop_rate = dropped_frames as f64 / frames as f64;
446        let fps = result.ops_per_sec;
447        let actual = format!("{:.1} FPS, {:.1}% drops", fps, drop_rate * 100.0);
448        if drop_rate < 0.05 {
449            result = result.pass("60 FPS maintained (< 5% drops)", &actual);
450        } else {
451            result = result.fail("60 FPS maintained (< 5% drops)", &actual);
452            result.errors.push(StressError {
453                kind: StressErrorKind::FrameDrop,
454                message: format!("{} frames dropped", dropped_frames),
455                time_offset: elapsed,
456            });
457        }
458
459        result.memory.stable = true;
460        result
461    }
462
463    /// Run trace overhead stress test (Point 119)
464    /// Pass criteria: renacer overhead < 5% at saturation
465    fn run_trace_stress(&self) -> StressResult {
466        let start = Instant::now();
467        let mut result = StressResult::new(StressMode::Trace);
468
469        let _target_duration = Duration::from_secs(self.config.duration_secs);
470
471        // Measure baseline (no tracing)
472        let baseline_start = Instant::now();
473        let mut baseline_ops: u64 = 0;
474        let baseline_duration = Duration::from_secs(self.config.duration_secs / 2);
475
476        while baseline_start.elapsed() < baseline_duration {
477            for _ in 0..1000 {
478                baseline_ops += 1;
479                std::hint::black_box(baseline_ops);
480            }
481        }
482        let baseline_elapsed = baseline_start.elapsed();
483        let baseline_rate = baseline_ops as f64 / baseline_elapsed.as_secs_f64();
484
485        // Measure with simulated tracing
486        let traced_start = Instant::now();
487        let mut traced_ops: u64 = 0;
488        let traced_duration = Duration::from_secs(self.config.duration_secs / 2);
489
490        while traced_start.elapsed() < traced_duration {
491            for _ in 0..1000 {
492                traced_ops += 1;
493                // Simulate tracing overhead
494                std::hint::black_box(std::time::Instant::now());
495                std::hint::black_box(traced_ops);
496            }
497        }
498        let traced_elapsed = traced_start.elapsed();
499        let traced_rate = traced_ops as f64 / traced_elapsed.as_secs_f64();
500
501        let elapsed = start.elapsed();
502        result.duration = elapsed;
503        result.total_ops = baseline_ops + traced_ops;
504        result.ops_per_sec = traced_rate;
505
506        // Calculate overhead percentage
507        let overhead = if baseline_rate > 0.0 {
508            ((baseline_rate - traced_rate) / baseline_rate) * 100.0
509        } else {
510            0.0
511        };
512
513        // Pass criteria: < 5% overhead
514        if overhead < 5.0 {
515            result = result.pass(
516                "tracing overhead < 5%",
517                &format!("{:.2}% overhead", overhead),
518            );
519        } else {
520            result = result.fail(
521                "tracing overhead < 5%",
522                &format!("{:.2}% overhead", overhead),
523            );
524        }
525
526        result.memory.stable = true;
527        result
528    }
529
530    /// Run full system stress test (Point 123)
531    /// Combines all stress modes
532    fn run_full_stress(&self) -> StressResult {
533        let start = Instant::now();
534        let mut result = StressResult::new(StressMode::Full);
535
536        // Run each sub-test for a portion of the duration
537        let sub_duration = self.config.duration_secs / 4;
538
539        let atomics_config = StressConfig::atomics(sub_duration, self.config.concurrency);
540        let atomics_result = StressRunner::new(atomics_config).run();
541
542        let worker_config = StressConfig::worker_msg(sub_duration, self.config.concurrency);
543        let worker_result = StressRunner::new(worker_config).run();
544
545        let render_config = StressConfig::render(sub_duration);
546        let render_result = StressRunner::new(render_config).run();
547
548        let trace_config = StressConfig::trace(sub_duration);
549        let trace_result = StressRunner::new(trace_config).run();
550
551        let elapsed = start.elapsed();
552        result.duration = elapsed;
553        result.total_ops = atomics_result.total_ops
554            + worker_result.total_ops
555            + render_result.total_ops
556            + trace_result.total_ops;
557
558        // All sub-tests must pass
559        let all_passed = atomics_result.passed
560            && worker_result.passed
561            && render_result.passed
562            && trace_result.passed;
563
564        if all_passed {
565            result = result.pass(
566                "all stress tests pass",
567                &format!(
568                    "atomics: {}, worker: {}, render: {}, trace: {}",
569                    if atomics_result.passed { "✓" } else { "✗" },
570                    if worker_result.passed { "✓" } else { "✗" },
571                    if render_result.passed { "✓" } else { "✗" },
572                    if trace_result.passed { "✓" } else { "✗" },
573                ),
574            );
575        } else {
576            result = result.fail(
577                "all stress tests pass",
578                &format!(
579                    "atomics: {}, worker: {}, render: {}, trace: {}",
580                    if atomics_result.passed { "✓" } else { "✗" },
581                    if worker_result.passed { "✓" } else { "✗" },
582                    if render_result.passed { "✓" } else { "✗" },
583                    if trace_result.passed { "✓" } else { "✗" },
584                ),
585            );
586        }
587
588        // Collect errors from all sub-tests
589        result.errors.extend(atomics_result.errors);
590        result.errors.extend(worker_result.errors);
591        result.errors.extend(render_result.errors);
592        result.errors.extend(trace_result.errors);
593
594        result.memory.stable = atomics_result.memory.stable
595            && worker_result.memory.stable
596            && render_result.memory.stable
597            && trace_result.memory.stable;
598
599        result
600    }
601}
602
603// =============================================================================
604// Rendering
605// =============================================================================
606
607/// Render stress test result as text report
608pub fn render_stress_report(result: &StressResult) -> String {
609    let mut output = String::new();
610
611    let status = if result.passed {
612        "✅ PASS"
613    } else {
614        "❌ FAIL"
615    };
616
617    output.push_str(&format!("STRESS TEST: {} [{}]\n", result.mode, status));
618    output.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n");
619
620    output.push_str(&format!("Duration: {:?}\n", result.duration));
621    output.push_str(&format!("Operations: {}\n", result.total_ops));
622    output.push_str(&format!(
623        "Throughput: {:.0} ops/sec\n\n",
624        result.ops_per_sec
625    ));
626
627    output.push_str("Pass Criteria:\n");
628    output.push_str(&format!("  Expected: {}\n", result.pass_criteria));
629    output.push_str(&format!("  Actual:   {}\n\n", result.actual_value));
630
631    output.push_str("Memory:\n");
632    output.push_str(&format!(
633        "  Stable: {}\n",
634        if result.memory.stable {
635            "Yes"
636        } else {
637            "No (potential leak)"
638        }
639    ));
640    if result.memory.initial_bytes > 0 {
641        output.push_str(&format!(
642            "  Growth: {:.1}%\n",
643            result.memory.growth_percent()
644        ));
645    }
646
647    if !result.errors.is_empty() {
648        output.push_str(&format!("\nErrors ({}):\n", result.errors.len()));
649        for err in &result.errors {
650            output.push_str(&format!(
651                "  [{:?}] {} - {}\n",
652                err.time_offset, err.kind, err.message
653            ));
654        }
655    }
656
657    output
658}
659
660/// Render stress test result as JSON
661pub fn render_stress_json(result: &StressResult) -> String {
662    serde_json::to_string_pretty(result).unwrap_or_else(|_| "{}".to_string())
663}
664
665// =============================================================================
666// Tests
667// =============================================================================
668
669#[cfg(test)]
670#[allow(clippy::unwrap_used, clippy::expect_used)]
671mod tests {
672    use super::*;
673    use std::str::FromStr;
674
675    #[test]
676    fn test_stress_mode_from_str() {
677        assert_eq!(
678            StressMode::from_str("atomics").unwrap(),
679            StressMode::Atomics
680        );
681        assert_eq!(
682            StressMode::from_str("worker-msg").unwrap(),
683            StressMode::WorkerMsg
684        );
685        assert_eq!(StressMode::from_str("render").unwrap(), StressMode::Render);
686        assert_eq!(StressMode::from_str("trace").unwrap(), StressMode::Trace);
687        assert_eq!(StressMode::from_str("full").unwrap(), StressMode::Full);
688    }
689
690    #[test]
691    fn test_stress_mode_display() {
692        assert_eq!(StressMode::Atomics.to_string(), "atomics");
693        assert_eq!(StressMode::WorkerMsg.to_string(), "worker-msg");
694        assert_eq!(StressMode::Render.to_string(), "render");
695    }
696
697    #[test]
698    fn test_stress_config_atomics() {
699        let config = StressConfig::atomics(30, 4);
700        assert_eq!(config.mode, StressMode::Atomics);
701        assert_eq!(config.duration_secs, 30);
702        assert_eq!(config.concurrency, 4);
703    }
704
705    #[test]
706    fn test_stress_config_worker_msg() {
707        let config = StressConfig::worker_msg(60, 8);
708        assert_eq!(config.mode, StressMode::WorkerMsg);
709        assert_eq!(config.duration_secs, 60);
710        assert_eq!(config.concurrency, 8);
711    }
712
713    #[test]
714    fn test_stress_result_pass() {
715        let result = StressResult::new(StressMode::Atomics).pass("> 10k ops/sec", "15000 ops/sec");
716        assert!(result.passed);
717        assert_eq!(result.pass_criteria, "> 10k ops/sec");
718        assert_eq!(result.actual_value, "15000 ops/sec");
719    }
720
721    #[test]
722    fn test_stress_result_fail() {
723        let result = StressResult::new(StressMode::Atomics).fail("> 10k ops/sec", "5000 ops/sec");
724        assert!(!result.passed);
725    }
726
727    #[test]
728    fn test_memory_stats_growth() {
729        let stats = MemoryStats {
730            initial_bytes: 1000,
731            final_bytes: 1100,
732            peak_bytes: 1200,
733            stable: true,
734        };
735        assert!((stats.growth_percent() - 10.0).abs() < 0.001);
736    }
737
738    #[test]
739    fn test_stress_error_kind_display() {
740        assert_eq!(StressErrorKind::LockTimeout.to_string(), "Lock Timeout");
741        assert_eq!(StressErrorKind::QueueOverflow.to_string(), "Queue Overflow");
742        assert_eq!(StressErrorKind::FrameDrop.to_string(), "Frame Drop");
743    }
744
745    #[test]
746    fn test_run_atomics_stress() {
747        let config = StressConfig::atomics(1, 1); // 1 second test
748        let runner = StressRunner::new(config);
749        let result = runner.run();
750
751        assert_eq!(result.mode, StressMode::Atomics);
752        assert!(result.total_ops > 0);
753        assert!(result.ops_per_sec > 0.0);
754        assert!(result.passed); // Should pass with simulated ops
755    }
756
757    #[test]
758    fn test_run_worker_msg_stress() {
759        let config = StressConfig::worker_msg(1, 1);
760        let runner = StressRunner::new(config);
761        let result = runner.run();
762
763        assert_eq!(result.mode, StressMode::WorkerMsg);
764        assert!(result.total_ops > 0);
765        assert!(result.passed);
766    }
767
768    #[test]
769    fn test_run_render_stress() {
770        let config = StressConfig::render(1);
771        let runner = StressRunner::new(config);
772        let result = runner.run();
773
774        assert_eq!(result.mode, StressMode::Render);
775        assert!(result.total_ops > 0);
776        // FPS should be around 60
777        assert!(result.ops_per_sec > 50.0 && result.ops_per_sec < 70.0);
778    }
779
780    #[test]
781    fn test_run_trace_stress() {
782        let config = StressConfig::trace(2); // Need at least 2 seconds (1 baseline, 1 traced)
783        let runner = StressRunner::new(config);
784        let result = runner.run();
785
786        assert_eq!(result.mode, StressMode::Trace);
787        assert!(result.total_ops > 0);
788    }
789
790    #[test]
791    fn test_run_full_stress() {
792        let config = StressConfig::full(4, 1); // 1 second per sub-test
793        let runner = StressRunner::new(config);
794        let result = runner.run();
795
796        assert_eq!(result.mode, StressMode::Full);
797        assert!(result.total_ops > 0);
798    }
799
800    #[test]
801    fn test_render_stress_report() {
802        let mut result = StressResult::new(StressMode::Atomics);
803        result.duration = Duration::from_secs(30);
804        result.total_ops = 300_000;
805        result.ops_per_sec = 10_000.0;
806        result = result.pass("> 10k ops/sec", "10000 ops/sec");
807
808        let report = render_stress_report(&result);
809        assert!(report.contains("STRESS TEST: atomics"));
810        assert!(report.contains("PASS"));
811        assert!(report.contains("10000 ops/sec"));
812    }
813
814    #[test]
815    fn test_render_stress_json() {
816        let result = StressResult::new(StressMode::Atomics);
817        let json = render_stress_json(&result);
818        assert!(json.contains("Atomics"));
819        assert!(json.contains("mode"));
820    }
821
822    #[test]
823    fn test_stress_mode_display_all() {
824        // Cover all Display branches
825        assert_eq!(format!("{}", StressMode::Atomics), "atomics");
826        assert_eq!(format!("{}", StressMode::WorkerMsg), "worker-msg");
827        assert_eq!(format!("{}", StressMode::Render), "render");
828        assert_eq!(format!("{}", StressMode::Trace), "trace");
829        assert_eq!(format!("{}", StressMode::Full), "full");
830    }
831
832    #[test]
833    fn test_stress_mode_from_str_all_variants() {
834        assert_eq!(
835            "atomics".parse::<StressMode>().unwrap(),
836            StressMode::Atomics
837        );
838        assert_eq!(
839            "worker-msg".parse::<StressMode>().unwrap(),
840            StressMode::WorkerMsg
841        );
842        assert_eq!(
843            "workermsg".parse::<StressMode>().unwrap(),
844            StressMode::WorkerMsg
845        );
846        assert_eq!(
847            "worker_msg".parse::<StressMode>().unwrap(),
848            StressMode::WorkerMsg
849        );
850        assert_eq!("render".parse::<StressMode>().unwrap(), StressMode::Render);
851        assert_eq!("trace".parse::<StressMode>().unwrap(), StressMode::Trace);
852        assert_eq!("full".parse::<StressMode>().unwrap(), StressMode::Full);
853    }
854
855    #[test]
856    fn test_stress_mode_from_str_unknown() {
857        let result = "unknown_mode".parse::<StressMode>();
858        assert!(result.is_err());
859        assert!(result.unwrap_err().contains("Unknown stress mode"));
860    }
861
862    // Additional coverage tests
863
864    #[test]
865    fn test_stress_config_render() {
866        let config = StressConfig::render(45);
867        assert_eq!(config.mode, StressMode::Render);
868        assert_eq!(config.duration_secs, 45);
869        assert_eq!(config.concurrency, 1);
870    }
871
872    #[test]
873    fn test_stress_config_trace() {
874        let config = StressConfig::trace(20);
875        assert_eq!(config.mode, StressMode::Trace);
876        assert_eq!(config.duration_secs, 20);
877        assert_eq!(config.concurrency, 1);
878    }
879
880    #[test]
881    fn test_stress_config_full() {
882        let config = StressConfig::full(120, 16);
883        assert_eq!(config.mode, StressMode::Full);
884        assert_eq!(config.duration_secs, 120);
885        assert_eq!(config.concurrency, 16);
886    }
887
888    #[test]
889    fn test_stress_config_default() {
890        let config = StressConfig::default();
891        assert_eq!(config.mode, StressMode::Atomics);
892        assert_eq!(config.duration_secs, 30);
893        assert_eq!(config.concurrency, 4);
894        assert_eq!(config.target_ops_per_sec, 0);
895        assert_eq!(config.warmup_secs, 5);
896    }
897
898    #[test]
899    fn test_memory_stats_growth_zero_initial() {
900        let stats = MemoryStats {
901            initial_bytes: 0,
902            final_bytes: 1000,
903            peak_bytes: 1000,
904            stable: true,
905        };
906        assert_eq!(stats.growth_percent(), 0.0);
907    }
908
909    #[test]
910    fn test_stress_error_kind_display_all() {
911        assert_eq!(StressErrorKind::LockTimeout.to_string(), "Lock Timeout");
912        assert_eq!(StressErrorKind::QueueOverflow.to_string(), "Queue Overflow");
913        assert_eq!(StressErrorKind::FrameDrop.to_string(), "Frame Drop");
914        assert_eq!(StressErrorKind::OutOfMemory.to_string(), "Out of Memory");
915        assert_eq!(StressErrorKind::WorkerCrash.to_string(), "Worker Crash");
916        assert_eq!(StressErrorKind::Other.to_string(), "Other");
917    }
918
919    #[test]
920    fn test_render_stress_report_with_memory_growth() {
921        let mut result = StressResult::new(StressMode::Atomics);
922        result.duration = Duration::from_secs(30);
923        result.total_ops = 300_000;
924        result.ops_per_sec = 10_000.0;
925        result.memory = MemoryStats {
926            initial_bytes: 1000,
927            final_bytes: 1500,
928            peak_bytes: 1600,
929            stable: false,
930        };
931        result = result.fail("> 10k ops/sec", "10000 ops/sec");
932
933        let report = render_stress_report(&result);
934        assert!(report.contains("No (potential leak)"));
935        assert!(report.contains("Growth:"));
936        assert!(report.contains("50.0%"));
937    }
938
939    #[test]
940    fn test_render_stress_report_with_errors() {
941        let mut result = StressResult::new(StressMode::Render);
942        result.duration = Duration::from_secs(30);
943        result.errors.push(StressError {
944            kind: StressErrorKind::FrameDrop,
945            message: "10 frames dropped".to_string(),
946            time_offset: Duration::from_secs(15),
947        });
948        result.errors.push(StressError {
949            kind: StressErrorKind::LockTimeout,
950            message: "Lock timed out".to_string(),
951            time_offset: Duration::from_secs(20),
952        });
953
954        let report = render_stress_report(&result);
955        assert!(report.contains("Errors (2)"));
956        assert!(report.contains("Frame Drop"));
957        assert!(report.contains("Lock Timeout"));
958    }
959
960    #[test]
961    fn test_latency_stats_default() {
962        let stats = LatencyStats::default();
963        assert_eq!(stats.min_us, 0);
964        assert_eq!(stats.max_us, 0);
965        assert_eq!(stats.mean_us, 0);
966        assert_eq!(stats.p50_us, 0);
967        assert_eq!(stats.p95_us, 0);
968        assert_eq!(stats.p99_us, 0);
969    }
970
971    #[test]
972    fn test_stress_error_serialization() {
973        let error = StressError {
974            kind: StressErrorKind::OutOfMemory,
975            message: "Allocation failed".to_string(),
976            time_offset: Duration::from_millis(500),
977        };
978
979        let json = serde_json::to_string(&error).unwrap();
980        assert!(json.contains("OutOfMemory"));
981        assert!(json.contains("Allocation failed"));
982
983        let parsed: StressError = serde_json::from_str(&json).unwrap();
984        assert_eq!(parsed.kind, StressErrorKind::OutOfMemory);
985    }
986
987    #[test]
988    fn test_stress_mode_default() {
989        let mode = StressMode::default();
990        assert_eq!(mode, StressMode::Atomics);
991    }
992
993    #[test]
994    fn test_memory_stats_default() {
995        let stats = MemoryStats::default();
996        assert_eq!(stats.initial_bytes, 0);
997        assert_eq!(stats.final_bytes, 0);
998        assert_eq!(stats.peak_bytes, 0);
999        assert!(!stats.stable);
1000    }
1001
1002    #[test]
1003    fn test_stress_result_new() {
1004        let result = StressResult::new(StressMode::WorkerMsg);
1005        assert_eq!(result.mode, StressMode::WorkerMsg);
1006        assert!(!result.passed);
1007        assert!(result.pass_criteria.is_empty());
1008        assert!(result.actual_value.is_empty());
1009        assert!(result.errors.is_empty());
1010    }
1011
1012    #[test]
1013    fn test_render_stress_json_error_handling() {
1014        let result = StressResult::new(StressMode::Atomics);
1015        let json = render_stress_json(&result);
1016        assert!(!json.is_empty());
1017        assert!(json.starts_with('{'));
1018    }
1019
1020    #[test]
1021    fn test_stress_config_serde() {
1022        let config = StressConfig::atomics(60, 8);
1023        let json = serde_json::to_string(&config).unwrap();
1024        assert!(json.contains("Atomics"));
1025        assert!(json.contains("60"));
1026        assert!(json.contains('8'));
1027
1028        let parsed: StressConfig = serde_json::from_str(&json).unwrap();
1029        assert_eq!(parsed.mode, StressMode::Atomics);
1030        assert_eq!(parsed.duration_secs, 60);
1031    }
1032}