Coverage Report

Created: 2026-01-23 22:55

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/trueno/src/simulation.rs
Line
Count
Source
1
//! Simulation Testing Framework (TRUENO-SPEC-012)
2
//!
3
//! Provides deterministic, reproducible, and falsifiable validation of compute
4
//! operations across all backends: SIMD (CPU), PTX (CUDA), and WGPU.
5
//!
6
//! This module integrates with the sovereign stack (simular) and follows
7
//! Toyota Production System principles:
8
//!
9
//! - **Jidoka**: Built-in quality - stop on defect
10
//! - **Poka-Yoke**: Mistake-proofing via type safety
11
//! - **Heijunka**: Leveled testing across backends
12
//! - **Genchi Genbutsu**: Visual inspection of results
13
//! - **Kaizen**: Continuous performance improvement
14
//!
15
//! # Example
16
//!
17
//! ```rust,ignore
18
//! use trueno::simulation::{SimTestConfig, BackendTolerance};
19
//!
20
//! let config = SimTestConfig::builder()
21
//!     .seed(42)
22
//!     .tolerance(BackendTolerance::default())
23
//!     .build();
24
//! ```
25
26
use crate::Backend;
27
use std::collections::VecDeque;
28
use std::marker::PhantomData;
29
use std::path::PathBuf;
30
31
// =============================================================================
32
// VISUAL REGRESSION TESTING (Genchi Genbutsu: Go and See)
33
// =============================================================================
34
35
/// RGB color for visualization
36
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37
pub struct Rgb {
38
    /// Red component
39
    pub r: u8,
40
    /// Green component
41
    pub g: u8,
42
    /// Blue component
43
    pub b: u8,
44
}
45
46
impl Rgb {
47
    /// Create new RGB color
48
    #[must_use]
49
0
    pub const fn new(r: u8, g: u8, b: u8) -> Self {
50
0
        Self { r, g, b }
51
0
    }
52
53
    /// Magenta for NaN values
54
    pub const NAN_COLOR: Self = Self::new(255, 0, 255);
55
    /// White for +Infinity
56
    pub const INF_COLOR: Self = Self::new(255, 255, 255);
57
    /// Black for -Infinity
58
    pub const NEG_INF_COLOR: Self = Self::new(0, 0, 0);
59
}
60
61
/// Color palette for heatmap rendering
62
#[derive(Debug, Clone)]
63
pub struct ColorPalette {
64
    colors: Vec<Rgb>,
65
}
66
67
impl Default for ColorPalette {
68
0
    fn default() -> Self {
69
0
        Self::viridis()
70
0
    }
71
}
72
73
impl ColorPalette {
74
    /// Viridis colorblind-friendly palette
75
    #[must_use]
76
0
    pub fn viridis() -> Self {
77
0
        Self {
78
0
            colors: vec![
79
0
                Rgb::new(68, 1, 84),
80
0
                Rgb::new(59, 82, 139),
81
0
                Rgb::new(33, 145, 140),
82
0
                Rgb::new(94, 201, 98),
83
0
                Rgb::new(253, 231, 37),
84
0
            ],
85
0
        }
86
0
    }
87
88
    /// Grayscale palette
89
    #[must_use]
90
0
    pub fn grayscale() -> Self {
91
0
        Self {
92
0
            colors: vec![
93
0
                Rgb::new(0, 0, 0),
94
0
                Rgb::new(128, 128, 128),
95
0
                Rgb::new(255, 255, 255),
96
0
            ],
97
0
        }
98
0
    }
99
100
    /// Interpolate color at position t (0.0 to 1.0)
101
    #[must_use]
102
    #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
103
0
    pub fn interpolate(&self, t: f32) -> Rgb {
104
0
        let t = t.clamp(0.0, 1.0);
105
0
        let n = self.colors.len() - 1;
106
0
        let idx = (t * n as f32).floor() as usize;
107
0
        let idx = idx.min(n - 1);
108
0
        let local_t = t * n as f32 - idx as f32;
109
110
0
        let c1 = &self.colors[idx];
111
0
        let c2 = &self.colors[idx + 1];
112
113
0
        Rgb {
114
0
            r: (c1.r as f32 + (c2.r as f32 - c1.r as f32) * local_t) as u8,
115
0
            g: (c1.g as f32 + (c2.g as f32 - c1.g as f32) * local_t) as u8,
116
0
            b: (c1.b as f32 + (c2.b as f32 - c1.b as f32) * local_t) as u8,
117
0
        }
118
0
    }
119
}
120
121
/// Visual regression test configuration (Genchi Genbutsu)
122
#[derive(Debug, Clone)]
123
pub struct VisualRegressionConfig {
124
    /// Golden baseline directory
125
    pub golden_dir: PathBuf,
126
    /// Output directory for test results
127
    pub output_dir: PathBuf,
128
    /// Maximum allowed different pixels (percentage)
129
    pub max_diff_pct: f64,
130
    /// Color palette for visualization
131
    pub palette: ColorPalette,
132
}
133
134
impl Default for VisualRegressionConfig {
135
0
    fn default() -> Self {
136
0
        Self {
137
0
            golden_dir: PathBuf::from("golden"),
138
0
            output_dir: PathBuf::from("test_output"),
139
0
            max_diff_pct: 0.0, // Exact match by default
140
0
            palette: ColorPalette::default(),
141
0
        }
142
0
    }
143
}
144
145
impl VisualRegressionConfig {
146
    /// Create new config with custom golden directory
147
    #[must_use]
148
0
    pub fn new(golden_dir: impl Into<PathBuf>) -> Self {
149
0
        Self {
150
0
            golden_dir: golden_dir.into(),
151
0
            ..Default::default()
152
0
        }
153
0
    }
154
155
    /// Set output directory
156
    #[must_use]
157
0
    pub fn with_output_dir(mut self, dir: impl Into<PathBuf>) -> Self {
158
0
        self.output_dir = dir.into();
159
0
        self
160
0
    }
161
162
    /// Set maximum diff percentage
163
    #[must_use]
164
0
    pub const fn with_max_diff_pct(mut self, pct: f64) -> Self {
165
0
        self.max_diff_pct = pct;
166
0
        self
167
0
    }
168
169
    /// Set color palette
170
    #[must_use]
171
0
    pub fn with_palette(mut self, palette: ColorPalette) -> Self {
172
0
        self.palette = palette;
173
0
        self
174
0
    }
175
}
176
177
/// Pixel diff result for visual regression testing
178
#[derive(Debug, Clone)]
179
pub struct PixelDiffResult {
180
    /// Number of pixels that differ
181
    pub different_pixels: usize,
182
    /// Total number of pixels
183
    pub total_pixels: usize,
184
    /// Maximum color difference found
185
    pub max_diff: u32,
186
}
187
188
impl PixelDiffResult {
189
    /// Calculate percentage of different pixels
190
    #[must_use]
191
0
    pub fn diff_percentage(&self) -> f64 {
192
0
        if self.total_pixels == 0 {
193
0
            0.0
194
        } else {
195
0
            (self.different_pixels as f64 / self.total_pixels as f64) * 100.0
196
        }
197
0
    }
198
199
    /// Check if images match within threshold
200
    #[must_use]
201
0
    pub fn matches(&self, threshold_pct: f64) -> bool {
202
0
        self.diff_percentage() <= threshold_pct
203
0
    }
204
205
    /// Create a passing result (no differences)
206
    #[must_use]
207
0
    pub const fn pass(total_pixels: usize) -> Self {
208
0
        Self {
209
0
            different_pixels: 0,
210
0
            total_pixels,
211
0
            max_diff: 0,
212
0
        }
213
0
    }
214
}
215
216
/// Simple buffer renderer for SIMD output visualization
217
///
218
/// Converts f32 buffers to raw RGBA bytes for testing
219
#[derive(Debug, Clone)]
220
pub struct BufferRenderer {
221
    palette: ColorPalette,
222
    range: Option<(f32, f32)>,
223
}
224
225
impl Default for BufferRenderer {
226
0
    fn default() -> Self {
227
0
        Self::new()
228
0
    }
229
}
230
231
impl BufferRenderer {
232
    /// Create renderer with auto-normalization
233
    #[must_use]
234
0
    pub fn new() -> Self {
235
0
        Self {
236
0
            palette: ColorPalette::default(),
237
0
            range: None,
238
0
        }
239
0
    }
240
241
    /// Set fixed range for normalization
242
    #[must_use]
243
0
    pub const fn with_range(mut self, min: f32, max: f32) -> Self {
244
0
        self.range = Some((min, max));
245
0
        self
246
0
    }
247
248
    /// Set color palette
249
    #[must_use]
250
0
    pub fn with_palette(mut self, palette: ColorPalette) -> Self {
251
0
        self.palette = palette;
252
0
        self
253
0
    }
254
255
    /// Render f32 buffer to raw RGBA bytes
256
    ///
257
    /// Returns Vec<u8> with RGBA pixels (4 bytes per pixel)
258
    #[must_use]
259
0
    pub fn render_to_rgba(&self, buffer: &[f32], width: u32, height: u32) -> Vec<u8> {
260
0
        assert_eq!(buffer.len(), (width * height) as usize);
261
262
0
        let (min_val, max_val) = self.range.unwrap_or_else(|| {
263
0
            let valid: Vec<f32> = buffer.iter().copied().filter(|v| v.is_finite()).collect();
264
0
            if valid.is_empty() {
265
0
                (0.0, 1.0)
266
            } else {
267
0
                let min = valid.iter().copied().fold(f32::INFINITY, f32::min);
268
0
                let max = valid.iter().copied().fold(f32::NEG_INFINITY, f32::max);
269
0
                (min, max.max(min + f32::EPSILON))
270
            }
271
0
        });
272
273
0
        let mut rgba = Vec::with_capacity(buffer.len() * 4);
274
275
0
        for &value in buffer {
276
0
            let color = if value.is_nan() {
277
0
                Rgb::NAN_COLOR
278
0
            } else if value.is_infinite() {
279
0
                if value > 0.0 {
280
0
                    Rgb::INF_COLOR
281
                } else {
282
0
                    Rgb::NEG_INF_COLOR
283
                }
284
            } else {
285
0
                let t = (value - min_val) / (max_val - min_val);
286
0
                self.palette.interpolate(t)
287
            };
288
289
0
            rgba.push(color.r);
290
0
            rgba.push(color.g);
291
0
            rgba.push(color.b);
292
0
            rgba.push(255); // Alpha
293
        }
294
295
0
        rgba
296
0
    }
297
298
    /// Compare two RGBA buffers and return diff result
299
    #[must_use]
300
0
    pub fn compare_rgba(&self, a: &[u8], b: &[u8], tolerance: u8) -> PixelDiffResult {
301
0
        if a == b {
302
0
            return PixelDiffResult::pass(a.len() / 4);
303
0
        }
304
305
0
        let min_len = a.len().min(b.len());
306
0
        let mut different = 0;
307
0
        let mut max_diff: u32 = 0;
308
309
        // Compare pixels (4 bytes each: RGBA)
310
0
        for i in (0..min_len).step_by(4) {
311
0
            let mut pixel_diff = false;
312
0
            for j in 0..4 {
313
0
                if i + j < min_len {
314
0
                    let diff = (a[i + j] as i32 - b[i + j] as i32).unsigned_abs();
315
0
                    if diff > tolerance as u32 {
316
0
                        pixel_diff = true;
317
0
                        max_diff = max_diff.max(diff);
318
0
                    }
319
0
                }
320
            }
321
0
            if pixel_diff {
322
0
                different += 1;
323
0
            }
324
        }
325
326
        // Count size difference as pixel differences
327
0
        if a.len() != b.len() {
328
0
            different += a.len().abs_diff(b.len()) / 4;
329
0
        }
330
331
0
        PixelDiffResult {
332
0
            different_pixels: different,
333
0
            total_pixels: min_len.max(a.len()).max(b.len()) / 4,
334
0
            max_diff,
335
0
        }
336
0
    }
337
}
338
339
/// Golden baseline manager for visual regression testing
340
#[derive(Debug, Clone)]
341
pub struct GoldenBaseline {
342
    config: VisualRegressionConfig,
343
}
344
345
impl GoldenBaseline {
346
    /// Create new golden baseline manager
347
    #[must_use]
348
0
    pub fn new(config: VisualRegressionConfig) -> Self {
349
0
        Self { config }
350
0
    }
351
352
    /// Get path for a golden baseline file
353
    #[must_use]
354
0
    pub fn golden_path(&self, name: &str) -> PathBuf {
355
0
        self.config.golden_dir.join(format!("{name}.golden"))
356
0
    }
357
358
    /// Get path for an output file
359
    #[must_use]
360
0
    pub fn output_path(&self, name: &str) -> PathBuf {
361
0
        self.config.output_dir.join(format!("{name}.output"))
362
0
    }
363
364
    /// Get the config
365
    #[must_use]
366
0
    pub const fn config(&self) -> &VisualRegressionConfig {
367
0
        &self.config
368
0
    }
369
}
370
371
// =============================================================================
372
// STRESS TESTING (Heijunka: Leveled Workload Testing)
373
// =============================================================================
374
375
/// Stress test configuration for trueno operations
376
#[derive(Debug, Clone)]
377
pub struct StressTestConfig {
378
    /// Number of cycles per backend
379
    pub cycles_per_backend: u32,
380
    /// Input sizes to test (leveled)
381
    pub input_sizes: Vec<usize>,
382
    /// Backends to stress test
383
    pub backends: Vec<Backend>,
384
    /// Performance thresholds
385
    pub thresholds: StressThresholds,
386
    /// Master seed for RNG
387
    pub master_seed: u64,
388
}
389
390
impl Default for StressTestConfig {
391
0
    fn default() -> Self {
392
0
        Self {
393
0
            cycles_per_backend: 100,
394
0
            input_sizes: vec![100, 1_000, 10_000, 100_000, 1_000_000],
395
0
            backends: vec![Backend::Scalar, Backend::AVX2],
396
0
            thresholds: StressThresholds::default(),
397
0
            master_seed: 42,
398
0
        }
399
0
    }
400
}
401
402
impl StressTestConfig {
403
    /// Create new stress test config
404
    #[must_use]
405
0
    pub fn new(master_seed: u64) -> Self {
406
0
        Self {
407
0
            master_seed,
408
0
            ..Default::default()
409
0
        }
410
0
    }
411
412
    /// Set cycles per backend
413
    #[must_use]
414
0
    pub const fn with_cycles(mut self, cycles: u32) -> Self {
415
0
        self.cycles_per_backend = cycles;
416
0
        self
417
0
    }
418
419
    /// Set input sizes
420
    #[must_use]
421
0
    pub fn with_input_sizes(mut self, sizes: Vec<usize>) -> Self {
422
0
        self.input_sizes = sizes;
423
0
        self
424
0
    }
425
426
    /// Set backends to test
427
    #[must_use]
428
0
    pub fn with_backends(mut self, backends: Vec<Backend>) -> Self {
429
0
        self.backends = backends;
430
0
        self
431
0
    }
432
433
    /// Set performance thresholds
434
    #[must_use]
435
0
    pub fn with_thresholds(mut self, thresholds: StressThresholds) -> Self {
436
0
        self.thresholds = thresholds;
437
0
        self
438
0
    }
439
440
    /// Calculate total test count
441
    #[must_use]
442
0
    pub fn total_tests(&self) -> usize {
443
0
        self.backends.len() * self.input_sizes.len() * self.cycles_per_backend as usize
444
0
    }
445
}
446
447
/// Performance thresholds for stress testing
448
#[derive(Debug, Clone)]
449
pub struct StressThresholds {
450
    /// Max time per operation (ms)
451
    pub max_op_time_ms: u64,
452
    /// Max memory per operation (bytes)
453
    pub max_memory_bytes: usize,
454
    /// Max variance in operation times (coefficient of variation)
455
    pub max_timing_variance: f64,
456
    /// Max allowed failure rate (0.0 to 1.0)
457
    pub max_failure_rate: f64,
458
}
459
460
impl Default for StressThresholds {
461
0
    fn default() -> Self {
462
0
        Self {
463
0
            max_op_time_ms: 1000,                // 1s max per op
464
0
            max_memory_bytes: 256 * 1024 * 1024, // 256MB max
465
0
            max_timing_variance: 0.5,            // 50% max variance
466
0
            max_failure_rate: 0.0,               // Zero failures allowed
467
0
        }
468
0
    }
469
}
470
471
impl StressThresholds {
472
    /// Strict thresholds for CI
473
    #[must_use]
474
0
    pub const fn strict() -> Self {
475
0
        Self {
476
0
            max_op_time_ms: 100,
477
0
            max_memory_bytes: 64 * 1024 * 1024,
478
0
            max_timing_variance: 0.2,
479
0
            max_failure_rate: 0.0,
480
0
        }
481
0
    }
482
483
    /// Relaxed thresholds for development
484
    #[must_use]
485
0
    pub const fn relaxed() -> Self {
486
0
        Self {
487
0
            max_op_time_ms: 5000,
488
0
            max_memory_bytes: 512 * 1024 * 1024,
489
0
            max_timing_variance: 1.0,
490
0
            max_failure_rate: 0.01,
491
0
        }
492
0
    }
493
}
494
495
/// Stress test result for a single operation
496
#[derive(Debug, Clone)]
497
pub struct StressResult {
498
    /// Backend used
499
    pub backend: Backend,
500
    /// Input size
501
    pub input_size: usize,
502
    /// Cycles completed
503
    pub cycles_completed: u32,
504
    /// Total tests passed
505
    pub tests_passed: u32,
506
    /// Total tests failed
507
    pub tests_failed: u32,
508
    /// Mean operation time (ms)
509
    pub mean_op_time_ms: f64,
510
    /// Max operation time (ms)
511
    pub max_op_time_ms: u64,
512
    /// Timing variance (coefficient of variation)
513
    pub timing_variance: f64,
514
    /// Detected anomalies
515
    pub anomalies: Vec<StressAnomaly>,
516
}
517
518
impl StressResult {
519
    /// Check if all tests passed
520
    #[must_use]
521
0
    pub fn passed(&self) -> bool {
522
0
        self.tests_failed == 0 && self.anomalies.is_empty()
523
0
    }
524
525
    /// Calculate pass rate
526
    #[must_use]
527
0
    pub fn pass_rate(&self) -> f64 {
528
0
        let total = self.tests_passed + self.tests_failed;
529
0
        if total == 0 {
530
0
            1.0
531
        } else {
532
0
            self.tests_passed as f64 / total as f64
533
        }
534
0
    }
535
}
536
537
/// Anomaly detected during stress testing
538
#[derive(Debug, Clone)]
539
pub struct StressAnomaly {
540
    /// Cycle where anomaly was detected
541
    pub cycle: u32,
542
    /// Type of anomaly
543
    pub kind: StressAnomalyKind,
544
    /// Description
545
    pub description: String,
546
}
547
548
/// Types of stress test anomalies
549
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
550
pub enum StressAnomalyKind {
551
    /// Operation too slow
552
    SlowOperation,
553
    /// High memory usage
554
    HighMemory,
555
    /// Test failure
556
    TestFailure,
557
    /// Timing spike
558
    TimingSpike,
559
    /// Non-deterministic output
560
    NonDeterministic,
561
}
562
563
/// Re-export SimRng from simular for deterministic testing
564
#[cfg(test)]
565
pub use simular::engine::rng::SimRng;
566
567
// =============================================================================
568
// BACKEND TOLERANCE (Poka-Yoke: Type-safe tolerance configuration)
569
// =============================================================================
570
571
/// Backend-specific tolerance configuration
572
///
573
/// Implements Poka-Yoke (mistake-proofing) by providing compile-time
574
/// guarantees for correct tolerance values per backend type.
575
#[derive(Debug, Clone, Copy, PartialEq)]
576
pub struct BackendTolerance {
577
    /// Scalar vs SIMD tolerance (should be exact: 0.0)
578
    pub scalar_vs_simd: f32,
579
    /// SIMD vs GPU tolerance (IEEE 754: 1e-5)
580
    pub simd_vs_gpu: f32,
581
    /// GPU vs GPU tolerance (same precision: 1e-6)
582
    pub gpu_vs_gpu: f32,
583
}
584
585
impl Default for BackendTolerance {
586
0
    fn default() -> Self {
587
0
        Self {
588
0
            scalar_vs_simd: 0.0,
589
0
            simd_vs_gpu: 1e-5,
590
0
            gpu_vs_gpu: 1e-6,
591
0
        }
592
0
    }
593
}
594
595
impl BackendTolerance {
596
    /// Strict tolerance for exact comparisons
597
    #[must_use]
598
0
    pub const fn strict() -> Self {
599
0
        Self {
600
0
            scalar_vs_simd: 0.0,
601
0
            simd_vs_gpu: 0.0,
602
0
            gpu_vs_gpu: 0.0,
603
0
        }
604
0
    }
605
606
    /// Relaxed tolerance for approximate comparisons
607
    #[must_use]
608
0
    pub const fn relaxed() -> Self {
609
0
        Self {
610
0
            scalar_vs_simd: 1e-6,
611
0
            simd_vs_gpu: 1e-4,
612
0
            gpu_vs_gpu: 1e-5,
613
0
        }
614
0
    }
615
616
    /// Get tolerance for comparing two backends
617
    #[must_use]
618
0
    pub fn for_backends(&self, a: Backend, b: Backend) -> f32 {
619
0
        match (a, b) {
620
0
            (Backend::Scalar, Backend::Scalar) => 0.0,
621
            (Backend::Scalar, Backend::SSE2 | Backend::AVX | Backend::AVX2 | Backend::AVX512)
622
            | (Backend::SSE2 | Backend::AVX | Backend::AVX2 | Backend::AVX512, Backend::Scalar) => {
623
0
                self.scalar_vs_simd
624
            }
625
0
            (Backend::GPU, Backend::GPU) => self.gpu_vs_gpu,
626
0
            (Backend::GPU, _) | (_, Backend::GPU) => self.simd_vs_gpu,
627
0
            _ => self.scalar_vs_simd, // SIMD vs SIMD
628
        }
629
0
    }
630
}
631
632
// =============================================================================
633
// BACKEND SELECTOR (Poka-Yoke: Type-safe backend selection)
634
// =============================================================================
635
636
/// Poka-Yoke: Type-safe backend selection
637
///
638
/// Provides compile-time and runtime guarantees for correct backend selection
639
/// based on input size and operation type.
640
#[derive(Debug, Clone)]
641
pub struct BackendSelector {
642
    /// Minimum size for GPU offload (default: 100,000)
643
    gpu_threshold: usize,
644
    /// Minimum size for parallel execution (default: 1,000)
645
    parallel_threshold: usize,
646
}
647
648
impl Default for BackendSelector {
649
0
    fn default() -> Self {
650
0
        Self {
651
0
            gpu_threshold: 100_000,
652
0
            parallel_threshold: 1_000,
653
0
        }
654
0
    }
655
}
656
657
impl BackendSelector {
658
    /// Create a new backend selector with custom thresholds
659
    #[must_use]
660
0
    pub const fn new(gpu_threshold: usize, parallel_threshold: usize) -> Self {
661
0
        Self {
662
0
            gpu_threshold,
663
0
            parallel_threshold,
664
0
        }
665
0
    }
666
667
    /// Get the GPU threshold
668
    #[must_use]
669
0
    pub const fn gpu_threshold(&self) -> usize {
670
0
        self.gpu_threshold
671
0
    }
672
673
    /// Get the parallel threshold
674
    #[must_use]
675
0
    pub const fn parallel_threshold(&self) -> usize {
676
0
        self.parallel_threshold
677
0
    }
678
679
    /// Select backend based on input size
680
    ///
681
    /// # Decision Logic (TRUENO-SPEC-012)
682
    ///
683
    /// - N < 1,000: Pure SIMD (no parallelization overhead)
684
    /// - 1,000 <= N < 100,000: SIMD + Parallel (Rayon)
685
    /// - N >= 100,000: GPU (if available), else SIMD + Parallel
686
    #[must_use]
687
0
    pub fn select_for_size(&self, size: usize, gpu_available: bool) -> BackendCategory {
688
0
        if size < self.parallel_threshold {
689
0
            BackendCategory::SimdOnly
690
0
        } else if size < self.gpu_threshold {
691
0
            BackendCategory::SimdParallel
692
0
        } else if gpu_available {
693
0
            BackendCategory::Gpu
694
        } else {
695
0
            BackendCategory::SimdParallel // Graceful fallback
696
        }
697
0
    }
698
699
    /// Check if size is at GPU threshold boundary (for testing)
700
    #[must_use]
701
0
    pub fn is_at_gpu_boundary(&self, size: usize) -> bool {
702
0
        size == self.gpu_threshold || size == self.gpu_threshold - 1
703
0
    }
704
705
    /// Check if size is at parallel threshold boundary (for testing)
706
    #[must_use]
707
0
    pub fn is_at_parallel_boundary(&self, size: usize) -> bool {
708
0
        size == self.parallel_threshold || size == self.parallel_threshold - 1
709
0
    }
710
}
711
712
/// Backend category for selection result
713
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
714
pub enum BackendCategory {
715
    /// Pure SIMD (N < 1,000)
716
    SimdOnly,
717
    /// SIMD with parallel execution (1,000 <= N < 100,000)
718
    SimdParallel,
719
    /// GPU compute (N >= 100,000)
720
    Gpu,
721
}
722
723
// =============================================================================
724
// JIDOKA GUARD (Built-in Quality: Stop on Defect)
725
// =============================================================================
726
727
/// Jidoka condition that triggers stop
728
#[derive(Debug, Clone, PartialEq)]
729
pub enum JidokaCondition {
730
    /// NaN detected in output
731
    NanDetected,
732
    /// Infinity detected in output
733
    InfDetected,
734
    /// Cross-backend divergence exceeds tolerance
735
    BackendDivergence {
736
        /// Tolerance threshold
737
        tolerance: f32,
738
    },
739
    /// Performance regression exceeds threshold
740
    PerformanceRegression {
741
        /// Threshold percentage
742
        threshold_pct: f32,
743
    },
744
    /// Determinism failure (same seed, different output)
745
    DeterminismFailure,
746
}
747
748
/// Jidoka action on condition trigger
749
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
750
pub enum JidokaAction {
751
    /// Stop immediately and report
752
    Stop,
753
    /// Log and continue (soft Jidoka)
754
    LogAndContinue,
755
    /// Trigger visual diff report
756
    VisualReport,
757
}
758
759
/// Jidoka error types
760
#[derive(Debug, Clone)]
761
pub enum JidokaError {
762
    /// NaN values detected
763
    NanDetected {
764
        /// Context description
765
        context: String,
766
        /// Indices of NaN values
767
        indices: Vec<usize>,
768
    },
769
    /// Infinity values detected
770
    InfDetected {
771
        /// Context description
772
        context: String,
773
        /// Indices of infinite values
774
        indices: Vec<usize>,
775
    },
776
    /// Backend divergence detected
777
    BackendDivergence {
778
        /// Context description
779
        context: String,
780
        /// Maximum difference found
781
        max_diff: f32,
782
        /// Tolerance threshold
783
        tolerance: f32,
784
    },
785
    /// Performance regression detected
786
    PerformanceRegression {
787
        /// Context description
788
        context: String,
789
        /// Actual regression percentage
790
        regression_pct: f32,
791
        /// Threshold percentage
792
        threshold_pct: f32,
793
    },
794
    /// Determinism failure detected
795
    DeterminismFailure {
796
        /// Context description
797
        context: String,
798
        /// First differing index
799
        first_diff_index: usize,
800
    },
801
}
802
803
impl std::fmt::Display for JidokaError {
804
0
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
805
0
        match self {
806
0
            Self::NanDetected { context, indices } => {
807
0
                write!(
808
0
                    f,
809
0
                    "Jidoka: NaN detected at {context} (indices: {indices:?})"
810
                )
811
            }
812
0
            Self::InfDetected { context, indices } => {
813
0
                write!(
814
0
                    f,
815
0
                    "Jidoka: Infinity detected at {context} (indices: {indices:?})"
816
                )
817
            }
818
            Self::BackendDivergence {
819
0
                context,
820
0
                max_diff,
821
0
                tolerance,
822
            } => {
823
0
                write!(
824
0
                    f,
825
0
                    "Jidoka: Backend divergence at {context} (max_diff: {max_diff}, tolerance: {tolerance})"
826
                )
827
            }
828
            Self::PerformanceRegression {
829
0
                context,
830
0
                regression_pct,
831
0
                threshold_pct,
832
            } => {
833
0
                write!(
834
0
                    f,
835
0
                    "Jidoka: Performance regression at {context} ({regression_pct:.2}% > {threshold_pct:.2}%)"
836
                )
837
            }
838
            Self::DeterminismFailure {
839
0
                context,
840
0
                first_diff_index,
841
            } => {
842
0
                write!(
843
0
                    f,
844
0
                    "Jidoka: Determinism failure at {context} (first diff at index {first_diff_index})"
845
                )
846
            }
847
        }
848
0
    }
849
}
850
851
impl std::error::Error for JidokaError {}
852
853
/// Jidoka guard for simulation tests
854
///
855
/// Implements Toyota Production System's Jidoka principle:
856
/// stop production when a defect is detected.
857
#[derive(Debug, Clone)]
858
pub struct JidokaGuard {
859
    /// Condition that triggers stop
860
    pub condition: JidokaCondition,
861
    /// Action to take on trigger
862
    pub action: JidokaAction,
863
    /// Context for debugging
864
    pub context: String,
865
}
866
867
impl JidokaGuard {
868
    /// Create a new Jidoka guard
869
    #[must_use]
870
0
    pub fn new(
871
0
        condition: JidokaCondition,
872
0
        action: JidokaAction,
873
0
        context: impl Into<String>,
874
0
    ) -> Self {
875
0
        Self {
876
0
            condition,
877
0
            action,
878
0
            context: context.into(),
879
0
        }
880
0
    }
881
882
    /// Create a NaN detection guard
883
    #[must_use]
884
0
    pub fn nan_guard(context: impl Into<String>) -> Self {
885
0
        Self::new(JidokaCondition::NanDetected, JidokaAction::Stop, context)
886
0
    }
887
888
    /// Create an infinity detection guard
889
    #[must_use]
890
0
    pub fn inf_guard(context: impl Into<String>) -> Self {
891
0
        Self::new(JidokaCondition::InfDetected, JidokaAction::Stop, context)
892
0
    }
893
894
    /// Create a backend divergence guard
895
    #[must_use]
896
0
    pub fn divergence_guard(tolerance: f32, context: impl Into<String>) -> Self {
897
0
        Self::new(
898
0
            JidokaCondition::BackendDivergence { tolerance },
899
0
            JidokaAction::Stop,
900
0
            context,
901
        )
902
0
    }
903
904
    /// Check output for NaN/Inf and return error if found
905
    ///
906
    /// # Errors
907
    ///
908
    /// Returns `JidokaError` if the condition is triggered
909
0
    pub fn check_output(&self, output: &[f32]) -> Result<(), JidokaError> {
910
0
        match &self.condition {
911
            JidokaCondition::NanDetected => {
912
0
                let nan_indices: Vec<usize> = output
913
0
                    .iter()
914
0
                    .enumerate()
915
0
                    .filter(|(_, x)| x.is_nan())
916
0
                    .map(|(i, _)| i)
917
0
                    .collect();
918
919
0
                if !nan_indices.is_empty() {
920
0
                    return Err(JidokaError::NanDetected {
921
0
                        context: self.context.clone(),
922
0
                        indices: nan_indices,
923
0
                    });
924
0
                }
925
            }
926
            JidokaCondition::InfDetected => {
927
0
                let inf_indices: Vec<usize> = output
928
0
                    .iter()
929
0
                    .enumerate()
930
0
                    .filter(|(_, x)| x.is_infinite())
931
0
                    .map(|(i, _)| i)
932
0
                    .collect();
933
934
0
                if !inf_indices.is_empty() {
935
0
                    return Err(JidokaError::InfDetected {
936
0
                        context: self.context.clone(),
937
0
                        indices: inf_indices,
938
0
                    });
939
0
                }
940
            }
941
0
            _ => {} // Other conditions handled by compare methods
942
        }
943
0
        Ok(())
944
0
    }
945
946
    /// Compare two outputs for backend divergence
947
    ///
948
    /// # Errors
949
    ///
950
    /// Returns `JidokaError` if divergence exceeds tolerance
951
0
    pub fn check_divergence(&self, a: &[f32], b: &[f32]) -> Result<(), JidokaError> {
952
0
        if let JidokaCondition::BackendDivergence { tolerance } = &self.condition {
953
0
            let max_diff = a
954
0
                .iter()
955
0
                .zip(b.iter())
956
0
                .map(|(x, y)| (x - y).abs())
957
0
                .fold(0.0_f32, f32::max);
958
959
0
            if max_diff > *tolerance {
960
0
                return Err(JidokaError::BackendDivergence {
961
0
                    context: self.context.clone(),
962
0
                    max_diff,
963
0
                    tolerance: *tolerance,
964
0
                });
965
0
            }
966
0
        }
967
0
        Ok(())
968
0
    }
969
970
    /// Check for determinism (same inputs should produce same outputs)
971
    ///
972
    /// # Errors
973
    ///
974
    /// Returns `JidokaError` if outputs differ
975
0
    pub fn check_determinism(&self, a: &[f32], b: &[f32]) -> Result<(), JidokaError> {
976
0
        if let JidokaCondition::DeterminismFailure = &self.condition {
977
0
            for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
978
                // Use bitwise comparison for exact equality
979
0
                if x.to_bits() != y.to_bits() {
980
0
                    return Err(JidokaError::DeterminismFailure {
981
0
                        context: self.context.clone(),
982
0
                        first_diff_index: i,
983
0
                    });
984
0
                }
985
            }
986
0
        }
987
0
        Ok(())
988
0
    }
989
}
990
991
// =============================================================================
992
// HEIJUNKA SCHEDULER (Leveled Testing)
993
// =============================================================================
994
995
/// Simulation test configuration
996
#[derive(Debug, Clone)]
997
pub struct SimulationTest {
998
    /// Backend to test
999
    pub backend: Backend,
1000
    /// Input size
1001
    pub input_size: usize,
1002
    /// Test cycle number
1003
    pub cycle: u32,
1004
    /// Seed for deterministic RNG
1005
    pub seed: u64,
1006
}
1007
1008
/// Heijunka: Balanced test distribution across backends and sizes
1009
///
1010
/// Implements Toyota Production System's Heijunka principle:
1011
/// level the workload to reduce waste and variability.
1012
#[derive(Debug)]
1013
pub struct HeijunkaScheduler {
1014
    /// Test queue balanced across backends
1015
    queue: VecDeque<SimulationTest>,
1016
    /// Backends to cycle through
1017
    backends: Vec<Backend>,
1018
}
1019
1020
impl HeijunkaScheduler {
1021
    /// Create a leveled test schedule
1022
    #[must_use]
1023
0
    pub fn new(
1024
0
        backends: Vec<Backend>,
1025
0
        input_sizes: Vec<usize>,
1026
0
        cycles_per_backend: u32,
1027
0
        master_seed: u64,
1028
0
    ) -> Self {
1029
0
        let mut queue = VecDeque::new();
1030
1031
        // Interleave tests across backends (leveling)
1032
0
        for size in &input_sizes {
1033
0
            for backend in &backends {
1034
0
                for cycle in 0..cycles_per_backend {
1035
0
                    let seed = compute_seed(*backend, *size, cycle, master_seed);
1036
0
                    queue.push_back(SimulationTest {
1037
0
                        backend: *backend,
1038
0
                        input_size: *size,
1039
0
                        cycle,
1040
0
                        seed,
1041
0
                    });
1042
0
                }
1043
            }
1044
        }
1045
1046
0
        Self {
1047
0
            queue,
1048
0
            backends: backends.clone(),
1049
0
        }
1050
0
    }
1051
1052
    /// Get the next test from the queue
1053
0
    pub fn next_test(&mut self) -> Option<SimulationTest> {
1054
0
        self.queue.pop_front()
1055
0
    }
1056
1057
    /// Get remaining test count
1058
    #[must_use]
1059
0
    pub fn remaining(&self) -> usize {
1060
0
        self.queue.len()
1061
0
    }
1062
1063
    /// Get backends being tested
1064
    #[must_use]
1065
0
    pub fn backends(&self) -> &[Backend] {
1066
0
        &self.backends
1067
0
    }
1068
1069
    /// Check if schedule is empty
1070
    #[must_use]
1071
0
    pub fn is_empty(&self) -> bool {
1072
0
        self.queue.is_empty()
1073
0
    }
1074
}
1075
1076
/// Compute deterministic seed for a test configuration
1077
0
fn compute_seed(backend: Backend, size: usize, cycle: u32, master_seed: u64) -> u64 {
1078
0
    let backend_bits = backend as u64;
1079
0
    let size_bits = size as u64;
1080
0
    let cycle_bits = u64::from(cycle);
1081
1082
0
    master_seed
1083
0
        .wrapping_add(backend_bits.wrapping_mul(0x9E37_79B9_7F4A_7C15))
1084
0
        .wrapping_add(size_bits.wrapping_mul(0x6A09_E667_BB67_AE85))
1085
0
        .wrapping_add(cycle_bits.wrapping_mul(0x3C6E_F372_FE94_F82B))
1086
0
}
1087
1088
// =============================================================================
1089
// SIMULATION TEST CONFIG (Builder Pattern)
1090
// =============================================================================
1091
1092
/// Simulation test configuration builder
1093
#[derive(Debug, Clone)]
1094
pub struct SimTestConfigBuilder<S> {
1095
    seed: u64,
1096
    tolerance: BackendTolerance,
1097
    backends: Vec<Backend>,
1098
    input_sizes: Vec<usize>,
1099
    cycles: u32,
1100
    _state: PhantomData<S>,
1101
}
1102
1103
/// Builder state: seed not set
1104
pub struct NeedsSeed;
1105
/// Builder state: ready to build
1106
pub struct Ready;
1107
1108
impl Default for SimTestConfigBuilder<NeedsSeed> {
1109
0
    fn default() -> Self {
1110
0
        Self::new()
1111
0
    }
1112
}
1113
1114
impl SimTestConfigBuilder<NeedsSeed> {
1115
    /// Create a new config builder
1116
    #[must_use]
1117
0
    pub fn new() -> Self {
1118
0
        Self {
1119
0
            seed: 0,
1120
0
            tolerance: BackendTolerance::default(),
1121
0
            backends: vec![Backend::Scalar, Backend::AVX2],
1122
0
            input_sizes: vec![100, 1_000, 10_000, 100_000],
1123
0
            cycles: 10,
1124
0
            _state: PhantomData,
1125
0
        }
1126
0
    }
1127
1128
    /// Set the master seed (required)
1129
    #[must_use]
1130
0
    pub fn seed(self, seed: u64) -> SimTestConfigBuilder<Ready> {
1131
0
        SimTestConfigBuilder {
1132
0
            seed,
1133
0
            tolerance: self.tolerance,
1134
0
            backends: self.backends,
1135
0
            input_sizes: self.input_sizes,
1136
0
            cycles: self.cycles,
1137
0
            _state: PhantomData,
1138
0
        }
1139
0
    }
1140
}
1141
1142
impl SimTestConfigBuilder<Ready> {
1143
    /// Set tolerance configuration
1144
    #[must_use]
1145
0
    pub fn tolerance(mut self, tolerance: BackendTolerance) -> Self {
1146
0
        self.tolerance = tolerance;
1147
0
        self
1148
0
    }
1149
1150
    /// Set backends to test
1151
    #[must_use]
1152
0
    pub fn backends(mut self, backends: Vec<Backend>) -> Self {
1153
0
        self.backends = backends;
1154
0
        self
1155
0
    }
1156
1157
    /// Set input sizes to test
1158
    #[must_use]
1159
0
    pub fn input_sizes(mut self, sizes: Vec<usize>) -> Self {
1160
0
        self.input_sizes = sizes;
1161
0
        self
1162
0
    }
1163
1164
    /// Set number of test cycles
1165
    #[must_use]
1166
0
    pub fn cycles(mut self, cycles: u32) -> Self {
1167
0
        self.cycles = cycles;
1168
0
        self
1169
0
    }
1170
1171
    /// Build the configuration
1172
    #[must_use]
1173
0
    pub fn build(self) -> SimTestConfig {
1174
0
        SimTestConfig {
1175
0
            seed: self.seed,
1176
0
            tolerance: self.tolerance,
1177
0
            backends: self.backends,
1178
0
            input_sizes: self.input_sizes,
1179
0
            cycles: self.cycles,
1180
0
        }
1181
0
    }
1182
}
1183
1184
/// Simulation test configuration
1185
#[derive(Debug, Clone)]
1186
pub struct SimTestConfig {
1187
    /// Master seed for deterministic RNG
1188
    pub seed: u64,
1189
    /// Backend tolerance configuration
1190
    pub tolerance: BackendTolerance,
1191
    /// Backends to test
1192
    pub backends: Vec<Backend>,
1193
    /// Input sizes to test
1194
    pub input_sizes: Vec<usize>,
1195
    /// Number of test cycles
1196
    pub cycles: u32,
1197
}
1198
1199
impl SimTestConfig {
1200
    /// Create a config builder
1201
    #[must_use]
1202
0
    pub fn builder() -> SimTestConfigBuilder<NeedsSeed> {
1203
0
        SimTestConfigBuilder::new()
1204
0
    }
1205
1206
    /// Create a Heijunka scheduler from this config
1207
    #[must_use]
1208
0
    pub fn create_scheduler(&self) -> HeijunkaScheduler {
1209
0
        HeijunkaScheduler::new(
1210
0
            self.backends.clone(),
1211
0
            self.input_sizes.clone(),
1212
0
            self.cycles,
1213
0
            self.seed,
1214
        )
1215
0
    }
1216
}
1217
1218
// =============================================================================
1219
// TESTS (EXTREME TDD)
1220
// =============================================================================
1221
1222
#[cfg(test)]
1223
mod tests {
1224
    use super::*;
1225
1226
    // =========================================================================
1227
    // SimRng Integration Tests (Phase 1, Task 1)
1228
    // =========================================================================
1229
1230
    #[test]
1231
    fn test_simrng_reproducibility() {
1232
        // Falsifiable claim B-016, B-017
1233
        let mut rng1 = SimRng::new(42);
1234
        let mut rng2 = SimRng::new(42);
1235
1236
        let seq1: Vec<f64> = (0..100).map(|_| rng1.gen_f64()).collect();
1237
        let seq2: Vec<f64> = (0..100).map(|_| rng2.gen_f64()).collect();
1238
1239
        assert_eq!(seq1, seq2, "Same seed must produce identical sequences");
1240
    }
1241
1242
    #[test]
1243
    fn test_simrng_different_seeds() {
1244
        // Falsifiable claim B-018
1245
        let mut rng1 = SimRng::new(42);
1246
        let mut rng2 = SimRng::new(43);
1247
1248
        let seq1: Vec<f64> = (0..100).map(|_| rng1.gen_f64()).collect();
1249
        let seq2: Vec<f64> = (0..100).map(|_| rng2.gen_f64()).collect();
1250
1251
        assert_ne!(
1252
            seq1, seq2,
1253
            "Different seeds must produce different sequences"
1254
        );
1255
    }
1256
1257
    #[test]
1258
    fn test_simrng_partitioning() {
1259
        // Falsifiable claim B-019
1260
        let mut rng = SimRng::new(42);
1261
        let partitions = rng.partition(4);
1262
1263
        assert_eq!(partitions.len(), 4);
1264
1265
        // Each partition should be independent
1266
        let mut seqs: Vec<Vec<f64>> = Vec::new();
1267
        for mut p in partitions {
1268
            seqs.push((0..10).map(|_| p.gen_f64()).collect());
1269
        }
1270
1271
        for i in 0..seqs.len() {
1272
            for j in (i + 1)..seqs.len() {
1273
                assert_ne!(seqs[i], seqs[j], "Partitions must be independent");
1274
            }
1275
        }
1276
    }
1277
1278
    #[test]
1279
    fn test_simrng_gen_f32_for_trueno() {
1280
        // Generate f32 test data using SimRng
1281
        let mut rng = SimRng::new(42);
1282
1283
        let test_data: Vec<f32> = (0..1000).map(|_| rng.gen_f64() as f32).collect();
1284
1285
        // Verify all values are in valid range
1286
        for v in &test_data {
1287
            assert!(v.is_finite(), "Generated value should be finite");
1288
            assert!(*v >= 0.0 && *v < 1.0, "Value should be in [0, 1)");
1289
        }
1290
    }
1291
1292
    // =========================================================================
1293
    // BackendSelector Tests (Phase 1, Task 2)
1294
    // =========================================================================
1295
1296
    #[test]
1297
    fn test_backend_selector_default_thresholds() {
1298
        // Falsifiable claim A-005, A-006
1299
        let selector = BackendSelector::default();
1300
1301
        assert_eq!(selector.gpu_threshold(), 100_000);
1302
        assert_eq!(selector.parallel_threshold(), 1_000);
1303
    }
1304
1305
    #[test]
1306
    fn test_backend_selector_simd_only() {
1307
        // N < 1,000 should use SIMD only
1308
        let selector = BackendSelector::default();
1309
1310
        assert_eq!(
1311
            selector.select_for_size(100, false),
1312
            BackendCategory::SimdOnly
1313
        );
1314
        assert_eq!(
1315
            selector.select_for_size(999, false),
1316
            BackendCategory::SimdOnly
1317
        );
1318
        assert_eq!(
1319
            selector.select_for_size(999, true),
1320
            BackendCategory::SimdOnly
1321
        );
1322
    }
1323
1324
    #[test]
1325
    fn test_backend_selector_simd_parallel() {
1326
        // 1,000 <= N < 100,000 should use SIMD + Parallel
1327
        let selector = BackendSelector::default();
1328
1329
        assert_eq!(
1330
            selector.select_for_size(1_000, false),
1331
            BackendCategory::SimdParallel
1332
        );
1333
        assert_eq!(
1334
            selector.select_for_size(50_000, false),
1335
            BackendCategory::SimdParallel
1336
        );
1337
        assert_eq!(
1338
            selector.select_for_size(99_999, false),
1339
            BackendCategory::SimdParallel
1340
        );
1341
    }
1342
1343
    #[test]
1344
    fn test_backend_selector_gpu() {
1345
        // N >= 100,000 should use GPU (if available)
1346
        let selector = BackendSelector::default();
1347
1348
        assert_eq!(
1349
            selector.select_for_size(100_000, true),
1350
            BackendCategory::Gpu
1351
        );
1352
        assert_eq!(
1353
            selector.select_for_size(1_000_000, true),
1354
            BackendCategory::Gpu
1355
        );
1356
    }
1357
1358
    #[test]
1359
    fn test_backend_selector_gpu_fallback() {
1360
        // N >= 100,000 without GPU should fallback to SIMD + Parallel
1361
        let selector = BackendSelector::default();
1362
1363
        assert_eq!(
1364
            selector.select_for_size(100_000, false),
1365
            BackendCategory::SimdParallel
1366
        );
1367
    }
1368
1369
    #[test]
1370
    fn test_backend_selector_boundary() {
1371
        // Falsifiable claim A-005
1372
        let selector = BackendSelector::default();
1373
1374
        // At GPU threshold boundary
1375
        assert!(selector.is_at_gpu_boundary(100_000));
1376
        assert!(selector.is_at_gpu_boundary(99_999));
1377
        assert!(!selector.is_at_gpu_boundary(99_998));
1378
1379
        // At parallel threshold boundary
1380
        assert!(selector.is_at_parallel_boundary(1_000));
1381
        assert!(selector.is_at_parallel_boundary(999));
1382
        assert!(!selector.is_at_parallel_boundary(998));
1383
    }
1384
1385
    // =========================================================================
1386
    // BackendTolerance Tests (Phase 1, Task 2)
1387
    // =========================================================================
1388
1389
    #[test]
1390
    fn test_backend_tolerance_default() {
1391
        let tolerance = BackendTolerance::default();
1392
1393
        assert_eq!(tolerance.scalar_vs_simd, 0.0);
1394
        assert!((tolerance.simd_vs_gpu - 1e-5).abs() < 1e-10);
1395
        assert!((tolerance.gpu_vs_gpu - 1e-6).abs() < 1e-10);
1396
    }
1397
1398
    #[test]
1399
    fn test_backend_tolerance_strict() {
1400
        let tolerance = BackendTolerance::strict();
1401
1402
        assert_eq!(tolerance.scalar_vs_simd, 0.0);
1403
        assert_eq!(tolerance.simd_vs_gpu, 0.0);
1404
        assert_eq!(tolerance.gpu_vs_gpu, 0.0);
1405
    }
1406
1407
    #[test]
1408
    fn test_backend_tolerance_for_backends() {
1409
        // Falsifiable claim A-002, A-003, A-004
1410
        let tolerance = BackendTolerance::default();
1411
1412
        // Scalar vs Scalar
1413
        assert_eq!(
1414
            tolerance.for_backends(Backend::Scalar, Backend::Scalar),
1415
            0.0
1416
        );
1417
1418
        // Scalar vs SIMD (should be exact)
1419
        assert_eq!(tolerance.for_backends(Backend::Scalar, Backend::AVX2), 0.0);
1420
1421
        // GPU vs GPU
1422
        assert_eq!(
1423
            tolerance.for_backends(Backend::GPU, Backend::GPU),
1424
            tolerance.gpu_vs_gpu
1425
        );
1426
1427
        // SIMD vs GPU
1428
        assert_eq!(
1429
            tolerance.for_backends(Backend::AVX2, Backend::GPU),
1430
            tolerance.simd_vs_gpu
1431
        );
1432
    }
1433
1434
    // =========================================================================
1435
    // JidokaGuard Tests (Phase 1, Task 3)
1436
    // =========================================================================
1437
1438
    #[test]
1439
    fn test_jidoka_nan_detection() {
1440
        // Falsifiable claim B-027
1441
        let guard = JidokaGuard::nan_guard("test_operation");
1442
        let output_with_nan = vec![1.0, 2.0, f32::NAN, 4.0];
1443
1444
        let result = guard.check_output(&output_with_nan);
1445
        assert!(result.is_err());
1446
1447
        if let Err(JidokaError::NanDetected { indices, .. }) = result {
1448
            assert_eq!(indices, vec![2]);
1449
        } else {
1450
            panic!("Expected NanDetected error");
1451
        }
1452
    }
1453
1454
    #[test]
1455
    fn test_jidoka_nan_no_false_positive() {
1456
        let guard = JidokaGuard::nan_guard("test_operation");
1457
        let clean_output = vec![1.0, 2.0, 3.0, 4.0];
1458
1459
        let result = guard.check_output(&clean_output);
1460
        assert!(result.is_ok());
1461
    }
1462
1463
    #[test]
1464
    fn test_jidoka_inf_detection() {
1465
        // Falsifiable claim B-028
1466
        let guard = JidokaGuard::inf_guard("test_operation");
1467
        let output_with_inf = vec![1.0, f32::INFINITY, 3.0, f32::NEG_INFINITY];
1468
1469
        let result = guard.check_output(&output_with_inf);
1470
        assert!(result.is_err());
1471
1472
        if let Err(JidokaError::InfDetected { indices, .. }) = result {
1473
            assert_eq!(indices, vec![1, 3]);
1474
        } else {
1475
            panic!("Expected InfDetected error");
1476
        }
1477
    }
1478
1479
    #[test]
1480
    fn test_jidoka_divergence_detection() {
1481
        // Falsifiable claim A-004
1482
        let guard = JidokaGuard::divergence_guard(1e-5, "cross_backend");
1483
        let a = vec![1.0, 2.0, 3.0, 4.0];
1484
        let b = vec![1.0, 2.0, 3.1, 4.0]; // 0.1 diff at index 2
1485
1486
        let result = guard.check_divergence(&a, &b);
1487
        assert!(result.is_err());
1488
1489
        if let Err(JidokaError::BackendDivergence { max_diff, .. }) = result {
1490
            assert!((max_diff - 0.1).abs() < 1e-6);
1491
        } else {
1492
            panic!("Expected BackendDivergence error");
1493
        }
1494
    }
1495
1496
    #[test]
1497
    fn test_jidoka_divergence_within_tolerance() {
1498
        let guard = JidokaGuard::divergence_guard(1e-5, "cross_backend");
1499
        let a = vec![1.0, 2.0, 3.0, 4.0];
1500
        let b = vec![1.0, 2.0, 3.0 + 1e-7, 4.0]; // Within tolerance
1501
1502
        let result = guard.check_divergence(&a, &b);
1503
        assert!(result.is_ok());
1504
    }
1505
1506
    #[test]
1507
    fn test_jidoka_determinism_check() {
1508
        // Falsifiable claim B-017
1509
        let guard = JidokaGuard::new(
1510
            JidokaCondition::DeterminismFailure,
1511
            JidokaAction::Stop,
1512
            "determinism_test",
1513
        );
1514
1515
        let a = vec![1.0, 2.0, 3.0, 4.0];
1516
        let b = vec![1.0, 2.0, 3.0, 4.0];
1517
1518
        let result = guard.check_determinism(&a, &b);
1519
        assert!(result.is_ok());
1520
    }
1521
1522
    #[test]
1523
    fn test_jidoka_determinism_failure() {
1524
        let guard = JidokaGuard::new(
1525
            JidokaCondition::DeterminismFailure,
1526
            JidokaAction::Stop,
1527
            "determinism_test",
1528
        );
1529
1530
        let a: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0];
1531
        let b: Vec<f32> = vec![1.0, 2.0, 3.000_001, 4.0]; // Different bit pattern
1532
1533
        // Verify they actually have different bits
1534
        assert_ne!(a[2].to_bits(), b[2].to_bits(), "Test values must differ");
1535
1536
        let result = guard.check_determinism(&a, &b);
1537
        assert!(result.is_err());
1538
1539
        if let Err(JidokaError::DeterminismFailure {
1540
            first_diff_index, ..
1541
        }) = result
1542
        {
1543
            assert_eq!(first_diff_index, 2);
1544
        } else {
1545
            panic!("Expected DeterminismFailure error");
1546
        }
1547
    }
1548
1549
    #[test]
1550
    fn test_jidoka_error_display() {
1551
        let err = JidokaError::NanDetected {
1552
            context: "test".to_string(),
1553
            indices: vec![0, 2],
1554
        };
1555
        let display = format!("{err}");
1556
        assert!(display.contains("NaN"));
1557
        assert!(display.contains("test"));
1558
1559
        let err2 = JidokaError::BackendDivergence {
1560
            context: "cross".to_string(),
1561
            max_diff: 0.01,
1562
            tolerance: 0.001,
1563
        };
1564
        let display2 = format!("{err2}");
1565
        assert!(display2.contains("divergence"));
1566
    }
1567
1568
    // =========================================================================
1569
    // HeijunkaScheduler Tests (Phase 1, Task 4)
1570
    // =========================================================================
1571
1572
    #[test]
1573
    fn test_heijunka_schedule_creation() {
1574
        let backends = vec![Backend::Scalar, Backend::AVX2];
1575
        let sizes = vec![100, 1000];
1576
        let cycles = 2;
1577
1578
        let scheduler = HeijunkaScheduler::new(backends.clone(), sizes, cycles, 42);
1579
1580
        // Should have backends.len() * sizes.len() * cycles tests
1581
        // 2 backends * 2 sizes * 2 cycles = 8 tests
1582
        assert_eq!(scheduler.remaining(), 8);
1583
    }
1584
1585
    #[test]
1586
    fn test_heijunka_deterministic_seeds() {
1587
        // Falsifiable claim B-017
1588
        let backends = vec![Backend::Scalar, Backend::AVX2];
1589
        let sizes = vec![100, 1000];
1590
        let cycles = 2;
1591
1592
        let mut scheduler1 = HeijunkaScheduler::new(backends.clone(), sizes.clone(), cycles, 42);
1593
        let mut scheduler2 = HeijunkaScheduler::new(backends, sizes, cycles, 42);
1594
1595
        // Seeds should be identical for same configuration
1596
        while let (Some(t1), Some(t2)) = (scheduler1.next_test(), scheduler2.next_test()) {
1597
            assert_eq!(t1.seed, t2.seed, "Seeds must be deterministic");
1598
        }
1599
    }
1600
1601
    #[test]
1602
    fn test_heijunka_consumes_all_tests() {
1603
        let backends = vec![Backend::Scalar];
1604
        let sizes = vec![100];
1605
        let cycles = 5;
1606
1607
        let mut scheduler = HeijunkaScheduler::new(backends, sizes, cycles, 42);
1608
1609
        let mut count = 0;
1610
        while scheduler.next_test().is_some() {
1611
            count += 1;
1612
        }
1613
1614
        assert_eq!(count, 5);
1615
        assert!(scheduler.is_empty());
1616
    }
1617
1618
    #[test]
1619
    fn test_heijunka_different_master_seeds() {
1620
        // Falsifiable claim B-018
1621
        let backends = vec![Backend::Scalar];
1622
        let sizes = vec![100];
1623
        let cycles = 1;
1624
1625
        let mut scheduler1 = HeijunkaScheduler::new(backends.clone(), sizes.clone(), cycles, 42);
1626
        let mut scheduler2 = HeijunkaScheduler::new(backends, sizes, cycles, 43);
1627
1628
        let t1 = scheduler1.next_test().unwrap();
1629
        let t2 = scheduler2.next_test().unwrap();
1630
1631
        assert_ne!(
1632
            t1.seed, t2.seed,
1633
            "Different master seeds must produce different test seeds"
1634
        );
1635
    }
1636
1637
    // =========================================================================
1638
    // SimTestConfig Tests
1639
    // =========================================================================
1640
1641
    #[test]
1642
    fn test_sim_test_config_builder() {
1643
        let config = SimTestConfig::builder()
1644
            .seed(42)
1645
            .tolerance(BackendTolerance::strict())
1646
            .backends(vec![Backend::Scalar, Backend::AVX2, Backend::GPU])
1647
            .input_sizes(vec![100, 1000, 10000])
1648
            .cycles(5)
1649
            .build();
1650
1651
        assert_eq!(config.seed, 42);
1652
        assert_eq!(config.backends.len(), 3);
1653
        assert_eq!(config.input_sizes.len(), 3);
1654
        assert_eq!(config.cycles, 5);
1655
    }
1656
1657
    #[test]
1658
    fn test_sim_test_config_creates_scheduler() {
1659
        let config = SimTestConfig::builder()
1660
            .seed(42)
1661
            .backends(vec![Backend::Scalar, Backend::AVX2])
1662
            .input_sizes(vec![100, 1000])
1663
            .cycles(3)
1664
            .build();
1665
1666
        let scheduler = config.create_scheduler();
1667
1668
        // 2 backends * 2 sizes * 3 cycles = 12 tests
1669
        assert_eq!(scheduler.remaining(), 12);
1670
    }
1671
1672
    // =========================================================================
1673
    // Integration Tests
1674
    // =========================================================================
1675
1676
    #[test]
1677
    fn test_full_simulation_workflow() {
1678
        // Create test configuration
1679
        let config = SimTestConfig::builder()
1680
            .seed(42)
1681
            .tolerance(BackendTolerance::default())
1682
            .backends(vec![Backend::Scalar, Backend::AVX2])
1683
            .input_sizes(vec![100])
1684
            .cycles(2)
1685
            .build();
1686
1687
        // Create scheduler
1688
        let mut scheduler = config.create_scheduler();
1689
1690
        // Create Jidoka guards
1691
        let nan_guard = JidokaGuard::nan_guard("simulation_test");
1692
1693
        // Run through all tests
1694
        let mut test_count = 0;
1695
        while let Some(test) = scheduler.next_test() {
1696
            // Generate deterministic test data
1697
            let mut rng = SimRng::new(test.seed);
1698
            let data: Vec<f32> = (0..test.input_size).map(|_| rng.gen_f64() as f32).collect();
1699
1700
            // Check for NaN (should pass with valid data)
1701
            let result = nan_guard.check_output(&data);
1702
            assert!(result.is_ok(), "Generated data should not contain NaN");
1703
1704
            test_count += 1;
1705
        }
1706
1707
        // 2 backends * 1 size * 2 cycles = 4 tests
1708
        assert_eq!(test_count, 4);
1709
    }
1710
1711
    // =========================================================================
1712
    // Edge Cases
1713
    // =========================================================================
1714
1715
    #[test]
1716
    fn test_empty_output_checks() {
1717
        let guard = JidokaGuard::nan_guard("empty_test");
1718
        let result = guard.check_output(&[]);
1719
        assert!(result.is_ok());
1720
    }
1721
1722
    #[test]
1723
    fn test_single_element_checks() {
1724
        let guard = JidokaGuard::nan_guard("single_test");
1725
1726
        assert!(guard.check_output(&[1.0]).is_ok());
1727
        assert!(guard.check_output(&[f32::NAN]).is_err());
1728
    }
1729
1730
    #[test]
1731
    fn test_backend_category_debug() {
1732
        let category = BackendCategory::Gpu;
1733
        let debug = format!("{category:?}");
1734
        assert!(debug.contains("Gpu"));
1735
    }
1736
1737
    #[test]
1738
    fn test_jidoka_condition_clone() {
1739
        let condition = JidokaCondition::BackendDivergence { tolerance: 1e-5 };
1740
        let cloned = condition.clone();
1741
        assert_eq!(condition, cloned);
1742
    }
1743
1744
    #[test]
1745
    fn test_jidoka_action_eq() {
1746
        assert_eq!(JidokaAction::Stop, JidokaAction::Stop);
1747
        assert_ne!(JidokaAction::Stop, JidokaAction::LogAndContinue);
1748
    }
1749
1750
    // =========================================================================
1751
    // Visual Regression Testing (Phase 2)
1752
    // =========================================================================
1753
1754
    #[test]
1755
    fn test_rgb_color_creation() {
1756
        let color = Rgb::new(255, 128, 64);
1757
        assert_eq!(color.r, 255);
1758
        assert_eq!(color.g, 128);
1759
        assert_eq!(color.b, 64);
1760
    }
1761
1762
    #[test]
1763
    fn test_rgb_special_colors() {
1764
        assert_eq!(Rgb::NAN_COLOR, Rgb::new(255, 0, 255));
1765
        assert_eq!(Rgb::INF_COLOR, Rgb::new(255, 255, 255));
1766
        assert_eq!(Rgb::NEG_INF_COLOR, Rgb::new(0, 0, 0));
1767
    }
1768
1769
    #[test]
1770
    fn test_color_palette_viridis() {
1771
        let palette = ColorPalette::viridis();
1772
        assert_eq!(palette.colors.len(), 5);
1773
1774
        // Test interpolation at boundaries
1775
        let at_0 = palette.interpolate(0.0);
1776
        let at_1 = palette.interpolate(1.0);
1777
1778
        // Viridis starts dark purple, ends yellow
1779
        assert_eq!(at_0, Rgb::new(68, 1, 84));
1780
        assert_eq!(at_1, Rgb::new(253, 231, 37));
1781
    }
1782
1783
    #[test]
1784
    fn test_color_palette_grayscale() {
1785
        let palette = ColorPalette::grayscale();
1786
        assert_eq!(palette.colors.len(), 3);
1787
1788
        let at_0 = palette.interpolate(0.0);
1789
        let at_1 = palette.interpolate(1.0);
1790
1791
        assert_eq!(at_0, Rgb::new(0, 0, 0));
1792
        assert_eq!(at_1, Rgb::new(255, 255, 255));
1793
    }
1794
1795
    #[test]
1796
    fn test_color_palette_interpolation_midpoint() {
1797
        let palette = ColorPalette::grayscale();
1798
        let at_mid = palette.interpolate(0.5);
1799
1800
        // Should be close to gray
1801
        assert_eq!(at_mid, Rgb::new(128, 128, 128));
1802
    }
1803
1804
    #[test]
1805
    fn test_color_palette_clamping() {
1806
        let palette = ColorPalette::viridis();
1807
1808
        // Values outside [0, 1] should be clamped
1809
        let at_neg = palette.interpolate(-0.5);
1810
        let at_over = palette.interpolate(1.5);
1811
1812
        assert_eq!(at_neg, palette.interpolate(0.0));
1813
        assert_eq!(at_over, palette.interpolate(1.0));
1814
    }
1815
1816
    #[test]
1817
    fn test_visual_regression_config_default() {
1818
        let config = VisualRegressionConfig::default();
1819
1820
        assert_eq!(config.golden_dir, PathBuf::from("golden"));
1821
        assert_eq!(config.output_dir, PathBuf::from("test_output"));
1822
        assert_eq!(config.max_diff_pct, 0.0);
1823
    }
1824
1825
    #[test]
1826
    fn test_visual_regression_config_builder() {
1827
        let config = VisualRegressionConfig::new("my_golden")
1828
            .with_output_dir("my_output")
1829
            .with_max_diff_pct(1.5)
1830
            .with_palette(ColorPalette::grayscale());
1831
1832
        assert_eq!(config.golden_dir, PathBuf::from("my_golden"));
1833
        assert_eq!(config.output_dir, PathBuf::from("my_output"));
1834
        assert_eq!(config.max_diff_pct, 1.5);
1835
    }
1836
1837
    #[test]
1838
    fn test_pixel_diff_result_percentage() {
1839
        let result = PixelDiffResult {
1840
            different_pixels: 10,
1841
            total_pixels: 100,
1842
            max_diff: 50,
1843
        };
1844
1845
        assert_eq!(result.diff_percentage(), 10.0);
1846
        assert!(!result.matches(5.0));
1847
        assert!(result.matches(10.0));
1848
        assert!(result.matches(15.0));
1849
    }
1850
1851
    #[test]
1852
    fn test_pixel_diff_result_zero_total() {
1853
        let result = PixelDiffResult {
1854
            different_pixels: 0,
1855
            total_pixels: 0,
1856
            max_diff: 0,
1857
        };
1858
1859
        assert_eq!(result.diff_percentage(), 0.0);
1860
    }
1861
1862
    #[test]
1863
    fn test_pixel_diff_result_pass() {
1864
        let result = PixelDiffResult::pass(100);
1865
1866
        assert_eq!(result.different_pixels, 0);
1867
        assert_eq!(result.total_pixels, 100);
1868
        assert_eq!(result.max_diff, 0);
1869
        assert!(result.matches(0.0));
1870
    }
1871
1872
    #[test]
1873
    fn test_buffer_renderer_default() {
1874
        let renderer = BufferRenderer::default();
1875
        assert!(renderer.range.is_none());
1876
    }
1877
1878
    #[test]
1879
    fn test_buffer_renderer_with_range() {
1880
        let renderer = BufferRenderer::new().with_range(0.0, 10.0);
1881
        assert_eq!(renderer.range, Some((0.0, 10.0)));
1882
    }
1883
1884
    #[test]
1885
    fn test_buffer_renderer_with_palette() {
1886
        let renderer = BufferRenderer::new().with_palette(ColorPalette::grayscale());
1887
        assert_eq!(renderer.palette.colors.len(), 3);
1888
    }
1889
1890
    #[test]
1891
    fn test_buffer_renderer_rgba_output() {
1892
        let renderer = BufferRenderer::new();
1893
        let buffer: Vec<f32> = (0..4).map(|i| i as f32 / 3.0).collect();
1894
        let rgba = renderer.render_to_rgba(&buffer, 2, 2);
1895
1896
        // 4 pixels * 4 bytes = 16 bytes
1897
        assert_eq!(rgba.len(), 16);
1898
1899
        // Check alpha channel is always 255
1900
        for i in (3..16).step_by(4) {
1901
            assert_eq!(rgba[i], 255);
1902
        }
1903
    }
1904
1905
    #[test]
1906
    fn test_buffer_renderer_nan_handling() {
1907
        let renderer = BufferRenderer::new();
1908
        let buffer = vec![0.0, f32::NAN, 1.0, 0.5];
1909
        let rgba = renderer.render_to_rgba(&buffer, 2, 2);
1910
1911
        // Second pixel should be NAN_COLOR (magenta: 255, 0, 255)
1912
        assert_eq!(rgba[4], 255); // R
1913
        assert_eq!(rgba[5], 0); // G
1914
        assert_eq!(rgba[6], 255); // B
1915
        assert_eq!(rgba[7], 255); // A
1916
    }
1917
1918
    #[test]
1919
    fn test_buffer_renderer_inf_handling() {
1920
        let renderer = BufferRenderer::new();
1921
        let buffer = vec![f32::INFINITY, f32::NEG_INFINITY, 0.5, 0.5];
1922
        let rgba = renderer.render_to_rgba(&buffer, 2, 2);
1923
1924
        // First pixel: +INF should be white
1925
        assert_eq!(rgba[0], 255);
1926
        assert_eq!(rgba[1], 255);
1927
        assert_eq!(rgba[2], 255);
1928
1929
        // Second pixel: -INF should be black
1930
        assert_eq!(rgba[4], 0);
1931
        assert_eq!(rgba[5], 0);
1932
        assert_eq!(rgba[6], 0);
1933
    }
1934
1935
    #[test]
1936
    fn test_buffer_renderer_compare_identical() {
1937
        let renderer = BufferRenderer::new();
1938
        let buffer: Vec<f32> = (0..16).map(|i| i as f32 / 15.0).collect();
1939
        let rgba = renderer.render_to_rgba(&buffer, 4, 4);
1940
1941
        let result = renderer.compare_rgba(&rgba, &rgba, 0);
1942
        assert_eq!(result.different_pixels, 0);
1943
        assert!(result.matches(0.0));
1944
    }
1945
1946
    #[test]
1947
    fn test_buffer_renderer_compare_different() {
1948
        let renderer = BufferRenderer::new();
1949
        let buffer_a: Vec<f32> = (0..16).map(|i| i as f32 / 15.0).collect();
1950
        let buffer_b: Vec<f32> = (0..16).map(|i| 1.0 - i as f32 / 15.0).collect();
1951
1952
        let rgba_a = renderer.render_to_rgba(&buffer_a, 4, 4);
1953
        let rgba_b = renderer.render_to_rgba(&buffer_b, 4, 4);
1954
1955
        let result = renderer.compare_rgba(&rgba_a, &rgba_b, 0);
1956
        assert!(result.different_pixels > 0);
1957
    }
1958
1959
    #[test]
1960
    fn test_buffer_renderer_compare_with_tolerance() {
1961
        let renderer = BufferRenderer::new();
1962
        let rgba_a = vec![100, 100, 100, 255];
1963
        let rgba_b = vec![105, 102, 98, 255];
1964
1965
        // With tolerance 10, should match
1966
        let result = renderer.compare_rgba(&rgba_a, &rgba_b, 10);
1967
        assert_eq!(result.different_pixels, 0);
1968
1969
        // With tolerance 1, should differ
1970
        let result_strict = renderer.compare_rgba(&rgba_a, &rgba_b, 1);
1971
        assert!(result_strict.different_pixels > 0);
1972
    }
1973
1974
    #[test]
1975
    fn test_golden_baseline_paths() {
1976
        let config = VisualRegressionConfig::new("/test/golden").with_output_dir("/test/output");
1977
        let baseline = GoldenBaseline::new(config);
1978
1979
        assert_eq!(
1980
            baseline.golden_path("relu_4x4"),
1981
            PathBuf::from("/test/golden/relu_4x4.golden")
1982
        );
1983
        assert_eq!(
1984
            baseline.output_path("relu_4x4"),
1985
            PathBuf::from("/test/output/relu_4x4.output")
1986
        );
1987
    }
1988
1989
    #[test]
1990
    fn test_golden_baseline_config_access() {
1991
        let config = VisualRegressionConfig::new("/golden").with_max_diff_pct(2.5);
1992
        let baseline = GoldenBaseline::new(config);
1993
1994
        assert_eq!(baseline.config().max_diff_pct, 2.5);
1995
    }
1996
1997
    // =========================================================================
1998
    // Stress Testing (Phase 3)
1999
    // =========================================================================
2000
2001
    #[test]
2002
    fn test_stress_test_config_default() {
2003
        let config = StressTestConfig::default();
2004
2005
        assert_eq!(config.cycles_per_backend, 100);
2006
        assert_eq!(config.input_sizes.len(), 5);
2007
        assert_eq!(config.backends.len(), 2);
2008
        assert_eq!(config.master_seed, 42);
2009
    }
2010
2011
    #[test]
2012
    fn test_stress_test_config_builder() {
2013
        let config = StressTestConfig::new(123)
2014
            .with_cycles(50)
2015
            .with_input_sizes(vec![100, 1000])
2016
            .with_backends(vec![Backend::Scalar])
2017
            .with_thresholds(StressThresholds::strict());
2018
2019
        assert_eq!(config.master_seed, 123);
2020
        assert_eq!(config.cycles_per_backend, 50);
2021
        assert_eq!(config.input_sizes.len(), 2);
2022
        assert_eq!(config.backends.len(), 1);
2023
    }
2024
2025
    #[test]
2026
    fn test_stress_test_config_total_tests() {
2027
        let config = StressTestConfig::default()
2028
            .with_cycles(10)
2029
            .with_input_sizes(vec![100, 1000, 10000])
2030
            .with_backends(vec![Backend::Scalar, Backend::AVX2]);
2031
2032
        // 2 backends * 3 sizes * 10 cycles = 60 tests
2033
        assert_eq!(config.total_tests(), 60);
2034
    }
2035
2036
    #[test]
2037
    fn test_stress_thresholds_default() {
2038
        let thresholds = StressThresholds::default();
2039
2040
        assert_eq!(thresholds.max_op_time_ms, 1000);
2041
        assert_eq!(thresholds.max_memory_bytes, 256 * 1024 * 1024);
2042
        assert!((thresholds.max_timing_variance - 0.5).abs() < 0.001);
2043
        assert_eq!(thresholds.max_failure_rate, 0.0);
2044
    }
2045
2046
    #[test]
2047
    fn test_stress_thresholds_strict() {
2048
        let thresholds = StressThresholds::strict();
2049
2050
        assert_eq!(thresholds.max_op_time_ms, 100);
2051
        assert_eq!(thresholds.max_memory_bytes, 64 * 1024 * 1024);
2052
        assert!((thresholds.max_timing_variance - 0.2).abs() < 0.001);
2053
    }
2054
2055
    #[test]
2056
    fn test_stress_thresholds_relaxed() {
2057
        let thresholds = StressThresholds::relaxed();
2058
2059
        assert_eq!(thresholds.max_op_time_ms, 5000);
2060
        assert_eq!(thresholds.max_memory_bytes, 512 * 1024 * 1024);
2061
        assert!((thresholds.max_timing_variance - 1.0).abs() < 0.001);
2062
    }
2063
2064
    #[test]
2065
    fn test_stress_result_passed() {
2066
        let result = StressResult {
2067
            backend: Backend::Scalar,
2068
            input_size: 1000,
2069
            cycles_completed: 10,
2070
            tests_passed: 100,
2071
            tests_failed: 0,
2072
            mean_op_time_ms: 50.0,
2073
            max_op_time_ms: 100,
2074
            timing_variance: 0.1,
2075
            anomalies: vec![],
2076
        };
2077
2078
        assert!(result.passed());
2079
        assert_eq!(result.pass_rate(), 1.0);
2080
    }
2081
2082
    #[test]
2083
    fn test_stress_result_failed() {
2084
        let result = StressResult {
2085
            backend: Backend::AVX2,
2086
            input_size: 10000,
2087
            cycles_completed: 10,
2088
            tests_passed: 95,
2089
            tests_failed: 5,
2090
            mean_op_time_ms: 100.0,
2091
            max_op_time_ms: 500,
2092
            timing_variance: 0.3,
2093
            anomalies: vec![],
2094
        };
2095
2096
        assert!(!result.passed()); // Failed because tests_failed > 0
2097
        assert!((result.pass_rate() - 0.95).abs() < 0.001);
2098
    }
2099
2100
    #[test]
2101
    fn test_stress_result_with_anomaly() {
2102
        let result = StressResult {
2103
            backend: Backend::Scalar,
2104
            input_size: 1000,
2105
            cycles_completed: 10,
2106
            tests_passed: 100,
2107
            tests_failed: 0,
2108
            mean_op_time_ms: 50.0,
2109
            max_op_time_ms: 100,
2110
            timing_variance: 0.1,
2111
            anomalies: vec![StressAnomaly {
2112
                cycle: 5,
2113
                kind: StressAnomalyKind::SlowOperation,
2114
                description: "Operation took 200ms".to_string(),
2115
            }],
2116
        };
2117
2118
        assert!(!result.passed()); // Failed because anomalies not empty
2119
    }
2120
2121
    #[test]
2122
    fn test_stress_anomaly_kinds() {
2123
        assert_eq!(
2124
            StressAnomalyKind::SlowOperation,
2125
            StressAnomalyKind::SlowOperation
2126
        );
2127
        assert_ne!(
2128
            StressAnomalyKind::SlowOperation,
2129
            StressAnomalyKind::TestFailure
2130
        );
2131
2132
        // Test all variants exist
2133
        let _slow = StressAnomalyKind::SlowOperation;
2134
        let _mem = StressAnomalyKind::HighMemory;
2135
        let _fail = StressAnomalyKind::TestFailure;
2136
        let _spike = StressAnomalyKind::TimingSpike;
2137
        let _ndet = StressAnomalyKind::NonDeterministic;
2138
    }
2139
2140
    #[test]
2141
    fn test_stress_result_zero_tests() {
2142
        let result = StressResult {
2143
            backend: Backend::Scalar,
2144
            input_size: 0,
2145
            cycles_completed: 0,
2146
            tests_passed: 0,
2147
            tests_failed: 0,
2148
            mean_op_time_ms: 0.0,
2149
            max_op_time_ms: 0,
2150
            timing_variance: 0.0,
2151
            anomalies: vec![],
2152
        };
2153
2154
        // Zero tests should still pass with pass_rate of 1.0
2155
        assert!(result.passed());
2156
        assert_eq!(result.pass_rate(), 1.0);
2157
    }
2158
}
2159
2160
#[cfg(test)]
2161
mod proptests {
2162
    use super::*;
2163
    use proptest::prelude::*;
2164
2165
    proptest! {
2166
        /// Falsifiable claim A-009: Backend selection is deterministic
2167
        #[test]
2168
        fn prop_backend_selection_deterministic(size in 0usize..1_000_000) {
2169
            let selector = BackendSelector::default();
2170
2171
            let result1 = selector.select_for_size(size, true);
2172
            let result2 = selector.select_for_size(size, true);
2173
2174
            prop_assert_eq!(result1, result2);
2175
        }
2176
2177
        /// Falsifiable claim: compute_seed is deterministic
2178
        #[test]
2179
        fn prop_compute_seed_deterministic(
2180
            backend_idx in 0u8..8,
2181
            size in 0usize..1_000_000,
2182
            cycle in 0u32..100,
2183
            master_seed in any::<u64>()
2184
        ) {
2185
            let backend = match backend_idx {
2186
                0 => Backend::Scalar,
2187
                1 => Backend::SSE2,
2188
                2 => Backend::AVX,
2189
                3 => Backend::AVX2,
2190
                4 => Backend::AVX512,
2191
                5 => Backend::NEON,
2192
                6 => Backend::WasmSIMD,
2193
                _ => Backend::GPU,
2194
            };
2195
2196
            let seed1 = compute_seed(backend, size, cycle, master_seed);
2197
            let seed2 = compute_seed(backend, size, cycle, master_seed);
2198
2199
            prop_assert_eq!(seed1, seed2);
2200
        }
2201
2202
        /// Falsifiable claim: NaN detection never misses
2203
        #[test]
2204
        fn prop_nan_detection_complete(values in prop::collection::vec(-1000.0f32..1000.0, 0..100)) {
2205
            let guard = JidokaGuard::nan_guard("test");
2206
2207
            // Clean input should pass
2208
            let result = guard.check_output(&values);
2209
            prop_assert!(result.is_ok());
2210
        }
2211
    }
2212
}