Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/trueno/src/simulation/stress.rs
Line
Count
Source
1
//! Stress Testing (Heijunka: Leveled Workload Testing)
2
//!
3
//! Provides configurable stress testing infrastructure for validating
4
//! compute operations under various load conditions.
5
6
use crate::Backend;
7
8
/// Stress test configuration for trueno operations
9
#[derive(Debug, Clone)]
10
pub struct StressTestConfig {
11
    /// Number of cycles per backend
12
    pub cycles_per_backend: u32,
13
    /// Input sizes to test (leveled)
14
    pub input_sizes: Vec<usize>,
15
    /// Backends to stress test
16
    pub backends: Vec<Backend>,
17
    /// Performance thresholds
18
    pub thresholds: StressThresholds,
19
    /// Master seed for RNG
20
    pub master_seed: u64,
21
}
22
23
impl Default for StressTestConfig {
24
0
    fn default() -> Self {
25
0
        Self {
26
0
            cycles_per_backend: 100,
27
0
            input_sizes: vec![100, 1_000, 10_000, 100_000, 1_000_000],
28
0
            backends: vec![Backend::Scalar, Backend::AVX2],
29
0
            thresholds: StressThresholds::default(),
30
0
            master_seed: 42,
31
0
        }
32
0
    }
33
}
34
35
impl StressTestConfig {
36
    /// Create new stress test config
37
    #[must_use]
38
0
    pub fn new(master_seed: u64) -> Self {
39
0
        Self {
40
0
            master_seed,
41
0
            ..Default::default()
42
0
        }
43
0
    }
44
45
    /// Set cycles per backend
46
    #[must_use]
47
0
    pub const fn with_cycles(mut self, cycles: u32) -> Self {
48
0
        self.cycles_per_backend = cycles;
49
0
        self
50
0
    }
51
52
    /// Set input sizes
53
    #[must_use]
54
0
    pub fn with_input_sizes(mut self, sizes: Vec<usize>) -> Self {
55
0
        self.input_sizes = sizes;
56
0
        self
57
0
    }
58
59
    /// Set backends to test
60
    #[must_use]
61
0
    pub fn with_backends(mut self, backends: Vec<Backend>) -> Self {
62
0
        self.backends = backends;
63
0
        self
64
0
    }
65
66
    /// Set performance thresholds
67
    #[must_use]
68
0
    pub fn with_thresholds(mut self, thresholds: StressThresholds) -> Self {
69
0
        self.thresholds = thresholds;
70
0
        self
71
0
    }
72
73
    /// Calculate total test count
74
    #[must_use]
75
0
    pub fn total_tests(&self) -> usize {
76
0
        self.backends.len() * self.input_sizes.len() * self.cycles_per_backend as usize
77
0
    }
78
}
79
80
/// Performance thresholds for stress testing
81
#[derive(Debug, Clone)]
82
pub struct StressThresholds {
83
    /// Max time per operation (ms)
84
    pub max_op_time_ms: u64,
85
    /// Max memory per operation (bytes)
86
    pub max_memory_bytes: usize,
87
    /// Max variance in operation times (coefficient of variation)
88
    pub max_timing_variance: f64,
89
    /// Max allowed failure rate (0.0 to 1.0)
90
    pub max_failure_rate: f64,
91
}
92
93
impl Default for StressThresholds {
94
0
    fn default() -> Self {
95
0
        Self {
96
0
            max_op_time_ms: 1000,                // 1s max per op
97
0
            max_memory_bytes: 256 * 1024 * 1024, // 256MB max
98
0
            max_timing_variance: 0.5,            // 50% max variance
99
0
            max_failure_rate: 0.0,               // Zero failures allowed
100
0
        }
101
0
    }
102
}
103
104
impl StressThresholds {
105
    /// Strict thresholds for CI
106
    #[must_use]
107
0
    pub const fn strict() -> Self {
108
0
        Self {
109
0
            max_op_time_ms: 100,
110
0
            max_memory_bytes: 64 * 1024 * 1024,
111
0
            max_timing_variance: 0.2,
112
0
            max_failure_rate: 0.0,
113
0
        }
114
0
    }
115
116
    /// Relaxed thresholds for development
117
    #[must_use]
118
0
    pub const fn relaxed() -> Self {
119
0
        Self {
120
0
            max_op_time_ms: 5000,
121
0
            max_memory_bytes: 512 * 1024 * 1024,
122
0
            max_timing_variance: 1.0,
123
0
            max_failure_rate: 0.01,
124
0
        }
125
0
    }
126
}
127
128
/// Stress test result for a single operation
129
#[derive(Debug, Clone)]
130
pub struct StressResult {
131
    /// Backend used
132
    pub backend: Backend,
133
    /// Input size
134
    pub input_size: usize,
135
    /// Cycles completed
136
    pub cycles_completed: u32,
137
    /// Total tests passed
138
    pub tests_passed: u32,
139
    /// Total tests failed
140
    pub tests_failed: u32,
141
    /// Mean operation time (ms)
142
    pub mean_op_time_ms: f64,
143
    /// Max operation time (ms)
144
    pub max_op_time_ms: u64,
145
    /// Timing variance (coefficient of variation)
146
    pub timing_variance: f64,
147
    /// Detected anomalies
148
    pub anomalies: Vec<StressAnomaly>,
149
}
150
151
impl StressResult {
152
    /// Check if all tests passed
153
    #[must_use]
154
0
    pub fn passed(&self) -> bool {
155
0
        self.tests_failed == 0 && self.anomalies.is_empty()
156
0
    }
157
158
    /// Calculate pass rate
159
    #[must_use]
160
0
    pub fn pass_rate(&self) -> f64 {
161
0
        let total = self.tests_passed + self.tests_failed;
162
0
        if total == 0 {
163
0
            1.0
164
        } else {
165
0
            self.tests_passed as f64 / total as f64
166
        }
167
0
    }
168
}
169
170
/// Anomaly detected during stress testing
171
#[derive(Debug, Clone)]
172
pub struct StressAnomaly {
173
    /// Cycle where anomaly was detected
174
    pub cycle: u32,
175
    /// Type of anomaly
176
    pub kind: StressAnomalyKind,
177
    /// Description
178
    pub description: String,
179
}
180
181
/// Types of stress test anomalies
182
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183
pub enum StressAnomalyKind {
184
    /// Operation too slow
185
    SlowOperation,
186
    /// High memory usage
187
    HighMemory,
188
    /// Test failure
189
    TestFailure,
190
    /// Timing spike
191
    TimingSpike,
192
    /// Non-deterministic output
193
    NonDeterministic,
194
}
195
196
#[cfg(test)]
197
mod tests {
198
    use super::*;
199
200
    #[test]
201
    fn test_stress_test_config_default() {
202
        let config = StressTestConfig::default();
203
204
        assert_eq!(config.cycles_per_backend, 100);
205
        assert_eq!(config.input_sizes.len(), 5);
206
        assert_eq!(config.backends.len(), 2);
207
        assert_eq!(config.master_seed, 42);
208
    }
209
210
    #[test]
211
    fn test_stress_test_config_builder() {
212
        let config = StressTestConfig::new(123)
213
            .with_cycles(50)
214
            .with_input_sizes(vec![100, 1000])
215
            .with_backends(vec![Backend::Scalar])
216
            .with_thresholds(StressThresholds::strict());
217
218
        assert_eq!(config.master_seed, 123);
219
        assert_eq!(config.cycles_per_backend, 50);
220
        assert_eq!(config.input_sizes.len(), 2);
221
        assert_eq!(config.backends.len(), 1);
222
    }
223
224
    #[test]
225
    fn test_stress_test_config_total_tests() {
226
        let config = StressTestConfig::default()
227
            .with_cycles(10)
228
            .with_input_sizes(vec![100, 1000, 10000])
229
            .with_backends(vec![Backend::Scalar, Backend::AVX2]);
230
231
        // 2 backends * 3 sizes * 10 cycles = 60 tests
232
        assert_eq!(config.total_tests(), 60);
233
    }
234
235
    #[test]
236
    fn test_stress_thresholds_default() {
237
        let thresholds = StressThresholds::default();
238
239
        assert_eq!(thresholds.max_op_time_ms, 1000);
240
        assert_eq!(thresholds.max_memory_bytes, 256 * 1024 * 1024);
241
        assert!((thresholds.max_timing_variance - 0.5).abs() < 0.001);
242
        assert_eq!(thresholds.max_failure_rate, 0.0);
243
    }
244
245
    #[test]
246
    fn test_stress_thresholds_strict() {
247
        let thresholds = StressThresholds::strict();
248
249
        assert_eq!(thresholds.max_op_time_ms, 100);
250
        assert_eq!(thresholds.max_memory_bytes, 64 * 1024 * 1024);
251
        assert!((thresholds.max_timing_variance - 0.2).abs() < 0.001);
252
    }
253
254
    #[test]
255
    fn test_stress_thresholds_relaxed() {
256
        let thresholds = StressThresholds::relaxed();
257
258
        assert_eq!(thresholds.max_op_time_ms, 5000);
259
        assert_eq!(thresholds.max_memory_bytes, 512 * 1024 * 1024);
260
        assert!((thresholds.max_timing_variance - 1.0).abs() < 0.001);
261
    }
262
263
    #[test]
264
    fn test_stress_result_passed() {
265
        let result = StressResult {
266
            backend: Backend::Scalar,
267
            input_size: 1000,
268
            cycles_completed: 10,
269
            tests_passed: 100,
270
            tests_failed: 0,
271
            mean_op_time_ms: 50.0,
272
            max_op_time_ms: 100,
273
            timing_variance: 0.1,
274
            anomalies: vec![],
275
        };
276
277
        assert!(result.passed());
278
        assert_eq!(result.pass_rate(), 1.0);
279
    }
280
281
    #[test]
282
    fn test_stress_result_failed() {
283
        let result = StressResult {
284
            backend: Backend::AVX2,
285
            input_size: 10000,
286
            cycles_completed: 10,
287
            tests_passed: 95,
288
            tests_failed: 5,
289
            mean_op_time_ms: 100.0,
290
            max_op_time_ms: 500,
291
            timing_variance: 0.3,
292
            anomalies: vec![],
293
        };
294
295
        assert!(!result.passed()); // Failed because tests_failed > 0
296
        assert!((result.pass_rate() - 0.95).abs() < 0.001);
297
    }
298
299
    #[test]
300
    fn test_stress_result_with_anomaly() {
301
        let result = StressResult {
302
            backend: Backend::Scalar,
303
            input_size: 1000,
304
            cycles_completed: 10,
305
            tests_passed: 100,
306
            tests_failed: 0,
307
            mean_op_time_ms: 50.0,
308
            max_op_time_ms: 100,
309
            timing_variance: 0.1,
310
            anomalies: vec![StressAnomaly {
311
                cycle: 5,
312
                kind: StressAnomalyKind::SlowOperation,
313
                description: "Operation took 200ms".to_string(),
314
            }],
315
        };
316
317
        assert!(!result.passed()); // Failed because anomalies not empty
318
    }
319
320
    #[test]
321
    fn test_stress_anomaly_kinds() {
322
        assert_eq!(
323
            StressAnomalyKind::SlowOperation,
324
            StressAnomalyKind::SlowOperation
325
        );
326
        assert_ne!(
327
            StressAnomalyKind::SlowOperation,
328
            StressAnomalyKind::TestFailure
329
        );
330
331
        // Test all variants exist
332
        let _slow = StressAnomalyKind::SlowOperation;
333
        let _mem = StressAnomalyKind::HighMemory;
334
        let _fail = StressAnomalyKind::TestFailure;
335
        let _spike = StressAnomalyKind::TimingSpike;
336
        let _ndet = StressAnomalyKind::NonDeterministic;
337
    }
338
339
    #[test]
340
    fn test_stress_result_zero_tests() {
341
        let result = StressResult {
342
            backend: Backend::Scalar,
343
            input_size: 0,
344
            cycles_completed: 0,
345
            tests_passed: 0,
346
            tests_failed: 0,
347
            mean_op_time_ms: 0.0,
348
            max_op_time_ms: 0,
349
            timing_variance: 0.0,
350
            anomalies: vec![],
351
        };
352
353
        // Zero tests should still pass with pass_rate of 1.0
354
        assert!(result.passed());
355
        assert_eq!(result.pass_rate(), 1.0);
356
    }
357
}