tower_http_cache/chunks.rs
1//! Chunk-based caching for large files and efficient range request handling.
2//!
3//! This module provides a chunk-based caching system that splits large files into
4//! fixed-size chunks, enabling efficient storage and retrieval of partial content.
5//! This is particularly useful for:
6//! - Video streaming with range requests
7//! - Large file downloads with resume support
8//! - Efficient memory usage for large responses
9//!
10//! # Examples
11//!
12//! ```rust
13//! use tower_http_cache::chunks::{ChunkCache, ChunkMetadata};
14//! use bytes::Bytes;
15//! use http::StatusCode;
16//!
17//! # tokio_test::block_on(async {
18//! let chunk_cache = ChunkCache::new(1024 * 1024); // 1MB chunks
19//!
20//! let metadata = ChunkMetadata {
21//! total_size: 10 * 1024 * 1024, // 10MB file
22//! content_type: "video/mp4".to_string(),
23//! etag: Some("abc123".to_string()),
24//! last_modified: None,
25//! status: StatusCode::OK,
26//! version: http::Version::HTTP_11,
27//! headers: vec![],
28//! };
29//!
30//! let entry = chunk_cache.get_or_create("video.mp4".to_string(), metadata);
31//!
32//! // Add chunks as they're received
33//! entry.add_chunk(0, Bytes::from(vec![0u8; 1024 * 1024]));
34//! entry.add_chunk(1, Bytes::from(vec![1u8; 1024 * 1024]));
35//!
36//! // Retrieve a range
37//! let range = entry.get_range(0, 1024 * 1024 - 1);
38//! assert!(range.is_some());
39//! # });
40//! ```
41
42use bytes::Bytes;
43use dashmap::DashMap;
44use http::{StatusCode, Version};
45use std::sync::Arc;
46
47/// Chunk-based cache entry for large files.
48///
49/// A `ChunkedEntry` splits a large file into fixed-size chunks, allowing
50/// efficient storage and retrieval of partial content. This is particularly
51/// useful for range requests where only a portion of the file is needed.
52#[derive(Debug, Clone)]
53pub struct ChunkedEntry {
54 /// Metadata about the full file
55 pub metadata: ChunkMetadata,
56
57 /// Map of chunk index to chunk data
58 pub chunks: Arc<DashMap<u64, Bytes>>,
59
60 /// Chunk size in bytes (default: 1MB)
61 pub chunk_size: usize,
62}
63
64/// Metadata for a chunked cache entry.
65///
66/// Contains HTTP response metadata without the body, which is stored
67/// separately in chunks.
68#[derive(Debug, Clone)]
69pub struct ChunkMetadata {
70 /// Total size of the file in bytes
71 pub total_size: u64,
72
73 /// Content-Type header value
74 pub content_type: String,
75
76 /// ETag header value, if present
77 pub etag: Option<String>,
78
79 /// Last-Modified header value, if present
80 pub last_modified: Option<String>,
81
82 /// HTTP status code
83 pub status: StatusCode,
84
85 /// HTTP version
86 pub version: Version,
87
88 /// Additional headers (name, value) pairs
89 pub headers: Vec<(String, Vec<u8>)>,
90}
91
92impl ChunkedEntry {
93 /// Creates a new chunked entry with the given metadata and chunk size.
94 ///
95 /// # Arguments
96 ///
97 /// * `metadata` - File metadata including size, content type, and headers
98 /// * `chunk_size` - Size of each chunk in bytes
99 ///
100 /// # Examples
101 ///
102 /// ```rust
103 /// use tower_http_cache::chunks::{ChunkedEntry, ChunkMetadata};
104 /// use http::StatusCode;
105 ///
106 /// let metadata = ChunkMetadata {
107 /// total_size: 10_000_000,
108 /// content_type: "video/mp4".to_string(),
109 /// etag: None,
110 /// last_modified: None,
111 /// status: StatusCode::OK,
112 /// version: http::Version::HTTP_11,
113 /// headers: vec![],
114 /// };
115 ///
116 /// let entry = ChunkedEntry::new(metadata, 1024 * 1024);
117 /// assert_eq!(entry.chunk_size, 1024 * 1024);
118 /// ```
119 pub fn new(metadata: ChunkMetadata, chunk_size: usize) -> Self {
120 Self {
121 metadata,
122 chunks: Arc::new(DashMap::new()),
123 chunk_size,
124 }
125 }
126
127 /// Adds a chunk to the entry.
128 ///
129 /// # Arguments
130 ///
131 /// * `index` - Chunk index (0-based)
132 /// * `data` - Chunk data
133 ///
134 /// # Examples
135 ///
136 /// ```rust
137 /// # use tower_http_cache::chunks::{ChunkedEntry, ChunkMetadata};
138 /// # use bytes::Bytes;
139 /// # use http::StatusCode;
140 /// # let metadata = ChunkMetadata {
141 /// # total_size: 1024,
142 /// # content_type: "text/plain".to_string(),
143 /// # etag: None,
144 /// # last_modified: None,
145 /// # status: StatusCode::OK,
146 /// # version: http::Version::HTTP_11,
147 /// # headers: vec![],
148 /// # };
149 /// let entry = ChunkedEntry::new(metadata, 512);
150 /// entry.add_chunk(0, Bytes::from("Hello, world!"));
151 /// ```
152 pub fn add_chunk(&self, index: u64, data: Bytes) {
153 self.chunks.insert(index, data);
154 }
155
156 /// Gets a chunk by index.
157 ///
158 /// Returns `None` if the chunk is not cached.
159 ///
160 /// # Arguments
161 ///
162 /// * `index` - Chunk index (0-based)
163 ///
164 /// # Examples
165 ///
166 /// ```rust
167 /// # use tower_http_cache::chunks::{ChunkedEntry, ChunkMetadata};
168 /// # use bytes::Bytes;
169 /// # use http::StatusCode;
170 /// # let metadata = ChunkMetadata {
171 /// # total_size: 1024,
172 /// # content_type: "text/plain".to_string(),
173 /// # etag: None,
174 /// # last_modified: None,
175 /// # status: StatusCode::OK,
176 /// # version: http::Version::HTTP_11,
177 /// # headers: vec![],
178 /// # };
179 /// let entry = ChunkedEntry::new(metadata, 512);
180 /// entry.add_chunk(0, Bytes::from("Hello"));
181 ///
182 /// let chunk = entry.get_chunk(0);
183 /// assert!(chunk.is_some());
184 /// assert_eq!(chunk.unwrap(), "Hello");
185 /// ```
186 pub fn get_chunk(&self, index: u64) -> Option<Bytes> {
187 self.chunks.get(&index).map(|r| r.value().clone())
188 }
189
190 /// Gets a byte range from the cached chunks.
191 ///
192 /// Returns `None` if any required chunks are missing from the cache.
193 /// The range is inclusive: [start, end].
194 ///
195 /// # Arguments
196 ///
197 /// * `start` - Starting byte offset (inclusive)
198 /// * `end` - Ending byte offset (inclusive)
199 ///
200 /// # Examples
201 ///
202 /// ```rust
203 /// # use tower_http_cache::chunks::{ChunkedEntry, ChunkMetadata};
204 /// # use bytes::Bytes;
205 /// # use http::StatusCode;
206 /// # let metadata = ChunkMetadata {
207 /// # total_size: 2048,
208 /// # content_type: "text/plain".to_string(),
209 /// # etag: None,
210 /// # last_modified: None,
211 /// # status: StatusCode::OK,
212 /// # version: http::Version::HTTP_11,
213 /// # headers: vec![],
214 /// # };
215 /// let entry = ChunkedEntry::new(metadata, 1024);
216 /// entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
217 /// entry.add_chunk(1, Bytes::from(vec![1u8; 1024]));
218 ///
219 /// // Get first 512 bytes
220 /// let range = entry.get_range(0, 511);
221 /// assert!(range.is_some());
222 /// assert_eq!(range.unwrap().len(), 512);
223 /// ```
224 pub fn get_range(&self, start: u64, end: u64) -> Option<Bytes> {
225 let start_chunk = start / self.chunk_size as u64;
226 let end_chunk = end / self.chunk_size as u64;
227
228 let mut result = Vec::new();
229
230 for chunk_idx in start_chunk..=end_chunk {
231 let chunk = self.get_chunk(chunk_idx)?;
232
233 // Calculate byte offsets within this chunk
234 let chunk_start = chunk_idx * self.chunk_size as u64;
235 let chunk_end = chunk_start + chunk.len() as u64;
236
237 let data_start = if start > chunk_start {
238 (start - chunk_start) as usize
239 } else {
240 0
241 };
242
243 let data_end = if end < chunk_end {
244 (end - chunk_start + 1) as usize
245 } else {
246 chunk.len()
247 };
248
249 result.extend_from_slice(&chunk[data_start..data_end]);
250 }
251
252 Some(Bytes::from(result))
253 }
254
255 /// Checks if all chunks are cached.
256 ///
257 /// Returns `true` if the entry contains all chunks needed to represent
258 /// the full file.
259 ///
260 /// # Examples
261 ///
262 /// ```rust
263 /// # use tower_http_cache::chunks::{ChunkedEntry, ChunkMetadata};
264 /// # use bytes::Bytes;
265 /// # use http::StatusCode;
266 /// # let metadata = ChunkMetadata {
267 /// # total_size: 2048,
268 /// # content_type: "text/plain".to_string(),
269 /// # etag: None,
270 /// # last_modified: None,
271 /// # status: StatusCode::OK,
272 /// # version: http::Version::HTTP_11,
273 /// # headers: vec![],
274 /// # };
275 /// let entry = ChunkedEntry::new(metadata, 1024);
276 /// assert!(!entry.is_complete());
277 ///
278 /// entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
279 /// entry.add_chunk(1, Bytes::from(vec![1u8; 1024]));
280 /// assert!(entry.is_complete());
281 /// ```
282 pub fn is_complete(&self) -> bool {
283 let total_chunks = self.metadata.total_size.div_ceil(self.chunk_size as u64);
284
285 self.chunks.len() as u64 == total_chunks
286 }
287
288 /// Gets the cache coverage percentage.
289 ///
290 /// Returns a percentage (0-100) indicating how much of the file is cached.
291 ///
292 /// # Examples
293 ///
294 /// ```rust
295 /// # use tower_http_cache::chunks::{ChunkedEntry, ChunkMetadata};
296 /// # use bytes::Bytes;
297 /// # use http::StatusCode;
298 /// # let metadata = ChunkMetadata {
299 /// # total_size: 4096,
300 /// # content_type: "text/plain".to_string(),
301 /// # etag: None,
302 /// # last_modified: None,
303 /// # status: StatusCode::OK,
304 /// # version: http::Version::HTTP_11,
305 /// # headers: vec![],
306 /// # };
307 /// let entry = ChunkedEntry::new(metadata, 1024);
308 /// assert_eq!(entry.coverage(), 0.0);
309 ///
310 /// entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
311 /// assert_eq!(entry.coverage(), 25.0);
312 ///
313 /// entry.add_chunk(1, Bytes::from(vec![1u8; 1024]));
314 /// assert_eq!(entry.coverage(), 50.0);
315 /// ```
316 pub fn coverage(&self) -> f64 {
317 let total_chunks = self.metadata.total_size.div_ceil(self.chunk_size as u64);
318
319 if total_chunks == 0 {
320 return 0.0;
321 }
322
323 (self.chunks.len() as f64 / total_chunks as f64) * 100.0
324 }
325}
326
327/// Chunk cache manager.
328///
329/// Manages multiple chunked entries, providing efficient lookup and storage
330/// of large files split into chunks.
331pub struct ChunkCache {
332 entries: Arc<DashMap<String, Arc<ChunkedEntry>>>,
333 default_chunk_size: usize,
334}
335
336impl ChunkCache {
337 /// Creates a new chunk cache with the given default chunk size.
338 ///
339 /// # Arguments
340 ///
341 /// * `default_chunk_size` - Default size of each chunk in bytes
342 ///
343 /// # Examples
344 ///
345 /// ```rust
346 /// use tower_http_cache::chunks::ChunkCache;
347 ///
348 /// // Create cache with 1MB chunks
349 /// let cache = ChunkCache::new(1024 * 1024);
350 /// ```
351 pub fn new(default_chunk_size: usize) -> Self {
352 Self {
353 entries: Arc::new(DashMap::new()),
354 default_chunk_size,
355 }
356 }
357
358 /// Gets or creates a chunked entry.
359 ///
360 /// If an entry exists for the key, returns it. Otherwise, creates a new
361 /// entry with the given metadata.
362 ///
363 /// # Arguments
364 ///
365 /// * `key` - Cache key
366 /// * `metadata` - File metadata (used only if creating new entry)
367 ///
368 /// # Examples
369 ///
370 /// ```rust
371 /// # use tower_http_cache::chunks::{ChunkCache, ChunkMetadata};
372 /// # use http::StatusCode;
373 /// let cache = ChunkCache::new(1024 * 1024);
374 ///
375 /// let metadata = ChunkMetadata {
376 /// total_size: 10_000_000,
377 /// content_type: "video/mp4".to_string(),
378 /// etag: None,
379 /// last_modified: None,
380 /// status: StatusCode::OK,
381 /// version: http::Version::HTTP_11,
382 /// headers: vec![],
383 /// };
384 ///
385 /// let entry = cache.get_or_create("video.mp4".to_string(), metadata);
386 /// ```
387 pub fn get_or_create(&self, key: String, metadata: ChunkMetadata) -> Arc<ChunkedEntry> {
388 self.entries
389 .entry(key)
390 .or_insert_with(|| Arc::new(ChunkedEntry::new(metadata, self.default_chunk_size)))
391 .clone()
392 }
393
394 /// Gets an entry if it exists.
395 ///
396 /// # Arguments
397 ///
398 /// * `key` - Cache key
399 ///
400 /// # Examples
401 ///
402 /// ```rust
403 /// # use tower_http_cache::chunks::{ChunkCache, ChunkMetadata};
404 /// # use http::StatusCode;
405 /// let cache = ChunkCache::new(1024 * 1024);
406 ///
407 /// assert!(cache.get("nonexistent").is_none());
408 ///
409 /// # let metadata = ChunkMetadata {
410 /// # total_size: 1024,
411 /// # content_type: "text/plain".to_string(),
412 /// # etag: None,
413 /// # last_modified: None,
414 /// # status: StatusCode::OK,
415 /// # version: http::Version::HTTP_11,
416 /// # headers: vec![],
417 /// # };
418 /// cache.get_or_create("exists".to_string(), metadata);
419 /// assert!(cache.get("exists").is_some());
420 /// ```
421 pub fn get(&self, key: &str) -> Option<Arc<ChunkedEntry>> {
422 self.entries.get(key).map(|r| r.value().clone())
423 }
424
425 /// Removes an entry from the cache.
426 ///
427 /// # Arguments
428 ///
429 /// * `key` - Cache key
430 ///
431 /// # Examples
432 ///
433 /// ```rust
434 /// # use tower_http_cache::chunks::{ChunkCache, ChunkMetadata};
435 /// # use http::StatusCode;
436 /// let cache = ChunkCache::new(1024 * 1024);
437 ///
438 /// # let metadata = ChunkMetadata {
439 /// # total_size: 1024,
440 /// # content_type: "text/plain".to_string(),
441 /// # etag: None,
442 /// # last_modified: None,
443 /// # status: StatusCode::OK,
444 /// # version: http::Version::HTTP_11,
445 /// # headers: vec![],
446 /// # };
447 /// cache.get_or_create("temp".to_string(), metadata);
448 /// assert!(cache.get("temp").is_some());
449 ///
450 /// cache.remove("temp");
451 /// assert!(cache.get("temp").is_none());
452 /// ```
453 pub fn remove(&self, key: &str) -> Option<Arc<ChunkedEntry>> {
454 self.entries.remove(key).map(|(_, v)| v)
455 }
456
457 /// Gets cache statistics.
458 ///
459 /// # Examples
460 ///
461 /// ```rust
462 /// # use tower_http_cache::chunks::{ChunkCache, ChunkMetadata};
463 /// # use bytes::Bytes;
464 /// # use http::StatusCode;
465 /// let cache = ChunkCache::new(1024);
466 ///
467 /// # let metadata = ChunkMetadata {
468 /// # total_size: 2048,
469 /// # content_type: "text/plain".to_string(),
470 /// # etag: None,
471 /// # last_modified: None,
472 /// # status: StatusCode::OK,
473 /// # version: http::Version::HTTP_11,
474 /// # headers: vec![],
475 /// # };
476 /// let entry = cache.get_or_create("file1".to_string(), metadata);
477 /// entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
478 ///
479 /// let stats = cache.stats();
480 /// assert_eq!(stats.total_entries, 1);
481 /// assert_eq!(stats.total_chunks, 1);
482 /// assert_eq!(stats.total_bytes, 1024);
483 /// ```
484 pub fn stats(&self) -> ChunkCacheStats {
485 let mut total_entries = 0;
486 let mut total_chunks = 0;
487 let mut total_bytes = 0u64;
488 let mut complete_entries = 0;
489
490 for entry in self.entries.iter() {
491 total_entries += 1;
492 let chunk_entry = entry.value();
493 let chunk_count = chunk_entry.chunks.len();
494 total_chunks += chunk_count;
495
496 for chunk in chunk_entry.chunks.iter() {
497 total_bytes += chunk.value().len() as u64;
498 }
499
500 if chunk_entry.is_complete() {
501 complete_entries += 1;
502 }
503 }
504
505 ChunkCacheStats {
506 total_entries,
507 complete_entries,
508 total_chunks,
509 total_bytes,
510 }
511 }
512}
513
514/// Statistics for chunk cache.
515#[derive(Debug, Clone)]
516#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
517pub struct ChunkCacheStats {
518 /// Total number of cached entries
519 pub total_entries: usize,
520
521 /// Number of entries with all chunks cached
522 pub complete_entries: usize,
523
524 /// Total number of cached chunks across all entries
525 pub total_chunks: usize,
526
527 /// Total bytes stored in cache
528 pub total_bytes: u64,
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 fn test_metadata(size: u64) -> ChunkMetadata {
536 ChunkMetadata {
537 total_size: size,
538 content_type: "application/octet-stream".to_string(),
539 etag: Some("test-etag".to_string()),
540 last_modified: None,
541 status: StatusCode::OK,
542 version: Version::HTTP_11,
543 headers: vec![],
544 }
545 }
546
547 #[test]
548 fn test_chunk_entry_new() {
549 let metadata = test_metadata(1024);
550 let entry = ChunkedEntry::new(metadata, 512);
551
552 assert_eq!(entry.chunk_size, 512);
553 assert_eq!(entry.metadata.total_size, 1024);
554 assert_eq!(entry.chunks.len(), 0);
555 }
556
557 #[test]
558 fn test_add_and_get_chunk() {
559 let metadata = test_metadata(2048);
560 let entry = ChunkedEntry::new(metadata, 1024);
561
562 let chunk_data = Bytes::from(vec![1u8; 1024]);
563 entry.add_chunk(0, chunk_data.clone());
564
565 let retrieved = entry.get_chunk(0);
566 assert!(retrieved.is_some());
567 assert_eq!(retrieved.unwrap(), chunk_data);
568 }
569
570 #[test]
571 fn test_get_nonexistent_chunk() {
572 let metadata = test_metadata(2048);
573 let entry = ChunkedEntry::new(metadata, 1024);
574
575 assert!(entry.get_chunk(0).is_none());
576 assert!(entry.get_chunk(999).is_none());
577 }
578
579 #[test]
580 fn test_get_range_single_chunk() {
581 let metadata = test_metadata(1024);
582 let entry = ChunkedEntry::new(metadata, 1024);
583
584 let chunk_data = Bytes::from((0..1024).map(|i| i as u8).collect::<Vec<u8>>());
585 entry.add_chunk(0, chunk_data);
586
587 // Get first 512 bytes
588 let range = entry.get_range(0, 511);
589 assert!(range.is_some());
590 let data = range.unwrap();
591 assert_eq!(data.len(), 512);
592 assert_eq!(data[0], 0);
593 assert_eq!(data[511], 255);
594 }
595
596 #[test]
597 fn test_get_range_multiple_chunks() {
598 let metadata = test_metadata(3072);
599 let entry = ChunkedEntry::new(metadata, 1024);
600
601 // Add 3 chunks
602 entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
603 entry.add_chunk(1, Bytes::from(vec![1u8; 1024]));
604 entry.add_chunk(2, Bytes::from(vec![2u8; 1024]));
605
606 // Get range spanning chunks 0 and 1
607 let range = entry.get_range(512, 1535);
608 assert!(range.is_some());
609 let data = range.unwrap();
610 assert_eq!(data.len(), 1024);
611 assert_eq!(data[0], 0); // From chunk 0
612 assert_eq!(data[512], 1); // From chunk 1
613 }
614
615 #[test]
616 fn test_get_range_missing_chunk() {
617 let metadata = test_metadata(2048);
618 let entry = ChunkedEntry::new(metadata, 1024);
619
620 entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
621 // Chunk 1 is missing
622
623 // Try to get range spanning both chunks
624 let range = entry.get_range(512, 1535);
625 assert!(range.is_none()); // Should fail because chunk 1 is missing
626 }
627
628 #[test]
629 fn test_is_complete_empty() {
630 let metadata = test_metadata(2048);
631 let entry = ChunkedEntry::new(metadata, 1024);
632
633 assert!(!entry.is_complete());
634 }
635
636 #[test]
637 fn test_is_complete_partial() {
638 let metadata = test_metadata(2048);
639 let entry = ChunkedEntry::new(metadata, 1024);
640
641 entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
642 assert!(!entry.is_complete());
643 }
644
645 #[test]
646 fn test_is_complete_full() {
647 let metadata = test_metadata(2048);
648 let entry = ChunkedEntry::new(metadata, 1024);
649
650 entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
651 entry.add_chunk(1, Bytes::from(vec![1u8; 1024]));
652 assert!(entry.is_complete());
653 }
654
655 #[test]
656 fn test_coverage() {
657 let metadata = test_metadata(4096);
658 let entry = ChunkedEntry::new(metadata, 1024);
659
660 assert_eq!(entry.coverage(), 0.0);
661
662 entry.add_chunk(0, Bytes::from(vec![0u8; 1024]));
663 assert_eq!(entry.coverage(), 25.0);
664
665 entry.add_chunk(1, Bytes::from(vec![1u8; 1024]));
666 assert_eq!(entry.coverage(), 50.0);
667
668 entry.add_chunk(2, Bytes::from(vec![2u8; 1024]));
669 assert_eq!(entry.coverage(), 75.0);
670
671 entry.add_chunk(3, Bytes::from(vec![3u8; 1024]));
672 assert_eq!(entry.coverage(), 100.0);
673 }
674
675 #[test]
676 fn test_chunk_cache_new() {
677 let cache = ChunkCache::new(1024 * 1024);
678 let stats = cache.stats();
679
680 assert_eq!(stats.total_entries, 0);
681 assert_eq!(stats.total_chunks, 0);
682 assert_eq!(stats.total_bytes, 0);
683 }
684
685 #[test]
686 fn test_chunk_cache_get_or_create() {
687 let cache = ChunkCache::new(1024);
688 let metadata = test_metadata(2048);
689
690 let entry1 = cache.get_or_create("key1".to_string(), metadata.clone());
691 let entry2 = cache.get_or_create("key1".to_string(), test_metadata(4096));
692
693 // Should return the same entry (original metadata)
694 assert_eq!(entry1.metadata.total_size, entry2.metadata.total_size);
695 assert_eq!(entry1.metadata.total_size, 2048);
696 }
697
698 #[test]
699 fn test_chunk_cache_get() {
700 let cache = ChunkCache::new(1024);
701
702 assert!(cache.get("nonexistent").is_none());
703
704 let metadata = test_metadata(1024);
705 cache.get_or_create("exists".to_string(), metadata);
706
707 assert!(cache.get("exists").is_some());
708 }
709
710 #[test]
711 fn test_chunk_cache_remove() {
712 let cache = ChunkCache::new(1024);
713 let metadata = test_metadata(1024);
714
715 cache.get_or_create("temp".to_string(), metadata);
716 assert!(cache.get("temp").is_some());
717
718 let removed = cache.remove("temp");
719 assert!(removed.is_some());
720 assert!(cache.get("temp").is_none());
721 }
722
723 #[test]
724 fn test_chunk_cache_stats() {
725 let cache = ChunkCache::new(1024);
726
727 let metadata1 = test_metadata(2048);
728 let entry1 = cache.get_or_create("file1".to_string(), metadata1);
729 entry1.add_chunk(0, Bytes::from(vec![0u8; 1024]));
730 entry1.add_chunk(1, Bytes::from(vec![1u8; 1024]));
731
732 let metadata2 = test_metadata(3072);
733 let entry2 = cache.get_or_create("file2".to_string(), metadata2);
734 entry2.add_chunk(0, Bytes::from(vec![2u8; 1024]));
735
736 let stats = cache.stats();
737 assert_eq!(stats.total_entries, 2);
738 assert_eq!(stats.complete_entries, 1); // Only entry1 is complete
739 assert_eq!(stats.total_chunks, 3);
740 assert_eq!(stats.total_bytes, 3072);
741 }
742}