## Coverage Report

#### Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/noah/src/realizar/src/warmup.rs

    Line
    Count
    Source
    1
    //! Model Warm-up and Pre-loading
    2
    //!
    3
    //! Reduces cold start latency by pre-loading models and running warm-up inference.
    4
    //!
    5
    //! ## Features
    6
    //!
    7
    //! - Pre-load models into memory before serving
    8
    //! - Run warm-up inference to JIT compile and optimize
    9
    //! - Validate model integrity before accepting traffic
    10
    //! - Background model loading for zero-downtime updates
    11
    //!
    12
    //! ## Example
    13
    //!
    14
    //! ```rust,ignore
    15
    //! use realizar::warmup::{WarmupConfig, ModelWarmer};
    16
    //!
    17
    //! let config = WarmupConfig::new()
    18
    //!     .with_warmup_iterations(3)
    19
    //!     .with_timeout(Duration::from_secs(30));
    20
    //!
    21
    //! let warmer = ModelWarmer::new(config);
    22
    //! warmer.warm_up(&model, &tokenizer).await?;
    23
    //! ```
    24
    //!
    25
    //! ## Toyota Way Principles
    26
    //!
    27
    //! - Heijunka: Level loading by pre-warming
    28
    //! - Jidoka: Validate model before serving
    29
    //! - Poka-Yoke: Prevent cold start errors
    30
    31
    use std::{
    32
        sync::{
    33
            atomic::{AtomicBool, AtomicU64, Ordering},
    34
            Arc,
    35
        },
    36
        time::{Duration, Instant},
    37
    };
    38
    39
    use serde::{Deserialize, Serialize};
    40
    41
    // ============================================================================
    42
    // WARM-001: Configuration
    43
    // ============================================================================
    44
    45
    /// Configuration for model warm-up
    46
    #[derive(Debug, Clone, Serialize, Deserialize)]
    47
    pub struct WarmupConfig {
    48
        /// Number of warm-up inference iterations
    49
        pub warmup_iterations: usize,
    50
        /// Timeout for warm-up process
    51
        pub timeout: Duration,
    52
        /// Sample prompt for warm-up inference
    53
        pub sample_prompt: String,
    54
        /// Maximum tokens for warm-up generation
    55
        pub sample_max_tokens: usize,
    56
        /// Validate model output during warm-up
    57
        pub validate_output: bool,
    58
        /// Run garbage collection after warm-up
    59
        pub gc_after_warmup: bool,
    60
        /// Log warm-up progress
    61
        pub verbose: bool,
    62
    }
    63
    64
    impl Default for WarmupConfig {
    65
    10
        fn default() -> Self {
    66
    10
            Self {
    67
    10
                warmup_iterations: 3,
    68
    10
                timeout: Duration::from_secs(60),
    69
    10
                sample_prompt: "Hello, world!".to_string(),
    70
    10
                sample_max_tokens: 10,
    71
    10
                validate_output: true,
    72
    10
                gc_after_warmup: true,
    73
    10
                verbose: false,
    74
    10
            }
    75
    10
        }
    76
    }
    77
    78
    impl WarmupConfig {
    79
        /// Create a new warm-up configuration
    80
        #[must_use]
    81
    8
        pub fn new() -> Self {
    82
    8
            Self::default()
    83
    8
        }
    84
    85
        /// Set number of warm-up iterations
    86
        #[must_use]
    87
    6
        pub fn with_warmup_iterations(mut self, n: usize) -> Self {
    88
    6
            self.warmup_iterations = n.max(1);
    89
    6
            self
    90
    6
        }
    91
    92
        /// Set warm-up timeout
    93
        #[must_use]
    94
    2
        pub fn with_timeout(mut self, timeout: Duration) -> Self {
    95
    2
            self.timeout = timeout;
    96
    2
            self
    97
    2
        }
    98
    99
        /// Set sample prompt for warm-up
    100
        #[must_use]
    101
    2
        pub fn with_sample_prompt(mut self, prompt: impl Into<String>) -> Self {
    102
    2
            self.sample_prompt = prompt.into();
    103
    2
            self
    104
    2
        }
    105
    106
        /// Set maximum tokens for warm-up generation
    107
        #[must_use]
    108
    1
        pub fn with_sample_max_tokens(mut self, n: usize) -> Self {
    109
    1
            self.sample_max_tokens = n;
    110
    1
            self
    111
    1
        }
    112
    113
        /// Enable/disable output validation
    114
        #[must_use]
    115
    1
        pub fn with_validate_output(mut self, validate: bool) -> Self {
    116
    1
            self.validate_output = validate;
    117
    1
            self
    118
    1
        }
    119
    120
        /// Enable/disable garbage collection after warm-up
    121
        #[must_use]
    122
    0
        pub fn with_gc_after_warmup(mut self, gc: bool) -> Self {
    123
    0
            self.gc_after_warmup = gc;
    124
    0
            self
    125
    0
        }
    126
    127
        /// Enable/disable verbose logging
    128
        #[must_use]
    129
    1
        pub fn with_verbose(mut self, verbose: bool) -> Self {
    130
    1
            self.verbose = verbose;
    131
    1
            self
    132
    1
        }
    133
    }
    134
    135
    // ============================================================================
    136
    // WARM-002: Warm-up Status
    137
    // ============================================================================
    138
    139
    /// Status of warm-up process
    140
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
    141
    pub enum WarmupStatus {
    142
        /// Not yet started
    143
        NotStarted,
    144
        /// Currently warming up
    145
        InProgress,
    146
        /// Warm-up completed successfully
    147
        Ready,
    148
        /// Warm-up failed
    149
        Failed,
    150
        /// Warm-up timed out
    151
        TimedOut,
    152
    }
    153
    154
    impl WarmupStatus {
    155
        /// Check if model is ready to serve
    156
        #[must_use]
    157
    5
        pub fn is_ready(&self) -> bool {
    158
    5
            matches!4(self, Self::Ready)
    159
    5
        }
    160
    161
        /// Check if warm-up is still in progress
    162
        #[must_use]
    163
    3
        pub fn is_in_progress(&self) -> bool {
    164
    3
            matches!2(self, Self::InProgress)
    165
    3
        }
    166
    167
        /// Check if warm-up failed
    168
        #[must_use]
    169
    5
        pub fn has_failed(&self) -> bool {
    170
    5
            matches!3(self, Self::Failed | Self::TimedOut)
    171
    5
        }
    172
    }
    173
    174
    // ============================================================================
    175
    // WARM-003: Warm-up Result
    176
    // ============================================================================
    177
    178
    /// Result of warm-up process
    179
    #[derive(Debug, Clone, Serialize, Deserialize)]
    180
    pub struct WarmupResult {
    181
        /// Final status
    182
        pub status: WarmupStatus,
    183
        /// Number of iterations completed
    184
        pub iterations_completed: usize,
    185
        /// Total warm-up duration
    186
        pub total_duration: Duration,
    187
        /// Average inference latency during warm-up
    188
        pub avg_latency: Duration,
    189
        /// First inference latency (cold)
    190
        pub first_latency: Duration,
    191
        /// Last inference latency (warm)
    192
        pub last_latency: Duration,
    193
        /// Speedup factor (first / last)
    194
        pub speedup_factor: f64,
    195
        /// Error message if failed
    196
        pub error: Option<String>,
    197
    }
    198
    199
    impl WarmupResult {
    200
        /// Create a successful result
    201
        #[must_use]
    202
    4
        pub fn success(iterations: usize, duration: Duration, latencies: &[Duration]) -> Self {
    203
    4
            let first = latencies.first().copied().unwrap_or(Duration::ZERO);
    204
    4
            let last = latencies.last().copied().unwrap_or(Duration::ZERO);
    205
    4
            let avg = if latencies.is_empty() {
    206
    1
                Duration::ZERO
    207
            } else {
    208
    3
                Duration::from_nanos(
    209
    7
                    latencies3.iter3().map3(|d| d.as_nanos() as u64).sum3::<u64>() / latencies.len() as u643,
    210
                )
    211
            };
    212
    213
    4
            let speedup = if last.as_nanos() > 0 {
    214
    3
                first.as_nanos() as f64 / last.as_nanos() as f64
    215
            } else {
    216
    1
                1.0
    217
            };
    218
    219
    4
            Self {
    220
    4
                status: WarmupStatus::Ready,
    221
    4
                iterations_completed: iterations,
    222
    4
                total_duration: duration,
    223
    4
                avg_latency: avg,
    224
    4
                first_latency: first,
    225
    4
                last_latency: last,
    226
    4
                speedup_factor: speedup,
    227
    4
                error: None,
    228
    4
            }
    229
    4
        }
    230
    231
        /// Create a failed result
    232
        #[must_use]
    233
    1
        pub fn failed(error: impl Into<String>, iterations: usize, duration: Duration) -> Self {
    234
    1
            Self {
    235
    1
                status: WarmupStatus::Failed,
    236
    1
                iterations_completed: iterations,
    237
    1
                total_duration: duration,
    238
    1
                avg_latency: Duration::ZERO,
    239
    1
                first_latency: Duration::ZERO,
    240
    1
                last_latency: Duration::ZERO,
    241
    1
                speedup_factor: 0.0,
    242
    1
                error: Some(error.into()),
    243
    1
            }
    244
    1
        }
    245
    246
        /// Create a timed out result
    247
        #[must_use]
    248
    2
        pub fn timed_out(iterations: usize, duration: Duration) -> Self {
    249
    2
            Self {
    250
    2
                status: WarmupStatus::TimedOut,
    251
    2
                iterations_completed: iterations,
    252
    2
                total_duration: duration,
    253
    2
                avg_latency: Duration::ZERO,
    254
    2
                first_latency: Duration::ZERO,
    255
    2
                last_latency: Duration::ZERO,
    256
    2
                speedup_factor: 0.0,
    257
    2
                error: Some("Warm-up timed out".to_string()),
    258
    2
            }
    259
    2
        }
    260
    }
    261
    262
    // ============================================================================
    263
    // WARM-004: Model Health
    264
    // ============================================================================
    265
    266
    /// Model health status for readiness probes
    267
    #[derive(Debug, Clone)]
    268
    pub struct ModelHealth {
    269
        /// Whether model is ready
    270
        ready: Arc<AtomicBool>,
    271
        /// Warm-up status
    272
        status: Arc<std::sync::RwLock<WarmupStatus>>,
    273
        /// Total requests served
    274
        requests_served: Arc<AtomicU64>,
    275
        /// Failed requests
    276
        requests_failed: Arc<AtomicU64>,
    277
        /// Last health check timestamp
    278
        last_health_check: Arc<std::sync::RwLock<Instant>>,
    279
        /// Model load timestamp
    280
        loaded_at: Instant,
    281
    }
    282
    283
    impl Default for ModelHealth {
    284
    0
        fn default() -> Self {
    285
    0
            Self::new()
    286
    0
        }
    287
    }
    288
    289
    impl ModelHealth {
    290
        /// Create new health tracker
    291
        #[must_use]
    292
    7
        pub fn new() -> Self {
    293
    7
            Self {
    294
    7
                ready: Arc::new(AtomicBool::new(false)),
    295
    7
                status: Arc::new(std::sync::RwLock::new(WarmupStatus::NotStarted)),
    296
    7
                requests_served: Arc::new(AtomicU64::new(0)),
    297
    7
                requests_failed: Arc::new(AtomicU64::new(0)),
    298
    7
                last_health_check: Arc::new(std::sync::RwLock::new(Instant::now())),
    299
    7
                loaded_at: Instant::now(),
    300
    7
            }
    301
    7
        }
    302
    303
        /// Check if model is ready
    304
        #[must_use]
    305
    6
        pub fn is_ready(&self) -> bool {
    306
    6
            self.ready.load(Ordering::Acquire)
    307
    6
        }
    308
    309
        /// Set ready status
    310
    4
        pub fn set_ready(&self, ready: bool) {
    311
    4
            self.ready.store(ready, Ordering::Release);
    312
    4
        }
    313
    314
        /// Get current status
    315
        #[must_use]
    316
    4
        pub fn status(&self) -> WarmupStatus {
    317
    4
            *self.status.read().expect("test")
    318
    4
        }
    319
    320
        /// Set status
    321
    3
        pub fn set_status(&self, status: WarmupStatus) {
    322
    3
            *self.status.write().expect("test") = status;
    323
    3
            if status == WarmupStatus::Ready {
    324
    2
                self.set_ready(true);
    325
    2
            }1
    326
    3
        }
    327
    328
        /// Record a successful request
    329
    5
        pub fn record_success(&self) {
    330
    5
            self.requests_served.fetch_add(1, Ordering::Relaxed);
    331
    5
        }
    332
    333
        /// Record a failed request
    334
    2
        pub fn record_failure(&self) {
    335
    2
            self.requests_failed.fetch_add(1, Ordering::Relaxed);
    336
    2
        }
    337
    338
        /// Get total requests served
    339
        #[must_use]
    340
    6
        pub fn total_requests(&self) -> u64 {
    341
    6
            self.requests_served.load(Ordering::Relaxed)
    342
    6
        }
    343
    344
        /// Get failed requests
    345
        #[must_use]
    346
    5
        pub fn failed_requests(&self) -> u64 {
    347
    5
            self.requests_failed.load(Ordering::Relaxed)
    348
    5
        }
    349
    350
        /// Get error rate
    351
        #[must_use]
    352
    3
        pub fn error_rate(&self) -> f64 {
    353
    3
            let total = self.total_requests();
    354
    3
            let failed = self.failed_requests();
    355
    3
            if total == 0 {
    356
    1
                0.0
    357
            } else {
    358
    2
                failed as f64 / total as f64
    359
            }
    360
    3
        }
    361
    362
        /// Update health check timestamp
    363
    1
        pub fn touch(&self) {
    364
    1
            *self.last_health_check.write().expect("test") = Instant::now();
    365
    1
        }
    366
    367
        /// Get uptime since model was loaded
    368
        #[must_use]
    369
    1
        pub fn uptime(&self) -> Duration {
    370
    1
            self.loaded_at.elapsed()
    371
    1
        }
    372
    373
        /// Get time since last health check
    374
        #[must_use]
    375
    3
        pub fn time_since_last_check(&self) -> Duration {
    376
    3
            self.last_health_check.read().expect("test").elapsed()
    377
    3
        }
    378
    379
        /// Generate health report
    380
        #[must_use]
    381
    1
        pub fn report(&self) -> HealthReport {
    382
    1
            HealthReport {
    383
    1
                ready: self.is_ready(),
    384
    1
                status: self.status(),
    385
    1
                uptime_secs: self.uptime().as_secs_f64(),
    386
    1
                total_requests: self.total_requests(),
    387
    1
                failed_requests: self.failed_requests(),
    388
    1
                error_rate: self.error_rate(),
    389
    1
                time_since_last_check_secs: self.time_since_last_check().as_secs_f64(),
    390
    1
            }
    391
    1
        }
    392
    }
    393
    394
    /// Health report for API responses
    395
    #[derive(Debug, Clone, Serialize, Deserialize)]
    396
    pub struct HealthReport {
    397
        /// Is model ready to serve
    398
        pub ready: bool,
    399
        /// Current warm-up status
    400
        pub status: WarmupStatus,
    401
        /// Uptime in seconds
    402
        pub uptime_secs: f64,
    403
        /// Total requests served
    404
        pub total_requests: u64,
    405
        /// Failed requests
    406
        pub failed_requests: u64,
    407
        /// Error rate (0.0 - 1.0)
    408
        pub error_rate: f64,
    409
        /// Time since last health check in seconds
    410
        pub time_since_last_check_secs: f64,
    411
    }
    412
    413
    // ============================================================================
    414
    // WARM-005: Warm-up Executor
    415
    // ============================================================================
    416
    417
    /// Executes model warm-up process
    418
    #[derive(Debug, Clone)]
    419
    pub struct WarmupExecutor {
    420
        config: WarmupConfig,
    421
    }
    422
    423
    impl WarmupExecutor {
    424
        /// Create a new warm-up executor
    425
        #[must_use]
    426
    4
        pub fn new(config: WarmupConfig) -> Self {
    427
    4
            Self { config }
    428
    4
        }
    429
    430
        /// Get configuration
    431
        #[must_use]
    432
    2
        pub fn config(&self) -> &WarmupConfig {
    433
    2
            &self.config
    434
    2
        }
    435
    436
        /// Simulate warm-up (for testing without actual model)
    437
        ///
    438
        /// This runs the warm-up process with test inference delays.
    439
        #[must_use]
    440
    1
        pub fn simulate_warmup(&self) -> WarmupResult {
    441
    1
            let start = Instant::now();
    442
    1
            let mut latencies = Vec::with_capacity(self.config.warmup_iterations);
    443
    444
            // Simulate decreasing latency as model warms up
    445
    3
            for i in 0..self.config.warmup_iterations1 {
    446
                // First iteration is "cold" (slower)
    447
    3
                let base_latency_us = if i == 0 { 10001 } else { 1002 };
    448
    3
                let jitter = (i * 10) as u64;
    449
    3
                let latency = Duration::from_micros(base_latency_us - jitter.min(50));
    450
    3
                latencies.push(latency);
    451
            }
    452
    453
    1
            WarmupResult::success(self.config.warmup_iterations, start.elapsed(), &latencies)
    454
    1
        }
    455
    456
        /// Check if timeout has been exceeded
    457
        #[allow(dead_code)]
    458
    1
        fn check_timeout(&self, start: Instant, iterations: usize) -> Option<WarmupResult> {
    459
    1
            if start.elapsed() > self.config.timeout {
    460
    1
                Some(WarmupResult::timed_out(iterations, start.elapsed()))
    461
            } else {
    462
    0
                None
    463
            }
    464
    1
        }
    465
    }
    466
    467
    impl Default for WarmupExecutor {
    468
    1
        fn default() -> Self {
    469
    1
            Self::new(WarmupConfig::default())
    470
    1
        }
    471
    }
    472
    473
    // ============================================================================
    474
    // WARM-006: Pre-load Configuration
    475
    // ============================================================================
    476
    477
    /// Configuration for model pre-loading
    478
    #[derive(Debug, Clone, Serialize, Deserialize)]
    479
    pub struct PreloadConfig {
    480
        /// Models to pre-load on startup
    481
        pub models: Vec<PreloadModelConfig>,
    482
        /// Load models in parallel
    483
        pub parallel_loading: bool,
    484
        /// Maximum concurrent model loads
    485
        pub max_concurrent: usize,
    486
        /// Fail startup if any model fails to load
    487
        pub fail_fast: bool,
    488
    }
    489
    490
    impl Default for PreloadConfig {
    491
    3
        fn default() -> Self {
    492
    3
            Self {
    493
    3
                models: Vec::new(),
    494
    3
                parallel_loading: true,
    495
    3
                max_concurrent: 4,
    496
    3
                fail_fast: false,
    497
    3
            }
    498
    3
        }
    499
    }
    500
    501
    impl PreloadConfig {
    502
        /// Create new pre-load configuration
    503
        #[must_use]
    504
    2
        pub fn new() -> Self {
    505
    2
            Self::default()
    506
    2
        }
    507
    508
        /// Add a model to pre-load
    509
        #[must_use]
    510
    1
        pub fn with_model(mut self, model: PreloadModelConfig) -> Self {
    511
    1
            self.models.push(model);
    512
    1
            self
    513
    1
        }
    514
    515
        /// Set parallel loading
    516
        #[must_use]
    517
    1
        pub fn with_parallel_loading(mut self, parallel: bool) -> Self {
    518
    1
            self.parallel_loading = parallel;
    519
    1
            self
    520
    1
        }
    521
    522
        /// Set maximum concurrent loads
    523
        #[must_use]
    524
    2
        pub fn with_max_concurrent(mut self, max: usize) -> Self {
    525
    2
            self.max_concurrent = max.max(1);
    526
    2
            self
    527
    2
        }
    528
    529
        /// Set fail-fast behavior
    530
        #[must_use]
    531
    1
        pub fn with_fail_fast(mut self, fail_fast: bool) -> Self {
    532
    1
            self.fail_fast = fail_fast;
    533
    1
            self
    534
    1
        }
    535
    }
    536
    537
    /// Configuration for a single model to pre-load
    538
    #[derive(Debug, Clone, Serialize, Deserialize)]
    539
    pub struct PreloadModelConfig {
    540
        /// Model identifier
    541
        pub model_id: String,
    542
        /// Model URI (pacha://, file://, hf://)
    543
        pub uri: String,
    544
        /// Priority (lower = load first)
    545
        pub priority: u32,
    546
        /// Run warm-up after loading
    547
        pub warmup: bool,
    548
        /// Warm-up configuration
    549
        pub warmup_config: Option<WarmupConfig>,
    550
    }
    551
    552
    impl PreloadModelConfig {
    553
        /// Create new model pre-load config
    554
        #[must_use]
    555
    4
        pub fn new(model_id: impl Into<String>, uri: impl Into<String>) -> Self {
    556
    4
            Self {
    557
    4
                model_id: model_id.into(),
    558
    4
                uri: uri.into(),
    559
    4
                priority: 100,
    560
    4
                warmup: true,
    561
    4
                warmup_config: None,
    562
    4
            }
    563
    4
        }
    564
    565
        /// Set priority
    566
        #[must_use]
    567
    1
        pub fn with_priority(mut self, priority: u32) -> Self {
    568
    1
            self.priority = priority;
    569
    1
            self
    570
    1
        }
    571
    572
        /// Enable/disable warm-up
    573
        #[must_use]
    574
    2
        pub fn with_warmup(mut self, warmup: bool) -> Self {
    575
    2
            self.warmup = warmup;
    576
    2
            self
    577
    2
        }
    578
    579
        /// Set warm-up configuration
    580
        #[must_use]
    581
    2
        pub fn with_warmup_config(mut self, config: WarmupConfig) -> Self {
    582
    2
            self.warmup_config = Some(config);
    583
    2
            self.warmup = true;
    584
    2
            self
    585
    2
        }
    586
    }
    587
    588
    // ============================================================================
    589
    // Tests
    590
    // ============================================================================
    591
    592
    #[cfg(test)]
    593
    mod tests {
    594
        use super::*;
    595
    596
        // ========================================================================
    597
        // WARM-001: Configuration Tests
    598
        // ========================================================================
    599
    600
        #[test]
    601
    1
        fn test_warmup_config_default() {
    602
    1
            let config = WarmupConfig::default();
    603
    1
            assert_eq!(config.warmup_iterations, 3);
    604
    1
            assert_eq!(config.timeout, Duration::from_secs(60));
    605
    1
            assert_eq!(config.sample_max_tokens, 10);
    606
    1
            assert!(config.validate_output);
    607
    1
        }
    608
    609
        #[test]
    610
    1
        fn test_warmup_config_builder() {
    611
    1
            let config = WarmupConfig::new()
    612
    1
                .with_warmup_iterations(5)
    613
    1
                .with_timeout(Duration::from_secs(120))
    614
    1
                .with_sample_prompt("Test prompt")
    615
    1
                .with_sample_max_tokens(20)
    616
    1
                .with_validate_output(false)
    617
    1
                .with_verbose(true);
    618
    619
    1
            assert_eq!(config.warmup_iterations, 5);
    620
    1
            assert_eq!(config.timeout, Duration::from_secs(120));
    621
    1
            assert_eq!(config.sample_prompt, "Test prompt");
    622
    1
            assert_eq!(config.sample_max_tokens, 20);
    623
    1
            assert!(!config.validate_output);
    624
    1
            assert!(config.verbose);
    625
    1
        }
    626
    627
        #[test]
    628
    1
        fn test_warmup_config_min_iterations() {
    629
    1
            let config = WarmupConfig::new().with_warmup_iterations(0);
    630
    1
            assert_eq!(config.warmup_iterations, 1);
    631
    1
        }
    632
    633
        // ========================================================================
    634
        // WARM-002: Status Tests
    635
        // ========================================================================
    636
    637
        #[test]
    638
    1
        fn test_warmup_status_is_ready() {
    639
    1
            assert!(!WarmupStatus::NotStarted.is_ready());
    640
    1
            assert!(!WarmupStatus::InProgress.is_ready());
    641
    1
            assert!(WarmupStatus::Ready.is_ready());
    642
    1
            assert!(!WarmupStatus::Failed.is_ready());
    643
    1
            assert!(!WarmupStatus::TimedOut.is_ready());
    644
    1
        }
    645
    646
        #[test]
    647
    1
        fn test_warmup_status_is_in_progress() {
    648
    1
            assert!(!WarmupStatus::NotStarted.is_in_progress());
    649
    1
            assert!(WarmupStatus::InProgress.is_in_progress());
    650
    1
            assert!(!WarmupStatus::Ready.is_in_progress());
    651
    1
        }
    652
    653
        #[test]
    654
    1
        fn test_warmup_status_has_failed() {
    655
    1
            assert!(!WarmupStatus::NotStarted.has_failed());
    656
    1
            assert!(!WarmupStatus::InProgress.has_failed());
    657
    1
            assert!(!WarmupStatus::Ready.has_failed());
    658
    1
            assert!(WarmupStatus::Failed.has_failed());
    659
    1
            assert!(WarmupStatus::TimedOut.has_failed());
    660
    1
        }
    661
    662
        // ========================================================================
    663
        // WARM-003: Result Tests
    664
        // ========================================================================
    665
    666
        #[test]
    667
    1
        fn test_warmup_result_success() {
    668
    1
            let latencies = vec![
    669
    1
                Duration::from_millis(100),
    670
    1
                Duration::from_millis(50),
    671
    1
                Duration::from_millis(25),
    672
            ];
    673
    674
    1
            let result = WarmupResult::success(3, Duration::from_millis(200), &latencies);
    675
    676
    1
            assert_eq!(result.status, WarmupStatus::Ready);
    677
    1
            assert_eq!(result.iterations_completed, 3);
    678
    1
            assert_eq!(result.first_latency, Duration::from_millis(100));
    679
    1
            assert_eq!(result.last_latency, Duration::from_millis(25));
    680
    1
            assert!(result.speedup_factor > 1.0);
    681
    1
            assert!(result.error.is_none());
    682
    1
        }
    683
    684
        #[test]
    685
    1
        fn test_warmup_result_failed() {
    686
    1
            let result = WarmupResult::failed("Test error", 2, Duration::from_secs(5));
    687
    688
    1
            assert_eq!(result.status, WarmupStatus::Failed);
    689
    1
            assert_eq!(result.iterations_completed, 2);
    690
    1
            assert_eq!(result.error, Some("Test error".to_string()));
    691
    1
        }
    692
    693
        #[test]
    694
    1
        fn test_warmup_result_timed_out() {
    695
    1
            let result = WarmupResult::timed_out(1, Duration::from_secs(60));
    696
    697
    1
            assert_eq!(result.status, WarmupStatus::TimedOut);
    698
    1
            assert!(result.error.is_some());
    699
    1
            assert!(result.error.expect("test").contains("timed out"));
    700
    1
        }
    701
    702
        #[test]
    703
    1
        fn test_warmup_result_empty_latencies() {
    704
    1
            let result = WarmupResult::success(0, Duration::ZERO, &[]);
    705
    706
    1
            assert_eq!(result.first_latency, Duration::ZERO);
    707
    1
            assert_eq!(result.last_latency, Duration::ZERO);
    708
    1
            assert!((result.speedup_factor - 1.0).abs() < f64::EPSILON);
    709
    1
        }
    710
    711
        // ========================================================================
    712
        // WARM-004: Health Tests
    713
        // ========================================================================
    714
    715
        #[test]
    716
    1
        fn test_model_health_new() {
    717
    1
            let health = ModelHealth::new();
    718
    1
            assert!(!health.is_ready());
    719
    1
            assert_eq!(health.status(), WarmupStatus::NotStarted);
    720
    1
            assert_eq!(health.total_requests(), 0);
    721
    1
        }
    722
    723
        #[test]
    724
    1
        fn test_model_health_set_ready() {
    725
    1
            let health = ModelHealth::new();
    726
    1
            health.set_ready(true);
    727
    1
            assert!(health.is_ready());
    728
    729
    1
            health.set_ready(false);
    730
    1
            assert!(!health.is_ready());
    731
    1
        }
    732
    733
        #[test]
    734
    1
        fn test_model_health_set_status() {
    735
    1
            let health = ModelHealth::new();
    736
    1
            health.set_status(WarmupStatus::InProgress);
    737
    1
            assert_eq!(health.status(), WarmupStatus::InProgress);
    738
    1
            assert!(!health.is_ready());
    739
    740
    1
            health.set_status(WarmupStatus::Ready);
    741
    1
            assert_eq!(health.status(), WarmupStatus::Ready);
    742
    1
            assert!(health.is_ready());
    743
    1
        }
    744
    745
        #[test]
    746
    1
        fn test_model_health_record_requests() {
    747
    1
            let health = ModelHealth::new();
    748
    749
    1
            health.record_success();
    750
    1
            health.record_success();
    751
    1
            health.record_failure();
    752
    753
    1
            assert_eq!(health.total_requests(), 2);
    754
    1
            assert_eq!(health.failed_requests(), 1);
    755
    1
        }
    756
    757
        #[test]
    758
    1
        fn test_model_health_error_rate() {
    759
    1
            let health = ModelHealth::new();
    760
    761
            // No requests yet
    762
    1
            assert!((health.error_rate() - 0.0).abs() < f64::EPSILON);
    763
    764
            // 2 success, 1 failure
    765
    1
            health.record_success();
    766
    1
            health.record_success();
    767
    1
            health.record_failure();
    768
    769
            // Error rate should be 0 since failed_requests/total_requests
    770
            // But total_requests only counts successes
    771
            // So error rate = 1 / 2 = 0.5... wait, let me check the implementation
    772
            // Actually: total_requests counts successes, error_rate = failed / total
    773
            // So 1 / 2 = 0.5
    774
    1
            assert!((health.error_rate() - 0.5).abs() < f64::EPSILON);
    775
    1
        }
    776
    777
        #[test]
    778
    1
        fn test_model_health_touch() {
    779
    1
            let health = ModelHealth::new();
    780
    1
            std::thread::sleep(Duration::from_millis(10));
    781
    1
            let before = health.time_since_last_check();
    782
    1
            health.touch();
    783
    1
            let after = health.time_since_last_check();
    784
    1
            assert!(after < before);
    785
    1
        }
    786
    787
        #[test]
    788
    1
        fn test_model_health_report() {
    789
    1
            let health = ModelHealth::new();
    790
    1
            health.set_status(WarmupStatus::Ready);
    791
    1
            health.record_success();
    792
    793
    1
            let report = health.report();
    794
    1
            assert!(report.ready);
    795
    1
            assert_eq!(report.status, WarmupStatus::Ready);
    796
    1
            assert_eq!(report.total_requests, 1);
    797
    1
            assert_eq!(report.failed_requests, 0);
    798
    1
            assert!(report.uptime_secs >= 0.0);
    799
    1
        }
    800
    801
        // ========================================================================
    802
        // WARM-005: Executor Tests
    803
        // ========================================================================
    804
    805
        #[test]
    806
    1
        fn test_warmup_executor_new() {
    807
    1
            let config = WarmupConfig::new().with_warmup_iterations(5);
    808
    1
            let executor = WarmupExecutor::new(config.clone());
    809
    1
            assert_eq!(executor.config().warmup_iterations, 5);
    810
    1
        }
    811
    812
        #[test]
    813
    1
        fn test_warmup_executor_simulate() {
    814
    1
            let config = WarmupConfig::new().with_warmup_iterations(3);
    815
    1
            let executor = WarmupExecutor::new(config);
    816
    817
    1
            let result = executor.simulate_warmup();
    818
    819
    1
            assert_eq!(result.status, WarmupStatus::Ready);
    820
    1
            assert_eq!(result.iterations_completed, 3);
    821
    1
            assert!(result.first_latency > Duration::ZERO);
    822
    1
            assert!(result.last_latency > Duration::ZERO);
    823
            // First (cold) should be slower than last (warm)
    824
    1
            assert!(result.first_latency > result.last_latency);
    825
    1
            assert!(result.speedup_factor > 1.0);
    826
    1
        }
    827
    828
        #[test]
    829
    1
        fn test_warmup_executor_check_timeout() {
    830
    1
            let config = WarmupConfig::new().with_timeout(Duration::from_millis(1));
    831
    1
            let executor = WarmupExecutor::new(config);
    832
    833
    1
            let start = Instant::now();
    834
    1
            std::thread::sleep(Duration::from_millis(10));
    835
    836
    1
            let result = executor.check_timeout(start, 0);
    837
    1
            assert!(result.is_some());
    838
    1
            assert_eq!(result.expect("test").status, WarmupStatus::TimedOut);
    839
    1
        }
    840
    841
        #[test]
    842
    1
        fn test_warmup_executor_default() {
    843
    1
            let executor = WarmupExecutor::default();
    844
    1
            assert_eq!(executor.config().warmup_iterations, 3);
    845
    1
        }
    846
    847
        // ========================================================================
    848
        // WARM-006: Preload Config Tests
    849
        // ========================================================================
    850
    851
        #[test]
    852
    1
        fn test_preload_config_default() {
    853
    1
            let config = PreloadConfig::default();
    854
    1
            assert!(config.models.is_empty());
    855
    1
            assert!(config.parallel_loading);
    856
    1
            assert_eq!(config.max_concurrent, 4);
    857
    1
            assert!(!config.fail_fast);
    858
    1
        }
    859
    860
        #[test]
    861
    1
        fn test_preload_config_builder() {
    862
    1
            let model = PreloadModelConfig::new("llama", "pacha://llama:7b");
    863
    864
    1
            let config = PreloadConfig::new()
    865
    1
                .with_model(model)
    866
    1
                .with_parallel_loading(false)
    867
    1
                .with_max_concurrent(2)
    868
    1
                .with_fail_fast(true);
    869
    870
    1
            assert_eq!(config.models.len(), 1);
    871
    1
            assert!(!config.parallel_loading);
    872
    1
            assert_eq!(config.max_concurrent, 2);
    873
    1
            assert!(config.fail_fast);
    874
    1
        }
    875
    876
        #[test]
    877
    1
        fn test_preload_config_min_concurrent() {
    878
    1
            let config = PreloadConfig::new().with_max_concurrent(0);
    879
    1
            assert_eq!(config.max_concurrent, 1);
    880
    1
        }
    881
    882
        #[test]
    883
    1
        fn test_preload_model_config_new() {
    884
    1
            let model = PreloadModelConfig::new("gpt2", "hf://gpt2");
    885
    1
            assert_eq!(model.model_id, "gpt2");
    886
    1
            assert_eq!(model.uri, "hf://gpt2");
    887
    1
            assert_eq!(model.priority, 100);
    888
    1
            assert!(model.warmup);
    889
    1
            assert!(model.warmup_config.is_none());
    890
    1
        }
    891
    892
        #[test]
    893
    1
        fn test_preload_model_config_builder() {
    894
    1
            let warmup_config = WarmupConfig::new().with_warmup_iterations(5);
    895
    896
    1
            let model = PreloadModelConfig::new("llama", "file://model.gguf")
    897
    1
                .with_priority(10)
    898
    1
                .with_warmup(true)
    899
    1
                .with_warmup_config(warmup_config);
    900
    901
    1
            assert_eq!(model.priority, 10);
    902
    1
            assert!(model.warmup);
    903
    1
            assert!(model.warmup_config.is_some());
    904
    1
            assert_eq!(model.warmup_config.expect("test").warmup_iterations, 5);
    905
    1
        }
    906
    907
        #[test]
    908
    1
        fn test_preload_model_config_with_warmup_enables_warmup() {
    909
    1
            let model = PreloadModelConfig::new("test", "file://test.gguf")
    910
    1
                .with_warmup(false)
    911
    1
                .with_warmup_config(WarmupConfig::new());
    912
    913
            // Setting warmup_config should enable warmup
    914
    1
            assert!(model.warmup);
    915
    1
        }
    916
    917
        // ========================================================================
    918
        // Serialization Tests
    919
        // ========================================================================
    920
    921
        #[test]
    922
    1
        fn test_warmup_config_serialization() {
    923
    1
            let config = WarmupConfig::new()
    924
    1
                .with_warmup_iterations(5)
    925
    1
                .with_sample_prompt("Hello");
    926
    927
    1
            let json = serde_json::to_string(&config).expect("test");
    928
    1
            assert!(json.contains("5"));
    929
    1
            assert!(json.contains("Hello"));
    930
    931
    1
            let deserialized: WarmupConfig = serde_json::from_str(&json).expect("test");
    932
    1
            assert_eq!(deserialized.warmup_iterations, 5);
    933
    1
        }
    934
    935
        #[test]
    936
    1
        fn test_warmup_status_serialization() {
    937
    1
            let status = WarmupStatus::Ready;
    938
    1
            let json = serde_json::to_string(&status).expect("test");
    939
    1
            let deserialized: WarmupStatus = serde_json::from_str(&json).expect("test");
    940
    1
            assert_eq!(deserialized, WarmupStatus::Ready);
    941
    1
        }
    942
    943
        #[test]
    944
    1
        fn test_warmup_result_serialization() {
    945
    1
            let result =
    946
    1
                WarmupResult::success(3, Duration::from_millis(100), &[Duration::from_millis(50)]);
    947
    948
    1
            let json = serde_json::to_string(&result).expect("test");
    949
    1
            assert!(json.contains("Ready"));
    950
    1
            assert!(json.contains('3'));
    951
    1
        }
    952
    953
        #[test]
    954
    1
        fn test_health_report_serialization() {
    955
    1
            let report = HealthReport {
    956
    1
                ready: true,
    957
    1
                status: WarmupStatus::Ready,
    958
    1
                uptime_secs: 100.0,
    959
    1
                total_requests: 1000,
    960
    1
                failed_requests: 5,
    961
    1
                error_rate: 0.005,
    962
    1
                time_since_last_check_secs: 1.5,
    963
    1
            };
    964
    965
    1
            let json = serde_json::to_string(&report).expect("test");
    966
    1
            assert!(json.contains("true"));
    967
    1
            assert!(json.contains("1000"));
    968
    1
            assert!(json.contains("0.005"));
    969
    1
        }
    970
    }