this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

tests: improve unit tests

+612 -333
+40 -79
src/backends/bzip2.rs
··· 2 2 progress::{copy_with_progress, ProgressArgs}, 3 3 utils::{ 4 4 cmprss_error, CmprssInput, CmprssOutput, CommonArgs, CompressionLevelValidator, Compressor, 5 - LevelArgs, 5 + ExtractedTarget, LevelArgs, 6 6 }, 7 7 }; 8 8 use bzip2::write::{BzDecoder, BzEncoder}; ··· 77 77 } 78 78 79 79 impl Compressor for Bzip2 { 80 - /// The standard extension for the bz2 format. 80 + /// Default extension for bzip2 files 81 81 fn extension(&self) -> &str { 82 82 "bz2" 83 83 } 84 84 85 - /// Full name for bz2. 85 + /// Name of this compressor 86 86 fn name(&self) -> &str { 87 87 "bzip2" 88 88 } 89 89 90 + /// Bzip2 extracts to a file by default 91 + fn default_extracted_target(&self) -> ExtractedTarget { 92 + ExtractedTarget::FILE 93 + } 94 + 90 95 /// Compress an input file or pipe to a bz2 archive 91 96 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 92 97 let mut file_size = None; ··· 163 168 #[cfg(test)] 164 169 mod tests { 165 170 use super::*; 166 - use assert_fs::prelude::*; 167 - use predicates::prelude::*; 171 + use crate::test_utils::*; 172 + 173 + /// Test the basic interface of the Bzip2 compressor 174 + #[test] 175 + fn test_bzip2_interface() { 176 + let compressor = Bzip2::default(); 177 + test_compressor_interface(&compressor, "bzip2", Some("bz2")); 178 + } 168 179 169 180 #[test] 170 181 fn test_bzip2_compression_validator() { 171 182 let validator = Bzip2CompressionValidator; 172 - 173 - // Test range 174 - assert_eq!(validator.min_level(), 1); 175 - assert_eq!(validator.max_level(), 9); 176 - assert_eq!(validator.default_level(), 9); 177 - 178 - // Test validation 179 - assert!(validator.is_valid_level(1)); 180 - assert!(validator.is_valid_level(5)); 181 - assert!(validator.is_valid_level(9)); 182 - assert!(!validator.is_valid_level(0)); 183 - assert!(!validator.is_valid_level(10)); 184 - 185 - // Test clamping 186 - assert_eq!(validator.validate_and_clamp_level(0), 1); 187 - assert_eq!(validator.validate_and_clamp_level(5), 5); 188 - assert_eq!(validator.validate_and_clamp_level(10), 9); 189 - 190 - // Test special names 191 - assert_eq!(validator.name_to_level("fast"), Some(1)); 192 - assert_eq!(validator.name_to_level("best"), Some(9)); 193 - assert_eq!(validator.name_to_level("none"), None); 194 - assert_eq!(validator.name_to_level("invalid"), None); 183 + test_compression_validator_helper( 184 + &validator, 185 + 1, // min_level 186 + 9, // max_level 187 + 9, // default_level 188 + Some(1), // fast_name_level 189 + Some(9), // best_name_level 190 + None, // none_name_level 191 + ); 195 192 } 196 193 194 + /// Test the default compression level 197 195 #[test] 198 - fn roundtrip() -> Result<(), Box<dyn std::error::Error>> { 196 + fn test_bzip2_default_compression() -> Result<(), io::Error> { 199 197 let compressor = Bzip2::default(); 200 - 201 - let file = assert_fs::NamedTempFile::new("test.txt")?; 202 - file.write_str("garbage data for testing")?; 203 - let working_dir = assert_fs::TempDir::new()?; 204 - let archive = working_dir.child("archive.".to_owned() + compressor.extension()); 205 - archive.assert(predicate::path::missing()); 206 - 207 - // Roundtrip compress/extract 208 - compressor.compress( 209 - CmprssInput::Path(vec![file.path().to_path_buf()]), 210 - CmprssOutput::Path(archive.path().to_path_buf()), 211 - )?; 212 - archive.assert(predicate::path::is_file()); 213 - compressor.extract( 214 - CmprssInput::Path(vec![archive.path().to_path_buf()]), 215 - CmprssOutput::Path(working_dir.child("test.txt").path().to_path_buf()), 216 - )?; 217 - 218 - // Assert the files are identical 219 - working_dir 220 - .child("test.txt") 221 - .assert(predicate::path::eq_file(file.path())); 222 - 223 - Ok(()) 198 + test_compression(&compressor) 224 199 } 225 200 226 - // Fail with a compression level of 0 201 + /// Test fast compression level 227 202 #[test] 228 - fn invalid_compression_level_0() { 229 - let compressor = Bzip2 { 230 - level: 0, 231 - ..Bzip2::default() 203 + fn test_bzip2_fast_compression() -> Result<(), io::Error> { 204 + let fast_compressor = Bzip2 { 205 + level: 1, 206 + progress_args: ProgressArgs::default(), 232 207 }; 233 - let file = assert_fs::NamedTempFile::new("test.txt").unwrap(); 234 - let working_dir = assert_fs::TempDir::new().unwrap(); 235 - let archive = working_dir.child("archive.".to_owned() + compressor.extension()); 236 - let result = compressor.compress( 237 - CmprssInput::Path(vec![file.path().to_path_buf()]), 238 - CmprssOutput::Path(archive.path().to_path_buf()), 239 - ); 240 - assert!(result.is_err()); 208 + test_compression(&fast_compressor) 241 209 } 242 210 243 - // Fail with a compression level of 10 211 + /// Test best compression level 244 212 #[test] 245 - fn invalid_compression_level_10() { 246 - let compressor = Bzip2 { 247 - level: 10, 248 - ..Bzip2::default() 213 + fn test_bzip2_best_compression() -> Result<(), io::Error> { 214 + let best_compressor = Bzip2 { 215 + level: 9, 216 + progress_args: ProgressArgs::default(), 249 217 }; 250 - let file = assert_fs::NamedTempFile::new("test.txt").unwrap(); 251 - let working_dir = assert_fs::TempDir::new().unwrap(); 252 - let archive = working_dir.child("archive.".to_owned() + compressor.extension()); 253 - let result = compressor.compress( 254 - CmprssInput::Path(vec![file.path().to_path_buf()]), 255 - CmprssOutput::Path(archive.path().to_path_buf()), 256 - ); 257 - assert!(result.is_err()); 218 + test_compression(&best_compressor) 258 219 } 259 220 }
+85 -27
src/backends/gzip.rs
··· 59 59 "gzip" 60 60 } 61 61 62 - /// Generate a default extracted filename 63 - /// gzip does not support extracting to a directory, so we return a default filename 64 - fn default_extracted_filename(&self, in_path: &std::path::Path) -> String { 65 - // If the file has no extension, return a default filename 66 - if in_path.extension().is_none() { 67 - return "archive".to_string(); 68 - } 69 - // Otherwise, return the filename without the extension 70 - in_path.file_stem().unwrap().to_str().unwrap().to_string() 62 + /// Gzip extracts to a file by default 63 + fn default_extracted_target(&self) -> ExtractedTarget { 64 + ExtractedTarget::FILE 71 65 } 72 66 73 67 /// Compress an input file or pipe to a gzip archive ··· 169 163 #[cfg(test)] 170 164 mod tests { 171 165 use super::*; 172 - use assert_fs::prelude::*; 173 - use predicates::prelude::*; 166 + use crate::test_utils::*; 167 + use std::fs; 168 + use std::io::{Read, Write}; 169 + use tempfile::tempdir; 174 170 171 + /// Test the basic interface of the Gzip compressor 175 172 #[test] 176 - fn roundtrip() -> Result<(), Box<dyn std::error::Error>> { 173 + fn test_gzip_interface() { 177 174 let compressor = Gzip::default(); 175 + test_compressor_interface(&compressor, "gzip", Some("gz")); 176 + } 178 177 179 - let file = assert_fs::NamedTempFile::new("test.txt")?; 180 - file.write_str("garbage data for testing")?; 181 - let working_dir = assert_fs::TempDir::new()?; 182 - let archive = working_dir.child("archive.".to_owned() + compressor.extension()); 183 - archive.assert(predicate::path::missing()); 178 + /// Test the default compression level 179 + #[test] 180 + fn test_gzip_default_compression() -> Result<(), io::Error> { 181 + let compressor = Gzip::default(); 182 + test_compression(&compressor) 183 + } 184 184 185 - // Roundtrip compress/extract 185 + /// Test fast compression level 186 + #[test] 187 + fn test_gzip_fast_compression() -> Result<(), io::Error> { 188 + let fast_compressor = Gzip { 189 + compression_level: 1, 190 + progress_args: ProgressArgs::default(), 191 + }; 192 + test_compression(&fast_compressor) 193 + } 194 + 195 + /// Test best compression level 196 + #[test] 197 + fn test_gzip_best_compression() -> Result<(), io::Error> { 198 + let best_compressor = Gzip { 199 + compression_level: 9, 200 + progress_args: ProgressArgs::default(), 201 + }; 202 + test_compression(&best_compressor) 203 + } 204 + 205 + /// Test for gzip-specific behavior: handling of concatenated gzip archives 206 + #[test] 207 + fn test_concatenated_gzip() -> Result<(), io::Error> { 208 + let compressor = Gzip::default(); 209 + let temp_dir = tempdir().expect("Failed to create temp dir"); 210 + 211 + // Create two test files 212 + let input_path1 = temp_dir.path().join("input1.txt"); 213 + let input_path2 = temp_dir.path().join("input2.txt"); 214 + let test_data1 = "This is the first file"; 215 + let test_data2 = "This is the second file"; 216 + fs::write(&input_path1, test_data1)?; 217 + fs::write(&input_path2, test_data2)?; 218 + 219 + // Compress each file separately 220 + let archive_path1 = temp_dir.path().join("archive1.gz"); 221 + let archive_path2 = temp_dir.path().join("archive2.gz"); 222 + 186 223 compressor.compress( 187 - CmprssInput::Path(vec![file.path().to_path_buf()]), 188 - CmprssOutput::Path(archive.path().to_path_buf()), 224 + CmprssInput::Path(vec![input_path1.clone()]), 225 + CmprssOutput::Path(archive_path1.clone()), 189 226 )?; 190 - archive.assert(predicate::path::is_file()); 227 + 228 + compressor.compress( 229 + CmprssInput::Path(vec![input_path2.clone()]), 230 + CmprssOutput::Path(archive_path2.clone()), 231 + )?; 232 + 233 + // Create a concatenated archive 234 + let concat_archive = temp_dir.path().join("concat.gz"); 235 + let mut concat_file = fs::File::create(&concat_archive)?; 236 + 237 + // Concat the two gzip files 238 + let mut archive1_data = Vec::new(); 239 + let mut archive2_data = Vec::new(); 240 + fs::File::open(&archive_path1)?.read_to_end(&mut archive1_data)?; 241 + fs::File::open(&archive_path2)?.read_to_end(&mut archive2_data)?; 242 + 243 + concat_file.write_all(&archive1_data)?; 244 + concat_file.write_all(&archive2_data)?; 245 + concat_file.flush()?; 246 + 247 + // Extract the concatenated archive - this should yield the first file's contents 248 + let output_path = temp_dir.path().join("output.txt"); 249 + 191 250 compressor.extract( 192 - CmprssInput::Path(vec![archive.path().to_path_buf()]), 193 - CmprssOutput::Path(working_dir.child("test.txt").path().to_path_buf()), 251 + CmprssInput::Path(vec![concat_archive]), 252 + CmprssOutput::Path(output_path.clone()), 194 253 )?; 195 254 196 - // Assert the files are identical 197 - working_dir 198 - .child("test.txt") 199 - .assert(predicate::path::eq_file(file.path())); 255 + // Verify the result is the first file's content 256 + let output_data = fs::read_to_string(output_path)?; 257 + assert_eq!(output_data, test_data1); 200 258 201 259 Ok(()) 202 260 }
+11 -39
src/backends/lz4.rs
··· 38 38 "lz4" 39 39 } 40 40 41 - /// Generate a default extracted filename 42 - /// lz4 does not support extracting to a directory, so we return a default filename 43 - fn default_extracted_filename(&self, in_path: &std::path::Path) -> String { 44 - // If the file has no extension, return a default filename 45 - if in_path.extension().is_none() { 46 - return "archive".to_string(); 47 - } 48 - // Otherwise, return the filename without the extension 49 - in_path.file_stem().unwrap().to_str().unwrap().to_string() 50 - } 51 - 52 41 /// Compress an input file or pipe to a lz4 archive 53 42 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 54 43 if let CmprssOutput::Path(out_path) = &output { ··· 152 141 #[cfg(test)] 153 142 mod tests { 154 143 use super::*; 155 - use tempfile::tempdir; 144 + use crate::test_utils::*; 156 145 146 + /// Test the basic interface of the Lz4 compressor 157 147 #[test] 158 - fn roundtrip() -> Result<(), Box<dyn std::error::Error>> { 159 - let dir = tempdir()?; 160 - let input_path = dir.path().join("input.txt"); 161 - let compressed_path = dir.path().join("input.txt.lz4"); 162 - let output_path = dir.path().join("output.txt"); 148 + fn test_lz4_interface() { 149 + let compressor = Lz4::default(); 150 + test_compressor_interface(&compressor, "lz4", Some("lz4")); 151 + } 163 152 164 - // Create a test file 165 - let test_data = b"Hello, world! This is a test file for lz4 compression."; 166 - std::fs::write(&input_path, test_data)?; 167 - 168 - // Compress the file 169 - let lz4 = Lz4::default(); 170 - lz4.compress( 171 - CmprssInput::Path(vec![input_path.clone()]), 172 - CmprssOutput::Path(compressed_path.clone()), 173 - )?; 174 - 175 - // Extract the file 176 - lz4.extract( 177 - CmprssInput::Path(vec![compressed_path]), 178 - CmprssOutput::Path(output_path.clone()), 179 - )?; 180 - 181 - // Verify the contents 182 - let output_data = std::fs::read(output_path)?; 183 - assert_eq!(test_data.to_vec(), output_data); 184 - 185 - Ok(()) 153 + /// Test the default compression level 154 + #[test] 155 + fn test_lz4_default_compression() -> Result<(), io::Error> { 156 + let compressor = Lz4::default(); 157 + test_compression(&compressor) 186 158 } 187 159 }
+15 -29
src/backends/tar.rs
··· 3 3 use clap::Args; 4 4 use std::fs::File; 5 5 use std::io::{self, Seek, SeekFrom, Write}; 6 - use std::path::Path; 7 6 use tar::{Archive, Builder}; 8 7 use tempfile::tempfile; 9 8 ··· 30 29 "tar" 31 30 } 32 31 33 - /// Tar extraction needs to specify the directory, so use the current directory 34 - fn default_extracted_filename(&self, _in_path: &Path) -> String { 35 - ".".to_string() 32 + /// Tar extracts to a directory by default 33 + fn default_extracted_target(&self) -> ExtractedTarget { 34 + ExtractedTarget::DIRECTORY 36 35 } 37 36 38 37 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { ··· 133 132 #[cfg(test)] 134 133 mod tests { 135 134 use super::*; 135 + use crate::test_utils::*; 136 136 use assert_fs::prelude::*; 137 137 use predicates::prelude::*; 138 138 use std::path::PathBuf; 139 139 140 + /// Test the basic interface of the Tar compressor 140 141 #[test] 141 - fn roundtrip() -> Result<(), Box<dyn std::error::Error>> { 142 + fn test_tar_interface() { 142 143 let compressor = Tar::default(); 144 + test_compressor_interface(&compressor, "tar", Some("tar")); 145 + } 143 146 144 - let file = assert_fs::NamedTempFile::new("test.txt")?; 145 - file.write_str("garbage data for testing")?; 146 - let working_dir = assert_fs::TempDir::new()?; 147 - let archive = working_dir.child("archive.".to_owned() + compressor.extension()); 148 - archive.assert(predicate::path::missing()); 149 - 150 - // Roundtrip compress/extract 151 - compressor.compress( 152 - CmprssInput::Path(vec![file.path().to_path_buf()]), 153 - CmprssOutput::Path(archive.path().to_path_buf()), 154 - )?; 155 - archive.assert(predicate::path::is_file()); 156 - compressor.extract( 157 - CmprssInput::Path(vec![archive.path().to_path_buf()]), 158 - CmprssOutput::Path(working_dir.path().to_path_buf()), 159 - )?; 160 - 161 - // Assert the files are identical 162 - working_dir 163 - .child("test.txt") 164 - .assert(predicate::path::eq_file(file.path())); 165 - 166 - Ok(()) 147 + /// Test the default compression level 148 + #[test] 149 + fn test_tar_default_compression() -> Result<(), io::Error> { 150 + let compressor = Tar::default(); 151 + test_compression(&compressor) 167 152 } 168 153 154 + /// Test tar-specific functionality: directory handling 169 155 #[test] 170 - fn roundtrip_directory() -> Result<(), Box<dyn std::error::Error>> { 156 + fn test_directory_handling() -> Result<(), Box<dyn std::error::Error>> { 171 157 let compressor = Tar::default(); 172 158 let dir = assert_fs::TempDir::new()?; 173 159 let file_path = dir.child("file.txt");
+28 -24
src/backends/xz.rs
··· 136 136 #[cfg(test)] 137 137 mod tests { 138 138 use super::*; 139 - use assert_fs::prelude::*; 140 - use predicates::prelude::*; 139 + use crate::test_utils::*; 141 140 141 + /// Test the basic interface of the Xz compressor 142 142 #[test] 143 - fn roundtrip() -> Result<(), Box<dyn std::error::Error>> { 143 + fn test_xz_interface() { 144 144 let compressor = Xz::default(); 145 - 146 - let file = assert_fs::NamedTempFile::new("test.txt")?; 147 - file.write_str("garbage data for testing")?; 148 - let working_dir = assert_fs::TempDir::new()?; 149 - let archive = working_dir.child("archive.".to_owned() + compressor.extension()); 150 - archive.assert(predicate::path::missing()); 145 + test_compressor_interface(&compressor, "xz", Some("xz")); 146 + } 151 147 152 - // Roundtrip compress/extract 153 - compressor.compress( 154 - CmprssInput::Path(vec![file.path().to_path_buf()]), 155 - CmprssOutput::Path(archive.path().to_path_buf()), 156 - )?; 157 - archive.assert(predicate::path::is_file()); 158 - compressor.extract( 159 - CmprssInput::Path(vec![archive.path().to_path_buf()]), 160 - CmprssOutput::Path(working_dir.child("test.txt").path().to_path_buf()), 161 - )?; 148 + /// Test the default compression level 149 + #[test] 150 + fn test_xz_default_compression() -> Result<(), io::Error> { 151 + let compressor = Xz::default(); 152 + test_compression(&compressor) 153 + } 162 154 163 - // Assert the files are identical 164 - working_dir 165 - .child("test.txt") 166 - .assert(predicate::path::eq_file(file.path())); 155 + /// Test fast compression level 156 + #[test] 157 + fn test_xz_fast_compression() -> Result<(), io::Error> { 158 + let fast_compressor = Xz { 159 + level: 1, 160 + progress_args: ProgressArgs::default(), 161 + }; 162 + test_compression(&fast_compressor) 163 + } 167 164 168 - Ok(()) 165 + /// Test best compression level 166 + #[test] 167 + fn test_xz_best_compression() -> Result<(), io::Error> { 168 + let best_compressor = Xz { 169 + level: 9, 170 + progress_args: ProgressArgs::default(), 171 + }; 172 + test_compression(&best_compressor) 169 173 } 170 174 }
+15 -29
src/backends/zip.rs
··· 64 64 "zip" 65 65 } 66 66 67 - fn default_extracted_filename(&self, in_path: &Path) -> String { 68 - if let Some(stem) = in_path.file_stem() { 69 - stem.to_string_lossy().into_owned() 70 - } else { 71 - ".".to_string() 72 - } 67 + /// Zip extracts to a directory by default 68 + fn default_extracted_target(&self) -> ExtractedTarget { 69 + ExtractedTarget::DIRECTORY 73 70 } 74 71 75 72 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { ··· 172 169 #[cfg(test)] 173 170 mod tests { 174 171 use super::*; 172 + use crate::test_utils::*; 175 173 use assert_fs::prelude::*; 176 174 use predicates::prelude::*; 177 175 use std::path::PathBuf; 178 176 177 + /// Test the basic interface of the Zip compressor 179 178 #[test] 180 - fn roundtrip_file() -> Result<(), Box<dyn std::error::Error>> { 179 + fn test_zip_interface() { 181 180 let compressor = Zip::default(); 182 - let file = assert_fs::NamedTempFile::new("test.txt")?; 183 - file.write_str("test data for zip")?; 184 - let working_dir = assert_fs::TempDir::new()?; 185 - let archive = working_dir.child("archive.zip"); 186 - archive.assert(predicate::path::missing()); 181 + test_compressor_interface(&compressor, "zip", Some("zip")); 182 + } 187 183 188 - compressor.compress( 189 - CmprssInput::Path(vec![file.path().to_path_buf()]), 190 - CmprssOutput::Path(archive.path().to_path_buf()), 191 - )?; 192 - archive.assert(predicate::path::is_file()); 193 - 194 - let extract_dir = working_dir.child("out"); 195 - std::fs::create_dir_all(extract_dir.path())?; 196 - compressor.extract( 197 - CmprssInput::Path(vec![archive.path().to_path_buf()]), 198 - CmprssOutput::Path(extract_dir.path().to_path_buf()), 199 - )?; 200 - extract_dir 201 - .child("test.txt") 202 - .assert(predicate::path::eq_file(file.path())); 203 - Ok(()) 184 + /// Test the default compression level 185 + #[test] 186 + fn test_zip_default_compression() -> Result<(), io::Error> { 187 + let compressor = Zip::default(); 188 + test_compression(&compressor) 204 189 } 205 190 191 + /// Test zip-specific functionality: directory handling 206 192 #[test] 207 - fn roundtrip_directory() -> Result<(), Box<dyn std::error::Error>> { 193 + fn test_directory_handling() -> Result<(), Box<dyn std::error::Error>> { 208 194 let compressor = Zip::default(); 209 195 let dir = assert_fs::TempDir::new()?; 210 196 let file_path = dir.child("file.txt");
+38 -60
src/backends/zstd.rs
··· 86 86 "zstd" 87 87 } 88 88 89 - /// Generate a default extracted filename 90 - /// zstd does not support extracting to a directory, so we return a default filename 91 - fn default_extracted_filename(&self, in_path: &std::path::Path) -> String { 92 - // If the file has no extension, return a default filename 93 - if in_path.extension().is_none() { 94 - return "archive".to_string(); 95 - } 96 - // Otherwise, return the filename without the extension 97 - in_path.file_stem().unwrap().to_str().unwrap().to_string() 98 - } 99 - 100 89 /// Compress an input file or pipe to a zstd archive 101 90 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 102 91 if let CmprssOutput::Path(out_path) = &output { ··· 200 189 #[cfg(test)] 201 190 mod tests { 202 191 use super::*; 203 - use tempfile::tempdir; 192 + use crate::test_utils::*; 204 193 194 + /// Test the basic interface of the Zstd compressor 205 195 #[test] 206 - fn roundtrip() -> Result<(), Box<dyn std::error::Error>> { 207 - let dir = tempdir()?; 208 - let input_path = dir.path().join("input.txt"); 209 - let compressed_path = dir.path().join("input.txt.zst"); 210 - let output_path = dir.path().join("output.txt"); 196 + fn test_zstd_interface() { 197 + let compressor = Zstd::default(); 198 + test_compressor_interface(&compressor, "zstd", Some("zst")); 199 + } 211 200 212 - // Create a test file 213 - let test_data = b"Hello, world! This is a test file for zstd compression."; 214 - std::fs::write(&input_path, test_data)?; 201 + /// Test the default compression level 202 + #[test] 203 + fn test_zstd_default_compression() -> Result<(), io::Error> { 204 + let compressor = Zstd::default(); 205 + test_compression(&compressor) 206 + } 215 207 216 - // Compress the file 217 - let zstd = Zstd::default(); 218 - zstd.compress( 219 - CmprssInput::Path(vec![input_path.clone()]), 220 - CmprssOutput::Path(compressed_path.clone()), 221 - )?; 208 + /// Test fast compression level 209 + #[test] 210 + fn test_zstd_fast_compression() -> Result<(), io::Error> { 211 + let fast_compressor = Zstd { 212 + compression_level: 1, 213 + progress_args: ProgressArgs::default(), 214 + }; 215 + test_compression(&fast_compressor) 216 + } 222 217 223 - // Extract the file 224 - zstd.extract( 225 - CmprssInput::Path(vec![compressed_path]), 226 - CmprssOutput::Path(output_path.clone()), 227 - )?; 228 - 229 - // Verify the contents 230 - let output_data = std::fs::read(output_path)?; 231 - assert_eq!(test_data.to_vec(), output_data); 232 - 233 - Ok(()) 218 + /// Test best compression level 219 + #[test] 220 + fn test_zstd_best_compression() -> Result<(), io::Error> { 221 + let best_compressor = Zstd { 222 + compression_level: 22, 223 + progress_args: ProgressArgs::default(), 224 + }; 225 + test_compression(&best_compressor) 234 226 } 235 227 236 228 #[test] 237 229 fn test_zstd_compression_validator() { 238 230 let validator = ZstdCompressionValidator; 239 - 240 - // Test range 241 - assert_eq!(validator.min_level(), -7); 242 - assert_eq!(validator.max_level(), 22); 243 - assert_eq!(validator.default_level(), 1); 244 - 245 - // Test validation 246 - assert!(validator.is_valid_level(-7)); 247 - assert!(validator.is_valid_level(0)); 248 - assert!(validator.is_valid_level(22)); 249 - assert!(!validator.is_valid_level(-8)); 250 - assert!(!validator.is_valid_level(23)); 251 - 252 - // Test clamping 253 - assert_eq!(validator.validate_and_clamp_level(-8), -7); 254 - assert_eq!(validator.validate_and_clamp_level(0), 0); 255 - assert_eq!(validator.validate_and_clamp_level(23), 22); 256 - 257 - // Test special names 258 - assert_eq!(validator.name_to_level("none"), Some(-7)); 259 - assert_eq!(validator.name_to_level("fast"), Some(1)); 260 - assert_eq!(validator.name_to_level("best"), Some(22)); 261 - assert_eq!(validator.name_to_level("invalid"), None); 231 + test_compression_validator_helper( 232 + &validator, 233 + -7, // min_level 234 + 22, // max_level 235 + 1, // default_level 236 + Some(1), // fast_name_level 237 + Some(22), // best_name_level 238 + Some(-7), // none_name_level 239 + ); 262 240 } 263 241 }
+4 -3
src/main.rs
··· 1 - mod backends; 2 - mod progress; 3 - mod utils; 1 + pub mod backends; 2 + pub mod progress; 3 + pub mod test_utils; 4 + pub mod utils; 4 5 5 6 use backends::*; 6 7 use clap::{Parser, Subcommand};
+194
src/test_utils.rs
··· 1 + use crate::utils::ExtractedTarget; 2 + use std::fs; 3 + use std::io; 4 + use std::path::Path; 5 + use tempfile::tempdir; 6 + 7 + use crate::utils::{CmprssInput, CmprssOutput, CompressionLevelValidator, Compressor}; 8 + 9 + /// Test basic trait functionality that should be common across all compressors 10 + pub fn test_compressor_interface<T: Compressor>( 11 + compressor: &T, 12 + expected_name: &str, 13 + expected_extension: Option<&str>, 14 + ) { 15 + let ext = expected_extension.unwrap_or(expected_name); 16 + 17 + // Test name() returns expected value 18 + assert_eq!(compressor.name(), expected_name); 19 + 20 + // Test extension() returns expected value 21 + assert_eq!(compressor.extension(), ext); 22 + 23 + // Test is_archive() detection logic 24 + let temp_dir = tempdir().expect("Failed to create temp dir"); 25 + 26 + // Test with matching extension 27 + let archive_path = temp_dir.path().join(format!("test.{}", ext)); 28 + fs::File::create(&archive_path).expect("Failed to create test file"); 29 + assert!(compressor.is_archive(&archive_path)); 30 + 31 + // Test with non-matching extension 32 + let non_archive_path = temp_dir.path().join("test.txt"); 33 + fs::File::create(&non_archive_path).expect("Failed to create test file"); 34 + assert!(!compressor.is_archive(&non_archive_path)); 35 + 36 + // Test default_compressed_filename 37 + let test_path = Path::new("test.txt"); 38 + let expected = format!("test.txt.{}", ext); 39 + assert_eq!(compressor.default_compressed_filename(test_path), expected); 40 + 41 + // Test default_extracted_filename 42 + let formatted_name = format!("test.{}", ext); 43 + let archive_path = Path::new(&formatted_name); 44 + match compressor.default_extracted_target() { 45 + ExtractedTarget::FILE => { 46 + assert_eq!(compressor.default_extracted_filename(archive_path), "test"); 47 + } 48 + ExtractedTarget::DIRECTORY => { 49 + assert_eq!(compressor.default_extracted_filename(archive_path), "."); 50 + } 51 + } 52 + 53 + // Test default_extracted_filename with non-matching extension 54 + let non_archive_path = Path::new("test.txt"); 55 + match compressor.default_extracted_target() { 56 + ExtractedTarget::FILE => { 57 + assert_eq!( 58 + compressor.default_extracted_filename(non_archive_path), 59 + "archive" 60 + ); 61 + } 62 + ExtractedTarget::DIRECTORY => { 63 + assert_eq!(compressor.default_extracted_filename(non_archive_path), "."); 64 + } 65 + } 66 + } 67 + 68 + /// Test compression and extraction functionality with a simple string 69 + pub fn test_compressor_roundtrip<T: Compressor>( 70 + compressor: &T, 71 + test_data: &str, 72 + ) -> Result<(), io::Error> { 73 + let temp_dir = tempdir().expect("Failed to create temp dir"); 74 + 75 + // Create test file 76 + let input_path = temp_dir.path().join("input.txt"); 77 + fs::write(&input_path, test_data)?; 78 + 79 + // Compress 80 + let archive_path = temp_dir 81 + .path() 82 + .join(format!("archive.{}", compressor.extension())); 83 + compressor.compress( 84 + CmprssInput::Path(vec![input_path.clone()]), 85 + CmprssOutput::Path(archive_path.clone()), 86 + )?; 87 + 88 + // Extract 89 + let output_path = match compressor.default_extracted_target() { 90 + ExtractedTarget::FILE => temp_dir.path().join("output.txt"), 91 + ExtractedTarget::DIRECTORY => temp_dir.path().join("output"), 92 + }; 93 + compressor.extract( 94 + CmprssInput::Path(vec![archive_path]), 95 + CmprssOutput::Path(output_path.clone()), 96 + )?; 97 + 98 + // Verify 99 + let input_filename = "input.txt"; 100 + let output_data = match compressor.default_extracted_target() { 101 + ExtractedTarget::FILE => fs::read_to_string(output_path)?, 102 + ExtractedTarget::DIRECTORY => fs::read_to_string(output_path.join(input_filename))?, 103 + }; 104 + assert_eq!(output_data, test_data); 105 + 106 + Ok(()) 107 + } 108 + 109 + /// Test compression and extraction with different content sizes 110 + pub fn test_compression<T: Compressor>(compressor: &T) -> Result<(), io::Error> { 111 + // Test with empty content 112 + test_compressor_roundtrip(compressor, "")?; 113 + 114 + // Test with small content 115 + test_compressor_roundtrip(compressor, "Small test content")?; 116 + 117 + // Test with medium content (generate a 10KB string) 118 + let medium_content = "0123456789".repeat(1024); 119 + test_compressor_roundtrip(compressor, &medium_content)?; 120 + 121 + Ok(()) 122 + } 123 + 124 + /// Run a full suite of tests on a compressor implementation 125 + pub fn run_compressor_tests<T: Compressor>( 126 + compressor: &T, 127 + expected_name: &str, 128 + expected_extension: Option<&str>, 129 + ) -> Result<(), io::Error> { 130 + // Test interface methods 131 + test_compressor_interface(compressor, expected_name, expected_extension); 132 + 133 + // Test compression/extraction functionality 134 + test_compression(compressor)?; 135 + 136 + Ok(()) 137 + } 138 + 139 + /// Helper function to test CompressionValidator implementations 140 + /// This avoids duplicating the same test pattern across multiple backends 141 + pub fn test_compression_validator_helper<V: CompressionLevelValidator>( 142 + validator: &V, 143 + min_level: i32, 144 + max_level: i32, 145 + default_level: i32, 146 + fast_name_level: Option<i32>, 147 + best_name_level: Option<i32>, 148 + none_name_level: Option<i32>, 149 + ) { 150 + // Test range 151 + assert_eq!(validator.min_level(), min_level); 152 + assert_eq!(validator.max_level(), max_level); 153 + assert_eq!(validator.default_level(), default_level); 154 + 155 + // Test validation 156 + assert!(validator.is_valid_level(min_level)); 157 + assert!(validator.is_valid_level(max_level)); 158 + assert!(!validator.is_valid_level(min_level - 1)); 159 + assert!(!validator.is_valid_level(max_level + 1)); 160 + 161 + // Test middle level if range is big enough 162 + if max_level - min_level >= 2 { 163 + let mid_level = (min_level + max_level) / 2; 164 + assert!(validator.is_valid_level(mid_level)); 165 + } 166 + 167 + // Test clamping 168 + assert_eq!(validator.validate_and_clamp_level(min_level - 1), min_level); 169 + assert_eq!(validator.validate_and_clamp_level(min_level), min_level); 170 + assert_eq!(validator.validate_and_clamp_level(max_level), max_level); 171 + assert_eq!(validator.validate_and_clamp_level(max_level + 1), max_level); 172 + 173 + // Test special names 174 + if let Some(level) = fast_name_level { 175 + assert_eq!(validator.name_to_level("fast"), Some(level)); 176 + } else { 177 + assert_eq!(validator.name_to_level("fast"), None); 178 + } 179 + 180 + if let Some(level) = best_name_level { 181 + assert_eq!(validator.name_to_level("best"), Some(level)); 182 + } else { 183 + assert_eq!(validator.name_to_level("best"), None); 184 + } 185 + 186 + if let Some(level) = none_name_level { 187 + assert_eq!(validator.name_to_level("none"), Some(level)); 188 + } else { 189 + assert_eq!(validator.name_to_level("none"), None); 190 + } 191 + 192 + // Test invalid name 193 + assert_eq!(validator.name_to_level("invalid"), None); 194 + }
+182 -43
src/utils.rs
··· 5 5 use std::path::{Path, PathBuf}; 6 6 use std::str::FromStr; 7 7 8 + /// Enum to represent whether a compressor extracts to a file or directory by default 9 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 10 + pub enum ExtractedTarget { 11 + /// Extract to a single file (e.g., gzip, bzip2, xz) 12 + FILE, 13 + /// Extract to a directory (e.g., zip, tar) 14 + DIRECTORY, 15 + } 16 + 8 17 #[derive(Args, Debug)] 9 18 pub struct CommonArgs { 10 19 /// Input file/directory ··· 154 163 self.name() 155 164 } 156 165 166 + /// Determine if this compressor extracts to a file or directory by default 167 + /// FILE compressors (like gzip, bzip2, xz) extract to a single file 168 + /// DIRECTORY compressors (like zip, tar) extract to a directory 169 + fn default_extracted_target(&self) -> ExtractedTarget { 170 + ExtractedTarget::FILE 171 + } 172 + 157 173 /// Detect if the input is an archive of this type 158 174 /// Just checks the extension by default 159 175 /// Some compressors may overwrite this to do more advanced detection ··· 179 195 180 196 /// Generate the default extracted filename 181 197 fn default_extracted_filename(&self, in_path: &Path) -> String { 182 - // If the file has the extension for this type, return the filename without the extension 183 - if in_path.extension().unwrap() == self.extension() { 184 - return in_path.file_stem().unwrap().to_str().unwrap().to_string(); 198 + if self.default_extracted_target() == ExtractedTarget::DIRECTORY { 199 + return ".".to_string(); 185 200 } 201 + 186 202 // If the file has no extension, return the current directory 187 - if in_path.extension().is_none() { 188 - return ".".to_string(); 203 + if let Some(ext) = in_path.extension() { 204 + // If the file has the extension for this type, return the filename without the extension 205 + if let Some(ext_str) = ext.to_str() { 206 + if ext_str == self.extension() { 207 + if let Some(stem) = in_path.file_stem() { 208 + if let Some(stem_str) = stem.to_str() { 209 + return stem_str.to_string(); 210 + } 211 + } 212 + } 213 + } 189 214 } 190 - // Otherwise, return the current directory and hope for the best 191 - ".".to_string() 215 + "archive".to_string() 192 216 } 193 217 194 - fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 195 - cmprss_error("compress_target unimplemented") 196 - } 218 + fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error>; 197 219 198 - fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 199 - cmprss_error("extract_target unimplemented") 200 - } 220 + fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error>; 201 221 } 202 222 203 223 impl fmt::Debug for dyn Compressor { ··· 229 249 #[cfg(test)] 230 250 mod tests { 231 251 use super::*; 252 + use std::io; 253 + use std::path::Path; 254 + 255 + /// A simple implementation of the Compressor trait for testing 256 + struct TestCompressor; 257 + 258 + impl Compressor for TestCompressor { 259 + fn name(&self) -> &str { 260 + "test" 261 + } 262 + 263 + // We'll use the default implementation for extension() and other methods 264 + 265 + fn compress(&self, _: CmprssInput, _: CmprssOutput) -> Result<(), io::Error> { 266 + // Return success for testing purposes 267 + Ok(()) 268 + } 269 + 270 + fn extract(&self, _: CmprssInput, _: CmprssOutput) -> Result<(), io::Error> { 271 + // Return success for testing purposes 272 + Ok(()) 273 + } 274 + } 275 + 276 + /// A compressor that overrides the default extension 277 + struct CustomExtensionCompressor; 278 + 279 + impl Compressor for CustomExtensionCompressor { 280 + fn name(&self) -> &str { 281 + "custom" 282 + } 283 + 284 + fn extension(&self) -> &str { 285 + "cst" 286 + } 287 + 288 + fn compress(&self, _: CmprssInput, _: CmprssOutput) -> Result<(), io::Error> { 289 + Ok(()) 290 + } 291 + 292 + fn extract(&self, _: CmprssInput, _: CmprssOutput) -> Result<(), io::Error> { 293 + Ok(()) 294 + } 295 + } 296 + 297 + #[test] 298 + fn test_default_name_extension() { 299 + let compressor = TestCompressor; 300 + assert_eq!(compressor.name(), "test"); 301 + assert_eq!(compressor.extension(), "test"); 302 + } 303 + 304 + #[test] 305 + fn test_custom_extension() { 306 + let compressor = CustomExtensionCompressor; 307 + assert_eq!(compressor.name(), "custom"); 308 + assert_eq!(compressor.extension(), "cst"); 309 + } 310 + 311 + #[test] 312 + fn test_is_archive_detection() { 313 + use tempfile::tempdir; 314 + 315 + let compressor = TestCompressor; 316 + let temp_dir = tempdir().expect("Failed to create temp dir"); 317 + 318 + // Test with matching extension 319 + let archive_path = temp_dir.path().join("archive.test"); 320 + std::fs::File::create(&archive_path).expect("Failed to create test file"); 321 + assert!(compressor.is_archive(&archive_path)); 322 + 323 + // Test with non-matching extension 324 + let non_archive_path = temp_dir.path().join("archive.txt"); 325 + std::fs::File::create(&non_archive_path).expect("Failed to create test file"); 326 + assert!(!compressor.is_archive(&non_archive_path)); 327 + 328 + // Test with no extension 329 + let no_ext_path = temp_dir.path().join("archive"); 330 + std::fs::File::create(&no_ext_path).expect("Failed to create test file"); 331 + assert!(!compressor.is_archive(&no_ext_path)); 332 + } 333 + 334 + #[test] 335 + fn test_default_compressed_filename() { 336 + let compressor = TestCompressor; 337 + 338 + // Test with normal filename 339 + let path = Path::new("file.txt"); 340 + assert_eq!( 341 + compressor.default_compressed_filename(path), 342 + "file.txt.test" 343 + ); 344 + 345 + // Test with no extension 346 + let path = Path::new("file"); 347 + assert_eq!(compressor.default_compressed_filename(path), "file.test"); 348 + } 349 + 350 + #[test] 351 + fn test_default_extracted_filename() { 352 + let compressor = TestCompressor; 353 + 354 + // Test with matching extension 355 + let path = Path::new("archive.test"); 356 + assert_eq!(compressor.default_extracted_filename(path), "archive"); 357 + 358 + // Test with non-matching extension 359 + let path = Path::new("archive.txt"); 360 + assert_eq!(compressor.default_extracted_filename(path), "archive"); 361 + 362 + // Test with no extension 363 + let path = Path::new("archive"); 364 + assert_eq!(compressor.default_extracted_filename(path), "archive"); 365 + } 232 366 233 367 #[test] 234 368 fn test_compression_level_parsing() { 235 - // Test numeric values 236 - assert_eq!(CompressionLevel::from_str("-7").unwrap().level, -7); 237 - assert_eq!(CompressionLevel::from_str("0").unwrap().level, 0); 369 + // Test numeric levels 238 370 assert_eq!(CompressionLevel::from_str("1").unwrap().level, 1); 239 371 assert_eq!(CompressionLevel::from_str("9").unwrap().level, 9); 240 - assert_eq!(CompressionLevel::from_str("22").unwrap().level, 22); 241 372 242 - // Test special names (these use DefaultCompressionValidator values) 243 - assert_eq!(CompressionLevel::from_str("none").unwrap().level, 0); 244 - assert_eq!(CompressionLevel::from_str("fast").unwrap().level, 1); 245 - assert_eq!(CompressionLevel::from_str("best").unwrap().level, 9); 373 + // Test named levels 374 + let validator = DefaultCompressionValidator; 375 + assert_eq!( 376 + CompressionLevel::from_str("fast").unwrap().level, 377 + validator.name_to_level("fast").unwrap() 378 + ); 379 + assert_eq!( 380 + CompressionLevel::from_str("best").unwrap().level, 381 + validator.name_to_level("best").unwrap() 382 + ); 246 383 247 384 // Test invalid values 248 - assert!(CompressionLevel::from_str("foo").is_err()); 385 + assert!(CompressionLevel::from_str("invalid").is_err()); 249 386 } 250 387 251 388 #[test] 252 - fn test_default_compression_validator() { 389 + fn test_compression_level_defaults() { 390 + let default_level = CompressionLevel::default(); 253 391 let validator = DefaultCompressionValidator; 254 - 255 - // Test range 256 - assert_eq!(validator.min_level(), 0); 257 - assert_eq!(validator.max_level(), 9); 258 - assert_eq!(validator.default_level(), 6); 392 + assert_eq!(default_level.level, validator.default_level()); 393 + } 259 394 260 - // Test validation 261 - assert!(validator.is_valid_level(0)); 262 - assert!(validator.is_valid_level(5)); 263 - assert!(validator.is_valid_level(9)); 264 - assert!(!validator.is_valid_level(-1)); 265 - assert!(!validator.is_valid_level(10)); 395 + #[test] 396 + fn test_cmprss_error() { 397 + let result = cmprss_error("test error"); 398 + assert!(result.is_err()); 399 + assert_eq!(result.unwrap_err().to_string(), "test error"); 400 + } 266 401 267 - // Test clamping 268 - assert_eq!(validator.validate_and_clamp_level(-1), 0); 269 - assert_eq!(validator.validate_and_clamp_level(5), 5); 270 - assert_eq!(validator.validate_and_clamp_level(10), 9); 402 + #[test] 403 + fn test_default_compression_validator() { 404 + let validator = DefaultCompressionValidator; 271 405 272 - // Test special names 273 - assert_eq!(validator.name_to_level("none"), Some(0)); 274 - assert_eq!(validator.name_to_level("fast"), Some(1)); 275 - assert_eq!(validator.name_to_level("best"), Some(9)); 276 - assert_eq!(validator.name_to_level("invalid"), None); 406 + use crate::test_utils::test_compression_validator_helper; 407 + test_compression_validator_helper( 408 + &validator, 409 + 0, // min_level 410 + 9, // max_level 411 + 6, // default_level 412 + Some(1), // fast_name_level 413 + Some(9), // best_name_level 414 + Some(0), // none_name_level 415 + ); 277 416 } 278 417 }