## 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
    }