• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jzombie / rust-reqwest-drive / 23170738406

16 Mar 2026 11:26PM UTC coverage: 98.276%. First build
23170738406

Pull #10

github

web-flow
Merge 351c4b0e5 into 062de37c6
Pull Request #10: Throttle-only store bypass; optional auto-process-managed stores; bump deps

555 of 561 new or added lines in 3 files covered. (98.93%)

684 of 696 relevant lines covered (98.28%)

42492.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.95
/src/cache_middleware.rs
1
use async_trait::async_trait;
2
// Binary serialization
3
use bitcode::{Decode, Encode};
4
use bytes::Bytes;
5
use cache_manager::{CacheRoot, ProcessScopedCacheGroup};
6
use chrono::{DateTime, Utc};
7
use http::{Extensions, HeaderMap, HeaderValue, StatusCode};
8
use reqwest::{Request, Response};
9
use reqwest_middleware::{Middleware, Next, Result};
10
use simd_r_drive::traits::{DataStoreReader, DataStoreWriter};
11
use simd_r_drive::{DataStore, compute_hash};
12
use std::io;
13
use std::path::Path;
14
use std::sync::Arc;
15
use std::time::{Duration, SystemTime, UNIX_EPOCH}; // For parsing `Expires` headers
16

17
/// Per-request control for bypassing cache behavior.
18
///
19
/// When set to `CacheBypass(true)` in request extensions, the cache middleware
20
/// will skip both cache reads and cache writes for that request.
21
///
22
/// This is useful when you want a one-off fresh fetch while still reusing the
23
/// same client, cache store, and throttle middleware stack.
24
///
25
/// # Example
26
///
27
/// ```rust
28
/// use reqwest_drive::{CacheBypass, CachePolicy, ThrottlePolicy, init_cache_with_throttle};
29
/// use reqwest_middleware::ClientBuilder;
30
/// use tempfile::tempdir;
31
///
32
/// # #[tokio::main]
33
/// # async fn main() {
34
/// let temp_dir = tempdir().unwrap();
35
/// let cache_path = temp_dir.path().join("cache_storage.bin");
36
///
37
/// let (cache, throttle) = init_cache_with_throttle(
38
///     &cache_path,
39
///     CachePolicy::default(),
40
///     ThrottlePolicy::default(),
41
/// );
42
///
43
/// let client = ClientBuilder::new(reqwest::Client::new())
44
///     .with_arc(cache)
45
///     .with_arc(throttle)
46
///     .build();
47
///
48
/// let mut request = client.get("https://example.com");
49
/// request.extensions().insert(CacheBypass(true));
50
/// let _ = request.send().await;
51
/// # }
52
/// ```
53
#[derive(Clone, Copy, Debug, Default)]
54
pub struct CacheBypass(pub bool);
55

56
/// Per-request control for busting and refreshing cache behavior.
57
///
58
/// When set to `CacheBust(true)` in request extensions, the cache middleware
59
/// skips cache reads for that request, forces a fresh network fetch, and then
60
/// writes the new response back to cache (subject to `CachePolicy`).
61
///
62
/// This is useful when you want to refresh a stale entry and make future
63
/// non-busted requests use the updated cached response.
64
///
65
/// # Example
66
///
67
/// ```rust
68
/// use reqwest_drive::{CacheBust, CachePolicy, ThrottlePolicy, init_cache_with_throttle};
69
/// use reqwest_middleware::ClientBuilder;
70
/// use tempfile::tempdir;
71
///
72
/// # #[tokio::main]
73
/// # async fn main() {
74
/// let temp_dir = tempdir().unwrap();
75
/// let cache_path = temp_dir.path().join("cache_storage.bin");
76
///
77
/// let (cache, throttle) = init_cache_with_throttle(
78
///     &cache_path,
79
///     CachePolicy::default(),
80
///     ThrottlePolicy::default(),
81
/// );
82
///
83
/// let client = ClientBuilder::new(reqwest::Client::new())
84
///     .with_arc(cache)
85
///     .with_arc(throttle)
86
///     .build();
87
///
88
/// let mut request = client.get("https://example.com");
89
/// request.extensions().insert(CacheBust(true));
90
/// let _ = request.send().await;
91
/// # }
92
/// ```
93
#[derive(Clone, Copy, Debug, Default)]
94
pub struct CacheBust(pub bool);
95

96
/// Defines the caching policy for storing and retrieving responses.
97
#[derive(Clone, Debug)]
98
pub struct CachePolicy {
99
    /// Defines the caching policy for storing and retrieving responses.
100
    pub default_ttl: Duration,
101
    /// Determines whether cache expiration should respect HTTP headers.
102
    pub respect_headers: bool,
103
    /// Optional override for caching specific HTTP status codes.
104
    /// - If `None`, only success responses (`2xx`) are cached.
105
    /// - If `Some(Vec<u16>)`, only the specified status codes are cached.
106
    pub cache_status_override: Option<Vec<u16>>,
107
}
108

109
impl Default for CachePolicy {
110
    fn default() -> Self {
17✔
111
        Self {
17✔
112
            default_ttl: Duration::from_secs(60 * 60 * 24), // Default 1 day TTL
17✔
113
            respect_headers: true,                          // Use headers if available
17✔
114
            cache_status_override: None, // Default behavior: Cache only 2xx responses
17✔
115
        }
17✔
116
    }
17✔
117
}
118

119
/// Represents a cached HTTP response.
NEW
120
#[derive(Encode, Decode)]
×
121
struct CachedResponse {
122
    /// HTTP status code of the cached response.
123
    status: u16,
124
    /// HTTP headers stored as key-value pairs, where values are raw bytes.
125
    headers: Vec<(String, Vec<u8>)>,
126
    /// Response body stored as raw bytes.
127
    body: Vec<u8>,
128
    /// Unix timestamp (in milliseconds) indicating when the cache entry expires.
129
    expiration_timestamp: u64,
130
}
131

132
/// Provides an HTTP cache layer backed by a `SIMD R Drive` data store.
133
///
134
/// ## Concurrency model
135
///
136
/// - Thread-safe for concurrent access within a single process.
137
/// - Not multi-process safe for concurrent access to the same backing file.
138
///
139
/// If multiple processes need caching, use process-level coordination
140
/// (e.g., external locking/ownership) or separate cache files per process.
141
#[derive(Clone)]
142
pub struct DriveCache {
143
    store: Arc<DataStore>,
144
    policy: CachePolicy, // Configurable policy
145
    _process_scoped_group: Option<Arc<ProcessScopedCacheGroup>>,
146
}
147

148
impl DriveCache {
149
    /// Creates a new cache backed by a file-based data store.
150
    ///
151
    /// # Arguments
152
    ///
153
    /// * `cache_storage_file` - Path to the file where cached responses are stored.
154
    /// * `policy` - Configuration specifying cache expiration behavior.
155
    ///
156
    /// # Concurrency
157
    ///
158
    /// The cache is thread-safe within a process, but the backing file should
159
    /// not be shared for concurrent reads/writes across multiple processes.
160
    ///
161
    /// # Panics
162
    ///
163
    /// This function will panic if the `DataStore` fails to initialize.
164
    pub fn new(cache_storage_file: &Path, policy: CachePolicy) -> Self {
21✔
165
        Self {
21✔
166
            store: Arc::new(DataStore::open(cache_storage_file).unwrap()),
21✔
167
            policy,
21✔
168
            _process_scoped_group: None,
21✔
169
        }
21✔
170
    }
21✔
171

172
    /// Creates a new cache using discovered `.cache` root and a process-scoped storage bin.
173
    ///
174
    /// The cache group is derived from this crate name (`reqwest-drive`), and the entry
175
    /// file is created under a process/thread scoped subdirectory so callers do not need
176
    /// to manually provide a cache path.
177
    ///
178
    /// # Errors
179
    ///
180
    /// Returns an error if discovery or process-scoped directory/file initialization fails.
181
    pub fn new_process_scoped(policy: CachePolicy) -> io::Result<Self> {
2✔
182
        let cache_root = CacheRoot::from_discovery()?;
2✔
183
        let scoped_group = Arc::new(ProcessScopedCacheGroup::new(
2✔
184
            &cache_root,
2✔
185
            env!("CARGO_PKG_NAME"),
NEW
186
        )?);
×
187
        let cache_storage_file = scoped_group.touch_thread_entry("cache_storage.bin")?;
2✔
188
        let store = DataStore::open(&cache_storage_file).map_err(|err| {
2✔
NEW
189
            io::Error::other(format!(
×
190
                "failed to open DataStore at {}: {err}",
NEW
191
                cache_storage_file.display()
×
192
            ))
NEW
193
        })?;
×
194

195
        Ok(Self {
2✔
196
            store: Arc::new(store),
2✔
197
            policy,
2✔
198
            _process_scoped_group: Some(scoped_group),
2✔
199
        })
2✔
200
    }
2✔
201

202
    /// Creates a new cache using an existing `Arc<DataStore>`.
203
    ///
204
    /// This allows sharing the cache store across multiple components.
205
    ///
206
    /// # Arguments
207
    ///
208
    /// * `store` - A shared `Arc<DataStore>` instance.
209
    /// * `policy` - Cache expiration configuration.
210
    ///
211
    /// # Concurrency
212
    ///
213
    /// This is thread-safe within a process. Avoid concurrent multi-process
214
    /// access to the same underlying store/file.
215
    pub fn with_drive_arc(store: Arc<DataStore>, policy: CachePolicy) -> Self {
2✔
216
        Self {
2✔
217
            store,
2✔
218
            policy,
2✔
219
            _process_scoped_group: None,
2✔
220
        }
2✔
221
    }
2✔
222

223
    /// Checks whether a request is cached and still valid.
224
    ///
225
    /// This method retrieves the cache entry associated with the request
226
    /// and determines if it is still within its valid TTL.
227
    ///
228
    /// # Arguments
229
    ///
230
    /// * `req` - The HTTP request to check for a cached response.
231
    ///
232
    /// # Returns
233
    ///
234
    /// Returns `true` if the request has a valid cached response; otherwise, `false`.
235
    pub async fn is_cached(&self, req: &Request) -> bool {
65✔
236
        let store = self.store.as_ref();
65✔
237

238
        let cache_key = self.generate_cache_key(req);
65✔
239
        let cache_key_bytes = cache_key.as_bytes();
65✔
240

241
        // let store = self.store.read().await;
242
        if let Ok(Some(entry_handle)) = store.read(cache_key_bytes) {
65✔
243
            tracing::debug!("Entry handle: {:?}", entry_handle);
17✔
244

245
            if let Ok(cached) = bitcode::decode::<CachedResponse>(entry_handle.as_slice()) {
17✔
246
                let now = SystemTime::now()
17✔
247
                    .duration_since(UNIX_EPOCH)
17✔
248
                    .expect("Time went backwards")
17✔
249
                    .as_millis() as u64;
17✔
250

251
                // Extract TTL based on the policy (either from headers or default)
252
                let ttl = if self.policy.respect_headers {
17✔
253
                    // Convert headers back to HeaderMap to extract TTL
254
                    let mut headers = HeaderMap::new();
15✔
255
                    for (k, v) in cached.headers.iter() {
57✔
256
                        if let Ok(header_name) = k.parse::<http::HeaderName>()
57✔
257
                            && let Ok(header_value) = HeaderValue::from_bytes(v)
57✔
258
                        {
57✔
259
                            headers.insert(header_name, header_value);
57✔
260
                        }
57✔
261
                    }
262
                    Self::extract_ttl(&headers, &self.policy)
15✔
263
                } else {
264
                    self.policy.default_ttl
2✔
265
                };
266

267
                let expected_expiration = cached.expiration_timestamp + ttl.as_millis() as u64;
17✔
268

269
                // If expired, remove from cache
270
                if now >= expected_expiration {
17✔
271
                    // tracing::debug!("Determined cache is expired. now - expected_expiration: {:?}", now - expected_expiration);
272
                    tracing::debug!(
2✔
273
                        "Cache expires at: {}",
274
                        chrono::DateTime::from_timestamp_millis(expected_expiration as i64)
×
275
                            .unwrap()
×
276
                    );
277
                    tracing::debug!(
2✔
278
                        "Expiration timestamp: {}",
279
                        chrono::DateTime::from_timestamp_millis(cached.expiration_timestamp as i64)
×
280
                            .unwrap()
×
281
                    );
282
                    tracing::debug!(
2✔
283
                        "Now: {}",
284
                        chrono::DateTime::from_timestamp_millis(now as i64).unwrap()
×
285
                    );
286

287
                    store.delete(cache_key_bytes).ok();
2✔
288
                    return false;
2✔
289
                }
15✔
290

291
                return true;
15✔
292
            }
×
293
        }
48✔
294
        false
48✔
295
    }
65✔
296

297
    /// Generates a cache key based on request method, canonicalized URL, and relevant headers.
298
    ///
299
    /// The generated key is used to uniquely identify cached responses.
300
    ///
301
    /// Key strategy:
302
    /// - Includes request method.
303
    /// - Canonicalizes URL query parameters by sorting them by key/value.
304
    /// - Includes selected representation-affecting headers.
305
    /// - Hashes sensitive header values (e.g. Authorization) before adding them to key material.
306
    ///
307
    /// # Arguments
308
    ///
309
    /// * `req` - The HTTP request for which to generate a cache key.
310
    ///
311
    /// # Returns
312
    ///
313
    /// A string representing the cache key.
314
    fn generate_cache_key(&self, req: &Request) -> String {
85,107✔
315
        let method = req.method();
85,107✔
316
        let url = Self::canonicalize_url(req.url());
85,107✔
317
        let headers = req.headers();
85,107✔
318

319
        let relevant_headers = [
85,107✔
320
            "accept",
85,107✔
321
            "accept-language",
85,107✔
322
            "content-type",
85,107✔
323
            "authorization",
85,107✔
324
            "x-api-key",
85,107✔
325
        ];
85,107✔
326

327
        let header_string = relevant_headers
85,107✔
328
            .iter()
85,107✔
329
            .filter_map(|name| {
425,535✔
330
                headers.get(*name).map(|value| {
425,535✔
331
                    let value_str = if Self::is_sensitive_header(name) {
241,828✔
332
                        format!("h:{:016x}", compute_hash(value.as_bytes()))
96,616✔
333
                    } else {
334
                        value.to_str().unwrap_or_default().to_string()
145,212✔
335
                    };
336

337
                    format!("{}={}", name, value_str)
241,828✔
338
                })
241,828✔
339
            })
425,535✔
340
            .collect::<Vec<_>>()
85,107✔
341
            .join("&");
85,107✔
342

343
        format!("{} {} {}", method, url, header_string)
85,107✔
344
    }
85,107✔
345

346
    fn canonicalize_url(url: &reqwest::Url) -> String {
85,107✔
347
        let mut normalized = url.clone();
85,107✔
348

349
        let mut query_pairs = url
85,107✔
350
            .query_pairs()
85,107✔
351
            .map(|(k, v)| (k.into_owned(), v.into_owned()))
199,025✔
352
            .collect::<Vec<_>>();
85,107✔
353

354
        if !query_pairs.is_empty() {
85,107✔
355
            query_pairs.sort_by(|(k1, v1), (k2, v2)| k1.cmp(k2).then_with(|| v1.cmp(v2)));
214,445✔
356

357
            {
358
                let mut serializer = normalized.query_pairs_mut();
73,577✔
359
                serializer.clear();
73,577✔
360
                for (key, value) in query_pairs.iter() {
199,025✔
361
                    serializer.append_pair(key, value);
199,025✔
362
                }
199,025✔
363
            }
364
        }
11,530✔
365

366
        normalized.to_string()
85,107✔
367
    }
85,107✔
368

369
    fn is_sensitive_header(name: &str) -> bool {
241,828✔
370
        matches!(
96,616✔
371
            name,
241,828✔
372
            "authorization" | "proxy-authorization" | "cookie" | "x-api-key"
241,828✔
373
        )
374
    }
241,828✔
375

376
    /// Extracts the TTL from HTTP headers or falls back to the default TTL.
377
    ///
378
    /// # Arguments
379
    ///
380
    /// * `headers` - The HTTP headers to inspect.
381
    /// * `policy` - The cache policy specifying TTL behavior.
382
    ///
383
    /// # Returns
384
    ///
385
    /// A `Duration` indicating the cache expiration time.
386
    fn extract_ttl(headers: &HeaderMap, policy: &CachePolicy) -> Duration {
40✔
387
        if !policy.respect_headers {
40✔
388
            return policy.default_ttl;
1✔
389
        }
39✔
390

391
        if let Some(cache_control) = headers.get("cache-control")
39✔
392
            && let Ok(cache_control) = cache_control.to_str()
27✔
393
        {
394
            for directive in cache_control.split(',') {
28✔
395
                if let Some(max_age) = directive.trim().strip_prefix("max-age=")
28✔
396
                    && let Ok(seconds) = max_age.parse::<u64>()
27✔
397
                {
398
                    return Duration::from_secs(seconds);
27✔
399
                }
1✔
400
            }
401
        }
12✔
402

403
        if let Some(expires) = headers.get("expires")
12✔
404
            && let Ok(expires) = expires.to_str()
1✔
405
            && let Ok(expiry_time) = DateTime::parse_from_rfc2822(expires)
1✔
406
            && let Some(duration) = expiry_time.timestamp().checked_sub(Utc::now().timestamp())
1✔
407
            && duration > 0
1✔
408
        {
409
            return Duration::from_secs(duration as u64);
1✔
410
        }
11✔
411

412
        policy.default_ttl
11✔
413
    }
40✔
414
}
415

416
#[async_trait]
417
impl Middleware for DriveCache {
418
    /// Intercepts HTTP requests to apply caching behavior.
419
    ///
420
    /// This method first checks if a valid cached response exists for the incoming request.
421
    /// - If a cached response is found and still valid, it is returned immediately.
422
    /// - If no cache entry exists, the request is forwarded to the next middleware or backend.
423
    /// - If a response is received, it is cached according to the defined `CachePolicy`.
424
    ///
425
    /// This middleware **only caches GET and HEAD requests**. Other HTTP methods are passed through without caching.
426
    ///
427
    /// # Arguments
428
    ///
429
    /// * `req` - The incoming HTTP request.
430
    /// * `extensions` - A mutable reference to request extensions, which may store metadata.
431
    /// * `next` - The next middleware in the processing chain.
432
    ///
433
    /// # Returns
434
    ///
435
    /// A `Result<Response, reqwest_middleware::Error>` that contains either:
436
    /// - A cached response (if available).
437
    /// - A fresh response from the backend, which is then cached (if applicable).
438
    ///
439
    /// # Behavior
440
    ///
441
    /// - If the request is **already cached and valid**, returns the cached response.
442
    /// - If **no cache is found**, the request is sent to the backend, and the response is cached.
443
    /// - If **the cache has expired**, the old entry is deleted, and a fresh request is made.
444
    async fn handle(
445
        &self,
446
        req: Request,
447
        extensions: &mut Extensions,
448
        next: Next<'_>,
449
    ) -> Result<Response> {
36✔
450
        let bypass_cache = extensions
451
            .get::<CacheBypass>()
452
            .map(|flag| flag.0)
453
            .unwrap_or(false);
454
        let bust_cache = extensions
455
            .get::<CacheBust>()
456
            .map(|flag| flag.0)
457
            .unwrap_or(false);
458

459
        let cache_key = self.generate_cache_key(&req);
460

461
        tracing::debug!("Handle cache key: {}", cache_key);
462

463
        let store = self.store.as_ref();
464
        let cache_key_bytes = cache_key.as_bytes();
465

466
        if req.method() == "GET" || req.method() == "HEAD" {
467
            if !bypass_cache
468
                && !bust_cache
469
                && self.is_cached(&req).await
470
                && let Ok(Some(entry_handle)) = store.read(cache_key_bytes)
471
                && let Ok(cached) = bitcode::decode::<CachedResponse>(entry_handle.as_slice())
472
            {
473
                let mut headers = HeaderMap::new();
474
                for (k, v) in cached.headers {
475
                    if let Ok(header_name) = k.parse::<http::HeaderName>()
476
                        && let Ok(header_value) = HeaderValue::from_bytes(&v)
477
                    {
478
                        headers.insert(header_name, header_value);
479
                    }
480
                }
481
                let status = StatusCode::from_u16(cached.status).unwrap_or(StatusCode::OK);
482
                return Ok(build_response(status, headers, Bytes::from(cached.body)));
483
            }
484

485
            let response = next.run(req, extensions).await?;
486
            let status = response.status();
487
            let headers = response.headers().clone();
488
            let body = response.bytes().await?.to_vec();
489

490
            let ttl = Self::extract_ttl(&headers, &self.policy);
491
            let expiration_timestamp = SystemTime::now()
492
                .duration_since(UNIX_EPOCH)
493
                .expect("Time went backwards")
494
                .as_millis() as u64
495
                + ttl.as_millis() as u64;
496

497
            let body_clone = body.clone();
498

499
            let should_cache = match &self.policy.cache_status_override {
500
                Some(status_codes) => status_codes.contains(&status.as_u16()),
501
                None => status.is_success(),
502
            };
503

504
            if should_cache && !bypass_cache {
505
                let serialized = bitcode::encode(&CachedResponse {
506
                    status: status.as_u16(),
507
                    headers: headers
508
                        .iter()
509
                        .map(|(k, v)| (k.to_string(), v.as_bytes().to_vec()))
70✔
510
                        .collect(),
511
                    body,
512
                    expiration_timestamp,
513
                });
514

515
                tracing::debug!("Writing cache with key: {}", cache_key);
516
                store.write(cache_key_bytes, serialized.as_slice()).ok();
517
            }
518

519
            return Ok(build_response(status, headers, Bytes::from(body_clone)));
520
        }
521

522
        next.run(req, extensions).await
523
    }
36✔
524
}
525

526
/// Constructs a `reqwest::Response` from a given status code, headers, and body.
527
///
528
/// This function is used to rebuild an HTTP response from cached data,
529
/// ensuring that it correctly retains headers and status information.
530
///
531
/// # Arguments
532
///
533
/// * `status` - The HTTP status code of the response.
534
/// * `headers` - A `HeaderMap` containing response headers.
535
/// * `body` - A `Bytes` object containing the response body.
536
///
537
/// # Returns
538
///
539
/// A `reqwest::Response` representing the reconstructed HTTP response.
540
///
541
/// # Panics
542
///
543
/// This function will panic if the response body fails to be constructed.
544
fn build_response(status: StatusCode, headers: HeaderMap, body: Bytes) -> Response {
36✔
545
    let mut response_builder = http::Response::builder().status(status);
36✔
546

547
    for (key, value) in headers.iter() {
133✔
548
        response_builder = response_builder.header(key, value);
133✔
549
    }
133✔
550

551
    let http_response = response_builder
36✔
552
        .body(body)
36✔
553
        .expect("Failed to create HTTP response");
36✔
554

555
    Response::from(http_response)
36✔
556
}
36✔
557

558
#[cfg(test)]
559
mod tests {
560
    use super::*;
561
    use rand::rngs::StdRng;
562
    use rand::{RngExt, SeedableRng};
563
    use reqwest::Method;
564
    use std::collections::{HashMap, HashSet};
565
    use std::time::{SystemTime, UNIX_EPOCH};
566
    use tempfile::TempDir;
567

568
    fn build_request(method: Method, url: &str, headers: &[(&str, Option<&str>)]) -> Request {
35,007✔
569
        // Construct `reqwest::Request` directly rather than building a
570
        // `reqwest::Client` per-iteration. Creating a `Client` repeatedly in
571
        // tight loops was the dominant cost on some CI runners; building the
572
        // `Request` directly avoids that overhead while remaining functionally
573
        // equivalent for these key-generation tests.
574
        let mut request = Request::new(
35,007✔
575
            method,
35,007✔
576
            reqwest::Url::parse(url).expect("failed to parse request URL"),
35,007✔
577
        );
578

579
        for (name, value) in headers {
175,011✔
580
            if let Some(value) = value {
175,011✔
581
                let header_name = http::header::HeaderName::from_bytes(name.as_bytes())
116,691✔
582
                    .expect("invalid header name");
116,691✔
583
                let header_value =
116,691✔
584
                    http::header::HeaderValue::from_str(value).expect("invalid header value");
116,691✔
585
                request.headers_mut().insert(header_name, header_value);
116,691✔
586
            }
116,691✔
587
        }
588

589
        request
35,007✔
590
    }
35,007✔
591

592
    fn build_cache_for_tests() -> DriveCache {
3✔
593
        let temp_dir = TempDir::new().expect("failed to create temp dir");
3✔
594
        let cache_path = temp_dir.path().join("cache_key_matrix.bin");
3✔
595
        DriveCache::new(&cache_path, CachePolicy::default())
3✔
596
    }
3✔
597

598
    fn random_token(rng: &mut StdRng, min_len: usize, max_len: usize) -> String {
451,694✔
599
        let alphabet = b"abcdefghijklmnopqrstuvwxyz0123456789";
451,694✔
600
        let token_len = rng.random_range(min_len..=max_len);
451,694✔
601

602
        (0..token_len)
451,694✔
603
            .map(|_| {
3,583,828✔
604
                let index = rng.random_range(0..alphabet.len());
3,583,828✔
605
                alphabet[index] as char
3,583,828✔
606
            })
3,583,828✔
607
            .collect()
451,694✔
608
    }
451,694✔
609

610
    fn build_random_request(rng: &mut StdRng) -> Request {
49,999✔
611
        let methods = [
49,999✔
612
            Method::GET,
49,999✔
613
            Method::HEAD,
49,999✔
614
            Method::POST,
49,999✔
615
            Method::PUT,
49,999✔
616
            Method::PATCH,
49,999✔
617
            Method::DELETE,
49,999✔
618
        ];
49,999✔
619

620
        let method = methods[rng.random_range(0..methods.len())].clone();
49,999✔
621
        let mut url = format!(
49,999✔
622
            "https://example.test/{}/{}",
623
            random_token(rng, 3, 10),
49,999✔
624
            random_token(rng, 3, 10)
49,999✔
625
        );
626

627
        let query_pair_count = rng.random_range(0..=6);
49,999✔
628
        if query_pair_count > 0 {
49,999✔
629
            url.push('?');
42,942✔
630
            for query_index in 0..query_pair_count {
150,877✔
631
                if query_index > 0 {
150,877✔
632
                    url.push('&');
107,935✔
633
                }
107,935✔
634

635
                let query_key = random_token(rng, 1, 8);
150,877✔
636
                let query_value = random_token(rng, 0, 12);
150,877✔
637
                url.push_str(&query_key);
150,877✔
638
                url.push('=');
150,877✔
639
                url.push_str(&query_value);
150,877✔
640
            }
641
        }
7,057✔
642

643
        let mut request = Request::new(
49,999✔
644
            method,
49,999✔
645
            reqwest::Url::parse(&url).expect("failed to parse randomized URL"),
49,999✔
646
        );
647

648
        if rng.random::<bool>() {
49,999✔
649
            let accept_values = ["application/json", "text/plain", "*/*"];
25,001✔
650
            request.headers_mut().insert(
25,001✔
651
                http::header::ACCEPT,
25,001✔
652
                http::header::HeaderValue::from_str(accept_values[rng.random_range(0..3)])
25,001✔
653
                    .expect("invalid accept header value"),
25,001✔
654
            );
25,001✔
655
        }
25,001✔
656

657
        if rng.random::<bool>() {
49,999✔
658
            let language_values = ["en-US", "fr-FR", "es-ES", "de-DE"];
24,975✔
659
            request.headers_mut().insert(
24,975✔
660
                http::header::ACCEPT_LANGUAGE,
24,975✔
661
                http::header::HeaderValue::from_str(language_values[rng.random_range(0..4)])
24,975✔
662
                    .expect("invalid accept-language header value"),
24,975✔
663
            );
24,975✔
664
        }
25,024✔
665

666
        if rng.random::<bool>() {
49,999✔
667
            let content_type_values = ["application/json", "application/xml", "text/plain"];
25,211✔
668
            request.headers_mut().insert(
25,211✔
669
                http::header::CONTENT_TYPE,
25,211✔
670
                http::header::HeaderValue::from_str(content_type_values[rng.random_range(0..3)])
25,211✔
671
                    .expect("invalid content-type header value"),
25,211✔
672
            );
25,211✔
673
        }
25,211✔
674

675
        if rng.random::<bool>() {
49,999✔
676
            let authorization_value = format!("Bearer {}", random_token(rng, 16, 48));
24,989✔
677
            request.headers_mut().insert(
24,989✔
678
                http::header::AUTHORIZATION,
24,989✔
679
                http::header::HeaderValue::from_str(&authorization_value)
24,989✔
680
                    .expect("invalid authorization header value"),
24,989✔
681
            );
24,989✔
682
        }
25,010✔
683

684
        if rng.random::<bool>() {
49,999✔
685
            let api_key_value = random_token(rng, 12, 32);
24,953✔
686
            request.headers_mut().insert(
24,953✔
687
                http::header::HeaderName::from_static("x-api-key"),
24,953✔
688
                http::header::HeaderValue::from_str(&api_key_value)
24,953✔
689
                    .expect("invalid x-api-key header value"),
24,953✔
690
            );
24,953✔
691
        }
25,046✔
692

693
        request
49,999✔
694
    }
49,999✔
695

696
    #[test]
697
    fn fuzz_cache_key_hash_collisions_uses_library_key_generator() {
1✔
698
        let temp_dir = TempDir::new().expect("failed to create temp dir");
1✔
699
        let cache_path = temp_dir.path().join("cache_key_fuzz.bin");
1✔
700
        let cache = DriveCache::new(&cache_path, CachePolicy::default());
1✔
701

702
        let mut observed_hash_to_key: HashMap<u64, String> = HashMap::new();
1✔
703
        let mut random_generator = StdRng::seed_from_u64(0xD15EA5E5);
1✔
704

705
        let sample_count = 50_000;
1✔
706

707
        let mut distinct_key_count = 0usize;
1✔
708

709
        // Seed one known key first so the equality-assert branch is exercised
710
        // deterministically without relying on random hash collisions.
711
        let duplicate_request = build_request(
1✔
712
            Method::GET,
1✔
713
            "https://example.test/duplicate?a=1&b=2",
1✔
714
            &[("accept", Some("application/json"))],
1✔
715
        );
716
        let duplicate_key = cache.generate_cache_key(&duplicate_request);
1✔
717
        let duplicate_hash = compute_hash(duplicate_key.as_bytes());
1✔
718
        observed_hash_to_key.insert(duplicate_hash, duplicate_key.clone());
1✔
719
        if let Some(existing_key) = observed_hash_to_key.get(&duplicate_hash) {
1✔
720
            assert_eq!(existing_key, &duplicate_key);
1✔
NEW
721
        }
×
722

723
        for sample_index in 0..sample_count {
50,000✔
724
            let request = if sample_index == 0 {
50,000✔
725
                // Reuse the exact same request as the seeded entry so this
726
                // loop deterministically hits the "hash already seen" branch.
727
                // We are NOT expecting collisions between distinct keys.
728
                // A distinct-key collision still fails the test via `assert_eq!`.
729
                build_request(
1✔
730
                    Method::GET,
1✔
731
                    "https://example.test/duplicate?a=1&b=2",
1✔
732
                    &[("accept", Some("application/json"))],
1✔
733
                )
734
            } else {
735
                build_random_request(&mut random_generator)
49,999✔
736
            };
737

738
            let cache_key = cache.generate_cache_key(&request);
50,000✔
739
            let hash = compute_hash(cache_key.as_bytes());
50,000✔
740

741
            if let Some(existing_key) = observed_hash_to_key.get(&hash) {
50,000✔
742
                assert_eq!(
1✔
743
                    existing_key, &cache_key,
1✔
744
                    "hash collision detected for distinct cache keys"
745
                );
746
            } else {
49,999✔
747
                observed_hash_to_key.insert(hash, cache_key);
49,999✔
748
                distinct_key_count += 1;
49,999✔
749
            }
49,999✔
750
        }
751

752
        assert!(
1✔
753
            distinct_key_count > sample_count / 2,
1✔
754
            "random generation produced too few distinct keys"
755
        );
756
    }
1✔
757

758
    #[tokio::test]
759
    async fn is_cached_uses_default_ttl_when_respect_headers_is_disabled() {
1✔
760
        let temp_dir = TempDir::new().expect("failed to create temp dir");
1✔
761
        let cache_path = temp_dir.path().join("cache_default_ttl.bin");
1✔
762
        let cache = DriveCache::new(
1✔
763
            &cache_path,
1✔
764
            CachePolicy {
1✔
765
                default_ttl: Duration::from_secs(60),
1✔
766
                respect_headers: false,
1✔
767
                cache_status_override: None,
1✔
768
            },
1✔
769
        );
770

771
        let request = build_request(
1✔
772
            Method::GET,
1✔
773
            "https://example.test/default-ttl",
1✔
774
            &[("accept", Some("application/json"))],
1✔
775
        );
776
        let cache_key = cache.generate_cache_key(&request);
1✔
777
        let cache_key_bytes = cache_key.as_bytes();
1✔
778

779
        let now = SystemTime::now()
1✔
780
            .duration_since(UNIX_EPOCH)
1✔
781
            .expect("time went backwards")
1✔
782
            .as_millis() as u64;
1✔
783

784
        let cached = CachedResponse {
1✔
785
            status: 200,
1✔
786
            headers: vec![("cache-control".to_string(), b"max-age=0".to_vec())],
1✔
787
            body: b"ok".to_vec(),
1✔
788
            expiration_timestamp: now,
1✔
789
        };
1✔
790

791
        let serialized = bitcode::encode(&cached);
1✔
792
        cache
1✔
793
            .store
1✔
794
            .as_ref()
1✔
795
            .write(cache_key_bytes, serialized.as_slice())
1✔
796
            .expect("write cached entry");
1✔
797

798
        assert!(cache.is_cached(&request).await);
1✔
799
    }
1✔
800

801
    #[tokio::test]
802
    async fn is_cached_evicts_entry_when_expired() {
1✔
803
        let temp_dir = TempDir::new().expect("failed to create temp dir");
1✔
804
        let cache_path = temp_dir.path().join("cache_expired_evict.bin");
1✔
805
        let cache = DriveCache::new(
1✔
806
            &cache_path,
1✔
807
            CachePolicy {
1✔
808
                default_ttl: Duration::from_millis(0),
1✔
809
                respect_headers: false,
1✔
810
                cache_status_override: None,
1✔
811
            },
1✔
812
        );
813

814
        let request = build_request(
1✔
815
            Method::GET,
1✔
816
            "https://example.test/expired-entry",
1✔
817
            &[("accept", Some("application/json"))],
1✔
818
        );
819
        let cache_key = cache.generate_cache_key(&request);
1✔
820
        let cache_key_bytes = cache_key.as_bytes();
1✔
821

822
        let cached = CachedResponse {
1✔
823
            status: 200,
1✔
824
            headers: Vec::new(),
1✔
825
            body: b"stale".to_vec(),
1✔
826
            expiration_timestamp: 0,
1✔
827
        };
1✔
828

829
        let serialized = bitcode::encode(&cached);
1✔
830
        cache
1✔
831
            .store
1✔
832
            .as_ref()
1✔
833
            .write(cache_key_bytes, serialized.as_slice())
1✔
834
            .expect("write cached entry");
1✔
835

836
        assert!(!cache.is_cached(&request).await);
1✔
837
        let stored = cache
1✔
838
            .store
1✔
839
            .as_ref()
1✔
840
            .read(cache_key_bytes)
1✔
841
            .expect("read cache key after eviction");
1✔
842
        assert!(stored.is_none(), "expired key should be evicted");
1✔
843
    }
1✔
844

845
    #[test]
846
    fn extract_ttl_returns_default_when_header_respect_is_disabled() {
1✔
847
        let policy = CachePolicy {
1✔
848
            default_ttl: Duration::from_secs(321),
1✔
849
            respect_headers: false,
1✔
850
            cache_status_override: None,
1✔
851
        };
1✔
852

853
        let mut headers = HeaderMap::new();
1✔
854
        headers.insert("cache-control", HeaderValue::from_static("max-age=1"));
1✔
855

856
        assert_eq!(
1✔
857
            DriveCache::extract_ttl(&headers, &policy),
1✔
858
            policy.default_ttl
859
        );
860
    }
1✔
861

862
    #[test]
863
    fn extract_ttl_uses_cache_control_max_age_when_present() {
1✔
864
        let policy = CachePolicy {
1✔
865
            default_ttl: Duration::from_secs(321),
1✔
866
            respect_headers: true,
1✔
867
            cache_status_override: None,
1✔
868
        };
1✔
869

870
        let mut headers = HeaderMap::new();
1✔
871
        headers.insert(
1✔
872
            "cache-control",
873
            HeaderValue::from_static("public, max-age=42"),
1✔
874
        );
875

876
        assert_eq!(
1✔
877
            DriveCache::extract_ttl(&headers, &policy),
1✔
878
            Duration::from_secs(42)
1✔
879
        );
880
    }
1✔
881

882
    #[test]
883
    fn extract_ttl_uses_expires_header_when_cache_control_missing() {
1✔
884
        let policy = CachePolicy {
1✔
885
            default_ttl: Duration::from_secs(600),
1✔
886
            respect_headers: true,
1✔
887
            cache_status_override: None,
1✔
888
        };
1✔
889

890
        let mut headers = HeaderMap::new();
1✔
891
        let future = (Utc::now() + chrono::Duration::seconds(120)).to_rfc2822();
1✔
892
        headers.insert(
1✔
893
            "expires",
894
            HeaderValue::from_str(&future).expect("expires header should be valid"),
1✔
895
        );
896

897
        let ttl = DriveCache::extract_ttl(&headers, &policy);
1✔
898
        assert!(ttl > Duration::from_secs(0));
1✔
899
        assert!(ttl < policy.default_ttl);
1✔
900
    }
1✔
901

902
    #[test]
903
    fn exhaustive_cache_key_matrix_no_hash_collisions_for_distinct_keys() {
1✔
904
        let cache = build_cache_for_tests();
1✔
905

906
        let (methods, paths, queries) = (
1✔
907
            vec![
1✔
908
                Method::GET,
1✔
909
                Method::HEAD,
1✔
910
                Method::POST,
1✔
911
                Method::PUT,
1✔
912
                Method::PATCH,
1✔
913
                Method::DELETE,
1✔
914
            ],
1✔
915
            vec!["/resource", "/resource/v2", "/resource/deep/path"],
1✔
916
            vec![
1✔
917
                "", "?a=1", "?a=2", "?a=1&b=2", "?b=2&a=1", "?a=1&a=2", "?a=1&a=3", "?z=9",
1✔
918
            ],
1✔
919
        );
1✔
920

921
        let accept_values = [None, Some("application/json"), Some("text/plain")];
1✔
922
        let language_values = [None, Some("en-US"), Some("fr-FR")];
1✔
923
        let content_type_values = [None, Some("application/json"), Some("application/xml")];
1✔
924
        let authorization_values = [None, Some("Bearer alpha-token"), Some("Bearer beta-token")];
1✔
925
        let api_key_values = [None, Some("alpha-api-key"), Some("beta-api-key")];
1✔
926

927
        let mut hash_to_key: HashMap<u64, String> = HashMap::new();
1✔
928
        let mut distinct_keys: HashSet<String> = HashSet::new();
1✔
929
        let mut sample_count = 0usize;
1✔
930

931
        for method in &methods {
6✔
932
            for path in &paths {
18✔
933
                for query in &queries {
144✔
934
                    for accept in accept_values {
432✔
935
                        for accept_language in language_values {
1,296✔
936
                            for content_type in content_type_values {
3,888✔
937
                                for authorization in authorization_values {
11,664✔
938
                                    for api_key in api_key_values {
34,992✔
939
                                        sample_count += 1;
34,992✔
940

941
                                        let url = format!("https://example.test{}{}", path, query);
34,992✔
942
                                        let request = build_request(
34,992✔
943
                                            method.clone(),
34,992✔
944
                                            &url,
34,992✔
945
                                            &[
34,992✔
946
                                                ("accept", accept),
34,992✔
947
                                                ("accept-language", accept_language),
34,992✔
948
                                                ("content-type", content_type),
34,992✔
949
                                                ("authorization", authorization),
34,992✔
950
                                                ("x-api-key", api_key),
34,992✔
951
                                            ],
34,992✔
952
                                        );
953

954
                                        let cache_key = cache.generate_cache_key(&request);
34,992✔
955
                                        let hash = compute_hash(cache_key.as_bytes());
34,992✔
956

957
                                        if let Some(existing_key) = hash_to_key.get(&hash) {
34,992✔
958
                                            assert_eq!(
4,374✔
959
                                                existing_key, &cache_key,
4,374✔
960
                                                "hash collision detected for distinct cache keys"
961
                                            );
962
                                        } else {
30,618✔
963
                                            hash_to_key.insert(hash, cache_key.clone());
30,618✔
964
                                        }
30,618✔
965

966
                                        distinct_keys.insert(cache_key);
34,992✔
967
                                    }
968
                                }
969
                            }
970
                        }
971
                    }
972
                }
973
            }
974
        }
975

976
        let expected_sample_count = methods.len()
1✔
977
            * paths.len()
1✔
978
            * queries.len()
1✔
979
            * accept_values.len()
1✔
980
            * language_values.len()
1✔
981
            * content_type_values.len()
1✔
982
            * authorization_values.len()
1✔
983
            * api_key_values.len();
1✔
984
        assert_eq!(sample_count, expected_sample_count);
1✔
985
        assert!(
1✔
986
            distinct_keys.len() > sample_count / 2,
1✔
987
            "matrix generation produced too few distinct keys"
988
        );
989
    }
1✔
990

991
    #[test]
992
    fn cache_key_query_reordering_is_canonical_and_hash_stable() {
1✔
993
        let cache = build_cache_for_tests();
1✔
994

995
        let request_a = build_request(
1✔
996
            Method::GET,
1✔
997
            "https://example.test/resource?a=1&b=2",
1✔
998
            &[("accept", Some("application/json"))],
1✔
999
        );
1000
        let request_b = build_request(
1✔
1001
            Method::GET,
1✔
1002
            "https://example.test/resource?b=2&a=1",
1✔
1003
            &[("accept", Some("application/json"))],
1✔
1004
        );
1005

1006
        let key_a = cache.generate_cache_key(&request_a);
1✔
1007
        let key_b = cache.generate_cache_key(&request_b);
1✔
1008

1009
        assert_eq!(key_a, key_b);
1✔
1010
        assert_eq!(
1✔
1011
            compute_hash(key_a.as_bytes()),
1✔
1012
            compute_hash(key_b.as_bytes())
1✔
1013
        );
1014
    }
1✔
1015

1016
    #[test]
1017
    fn cache_key_changes_for_each_response_affecting_dimension() {
1✔
1018
        let cache = build_cache_for_tests();
1✔
1019

1020
        let base_request = build_request(
1✔
1021
            Method::GET,
1✔
1022
            "https://example.test/resource?a=1&b=2",
1✔
1023
            &[
1✔
1024
                ("accept", Some("application/json")),
1✔
1025
                ("accept-language", Some("en-US")),
1✔
1026
                ("content-type", Some("application/json")),
1✔
1027
                ("authorization", Some("Bearer alpha-token")),
1✔
1028
                ("x-api-key", Some("alpha-api-key")),
1✔
1029
            ],
1✔
1030
        );
1031
        let base_key = cache.generate_cache_key(&base_request);
1✔
1032
        let base_hash = compute_hash(base_key.as_bytes());
1✔
1033

1034
        let variants = vec![
1✔
1035
            build_request(
1✔
1036
                Method::POST,
1✔
1037
                "https://example.test/resource?a=1&b=2",
1✔
1038
                &[
1✔
1039
                    ("accept", Some("application/json")),
1✔
1040
                    ("accept-language", Some("en-US")),
1✔
1041
                    ("content-type", Some("application/json")),
1✔
1042
                    ("authorization", Some("Bearer alpha-token")),
1✔
1043
                    ("x-api-key", Some("alpha-api-key")),
1✔
1044
                ],
1✔
1045
            ),
1046
            build_request(
1✔
1047
                Method::GET,
1✔
1048
                "https://example.test/resource/v2?a=1&b=2",
1✔
1049
                &[
1✔
1050
                    ("accept", Some("application/json")),
1✔
1051
                    ("accept-language", Some("en-US")),
1✔
1052
                    ("content-type", Some("application/json")),
1✔
1053
                    ("authorization", Some("Bearer alpha-token")),
1✔
1054
                    ("x-api-key", Some("alpha-api-key")),
1✔
1055
                ],
1✔
1056
            ),
1057
            build_request(
1✔
1058
                Method::GET,
1✔
1059
                "https://example.test/resource?a=99&b=2",
1✔
1060
                &[
1✔
1061
                    ("accept", Some("application/json")),
1✔
1062
                    ("accept-language", Some("en-US")),
1✔
1063
                    ("content-type", Some("application/json")),
1✔
1064
                    ("authorization", Some("Bearer alpha-token")),
1✔
1065
                    ("x-api-key", Some("alpha-api-key")),
1✔
1066
                ],
1✔
1067
            ),
1068
            build_request(
1✔
1069
                Method::GET,
1✔
1070
                "https://example.test/resource?a=1&b=2",
1✔
1071
                &[
1✔
1072
                    ("accept", Some("text/plain")),
1✔
1073
                    ("accept-language", Some("en-US")),
1✔
1074
                    ("content-type", Some("application/json")),
1✔
1075
                    ("authorization", Some("Bearer alpha-token")),
1✔
1076
                    ("x-api-key", Some("alpha-api-key")),
1✔
1077
                ],
1✔
1078
            ),
1079
            build_request(
1✔
1080
                Method::GET,
1✔
1081
                "https://example.test/resource?a=1&b=2",
1✔
1082
                &[
1✔
1083
                    ("accept", Some("application/json")),
1✔
1084
                    ("accept-language", Some("fr-FR")),
1✔
1085
                    ("content-type", Some("application/json")),
1✔
1086
                    ("authorization", Some("Bearer alpha-token")),
1✔
1087
                    ("x-api-key", Some("alpha-api-key")),
1✔
1088
                ],
1✔
1089
            ),
1090
            build_request(
1✔
1091
                Method::GET,
1✔
1092
                "https://example.test/resource?a=1&b=2",
1✔
1093
                &[
1✔
1094
                    ("accept", Some("application/json")),
1✔
1095
                    ("accept-language", Some("en-US")),
1✔
1096
                    ("content-type", Some("application/xml")),
1✔
1097
                    ("authorization", Some("Bearer alpha-token")),
1✔
1098
                    ("x-api-key", Some("alpha-api-key")),
1✔
1099
                ],
1✔
1100
            ),
1101
            build_request(
1✔
1102
                Method::GET,
1✔
1103
                "https://example.test/resource?a=1&b=2",
1✔
1104
                &[
1✔
1105
                    ("accept", Some("application/json")),
1✔
1106
                    ("accept-language", Some("en-US")),
1✔
1107
                    ("content-type", Some("application/json")),
1✔
1108
                    ("authorization", Some("Bearer beta-token")),
1✔
1109
                    ("x-api-key", Some("alpha-api-key")),
1✔
1110
                ],
1✔
1111
            ),
1112
            build_request(
1✔
1113
                Method::GET,
1✔
1114
                "https://example.test/resource?a=1&b=2",
1✔
1115
                &[
1✔
1116
                    ("accept", Some("application/json")),
1✔
1117
                    ("accept-language", Some("en-US")),
1✔
1118
                    ("content-type", Some("application/json")),
1✔
1119
                    ("authorization", Some("Bearer alpha-token")),
1✔
1120
                    ("x-api-key", Some("beta-api-key")),
1✔
1121
                ],
1✔
1122
            ),
1123
        ];
1124

1125
        for variant in variants {
8✔
1126
            let variant_key = cache.generate_cache_key(&variant);
8✔
1127
            let variant_hash = compute_hash(variant_key.as_bytes());
8✔
1128

1129
            assert_ne!(
8✔
1130
                variant_key, base_key,
1131
                "variant unexpectedly produced same key"
1132
            );
1133
            assert_ne!(
8✔
1134
                variant_hash, base_hash,
1135
                "variant unexpectedly produced same hash"
1136
            );
1137
        }
1138
    }
1✔
1139

1140
    /*
1141
    Stress experiment (disabled):
1142
    - This 100,000,000-sample collision test worked (no collisions observed).
1143
    - End-to-end runtime was several hours, even in `--release` mode.
1144
    - A Rayon parallelization attempt did not produce a meaningful speedup for
1145
      this workload, so the test is commented out to keep normal test cycles fast.
1146

1147
        // Kept as commented code because it is only used by the disabled stress
1148
        // test below. If we re-enable that test in the future, this helper can be
1149
        // uncommented together with it.
1150
        // fn build_unique_request_from_index(index: u64) -> Request {
1151
        //     let methods = [
1152
        //         Method::GET,
1153
        //         Method::HEAD,
1154
        //         Method::POST,
1155
        //         Method::PUT,
1156
        //         Method::PATCH,
1157
        //         Method::DELETE,
1158
        //     ];
1159
        //
1160
        //     let method = methods[(index % methods.len() as u64) as usize].clone();
1161
        //     let path = format!("/resource/{}/{}/{}", index % 97, index % 503, index % 9973);
1162
        //
1163
        //     let query = format!(
1164
        //         "a={}&b={}&c={}&d={}",
1165
        //         index,
1166
        //         index.wrapping_mul(31),
1167
        //         index.rotate_left(7),
1168
        //         index ^ 0xA5A5_A5A5_A5A5_A5A5
1169
        //     );
1170
        //
1171
        //     let url = format!("https://example.test{}?{}", path, query);
1172
        //
1173
        //     let accept_values = ["application/json", "text/plain", "STAR_SLASH_STAR"];
1174
        //     let language_values = ["en-US", "fr-FR", "es-ES", "de-DE"];
1175
        //     let content_type_values = ["application/json", "application/xml", "text/plain"];
1176
        //
1177
        //     let mut request = Request::new(
1178
        //         method,
1179
        //         reqwest::Url::parse(&url).expect("failed to parse stress URL"),
1180
        //     );
1181
        //
1182
        //     request.headers_mut().insert(
1183
        //         http::header::ACCEPT,
1184
        //         http::header::HeaderValue::from_str(
1185
        //             accept_values[(index % accept_values.len() as u64) as usize],
1186
        //         )
1187
        //         .expect("invalid accept header value"),
1188
        //     );
1189
        //     request.headers_mut().insert(
1190
        //         http::header::ACCEPT_LANGUAGE,
1191
        //         http::header::HeaderValue::from_str(
1192
        //             language_values[(index % language_values.len() as u64) as usize],
1193
        //         )
1194
        //         .expect("invalid accept-language header value"),
1195
        //     );
1196
        //     request.headers_mut().insert(
1197
        //         http::header::CONTENT_TYPE,
1198
        //         http::header::HeaderValue::from_str(
1199
        //             content_type_values[(index % content_type_values.len() as u64) as usize],
1200
        //         )
1201
        //         .expect("invalid content-type header value"),
1202
        //     );
1203
        //
1204
        //     let authorization_value = format!("Bearer token-{:016x}", index);
1205
        //     request.headers_mut().insert(
1206
        //         http::header::AUTHORIZATION,
1207
        //         http::header::HeaderValue::from_str(&authorization_value)
1208
        //             .expect("invalid authorization header value"),
1209
        //     );
1210
        //
1211
        //     let api_key_value = format!("api-key-{:016x}", index.rotate_right(11));
1212
        //     request.headers_mut().insert(
1213
        //         http::header::HeaderName::from_static("x-api-key"),
1214
        //         http::header::HeaderValue::from_str(&api_key_value)
1215
        //             .expect("invalid x-api-key header value"),
1216
        //     );
1217
        //
1218
        //     request
1219
        // }
1220

1221
    #[test]
1222
    #[ignore = "expensive: runs 100,000,000 samples"]
1223
    fn cache_key_hash_collision_stress_100_million() {
1224
        init_test_tracing();
1225

1226
        let cache = build_cache_for_tests();
1227

1228
        let samples = 100_000_000_u64;
1229
        let mut seen_hashes: HashSet<u64> = HashSet::new();
1230
        let started_at = Instant::now();
1231

1232
        for index in 0..samples {
1233
            let request = build_unique_request_from_index(index);
1234
            let cache_key = cache.generate_cache_key(&request);
1235
            let hash = compute_hash(cache_key.as_bytes());
1236

1237
            assert!(
1238
                seen_hashes.insert(hash),
1239
                "hash collision detected in stress test at sample index {} (hash={})",
1240
                index,
1241
                hash
1242
            );
1243

1244
            let completed = index + 1;
1245
            if completed % 10_000 == 0 {
1246
                let elapsed = started_at.elapsed();
1247
                let pct = (completed as f64 / samples as f64) * 100.0;
1248
                tracing::info!(
1249
                    "stress progress: {}/{} ({:.4}%) elapsed={:?}",
1250
                    completed,
1251
                    samples,
1252
                    pct,
1253
                    elapsed
1254
                );
1255
            }
1256
        }
1257

1258
        tracing::info!(
1259
            "stress complete: {} samples in {:?}",
1260
            samples,
1261
            started_at.elapsed()
1262
        );
1263
    }
1264
    */
1265
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc