Skip to main content

scirs2_io/compression/
advanced.rs

1//! Advanced compression primitives for scientific data
2//!
3//! Provides high-level compressor structs and encoding utilities:
4//! - `ZstdCompressor` – pure-Rust Zstandard-like compression via `oxiarc_archive`
5//! - `LZ4Compressor` – pure-Rust LZ4 frame compression via `oxiarc_archive`
6//! - `RunLengthEncoding` – RLE for sparse/repetitive byte data
7//! - `DeltaEncoding` – delta encoding for sorted numeric streams
8//! - `FrameCompressor` – frame-based streaming compression/decompression
9//! - `CompressionBenchmark` – measure compression ratio and throughput
10
11#![allow(dead_code)]
12#![allow(missing_docs)]
13
14use crate::error::{IoError, Result};
15use oxiarc_archive::{lz4, zstd};
16use std::io::Write as _;
17use std::time::Instant;
18
19// ---------------------------------------------------------------------------
20// ZstdCompressor
21// ---------------------------------------------------------------------------
22
23/// Pure-Rust Zstandard-like compressor backed by `oxiarc_archive::zstd`.
24///
25/// Wraps the streaming writer/reader pair to provide a convenient compress/decompress API.
26#[derive(Debug, Clone)]
27pub struct ZstdCompressor {
28    /// Compression level (clamped to a valid range internally)
29    pub level: u8,
30}
31
32impl ZstdCompressor {
33    /// Create a compressor with the default compression level (3).
34    pub fn new() -> Self {
35        Self { level: 3 }
36    }
37
38    /// Create a compressor with a specific level.
39    pub fn with_level(level: u8) -> Self {
40        Self { level }
41    }
42
43    /// Compress `data` in one shot and return the compressed bytes.
44    pub fn compress(&self, data: &[u8]) -> Result<Vec<u8>> {
45        let writer = zstd::ZstdWriter::new();
46        writer
47            .compress(data)
48            .map_err(|e| IoError::CompressionError(format!("zstd compress: {e}")))
49    }
50
51    /// Decompress `data` compressed with this (or any compatible) Zstd compressor.
52    pub fn decompress(&self, data: &[u8]) -> Result<Vec<u8>> {
53        use std::io::Cursor;
54        let cursor = Cursor::new(data);
55        let mut reader = zstd::ZstdReader::new(cursor)
56            .map_err(|e| IoError::DecompressionError(format!("zstd reader: {e}")))?;
57        reader
58            .decompress()
59            .map_err(|e| IoError::DecompressionError(format!("zstd decompress: {e}")))
60    }
61
62    /// Compression ratio: `uncompressed_size / compressed_size`.
63    /// Returns 0.0 if compression fails.
64    pub fn ratio(&self, data: &[u8]) -> f64 {
65        match self.compress(data) {
66            Ok(c) if !c.is_empty() => data.len() as f64 / c.len() as f64,
67            _ => 0.0,
68        }
69    }
70}
71
72impl Default for ZstdCompressor {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78// ---------------------------------------------------------------------------
79// LZ4Compressor
80// ---------------------------------------------------------------------------
81
82/// Pure-Rust LZ4 frame compressor backed by `oxiarc_archive::lz4`.
83///
84/// LZ4 prioritises speed over compression ratio and is ideal for high-throughput
85/// real-time compression of scientific streams.
86#[derive(Debug, Clone)]
87pub struct LZ4Compressor;
88
89impl LZ4Compressor {
90    /// Create an LZ4 compressor.
91    pub fn new() -> Self {
92        Self
93    }
94
95    /// Compress `data` in one shot.
96    pub fn compress(&self, data: &[u8]) -> Result<Vec<u8>> {
97        let mut writer = lz4::Lz4Writer::new(Vec::new());
98        writer
99            .write_compressed(data)
100            .map_err(|e| IoError::CompressionError(format!("lz4 compress: {e}")))?;
101        Ok(writer.into_inner())
102    }
103
104    /// Decompress `data` previously compressed by this compressor.
105    pub fn decompress(&self, data: &[u8]) -> Result<Vec<u8>> {
106        use std::io::Cursor;
107        let cursor = Cursor::new(data);
108        let mut reader = lz4::Lz4Reader::new(cursor)
109            .map_err(|e| IoError::DecompressionError(format!("lz4 reader: {e}")))?;
110        reader
111            .decompress()
112            .map_err(|e| IoError::DecompressionError(format!("lz4 decompress: {e}")))
113    }
114
115    /// Compression ratio: `uncompressed_size / compressed_size`.
116    pub fn ratio(&self, data: &[u8]) -> f64 {
117        match self.compress(data) {
118            Ok(c) if !c.is_empty() => data.len() as f64 / c.len() as f64,
119            _ => 0.0,
120        }
121    }
122}
123
124impl Default for LZ4Compressor {
125    fn default() -> Self {
126        Self::new()
127    }
128}
129
130// ---------------------------------------------------------------------------
131// RunLengthEncoding
132// ---------------------------------------------------------------------------
133
134/// Run-length encoding for byte sequences.
135///
136/// Encoding format: a sequence of `(count: u8, byte: u8)` pairs.
137/// Runs longer than 255 are split into multiple pairs.
138///
139/// Best suited for sparse or highly repetitive data (e.g. binary masks, indicator arrays).
140pub struct RunLengthEncoding;
141
142impl RunLengthEncoding {
143    /// Encode `data` as RLE.  Returns the encoded bytes.
144    pub fn encode(data: &[u8]) -> Vec<u8> {
145        if data.is_empty() {
146            return Vec::new();
147        }
148        let mut out = Vec::new();
149        let mut count = 1u8;
150        let mut current = data[0];
151
152        for &b in &data[1..] {
153            if b == current && count < 255 {
154                count += 1;
155            } else {
156                out.push(count);
157                out.push(current);
158                current = b;
159                count = 1;
160            }
161        }
162        out.push(count);
163        out.push(current);
164        out
165    }
166
167    /// Decode RLE-encoded bytes.  Returns the original byte sequence.
168    pub fn decode(encoded: &[u8]) -> Result<Vec<u8>> {
169        if !encoded.len().is_multiple_of(2) {
170            return Err(IoError::DecompressionError(
171                "RLE: encoded length must be even".to_string(),
172            ));
173        }
174        let mut out = Vec::new();
175        let mut i = 0;
176        while i + 1 < encoded.len() {
177            let count = encoded[i] as usize;
178            let byte = encoded[i + 1];
179            for _ in 0..count {
180                out.push(byte);
181            }
182            i += 2;
183        }
184        Ok(out)
185    }
186
187    /// Compression ratio achieved on `data`.
188    pub fn ratio(data: &[u8]) -> f64 {
189        let encoded = Self::encode(data);
190        if encoded.is_empty() {
191            return 1.0;
192        }
193        data.len() as f64 / encoded.len() as f64
194    }
195}
196
197// ---------------------------------------------------------------------------
198// DeltaEncoding
199// ---------------------------------------------------------------------------
200
201/// Delta encoding for sorted or slowly-varying numeric data.
202///
203/// The first value is stored verbatim; subsequent values store the difference
204/// from the previous value.  This dramatically reduces entropy for monotonically
205/// sorted or slowly-varying integer or float sequences, making them highly
206/// compressible with a secondary compressor.
207pub struct DeltaEncoding;
208
209impl DeltaEncoding {
210    /// Encode a slice of `i64` values as deltas.
211    ///
212    /// The encoded format is `[n:u64_le][v0:i64_le][d1:i64_le]...[dn-1:i64_le]`
213    /// where `n` is the number of elements.
214    pub fn encode_i64(data: &[i64]) -> Vec<u8> {
215        let n = data.len();
216        let mut out = Vec::with_capacity(8 + n * 8);
217        out.extend_from_slice(&(n as u64).to_le_bytes());
218        if n == 0 {
219            return out;
220        }
221        out.extend_from_slice(&data[0].to_le_bytes());
222        for i in 1..n {
223            let delta = data[i].wrapping_sub(data[i - 1]);
224            out.extend_from_slice(&delta.to_le_bytes());
225        }
226        out
227    }
228
229    /// Decode delta-encoded `i64` values.
230    pub fn decode_i64(encoded: &[u8]) -> Result<Vec<i64>> {
231        if encoded.len() < 8 {
232            return Err(IoError::DecompressionError("Delta: too short".to_string()));
233        }
234        let n = u64::from_le_bytes(
235            encoded[..8]
236                .try_into()
237                .map_err(|_| IoError::DecompressionError("Delta: bad length prefix".to_string()))?,
238        ) as usize;
239        if n == 0 {
240            return Ok(Vec::new());
241        }
242        if encoded.len() < 8 + n * 8 {
243            return Err(IoError::DecompressionError(
244                "Delta: encoded data too short".to_string(),
245            ));
246        }
247        let mut out = Vec::with_capacity(n);
248        let first = i64::from_le_bytes(
249            encoded[8..16]
250                .try_into()
251                .map_err(|_| IoError::DecompressionError("Delta: bad first value".to_string()))?,
252        );
253        out.push(first);
254        let mut prev = first;
255        for i in 1..n {
256            let offset = 8 + i * 8;
257            let delta =
258                i64::from_le_bytes(encoded[offset..offset + 8].try_into().map_err(|_| {
259                    IoError::DecompressionError("Delta: bad delta value".to_string())
260                })?);
261            let val = prev.wrapping_add(delta);
262            out.push(val);
263            prev = val;
264        }
265        Ok(out)
266    }
267
268    /// Encode a slice of `f64` values as integer-quantised deltas.
269    ///
270    /// Values are multiplied by `scale` and rounded to `i64` before delta encoding.
271    /// Decoding divides by `scale` to recover approximate floats.
272    pub fn encode_f64(data: &[f64], scale: f64) -> Vec<u8> {
273        let ints: Vec<i64> = data.iter().map(|&v| (v * scale).round() as i64).collect();
274        Self::encode_i64(&ints)
275    }
276
277    /// Decode delta-encoded `f64` values (inverse of `encode_f64`).
278    pub fn decode_f64(encoded: &[u8], scale: f64) -> Result<Vec<f64>> {
279        let ints = Self::decode_i64(encoded)?;
280        Ok(ints.iter().map(|&v| v as f64 / scale).collect())
281    }
282}
283
284// ---------------------------------------------------------------------------
285// FrameCompressor
286// ---------------------------------------------------------------------------
287
288/// Frame-based streaming compression.
289///
290/// Splits input data into fixed-size frames and compresses each frame
291/// independently. The frame format is:
292/// ```text
293/// [magic: 4 bytes "FRCM"][version: u8][codec: u8][num_frames: u32_le]
294///   per frame: [compressed_size: u32_le][original_size: u32_le][data: ...]
295/// ```
296/// This format allows random frame access and partial decompression.
297pub struct FrameCompressor {
298    /// Frame size in bytes (default: 64 KiB)
299    pub frame_size: usize,
300    /// Compression codec
301    pub codec: FrameCodec,
302}
303
304/// Codec used by `FrameCompressor`.
305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306pub enum FrameCodec {
307    /// No compression (passthrough, for benchmarking)
308    None,
309    /// LZ4 compression per frame
310    Lz4,
311    /// Zstd compression per frame
312    Zstd,
313    /// RLE compression per frame
314    Rle,
315}
316
317const FRAME_MAGIC: &[u8; 4] = b"FRCM";
318const FRAME_VERSION: u8 = 1;
319
320impl FrameCompressor {
321    /// Create a frame compressor with default settings (64 KiB frames, LZ4).
322    pub fn new() -> Self {
323        Self {
324            frame_size: 64 * 1024,
325            codec: FrameCodec::Lz4,
326        }
327    }
328
329    /// Set frame size.
330    pub fn with_frame_size(mut self, size: usize) -> Self {
331        self.frame_size = size.max(64); // Minimum 64 bytes
332        self
333    }
334
335    /// Set codec.
336    pub fn with_codec(mut self, codec: FrameCodec) -> Self {
337        self.codec = codec;
338        self
339    }
340
341    /// Compress `data` into a framed stream.
342    pub fn compress(&self, data: &[u8]) -> Result<Vec<u8>> {
343        let frames: Vec<&[u8]> = data.chunks(self.frame_size).collect();
344        let num_frames = frames.len() as u32;
345
346        // Compress each frame
347        let mut compressed_frames: Vec<Vec<u8>> = Vec::with_capacity(frames.len());
348        for frame in &frames {
349            let c = self.compress_frame(frame)?;
350            compressed_frames.push(c);
351        }
352
353        // Serialise
354        let total_payload: usize = compressed_frames.iter().map(|f| 8 + f.len()).sum();
355        let mut out = Vec::with_capacity(10 + total_payload);
356        out.extend_from_slice(FRAME_MAGIC);
357        out.push(FRAME_VERSION);
358        out.push(self.codec as u8);
359        out.extend_from_slice(&num_frames.to_le_bytes());
360
361        for (cf, orig) in compressed_frames.iter().zip(frames.iter()) {
362            out.extend_from_slice(&(cf.len() as u32).to_le_bytes());
363            out.extend_from_slice(&(orig.len() as u32).to_le_bytes());
364            out.extend_from_slice(cf);
365        }
366        Ok(out)
367    }
368
369    /// Decompress a framed stream produced by `compress`.
370    pub fn decompress(&self, data: &[u8]) -> Result<Vec<u8>> {
371        if data.len() < 10 {
372            return Err(IoError::DecompressionError("Frame: too short".to_string()));
373        }
374        if &data[..4] != FRAME_MAGIC {
375            return Err(IoError::DecompressionError("Frame: bad magic".to_string()));
376        }
377        if data[4] != FRAME_VERSION {
378            return Err(IoError::DecompressionError(format!(
379                "Frame: unsupported version {}",
380                data[4]
381            )));
382        }
383        let codec = FrameCodec::from_u8(data[5]).ok_or_else(|| {
384            IoError::DecompressionError(format!("Frame: unknown codec {}", data[5]))
385        })?;
386        let num_frames = u32::from_le_bytes([data[6], data[7], data[8], data[9]]) as usize;
387
388        let mut out = Vec::new();
389        let mut pos = 10usize;
390        for _ in 0..num_frames {
391            if pos + 8 > data.len() {
392                return Err(IoError::DecompressionError(
393                    "Frame: truncated header".to_string(),
394                ));
395            }
396            let comp_size =
397                u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
398                    as usize;
399            let _orig_size =
400                u32::from_le_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]])
401                    as usize;
402            pos += 8;
403            if pos + comp_size > data.len() {
404                return Err(IoError::DecompressionError(
405                    "Frame: truncated data".to_string(),
406                ));
407            }
408            let frame_data = &data[pos..pos + comp_size];
409            pos += comp_size;
410
411            let decompressed = Self::decompress_frame_with_codec(frame_data, codec)?;
412            out.extend_from_slice(&decompressed);
413        }
414        Ok(out)
415    }
416
417    fn compress_frame(&self, frame: &[u8]) -> Result<Vec<u8>> {
418        match self.codec {
419            FrameCodec::None => Ok(frame.to_vec()),
420            FrameCodec::Lz4 => {
421                let c = LZ4Compressor::new();
422                c.compress(frame)
423            }
424            FrameCodec::Zstd => {
425                let c = ZstdCompressor::new();
426                c.compress(frame)
427            }
428            FrameCodec::Rle => Ok(RunLengthEncoding::encode(frame)),
429        }
430    }
431
432    fn decompress_frame_with_codec(frame: &[u8], codec: FrameCodec) -> Result<Vec<u8>> {
433        match codec {
434            FrameCodec::None => Ok(frame.to_vec()),
435            FrameCodec::Lz4 => LZ4Compressor::new().decompress(frame),
436            FrameCodec::Zstd => ZstdCompressor::new().decompress(frame),
437            FrameCodec::Rle => RunLengthEncoding::decode(frame),
438        }
439    }
440}
441
442impl Default for FrameCompressor {
443    fn default() -> Self {
444        Self::new()
445    }
446}
447
448impl FrameCodec {
449    fn from_u8(v: u8) -> Option<Self> {
450        match v {
451            0 => Some(Self::None),
452            1 => Some(Self::Lz4),
453            2 => Some(Self::Zstd),
454            3 => Some(Self::Rle),
455            _ => None,
456        }
457    }
458}
459
460// ---------------------------------------------------------------------------
461// CompressionBenchmark
462// ---------------------------------------------------------------------------
463
464/// Benchmark results for a single compression trial.
465#[derive(Debug, Clone)]
466pub struct BenchmarkResult {
467    /// Codec name
468    pub codec: String,
469    /// Input (uncompressed) size in bytes
470    pub input_size: usize,
471    /// Compressed size in bytes
472    pub compressed_size: usize,
473    /// Compression ratio (input / compressed)
474    pub ratio: f64,
475    /// Compression throughput in MiB/s
476    pub compress_mibps: f64,
477    /// Decompression throughput in MiB/s
478    pub decompress_mibps: f64,
479    /// Compression latency in microseconds
480    pub compress_us: u64,
481    /// Decompression latency in microseconds
482    pub decompress_us: u64,
483}
484
485impl BenchmarkResult {
486    /// Human-readable one-line summary.
487    pub fn summary(&self) -> String {
488        format!(
489            "{}: ratio={:.2}x  compress={:.1} MiB/s  decompress={:.1} MiB/s  ({} → {} bytes)",
490            self.codec,
491            self.ratio,
492            self.compress_mibps,
493            self.decompress_mibps,
494            self.input_size,
495            self.compressed_size,
496        )
497    }
498}
499
500/// Utility for benchmarking compression algorithms on a data sample.
501pub struct CompressionBenchmark {
502    /// Number of warm-up runs before timing
503    pub warmup_runs: usize,
504    /// Number of timed runs for averaging
505    pub timed_runs: usize,
506}
507
508impl CompressionBenchmark {
509    /// Create a benchmark with default settings (1 warm-up, 5 timed runs).
510    pub fn new() -> Self {
511        Self {
512            warmup_runs: 1,
513            timed_runs: 5,
514        }
515    }
516
517    /// Set warm-up run count.
518    pub fn with_warmup(mut self, n: usize) -> Self {
519        self.warmup_runs = n;
520        self
521    }
522
523    /// Set timed run count.
524    pub fn with_timed_runs(mut self, n: usize) -> Self {
525        self.timed_runs = n.max(1);
526        self
527    }
528
529    /// Benchmark `ZstdCompressor` on `data`.
530    pub fn bench_zstd(&self, data: &[u8]) -> Result<BenchmarkResult> {
531        let c = ZstdCompressor::new();
532        let compressed = c.compress(data)?;
533        self.measure(
534            "Zstd",
535            data,
536            &compressed,
537            || c.compress(data),
538            || c.decompress(&compressed),
539        )
540    }
541
542    /// Benchmark `LZ4Compressor` on `data`.
543    pub fn bench_lz4(&self, data: &[u8]) -> Result<BenchmarkResult> {
544        let c = LZ4Compressor::new();
545        let compressed = c.compress(data)?;
546        self.measure(
547            "LZ4",
548            data,
549            &compressed,
550            || c.compress(data),
551            || c.decompress(&compressed),
552        )
553    }
554
555    /// Benchmark `RunLengthEncoding` on `data`.
556    pub fn bench_rle(&self, data: &[u8]) -> Result<BenchmarkResult> {
557        let encoded = RunLengthEncoding::encode(data);
558        self.measure(
559            "RLE",
560            data,
561            &encoded,
562            || Ok(RunLengthEncoding::encode(data)),
563            || RunLengthEncoding::decode(&encoded),
564        )
565    }
566
567    /// Benchmark `FrameCompressor` (LZ4) on `data`.
568    pub fn bench_frame_lz4(&self, data: &[u8]) -> Result<BenchmarkResult> {
569        let fc = FrameCompressor::new();
570        let compressed = fc.compress(data)?;
571        self.measure(
572            "Frame-LZ4",
573            data,
574            &compressed,
575            || fc.compress(data),
576            || fc.decompress(&compressed),
577        )
578    }
579
580    /// Run all built-in benchmarks and return the results.
581    pub fn run_all(&self, data: &[u8]) -> Vec<BenchmarkResult> {
582        let mut results = Vec::new();
583        if let Ok(r) = self.bench_zstd(data) {
584            results.push(r);
585        }
586        if let Ok(r) = self.bench_lz4(data) {
587            results.push(r);
588        }
589        if let Ok(r) = self.bench_rle(data) {
590            results.push(r);
591        }
592        if let Ok(r) = self.bench_frame_lz4(data) {
593            results.push(r);
594        }
595        results
596    }
597
598    fn measure<C, D>(
599        &self,
600        name: &str,
601        data: &[u8],
602        compressed: &[u8],
603        compress_fn: C,
604        decompress_fn: D,
605    ) -> Result<BenchmarkResult>
606    where
607        C: Fn() -> Result<Vec<u8>>,
608        D: Fn() -> Result<Vec<u8>>,
609    {
610        // Warm up
611        for _ in 0..self.warmup_runs {
612            let _ = compress_fn();
613            let _ = decompress_fn();
614        }
615
616        // Time compression
617        let mut total_compress_us = 0u64;
618        for _ in 0..self.timed_runs {
619            let t = Instant::now();
620            compress_fn()?;
621            total_compress_us += t.elapsed().as_micros() as u64;
622        }
623        let avg_compress_us = total_compress_us / self.timed_runs as u64;
624
625        // Time decompression
626        let mut total_decompress_us = 0u64;
627        for _ in 0..self.timed_runs {
628            let t = Instant::now();
629            decompress_fn()?;
630            total_decompress_us += t.elapsed().as_micros() as u64;
631        }
632        let avg_decompress_us = total_decompress_us / self.timed_runs as u64;
633
634        let input_size = data.len();
635        let compressed_size = compressed.len();
636        let ratio = if compressed_size == 0 {
637            0.0
638        } else {
639            input_size as f64 / compressed_size as f64
640        };
641
642        let to_mibps = |us: u64| -> f64 {
643            if us == 0 {
644                return f64::INFINITY;
645            }
646            (input_size as f64 / (1024.0 * 1024.0)) / (us as f64 / 1_000_000.0)
647        };
648
649        Ok(BenchmarkResult {
650            codec: name.to_string(),
651            input_size,
652            compressed_size,
653            ratio,
654            compress_mibps: to_mibps(avg_compress_us),
655            decompress_mibps: to_mibps(avg_decompress_us),
656            compress_us: avg_compress_us,
657            decompress_us: avg_decompress_us,
658        })
659    }
660}
661
662impl Default for CompressionBenchmark {
663    fn default() -> Self {
664        Self::new()
665    }
666}
667
668// ---------------------------------------------------------------------------
669// Tests
670// ---------------------------------------------------------------------------
671
672#[cfg(test)]
673mod tests {
674    use super::*;
675
676    const REPETITIVE: &[u8] = b"aaaaaaaaabbbbbbbbbbccccccccddddddddeeeeeeee";
677    const TEXT: &[u8] =
678        b"The quick brown fox jumps over the lazy dog. Scientific data compression!";
679
680    #[test]
681    fn test_zstd_roundtrip() {
682        let c = ZstdCompressor::new();
683        let enc = c.compress(REPETITIVE).unwrap();
684        let dec = c.decompress(&enc).unwrap();
685        assert_eq!(dec, REPETITIVE);
686    }
687
688    #[test]
689    fn test_lz4_roundtrip() {
690        let c = LZ4Compressor::new();
691        let enc = c.compress(TEXT).unwrap();
692        let dec = c.decompress(&enc).unwrap();
693        assert_eq!(dec, TEXT);
694    }
695
696    #[test]
697    fn test_rle_roundtrip() {
698        let dec = RunLengthEncoding::decode(&RunLengthEncoding::encode(REPETITIVE)).unwrap();
699        assert_eq!(dec, REPETITIVE);
700    }
701
702    #[test]
703    fn test_rle_empty() {
704        let enc = RunLengthEncoding::encode(&[]);
705        assert!(enc.is_empty());
706        let dec = RunLengthEncoding::decode(&[]).unwrap();
707        assert!(dec.is_empty());
708    }
709
710    #[test]
711    fn test_rle_long_run() {
712        let data = vec![42u8; 512]; // Two runs of 255 + one of 2
713        let enc = RunLengthEncoding::encode(&data);
714        let dec = RunLengthEncoding::decode(&enc).unwrap();
715        assert_eq!(dec, data);
716    }
717
718    #[test]
719    fn test_delta_i64_roundtrip() {
720        let values: Vec<i64> = (0..100).map(|i| i * 7 + 1000).collect();
721        let enc = DeltaEncoding::encode_i64(&values);
722        let dec = DeltaEncoding::decode_i64(&enc).unwrap();
723        assert_eq!(dec, values);
724    }
725
726    #[test]
727    fn test_delta_f64_roundtrip() {
728        let values: Vec<f64> = (0..50).map(|i| i as f64 * 0.1).collect();
729        let enc = DeltaEncoding::encode_f64(&values, 1000.0);
730        let dec = DeltaEncoding::decode_f64(&enc, 1000.0).unwrap();
731        for (a, b) in values.iter().zip(dec.iter()) {
732            assert!((a - b).abs() < 0.001, "mismatch: {a} vs {b}");
733        }
734    }
735
736    #[test]
737    fn test_delta_empty() {
738        let enc = DeltaEncoding::encode_i64(&[]);
739        let dec = DeltaEncoding::decode_i64(&enc).unwrap();
740        assert!(dec.is_empty());
741    }
742
743    #[test]
744    fn test_frame_compressor_roundtrip_lz4() {
745        let data: Vec<u8> = (0u8..=255).cycle().take(200_000).collect();
746        let fc = FrameCompressor::new()
747            .with_frame_size(16 * 1024)
748            .with_codec(FrameCodec::Lz4);
749        let enc = fc.compress(&data).unwrap();
750        let dec = fc.decompress(&enc).unwrap();
751        assert_eq!(dec, data);
752    }
753
754    #[test]
755    fn test_frame_compressor_roundtrip_rle() {
756        let data = vec![0xABu8; 50_000];
757        let fc = FrameCompressor::new().with_codec(FrameCodec::Rle);
758        let enc = fc.compress(&data).unwrap();
759        let dec = fc.decompress(&enc).unwrap();
760        assert_eq!(dec, data);
761    }
762
763    #[test]
764    fn test_frame_compressor_bad_magic() {
765        let bad = b"XXXX\x01\x01\x00\x00\x00\x00".to_vec();
766        let fc = FrameCompressor::new();
767        assert!(fc.decompress(&bad).is_err());
768    }
769
770    #[test]
771    fn test_benchmark_runs() {
772        let data: Vec<u8> = (0u8..=255).cycle().take(10_000).collect();
773        let bm = CompressionBenchmark::new()
774            .with_warmup(0)
775            .with_timed_runs(1);
776        let results = bm.run_all(&data);
777        // At least zstd and lz4 should succeed
778        assert!(results.len() >= 2);
779        for r in &results {
780            assert!(r.ratio > 0.0);
781            assert!(!r.summary().is_empty());
782        }
783    }
784}