Creating Test Cases for Custom Compression Library

Mục tiêu: Developing test cases to validate the performance of a custom compression library across different data types and edge cases.


Creating test cases is a crucial step in ensuring your custom compression library works efficiently and reliably across different data types. In this task, you’ll create various test cases to validate the performance of your compression algorithm. Let’s break it down step by step.

Understanding the Importance of Diverse Test Cases

Before jumping into creating test cases, it’s important to understand why diverse test cases are essential:

  1. Different Data Types: Text, binary, and other data types compress differently. Text data might have repeating patterns, while binary data might have random patterns that your compression algorithm needs to handle efficiently.
  2. Edge Cases: Empty files, single-byte files, and large files with repeating patterns should be tested to ensure your algorithm handles all possible scenarios.
  3. Compression Efficiency: Testing different data types helps you understand how your algorithm performs under various conditions and identify potential improvements.

Creating Test Cases in Rust

For this task, you’ll create test cases using Rust’s built-in testing framework. You’ll create a tests module in your project structure. Here’s how you can structure your test cases:

// tests/mod.rs
#[cfg(test)]
mod text_tests {
    use super::*;
    use std::fs::File;
    use std::io::Write;
    use std::path::PathBuf;
    use tempfile::tempdir;

    #[test]
    fn test_compress_text_file() {
        // Create a temporary directory
        let temp_dir = tempdir().unwrap();

        // Create a sample text file
        let mut file_path = temp_dir.path().to_path_buf();
        file_path.push("sample.txt");
        let mut file = File::create(&file_path).unwrap();
        file.write_all(b"Lorem ipsum dolor sit amet, consectetur adipiscing elit.").unwrap();

        // Compress the file
        let compressed_data = compress_file(&file_path).unwrap();

        // Verify compression
        assert!(compressed_data.len() < 27); // Original length is 27 bytes
    }

    #[test]
    fn test_compress_empty_file() {
        let temp_dir = tempdir().unwrap();
        let mut file_path = temp_dir.path().to_path_buf();
        file_path.push("empty.txt");
        File::create(&file_path).unwrap();

        let compressed_data = compress_file(&file_path).unwrap();
        assert_eq!(compressed_data, Vec::new());
    }
}

#[cfg(test)]
mod binary_tests {
    use super::*;
    use rand::Rng;
    use std::fs::File;
    use std::io::Write;
    use std::path::PathBuf;
    use tempfile::tempdir;

    #[test]
    fn test_compress_binary_file() {
        let mut rng = rand::thread_rng();
        let mut data: Vec<u8> = (0..1024).map(|_| rng.gen()).collect();

        let temp_dir = tempdir().unwrap();
        let mut file_path = temp_dir.path().to_path_buf();
        file_path.push("random.bin");

        let mut file = File::create(&file_path).unwrap();
        file.write_all(&data).unwrap();

        let compressed_data = compress_file(&file_path).unwrap();
        // Depending on your compression algorithm, verify the compression ratio
        assert!(compressed_data.len() < data.len());
    }
}

Explanation of the Test Cases

  1. Text File Compression Test:
  2. Creates a temporary directory and a text file with known content.
  3. Compresses the file and verifies that the compressed data is smaller than the original.
  4. Empty File Test:
  5. Creates an empty file and verifies that the compressed data is an empty vector.
  6. Binary File Compression Test:
  7. Generates random binary data and writes it to a file.
  8. Compresses the file and verifies that the compressed data is smaller than the original.

Best Practices for Testing

  1. Keep Tests Simple and Focused: Each test should verify a specific aspect of your compression algorithm.
  2. Use Temporary Files: Avoid using real files that might interfere with your system or have unexpected permissions issues.
  3. Test Edge Cases: Include tests for empty files, single-byte files, and files with repeating patterns.
  4. Test Large Files: Create tests with large files (e.g., 1MB, 10MB) to verify performance under different loads.
  5. Document Your Tests: Keep a record of what each test is supposed to verify and the expected outcomes.

Next Steps

After creating these test cases, you’ll move on to:

  1. Benchmarking: Use Rust’s benchmarking tools to measure the performance of your compression and decompression functions.
  2. Comparison: Compare your library’s performance with existing compression libraries to identify areas for improvement.
  3. Documentation: Document your test results and analysis to understand how your algorithm performs under different conditions.

Further Reading

Benchmarking Compression Library in Rust

Mục tiêu: Measuring performance of a custom compression library using Rust’s benchmarking framework.


Implementing Benchmarking for Compression Library in Rust

To measure the performance of your custom compression library, you’ll use Rust’s built-in benchmarking framework. This will allow you to test both compression and decompression speeds accurately.

Step 1: Add Benchmarking Dependencies

First, you need to add the necessary benchmarking attributes. Rust’s standard library includes benchmarking support, so you don’t need additional dependencies beyond what you’ve already included in your project.

Step 2: Create Benchmark Functions

Create a new file called benches/compression_benches.rs. This file will contain all your benchmark tests:

// benches/compression_benches.rs
use compression_library::{compress, decompress};

#[cfg(test)]
mod benches {
    use super::*;
    use test::Bencher;

    #[bench]
    fn bench_compress_empty_data(b: &mut Bencher) {
        let data = vec![];
        b.iter(|| {
            let result = compress(&data);
            assert!(result.is_ok());
        });
    }

    #[bench]
    fn bench_compress_single_byte(b: &mut Bencher) {
        let data = vec![b'a'; 1];
        b.iter(|| {
            let result = compress(&data);
            assert!(result.is_ok());
        });
    }

    #[bench]
    fn bench_compress_text_data(b: &mut Bencher) {
        let data = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.".as_bytes().to_vec();
        b.iter(|| {
            let result = compress(&data);
            assert!(result.is_ok());
        });
    }

    #[bench]
    fn bench_compress_binary_data(b: &mut Bencher) {
        let data = vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
        b.iter(|| {
            let result = compress(&data);
            assert!(result.is_ok());
        });
    }

    #[bench]
    fn bench_decompress_empty_data(b: &mut Bencher) {
        let data = vec![];
        b.iter(|| {
            let result = decompress(&data);
            assert!(result.is_ok());
        });
    }

    #[bench]
    fn bench_decompress_single_byte(b: &mut Bencher) {
        let data = vec![b'a'; 1];
        b.iter(|| {
            let result = decompress(&data);
            assert!(result.is_ok());
        });
    }

    #[bench]
    fn bench_decompress_text_data(b: &mut Bencher) {
        let data = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.".as_bytes().to_vec();
        b.iter(|| {
            let result = decompress(&data);
            assert!(result.is_ok());
        });
    }

    #[bench]
    fn bench_decompress_binary_data(b: &mut Bencher) {
        let data = vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
        b.iter(|| {
            let result = decompress(&data);
            assert!(result.is_ok());
        });
    }

    #[bench]
    fn bench_real_world_compression(b: &mut Bencher) {
        // Example: Compressing a JSON file
        let data = r#"{
            \"name\": \"John\",
            \"age\": 30,
            \"city\": \"New York\"
        }"#
        .as_bytes()
        .to_vec();

        b.iter(|| {
            let result = compress(&data);
            assert!(result.is_ok());
        });
    }
}

Step 3: Running Benchmarks

To run the benchmarks, execute the following command:

cargo bench

You can also run specific benchmarks using:

cargo bench --bench <benchmark_name>

Step 4: Analyzing Results

When you run the benchmarks, you’ll get output similar to this:

running 10 tests
test bench_compress_empty_data ... bench:      12,345 ns/iter (+/- 123)
test bench_compress_single_byte ... bench:      23,456 ns/iter (+/- 234)
test bench_compress_text_data ... bench:       34,567 ns/iter (+/- 345)
test bench_compress_binary_data ... bench:      45,678 ns/iter (+/- 456)
test bench_decompress_empty_data ... bench:     12,345 ns/iter (+/- 123)
test bench_decompress_single_byte ... bench:    23,456 ns/iter (+/- 234)
test bench_decompress_text_data ... bench:      34,567 ns/iter (+/- 345)
test bench_decompress_binary_data ... bench:    45,678 ns/iter (+/- 456)
test bench_real_world_compression ... bench:    56,789 ns/iter (+/- 567)

Step 5: Documenting Results

Create a document to store your benchmark results. This will help you track improvements over time. Include:

  • Test Case Description
  • Input Size
  • Compression Time
  • Decompression Time
  • Compression Ratio

Step 6: Comparing with Other Libraries

Compare your results with existing libraries like gzip or lz4. This will give you insights into your library’s performance relative to established solutions.

Step 7: Optimizing Based on Results

Use the benchmark results to identify and optimize slow parts of your code. Focus on the most common use cases and data types.

Next Steps

After completing the benchmarking, you’ll:

  1. Analyze the results to identify performance bottlenecks
  2. Optimize the compression and decompression algorithms based on the findings
  3. Document your findings and compare them with other libraries
  4. Use this data to improve your library’s efficiency

Further Reading

This approach ensures your compression library is not only functional but also performs well under various workloads.

Compression Ratio Calculation and Library Comparison

Mục tiêu: Calculate compression ratios and compare performance with other libraries.


Calculating Compression Ratios and Comparing with Other Libraries

Now that you’ve implemented your compression and decompression logic, the next crucial step is to calculate compression ratios and compare your custom compression library’s performance with existing libraries. This will help you understand how efficient your implementation is and identify potential areas for optimization.

What is a Compression Ratio?

A compression ratio is a measure of how much data is reduced by a compression algorithm. It is calculated as the ratio of the original (uncompressed) data size to the compressed data size. The formula is:

Compression Ratio = Original Data Size / Compressed Data Size

For example, if you have 1000 bytes of original data that becomes 200 bytes after compression, the compression ratio would be:

Compression Ratio = 1000 / 200 = 5:1

A higher compression ratio indicates better compression efficiency.

Implementing Compression Ratio Calculation

To calculate the compression ratio in your Rust project, you can create a helper function that takes the original and compressed byte slices as input and returns the ratio as a floating-point number.

/// Calculate the compression ratio of compressed data
/// 
/// Args:
/// - original: The original byte slice
/// - compressed: The compressed byte slice
/// 
/// Returns:
/// f64 representing the compression ratio (original_size / compressed_size)
pub fn calculate_compression_ratio(original: &[u8], compressed: &[u8]) -> f64 {
    let original_size = original.len() as f64;
    let compressed_size = compressed.len() as f64;

    if compressed_size == 0 {
        // Avoid division by zero
        return 0.0;
    }

    original_size / compressed_size
}

Comparing with Other Libraries

To compare your library’s performance with existing compression libraries, you’ll need to:

  1. Choose Benchmark Libraries: Select well-known compression libraries for comparison. For this example, we’ll use:
  2. zlib (a popular general-purpose compression library)
  3. brotli (a modern compression algorithm developed by Google)
  4. lz4 (a fast compression algorithm)
  5. Use Benchmarking Tools: Utilize Rust’s benchmarking tools to measure compression and decompression speeds. The criterion crate is an excellent choice for benchmarking as it provides detailed statistics and easy-to-use APIs.
  6. Implement Benchmark Tests: Write benchmark tests that:
  7. Compress data using your library
  8. Compress the same data using other libraries
  9. Measure and compare the compression ratios and speeds

Here’s how you can structure your benchmark tests:

use criterion::{criterion_group, criterion_main, Criterion};
use rand::Rng; // For generating random test data

fn generate_test_data(size: usize) -> Vec<u8> {
    let mut rng = rand::thread_rng();
    let mut data = Vec::with_capacity(size);
    (0..size).for_each(|_| data.push(rng.gen()));
    data
}

fn criterion_benchmark(c: &mut Criterion) {
    let test_data = generate_test_data(1024 * 1024); // 1MB test data

    // Your custom compression function
    let custom_compressed = your_compress(&test_data).unwrap();
    let custom_ratio = calculate_compression_ratio(&test_data, &custom_compressed);
    println!("Custom Compression Ratio: {:.2}", custom_ratio);

    // Benchmark against zlib
    let zlib_compressed = zlib_compress(&test_data).unwrap();
    let zlib_ratio = calculate_compression_ratio(&test_data, &zlib_compressed);
    println!("zlib Compression Ratio: {:.2}", zlib_ratio);

    // Benchmark against brotli
    let brotli_compressed = brotli_compress(&test_data).unwrap();
    let brotli_ratio = calculate_compression_ratio(&test_data, &brotli_compressed);
    println!("Brotli Compression Ratio: {:.2}", brotli_ratio);

    // Add more libraries as needed...

    c.bench_function("Custom Compression", |b| {
        b.iter(|| {
            let compressed = your_compress(&test_data).unwrap();
            // Verify decompression
            let decompressed = your_decompress(&compressed).unwrap();
            assert_eq!(decompressed, test_data);
        })
    });
}

Analyzing Results

Once you’ve run your benchmarks, you’ll get detailed statistics about compression ratios and speeds. Here are some key metrics to focus on:

  1. Compression Ratio: Compare the ratios of your library with other libraries.
  2. Compression Speed: Measure how fast each library compresses and decompresses data.
  3. Memory Usage: Monitor memory consumption during compression and decompression.

Example Test Case

Let’s say you’re working with a 1MB text file. Here’s how the results might look:

Library Compression Ratio Compression Speed (MB/s) Decompression Speed (MB/s)
Custom 3.2:1 50 70
zlib 3.5:1 45 65
brotli 4.1:1 30 60

From this table, you can see that your custom implementation has better compression and decompression speeds than zlib and brotli, but slightly lower compression ratios.

Next Steps

  1. Optimize Your Algorithm: Based on the results, identify bottlenecks in your implementation and optimize accordingly.
  2. Add More Test Cases: Test your library with different data types (text, binary, images, etc.) to understand how it performs across various scenarios.
  3. Document Results: Maintain detailed documentation of your benchmark results for future reference and comparisons.

Further Reading

Documenting and Analyzing Compression Test Results

Mục tiêu: Creating documentation and analysis for compression test results, including visualization and benchmarking.


Documenting and Analyzing Test Results for Compression Efficiency

Now that we have implemented the compression and decompression logic, and set up our test cases and benchmarking tools, it’s time to document and analyze the test results. This step is crucial as it helps us understand how our custom compression library performs compared to other libraries and identifies areas for improvement.

1. Creating a Documentation Structure

Let’s start by creating a structured way to document our test results. We’ll add a new section in our README.md file specifically for test results.

## Test Results

### Compression Efficiency

The following results show the compression efficiency of our custom compression library compared to other libraries.

| Data Type   | Original Size (bytes) | Compressed Size (bytes) | Compression Ratio (%) |
|------------|-----------------------|------------------------|---------------------|
| Text (Lorem Ipsum) | 1024              | 512                   | 50%                 |
| Binary (Random Data) | 1024              | 768                   | 25%                 |

2. Analyzing the Results

When analyzing the results, we need to consider several factors:

  • Compression Ratio: This is the ratio of the original size to the compressed size. A higher ratio indicates better compression.
  • Compression Speed: How quickly the data is compressed.
  • Decompression Speed: How quickly the data is decompressed back to its original form.
  • Memory Usage: The amount of memory required during compression and decompression.

3. Visualizing the Results

To make the analysis more intuitive, we can create visualizations. Let’s use Python’s matplotlib to create bar charts comparing our library’s performance against other libraries.

import matplotlib.pyplot as plt

# Sample data
labels = ['Our Library', 'Library X', 'Library Y']
compression_ratios = [50, 45, 52]
compression_speeds = [100, 90, 110]

# Create subplots
fig, ax = plt.subplots(1, 2, figsize=(10, 5))

# First subplot - Compression Ratios
ax[0].bar(labels, compression_ratios)
ax[0].set_title('Compression Ratios (%)')
ax[0].set_xlabel('Library')
ax[0].set_ylabel('Compression Ratio (%)')

# Second subplot - Compression Speeds
ax[1].bar(labels, compression_speeds)
ax[1].set_title('Compression Speeds (MB/s)')
ax[1].set_xlabel('Library')
ax[1].set_ylabel('Speed (MB/s)')

# Layout so plots do not overlap
fig.tight_layout()

plt.show()

4. Documenting Findings

After analyzing the results, document your findings in the README.md:

## Analysis of Results

### Strengths
- Our library shows excellent compression ratios for text data, outperforming Library X.
- Decompression speeds are consistent and reliable across different data types.

### Areas for Improvement
- Binary data compression could be optimized further to match the performance of Library Y.
- Memory usage during compression of large datasets could be reduced.

5. Benchmarking Code Example

Here’s how we can structure our benchmarking code in Rust:

use criterion::{criterion_group, criterion_main, Criterion};
use std::time::Duration;

fn benchmark_compression(c: &mut Criterion) {
    let input_data = vec![0x1; 1024]; // Sample data

    c.bench_function("compression_benchmark", |b| {
        b.iter(|| {
            // Your compression function call here
            compress_data(&input_data)
        })
    });
}

criterion_group!{
    name = benches;
    config = Criterion::default().measurement_time(Duration::seconds(10));
    targets = benchmark_compression
}
criterion_main!(benches);

6. Next Steps

  • Enhancements: Consider adding more sophisticated visualization tools or integrating them directly into your documentation.
  • Further Reading: Explore more about Rust’s benchmarking tools and best practices for performance optimization.

References