diff --git a/Cargo.toml b/Cargo.toml index 0e70d9407..2be213b2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,10 +21,13 @@ tar = "0.4.33" xz2 = "0.1.6" zip = "0.5.11" - # Dependency from workspace oof = { path = "./oof" } +[dev-dependencies] +tempdir = "0.3.7" +rand = { version = "0.8.3", default-features = false, features = ["small_rng", "std"] } + [profile.release] lto = true codegen-units = 1 diff --git a/oof/src/error.rs b/oof/src/error.rs index 86ead8c96..306f17629 100644 --- a/oof/src/error.rs +++ b/oof/src/error.rs @@ -16,7 +16,7 @@ pub enum OofError<'t> { UnknownLongFlag(String), MisplacedShortArgFlagError(char), MissingValueToFlag(&'t Flag), - DuplicatedFlag(&'t Flag) + DuplicatedFlag(&'t Flag), } impl<'t> error::Error for OofError<'t> { diff --git a/oof/src/flags.rs b/oof/src/flags.rs index 043896e92..b70af33b7 100644 --- a/oof/src/flags.rs +++ b/oof/src/flags.rs @@ -13,11 +13,7 @@ pub struct ArgFlag; impl ArgFlag { pub fn long(name: &'static str) -> Flag { - Flag { - long: name, - short: None, - takes_value: true, - } + Flag { long: name, short: None, takes_value: true } } } @@ -40,11 +36,7 @@ impl std::fmt::Display for Flag { impl Flag { pub fn long(name: &'static str) -> Self { - Self { - long: name, - short: None, - takes_value: false, - } + Self { long: name, short: None, takes_value: false } } pub fn short(mut self, short_flag_char: char) -> Self { diff --git a/oof/src/lib.rs b/oof/src/lib.rs index 87445bd99..662186791 100644 --- a/oof/src/lib.rs +++ b/oof/src/lib.rs @@ -7,7 +7,10 @@ mod error; mod flags; pub mod util; -use std::{collections::BTreeMap, ffi::{OsStr, OsString}}; +use std::{ + collections::BTreeMap, + ffi::{OsStr, OsString}, +}; pub use error::OofError; pub use flags::{ArgFlag, Flag, FlagType, Flags}; @@ -103,9 +106,9 @@ pub fn filter_flags( } // If it is a flag, now we try to interpret it as valid utf-8 - let flag= match arg.to_str() { + let flag = match arg.to_str() { Some(arg) => arg, - None => return Err(OofError::InvalidUnicode(arg)) + None => return Err(OofError::InvalidUnicode(arg)), }; // Only one hyphen in the flag @@ -127,12 +130,11 @@ pub fn filter_flags( // Safety: this loop only runs when len >= 1, so this subtraction is safe let is_last_letter = i == letters.len() - 1; - let flag_info = short_flags_info.get(&letter).ok_or( - OofError::UnknownShortFlag(letter) - )?; + let flag_info = + short_flags_info.get(&letter).ok_or(OofError::UnknownShortFlag(letter))?; if !is_last_letter && flag_info.takes_value { - return Err(OofError::MisplacedShortArgFlagError(letter)) + return Err(OofError::MisplacedShortArgFlagError(letter)); // Because "-AB argument" only works if B takes values, not A. // That is, the short flag that takes values needs to come at the end // of this piece of text @@ -147,9 +149,8 @@ pub fn filter_flags( } // pop the next one - let flag_argument = iter.next().ok_or( - OofError::MissingValueToFlag(flag_info) - )?; + let flag_argument = + iter.next().ok_or(OofError::MissingValueToFlag(flag_info))?; // Otherwise, insert it. result_flags.argument_flags.insert(flag_name, flag_argument); @@ -167,9 +168,9 @@ pub fn filter_flags( if let FlagType::Long = flag_type { let flag = trim_double_hyphen(flag); - let flag_info = long_flags_info.get(flag).ok_or_else(|| { - OofError::UnknownLongFlag(String::from(flag)) - })?; + let flag_info = long_flags_info + .get(flag) + .ok_or_else(|| OofError::UnknownLongFlag(String::from(flag)))?; let flag_name = flag_info.long; @@ -179,9 +180,7 @@ pub fn filter_flags( return Err(OofError::DuplicatedFlag(flag_info)); } - let flag_argument = iter.next().ok_or( - OofError::MissingValueToFlag(flag_info) - )?; + let flag_argument = iter.next().ok_or(OofError::MissingValueToFlag(flag_info))?; result_flags.argument_flags.insert(flag_name, flag_argument); } else { // If it was already inserted @@ -209,9 +208,7 @@ where T: AsRef, U: AsRef, { - texts - .iter() - .any(|text| args.iter().any(|arg| arg.as_ref() == text.as_ref())) + texts.iter().any(|text| args.iter().any(|arg| arg.as_ref() == text.as_ref())) } #[cfg(test)] @@ -237,14 +234,8 @@ mod tests { assert_eq!(args, gen_args("ouch a.zip b.tar.gz c.tar")); assert!(flags.is_present("output_file")); - assert_eq!( - Some(&OsString::from("new_folder")), - flags.arg("output_file") - ); - assert_eq!( - Some(OsString::from("new_folder")), - flags.take_arg("output_file") - ); + assert_eq!(Some(&OsString::from("new_folder")), flags.arg("output_file")); + assert_eq!(Some(OsString::from("new_folder")), flags.take_arg("output_file")); assert!(!flags.is_present("output_file")); } @@ -291,10 +282,7 @@ mod tests { // TODO: remove should_panic and use proper error handling inside of filter_args #[should_panic] fn test_flag_info_with_long_flag_conflict() { - let flags_info = [ - ArgFlag::long("verbose").short('a'), - Flag::long("verbose").short('b'), - ]; + let flags_info = [ArgFlag::long("verbose").short('a'), Flag::long("verbose").short('b')]; // Should panic here let result = filter_flags(vec![], &flags_info); @@ -305,10 +293,8 @@ mod tests { // TODO: remove should_panic and use proper error handling inside of filter_args #[should_panic] fn test_flag_info_with_short_flag_conflict() { - let flags_info = [ - ArgFlag::long("output_file").short('o'), - Flag::long("verbose").short('o'), - ]; + let flags_info = + [ArgFlag::long("output_file").short('o'), Flag::long("verbose").short('o')]; // Should panic here filter_flags(vec![], &flags_info).unwrap_err(); diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 000000000..213e7f34e --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,13 @@ +# Normal features +max_width = 100 +imports_granularity = "Crate" +match_block_trailing_comma = true +overflow_delimited_expr = true +reorder_impl_items = true +use_field_init_shorthand = true +newline_style = "Unix" +edition = "2018" +reorder_imports = true +reorder_modules = true +use_try_shorthand = true +use_small_heuristics = "Max" diff --git a/src/cli.rs b/src/cli.rs index 43fddd82a..b79d58866 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -5,9 +5,8 @@ use std::{ vec::Vec, }; -use strsim::normalized_damerau_levenshtein; use oof::{arg_flag, flag}; - +use strsim::normalized_damerau_levenshtein; #[derive(PartialEq, Eq, Debug)] pub enum Command { @@ -36,11 +35,10 @@ pub struct ParsedArgs { pub flags: oof::Flags, } - -/// check_for_typo checks if the first argument is -/// a typo for the compress subcommand. +/// check_for_typo checks if the first argument is +/// a typo for the compress subcommand. /// Returns true if the arg is probably a typo or false otherwise. -fn is_typo<'a, P>(path: P) -> bool +fn is_typo<'a, P>(path: P) -> bool where P: AsRef + 'a, { @@ -66,12 +64,10 @@ where } else { Err(crate::Error::IoError(io_err)) } - } + }, } } - - fn canonicalize_files<'a, P>(files: Vec

) -> crate::Result> where P: AsRef + 'a, @@ -81,22 +77,14 @@ where pub fn parse_args_from(mut args: Vec) -> crate::Result { if oof::matches_any_arg(&args, &["--help", "-h"]) || args.is_empty() { - return Ok(ParsedArgs { - command: Command::ShowHelp, - flags: oof::Flags::default(), - }); + return Ok(ParsedArgs { command: Command::ShowHelp, flags: oof::Flags::default() }); } if oof::matches_any_arg(&args, &["--version"]) { - return Ok(ParsedArgs { - command: Command::ShowVersion, - flags: oof::Flags::default(), - }); + return Ok(ParsedArgs { command: Command::ShowVersion, flags: oof::Flags::default() }); } - let subcommands = &[ - "c", "compress" - ]; + let subcommands = &["c", "compress"]; let mut flags_info = vec![flag!('y', "yes"), flag!('n', "no")]; @@ -115,41 +103,33 @@ pub fn parse_args_from(mut args: Vec) -> crate::Result { let files = canonicalize_files(files)?; - let command = Command::Compress { - files, - compressed_output_path, - }; + let command = Command::Compress { files, compressed_output_path }; ParsedArgs { command, flags } - } + }, // Defaults to decompression when there is no subcommand None => { flags_info.push(arg_flag!('o', "output")); - { - let first_arg = args.first().unwrap(); + + if let Some(first_arg) = args.first() { if is_typo(first_arg) { return Err(crate::Error::CompressionTypo); } + } else { + todo!("Complain that no decompression arguments were given."); } - // Parse flags let (args, mut flags) = oof::filter_flags(args, &flags_info)?; - let files = args - .into_iter() - .map(canonicalize) - .collect::, _>>()?; + let files = args.into_iter().map(canonicalize).collect::, _>>()?; let output_folder = flags.take_arg("output").map(PathBuf::from); // TODO: ensure all files are decompressible - let command = Command::Decompress { - files, - output_folder, - }; + let command = Command::Decompress { files, output_folder }; ParsedArgs { command, flags } - } + }, _ => unreachable!("You should match each subcommand passed."), }; diff --git a/src/commands.rs b/src/commands.rs index 10ef89cac..ee36c60c6 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -24,20 +24,16 @@ use crate::{ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { match command { - Command::Compress { - files, - compressed_output_path, - } => compress_files(files, &compressed_output_path, flags)?, - Command::Decompress { - files, - output_folder, - } => { + Command::Compress { files, compressed_output_path } => { + compress_files(files, &compressed_output_path, flags)? + }, + Command::Decompress { files, output_folder } => { // From Option to Option<&Path> let output_folder = output_folder.as_ref().map(|path| Path::new(path)); for file in files.iter() { decompress_file(file, output_folder, flags)?; } - } + }, Command::ShowHelp => crate::help_command(), Command::ShowVersion => crate::version_command(), } @@ -53,7 +49,7 @@ fn get_compressor(file: &File) -> crate::Result<(Option, BoxedC None => { // This is reached when the output file given does not have an extension or has an unsupported one return Err(crate::Error::MissingExtensionError(file.path.to_path_buf())); - } + }, }; // Supported first compressors: @@ -93,7 +89,7 @@ fn get_decompressor(file: &File) -> crate::Result<(Option, Bo colors::reset() ); return Err(crate::Error::InvalidInput); - } + }, }; let second_decompressor: Box = match extension.second_ext { @@ -126,10 +122,7 @@ fn decompress_file_in_memory( ) -> crate::Result<()> { let output_file_path = utils::get_destination_path(&output_file); - let file_name = file_path - .file_stem() - .map(Path::new) - .unwrap_or(output_file_path); + let file_name = file_path.file_stem().map(Path::new).unwrap_or(output_file_path); if "." == file_name.as_os_str() { // I believe this is only possible when the supplied input has a name @@ -156,14 +149,10 @@ fn decompress_file_in_memory( let mut f = fs::File::create(output_file_path.join(file_name))?; f.write_all(&bytes)?; return Ok(()); - } + }, }; - let file = File { - path: file_name, - contents_in_memory: Some(bytes), - extension, - }; + let file = File { path: file_name, contents_in_memory: Some(bytes), extension }; let decompression_result = decompressor.decompress(file, &output_file, flags)?; if let DecompressionResult::FileInMemory(_) = decompression_result { @@ -196,11 +185,11 @@ fn compress_files( output.contents_in_memory = Some(bytes); entry = Entry::InMemory(output); second_compressor.compress(entry)? - } + }, None => { let entry = Entry::Files(files); second_compressor.compress(entry)? - } + }, }; println!( @@ -224,9 +213,7 @@ fn decompress_file( let file = File::from(file_path)?; // The file must have a supported decompressible format if file.extension == None { - return Err(crate::Error::MissingExtensionError(PathBuf::from( - file_path, - ))); + return Err(crate::Error::MissingExtensionError(PathBuf::from(file_path))); } let output = match output { @@ -251,7 +238,7 @@ fn decompress_file( extension, flags, )?; - } + }, DecompressionResult::FilesUnpacked(_files) => { // If the file's last extension was an archival method, // such as .tar, .zip or (to-do) .rar, then we won't look for @@ -260,7 +247,7 @@ fn decompress_file( // to worry about, at least at the moment. // TODO: use the `files` variable for something - } + }, } Ok(()) diff --git a/src/compressors/bzip.rs b/src/compressors/bzip.rs index 102de37c6..66a2ca43a 100644 --- a/src/compressors/bzip.rs +++ b/src/compressors/bzip.rs @@ -34,7 +34,7 @@ impl BzipCompressor { Some(bytes) => bytes, None => { return Err(crate::Error::InternalError); - } + }, }; Self::compress_bytes(&*bytes) diff --git a/src/compressors/gzip.rs b/src/compressors/gzip.rs index 8e16970c3..398ed58a5 100644 --- a/src/compressors/gzip.rs +++ b/src/compressors/gzip.rs @@ -38,7 +38,7 @@ impl GzipCompressor { Some(bytes) => bytes, None => { unreachable!(); - } + }, }; Self::compress_bytes(file_contents) diff --git a/src/compressors/lzma.rs b/src/compressors/lzma.rs index e8f18551c..45d36b2aa 100644 --- a/src/compressors/lzma.rs +++ b/src/compressors/lzma.rs @@ -38,7 +38,7 @@ impl LzmaCompressor { Some(bytes) => bytes, None => { unreachable!(); - } + }, }; Self::compress_bytes(file_contents) diff --git a/src/compressors/zip.rs b/src/compressors/zip.rs index 1d814d571..cb5a7178e 100644 --- a/src/compressors/zip.rs +++ b/src/compressors/zip.rs @@ -37,7 +37,7 @@ impl ZipCompressor { // TODO: error description, although this block should not be // reachable return Err(crate::Error::InvalidInput); - } + }, }; writer.write_all(&*input_bytes)?; diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index 4d9135864..fe2cff7f5 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -4,7 +4,6 @@ use std::{ path::{Path, PathBuf}, }; - use tar::{self, Archive}; use utils::colors; @@ -30,7 +29,7 @@ impl TarDecompressor { None => { let file = fs::File::open(&from.path)?; tar::Archive::new(Box::new(file)) - } + }, }; for file in archive.entries()? { diff --git a/src/decompressors/to_memory.rs b/src/decompressors/to_memory.rs index d7aaf639e..50105a5b7 100644 --- a/src/decompressors/to_memory.rs +++ b/src/decompressors/to_memory.rs @@ -3,7 +3,6 @@ use std::{ path::Path, }; - use utils::colors; use super::decompressor::{DecompressionResult, Decompressor}; diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index 501f25df6..d31115cbc 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -66,7 +66,7 @@ impl ZipDecompressor { _is_dir @ true => { println!("File {} extracted to \"{}\"", idx, file_path.display()); fs::create_dir_all(&file_path)?; - } + }, _is_file @ false => { if let Some(path) = file_path.parent() { if !path.exists() { @@ -83,7 +83,7 @@ impl ZipDecompressor { let mut output_file = fs::File::create(&file_path)?; io::copy(&mut file, &mut output_file)?; - } + }, } #[cfg(unix)] @@ -104,14 +104,14 @@ impl ZipDecompressor { // Decompressing a .zip archive loaded up in memory let mut archive = zip::ZipArchive::new(Cursor::new(bytes))?; Ok(Self::zip_decompress(&mut archive, into, flags)?) - } + }, None => { // Decompressing a .zip archive from the file system let file = fs::File::open(&from.path)?; let mut archive = zip::ZipArchive::new(file)?; Ok(Self::zip_decompress(&mut archive, into, flags)?) - } + }, } } } diff --git a/src/dialogs.rs b/src/dialogs.rs index 645a43250..5d5e44ed8 100644 --- a/src/dialogs.rs +++ b/src/dialogs.rs @@ -12,10 +12,7 @@ pub struct Error; impl<'a> Confirmation<'a> { pub fn new(prompt: &'a str, pattern: Option<&'a str>) -> Self { - Self { - prompt, - placeholder: pattern, - } + Self { prompt, placeholder: pattern } } pub fn ask(&self, substitute: Option<&'a str>) -> crate::Result { @@ -26,7 +23,14 @@ impl<'a> Confirmation<'a> { }; loop { - print!("{} [{}Y{}/{}n{}] ", message, colors::green(), colors::reset(), colors::red(), colors::reset()); + print!( + "{} [{}Y{}/{}n{}] ", + message, + colors::green(), + colors::reset(), + colors::red(), + colors::reset() + ); io::stdout().flush()?; let mut answer = String::new(); @@ -40,7 +44,7 @@ impl<'a> Confirmation<'a> { match trimmed_answer.to_ascii_lowercase().as_ref() { "y" | "yes" => return Ok(true), "n" | "no" => return Ok(false), - _ => {} + _ => {}, } } } diff --git a/src/error.rs b/src/error.rs index 2c869c0aa..83657e1b5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,5 @@ use std::{fmt, path::PathBuf}; + use crate::utils::colors; pub enum Error { @@ -36,18 +37,18 @@ impl fmt::Display for Error { write!(f, "{}[ERROR]{} ", colors::red(), colors::reset())?; // TODO: show MIME type of the unsupported file write!(f, "cannot compress to {:?}, likely because it has an unsupported (or missing) extension.", filename) - } + }, Error::WalkdirError => { // Already printed in the From block write!(f, "") - } + }, Error::FileNotFound(file) => { write!(f, "{}[ERROR]{} ", colors::red(), colors::reset())?; if file == &PathBuf::from("") { return write!(f, "file not found!"); } write!(f, "file {:?} not found!", file) - } + }, Error::CompressingRootFolder => { write!(f, "{}[ERROR]{} ", colors::red(), colors::reset())?; let spacing = " "; @@ -64,28 +65,27 @@ impl fmt::Display for Error { colors::green(), colors::reset() ) - } + }, Error::MissingArgumentsForCompression => { write!(f, "{}[ERROR]{} ", colors::red(), colors::reset())?; let spacing = " "; writeln!(f,"The compress subcommands demands at least 2 arguments, an input file and an output file.")?; - writeln!(f,"{}Example: `ouch compress img.jpeg img.zip", spacing)?; - write!(f,"{}For more information, run `ouch --help`", spacing) - } + writeln!(f, "{}Example: `ouch compress img.jpeg img.zip`", spacing)?; + write!(f, "{}For more information, run `ouch --help`", spacing) + }, Error::InternalError => { write!(f, "{}[ERROR]{} ", colors::red(), colors::reset())?; write!(f, "You've reached an internal error! This really should not have happened.\nPlease file an issue at {}https://github.com/vrmiguel/ouch{}", colors::green(), colors::reset()) - } + }, Error::IoError(io_err) => { write!(f, "{}[ERROR]{} {}", colors::red(), colors::reset(), io_err) - } - Error::CompressionTypo =>{ + }, + Error::CompressionTypo => { write!(f, "Did you mean {}ouch compress{}?", colors::magenta(), colors::reset()) - } + }, _err => { - // TODO - write!(f, "") - } + todo!(); + }, } } } @@ -96,9 +96,7 @@ impl From for Error { std::io::ErrorKind::NotFound => panic!("{}", err), std::io::ErrorKind::PermissionDenied => Self::PermissionDenied, std::io::ErrorKind::AlreadyExists => Self::AlreadyExists, - _other => { - Self::IoError(err) - } + _other => Self::IoError(err), } } } diff --git a/src/extension.rs b/src/extension.rs index 06de63d08..e6a313ae0 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -34,10 +34,7 @@ pub fn get_extension_from_filename(file_name: &OsStr) -> Option<(&OsStr, &OsStr) impl From for Extension { fn from(second_ext: CompressionFormat) -> Self { - Self { - first_ext: None, - second_ext, - } + Self { first_ext: None, second_ext } } } @@ -57,29 +54,22 @@ impl Extension { (os_str, snd) if os_str.is_empty() => (None, snd), (fst, snd) => (Some(fst), snd), }, - None => { - return Err(crate::Error::MissingExtensionError(PathBuf::from( - file_name, - ))) - } + None => return Err(crate::Error::MissingExtensionError(PathBuf::from(file_name))), }; let (first_ext, second_ext) = match (first_ext, second_ext) { (None, snd) => { let ext = compression_format_from(snd)?; (None, ext) - } + }, (Some(fst), snd) => { let snd = compression_format_from(snd)?; let fst = compression_format_from(fst).ok(); (fst, snd) - } + }, }; - Ok(Self { - first_ext, - second_ext, - }) + Ok(Self { first_ext, second_ext }) } } @@ -119,7 +109,7 @@ impl TryFrom<&PathBuf> for CompressionFormat { Some(ext) => ext, None => { return Err(crate::Error::MissingExtensionError(PathBuf::new())); - } + }, }; extension_from_os_str(ext) } @@ -141,16 +131,12 @@ impl TryFrom<&str> for CompressionFormat { impl Display for CompressionFormat { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Gzip => ".gz", - Bzip => ".bz", - Lzma => ".lz", - Tar => ".tar", - Zip => ".zip", - } - ) + write!(f, "{}", match self { + Gzip => ".gz", + Bzip => ".bz", + Lzma => ".lz", + Tar => ".tar", + Zip => ".zip", + }) } } diff --git a/src/file.rs b/src/file.rs index a3d262371..c14d2d8c2 100644 --- a/src/file.rs +++ b/src/file.rs @@ -21,10 +21,6 @@ impl<'a> File<'a> { pub fn from(path: &'a Path) -> crate::Result { let extension = Extension::from(path.as_ref()).ok(); - Ok(File { - path, - contents_in_memory: None, - extension, - }) + Ok(File { path, contents_in_memory: None, extension }) } } diff --git a/src/lib.rs b/src/lib.rs index 70fc3476a..c22759886 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,6 @@ mod error; mod extension; mod file; mod utils; -mod test; pub use error::{Error, Result}; @@ -73,10 +72,5 @@ Visit https://github.com/vrmiguel/ouch for more usage examples.", #[inline] fn version_command() { use utils::colors::*; - println!( - "{green}ouch{reset} {}", - crate::VERSION, - green = green(), - reset = reset(), - ); + println!("{green}ouch{reset} {}", crate::VERSION, green = green(), reset = reset(),); } diff --git a/src/test.rs b/src/test.rs deleted file mode 100644 index 95700a0ba..000000000 --- a/src/test.rs +++ /dev/null @@ -1,351 +0,0 @@ -use std::{fs, path::Path}; - -#[allow(dead_code)] -// ouch's command-line logic uses fs::canonicalize on its inputs so we cannot -// use made-up files for testing. -// make_dummy_file therefores creates a small temporary file to bypass fs::canonicalize errors -fn make_dummy_file<'a, P>(path: P) -> crate::Result<()> -where - P: AsRef + 'a, -{ - fs::write(path.as_ref(), &[2, 3, 4, 5, 6, 7, 8, 9, 10])?; - Ok(()) -} - -#[allow(dead_code)] -fn make_dummy_files<'a, P>(paths: &[P]) -> crate::Result<()> -where - P: AsRef + 'a, -{ - let _ = paths - .iter() - .map(make_dummy_file) - .map(Result::unwrap) - .collect::>(); - Ok(()) -} - -#[cfg(test)] -mod argparsing { - use super::make_dummy_files; - use crate::cli; - use crate::cli::Command; - use std::{ffi::OsString, fs, path::PathBuf}; - - fn gen_args(text: &str) -> Vec { - let args = text.split_whitespace(); - args.map(OsString::from).collect() - } - - macro_rules! parse { - ($input_text:expr) => {{ - let args = gen_args($input_text); - cli::parse_args_from(args).unwrap() - }}; - } - - #[test] - // The absolute flags that ignore all the other argparsing rules are --help and --version - fn test_absolute_flags() { - let expected = Command::ShowHelp; - assert_eq!(expected, parse!("").command); - assert_eq!(expected, parse!("-h").command); - assert_eq!(expected, parse!("--help").command); - assert_eq!(expected, parse!("aaaaaaaa --help -o -e aaa").command); - assert_eq!(expected, parse!("aaaaaaaa -h").command); - assert_eq!(expected, parse!("--help compress aaaaaaaa").command); - assert_eq!(expected, parse!("compress --help").command); - assert_eq!(expected, parse!("--version --help").command); - assert_eq!(expected, parse!("aaaaaaaa -v aaaa -h").command); - - let expected = Command::ShowVersion; - assert_eq!(expected, parse!("ouch --version").command); - assert_eq!(expected, parse!("ouch a --version b").command); - } - - #[test] - fn test_arg_parsing_compress_subcommand() -> crate::Result<()> { - let files = vec!["a", "b", "c"]; - make_dummy_files(&*files)?; - let files = files - .iter() - .map(fs::canonicalize) - .map(Result::unwrap) - .collect(); - - let expected = Command::Compress { - files, - compressed_output_path: "d".into(), - }; - assert_eq!(expected, parse!("compress a b c d").command); - - fs::remove_file("a")?; - fs::remove_file("b")?; - fs::remove_file("c")?; - Ok(()) - } - - #[test] - fn test_arg_parsing_decompress_subcommand() -> crate::Result<()> { - let files = vec!["d", "e", "f"]; - make_dummy_files(&*files)?; - - let files: Vec<_> = files.iter().map(PathBuf::from).collect(); - - let expected = Command::Decompress { - files: files - .iter() - .map(fs::canonicalize) - .map(Result::unwrap) - .collect(), - output_folder: None, - }; - - assert_eq!(expected, parse!("d e f").command); - - let expected = Command::Decompress { - files: files.iter().map(fs::canonicalize).map(Result::unwrap).collect(), - output_folder: Some("folder".into()), - }; - assert_eq!(expected, parse!("d e f --output folder").command); - assert_eq!(expected, parse!("d e --output folder f").command); - assert_eq!(expected, parse!("d --output folder e f").command); - assert_eq!(expected, parse!("--output folder d e f").command); - - assert_eq!(expected, parse!("d e f -o folder").command); - assert_eq!(expected, parse!("d e -o folder f").command); - assert_eq!(expected, parse!("d -o folder e f").command); - assert_eq!(expected, parse!("-o folder d e f").command); - - fs::remove_file("d")?; - fs::remove_file("e")?; - fs::remove_file("f")?; - Ok(()) - } -} - -#[cfg(test)] -mod byte_pretty_printing { - use crate::utils::Bytes; - #[test] - fn bytes() { - assert_eq!(&format!("{}", Bytes::new(234)), "234.00 B"); - - assert_eq!(&format!("{}", Bytes::new(999)), "999.00 B"); - } - - #[test] - fn kilobytes() { - assert_eq!(&format!("{}", Bytes::new(2234)), "2.23 kB"); - - assert_eq!(&format!("{}", Bytes::new(62500)), "62.50 kB"); - - assert_eq!(&format!("{}", Bytes::new(329990)), "329.99 kB"); - } - - #[test] - fn megabytes() { - assert_eq!(&format!("{}", Bytes::new(2750000)), "2.75 MB"); - - assert_eq!(&format!("{}", Bytes::new(55000000)), "55.00 MB"); - - assert_eq!(&format!("{}", Bytes::new(987654321)), "987.65 MB"); - } - - #[test] - fn gigabytes() { - assert_eq!(&format!("{}", Bytes::new(5280000000)), "5.28 GB"); - - assert_eq!(&format!("{}", Bytes::new(95200000000)), "95.20 GB"); - - assert_eq!(&format!("{}", Bytes::new(302000000000)), "302.00 GB"); - } -} - -// #[cfg(test)] -// mod cli { -// use super::*; - -// #[test] -// fn decompress_files_into_folder() -> crate::Result<()> { -// make_dummy_file("file.zip")?; -// let args = gen_args("ouch -i file.zip -o folder/"); -// let (command, flags) = cli::parse_args_and_flags_from(args)?; - -// assert_eq!( -// command, -// Command::Decompress { -// files: args, -// compressed_output_path: PathBuf, -// } // kind: Decompress(vec![File { -// // path: fs::canonicalize("file.zip")?, -// // contents_in_memory: None, -// // extension: Some(Extension::from(Zip)) -// // }]), -// // output: Some(File { -// // path: "folder".into(), -// // contents_in_memory: None, -// // extension: None -// // }), -// // } -// ); - -// fs::remove_file("file.zip")?; - -// Ok(()) -// } - -// #[test] -// fn decompress_files() -> crate::Result<()> { -// make_dummy_file("my-cool-file.zip")?; -// make_dummy_file("file.tar")?; -// let matches = -// clap_app().get_matches_from(vec!["ouch", "-i", "my-cool-file.zip", "file.tar"]); -// let command_from_matches = Command::try_from(matches)?; - -// assert_eq!( -// command_from_matches, -// Command { -// kind: Decompress(vec![ -// File { -// path: fs::canonicalize("my-cool-file.zip")?, -// contents_in_memory: None, -// extension: Some(Extension::from(Zip)) -// }, -// File { -// path: fs::canonicalize("file.tar")?, -// contents_in_memory: None, -// extension: Some(Extension::from(Tar)) -// } -// ],), -// output: None, -// } -// ); - -// fs::remove_file("my-cool-file.zip")?; -// fs::remove_file("file.tar")?; - -// Ok(()) -// } - -// #[test] -// fn compress_files() -> crate::Result<()> { -// make_dummy_file("file")?; -// make_dummy_file("file2.jpeg")?; -// make_dummy_file("file3.ok")?; - -// let matches = clap_app().get_matches_from(vec![ -// "ouch", -// "-i", -// "file", -// "file2.jpeg", -// "file3.ok", -// "-o", -// "file.tar", -// ]); -// let command_from_matches = Command::try_from(matches)?; - -// assert_eq!( -// command_from_matches, -// Command { -// kind: Compress(vec![ -// fs::canonicalize("file")?, -// fs::canonicalize("file2.jpeg")?, -// fs::canonicalize("file3.ok")? -// ]), -// output: Some(File { -// path: "file.tar".into(), -// contents_in_memory: None, -// extension: Some(Extension::from(Tar)) -// }), -// } -// ); - -// fs::remove_file("file")?; -// fs::remove_file("file2.jpeg")?; -// fs::remove_file("file3.ok")?; - -// Ok(()) -// } -// } - -// #[cfg(test)] -// mod cli_errors { - -// #[test] -// fn compress_files() -> crate::Result<()> { -// let matches = -// clap_app().get_matches_from(vec!["ouch", "-i", "a_file", "file2.jpeg", "file3.ok"]); -// let res = Command::try_from(matches); - -// assert_eq!( -// res, -// Err(crate::Error::InputsMustHaveBeenDecompressible( -// "a_file".into() -// )) -// ); - -// Ok(()) -// } -// } - -// #[cfg(test)] -// mod extension_extraction { - -// #[test] -// fn test_extension_zip() { -// let path = "filename.tar.zip"; -// assert_eq!( -// CompressionFormat::try_from(path), -// Ok(CompressionFormat::Zip) -// ); -// } - -// #[test] -// fn test_extension_tar_gz() { -// let extension = Extension::from(OsStr::new("folder.tar.gz")).unwrap(); -// assert_eq!( -// extension, -// Extension { -// first_ext: Some(CompressionFormat::Tar), -// second_ext: CompressionFormat::Gzip -// } -// ); -// } - -// #[test] -// fn test_extension_tar() { -// let path = "pictures.tar"; -// assert_eq!( -// CompressionFormat::try_from(path), -// Ok(CompressionFormat::Tar) -// ); -// } - -// #[test] -// fn test_extension_gz() { -// let path = "passwords.tar.gz"; -// assert_eq!( -// CompressionFormat::try_from(path), -// Ok(CompressionFormat::Gzip) -// ); -// } - -// #[test] -// fn test_extension_lzma() { -// let path = "mygame.tar.lzma"; -// assert_eq!( -// CompressionFormat::try_from(path), -// Ok(CompressionFormat::Lzma) -// ); -// } - -// #[test] -// fn test_extension_bz() { -// let path = "songs.tar.bz"; -// assert_eq!( -// CompressionFormat::try_from(path), -// Ok(CompressionFormat::Bzip) -// ); -// } -// } diff --git a/src/utils.rs b/src/utils.rs index 011d3ed15..d9895439b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -79,7 +79,7 @@ pub fn get_destination_path<'a>(dest: &'a Option) -> &'a Path { // Must be None according to the way command-line arg. parsing in Ouch works assert_eq!(output_file.extension, None); Path::new(&output_file.path) - } + }, None => Path::new("."), } } @@ -93,9 +93,7 @@ pub fn change_dir_and_return_parent(filename: &Path) -> crate::Result { return Err(crate::Error::CompressingRootFolder); }; - env::set_current_dir(parent) - .ok() - .ok_or(crate::Error::CompressingRootFolder)?; + env::set_current_dir(parent).ok().ok_or(crate::Error::CompressingRootFolder)?; Ok(previous_location) } @@ -107,11 +105,13 @@ pub fn permission_for_overwriting( ) -> crate::Result { match (flags.is_present("yes"), flags.is_present("no")) { (true, true) => { - unreachable!("This should've been cutted out in the ~/src/cli.rs filter flags function.") - } + unreachable!( + "This should've been cutted out in the ~/src/cli.rs filter flags function." + ) + }, (true, _) => return Ok(true), (_, true) => return Ok(false), - _ => {} + _ => {}, } let file_path_str = to_utf(path); @@ -181,9 +181,7 @@ impl Bytes { const UNIT_PREFIXES: [&'static str; 6] = ["", "k", "M", "G", "T", "P"]; pub fn new(bytes: u64) -> Self { - Self { - bytes: bytes as f64, - } + Self { bytes: bytes as f64 } } } diff --git a/tests/compress_and_decompress.rs b/tests/compress_and_decompress.rs new file mode 100644 index 000000000..98ab149f9 --- /dev/null +++ b/tests/compress_and_decompress.rs @@ -0,0 +1,139 @@ +use std::{ + env, fs, + io::prelude::*, + path::{Path, PathBuf}, +}; + +use ouch::{cli::Command, commands::run}; +use rand::{rngs::SmallRng, RngCore, SeedableRng}; +use tempdir::TempDir; + +#[test] +/// Tests each format that supports multiple files with random input. +/// TODO: test the remaining formats. +/// TODO2: Fix testing of .tar.zip and .zip.zip +fn test_each_format() { + let mut rng = SmallRng::from_entropy(); + test_compression_and_decompression(&mut rng, "tar"); + test_compression_and_decompression(&mut rng, "tar.gz"); + test_compression_and_decompression(&mut rng, "tar.bz"); + test_compression_and_decompression(&mut rng, "tar.bz2"); + test_compression_and_decompression(&mut rng, "tar.xz"); + test_compression_and_decompression(&mut rng, "tar.lz"); + test_compression_and_decompression(&mut rng, "tar.lzma"); + // test_compression_and_decompression(&mut rng, "tar.zip"); + test_compression_and_decompression(&mut rng, "zip"); + test_compression_and_decompression(&mut rng, "zip.gz"); + test_compression_and_decompression(&mut rng, "zip.bz"); + test_compression_and_decompression(&mut rng, "zip.bz2"); + test_compression_and_decompression(&mut rng, "zip.xz"); + test_compression_and_decompression(&mut rng, "zip.lz"); + test_compression_and_decompression(&mut rng, "zip.lzma"); + // test_compression_and_decompression(&mut rng, "zip.zip"); +} + +type FileContent = Vec; + +fn test_compression_and_decompression(rng: &mut impl RngCore, format: &str) { + // System temporary directory depends on the platform + // For linux it is /tmp + let system_tmp = env::temp_dir(); + // Create a folder that will be deleted on drop + let testing_dir = String::from("ouch-testing-") + format; + let testing_dir = TempDir::new_in(system_tmp, &testing_dir).expect("Could not create tempdir"); + let testing_dir = testing_dir.path(); + + // Quantity of compressed files vary from 1 to 10 + let quantity_of_files = rng.next_u32() % 10 + 1; + + let contents_of_files: Vec = + (0..quantity_of_files).map(|_| generate_random_file_content(rng)).collect(); + + let mut file_paths = create_files(&testing_dir, &contents_of_files); + let archive_path = compress_files(&testing_dir, &file_paths, &format); + let mut extracted_paths = extract_files(&archive_path); + + // // If you want to visualize the compressed and extracted files before auto-destruction: + // std::thread::sleep(std::time::Duration::from_secs(40)); + + file_paths.sort(); + extracted_paths.sort(); + + compare_paths(&file_paths, &extracted_paths); + compare_file_contents(&extracted_paths, &contents_of_files); +} + +// Crate file contents from 1024 up to 8192 random bytes +fn generate_random_file_content(rng: &mut impl RngCore) -> FileContent { + let quantity = 1024 + rng.next_u32() % (8192 - 1024); + let mut vec = vec![0; quantity as usize]; + rng.fill_bytes(&mut vec); + vec +} + +// Create files using the indexes as file names (eg. 0, 1, 2 and 3) +// Returns the paths +fn create_files(at: &Path, contents: &[FileContent]) -> Vec { + contents + .iter() + .enumerate() + .map(|(i, content)| { + let path = at.join(i.to_string()); + let mut file = fs::File::create(&path).expect("Could not create dummy test file"); + file.write_all(content).expect("Could not write to dummy test file"); + path + }) + .collect() +} + +fn compress_files(at: &Path, paths_to_compress: &[PathBuf], format: &str) -> PathBuf { + let archive_path = String::from("archive.") + format; + let archive_path = at.join(archive_path); + + let command = Command::Compress { + files: paths_to_compress.to_vec(), + compressed_output_path: archive_path.to_path_buf(), + }; + run(command, &oof::Flags::default()).expect("Failed to compress test dummy files"); + + archive_path +} + +fn extract_files(archive_path: &Path) -> Vec { + // We will extract in the same folder as the archive + // If the archive is at: + // /tmp/ouch-testing-tar.Rbq4DusBrtF8/archive.tar + // Then the extraction_output_folder will be: + // /tmp/ouch-testing-tar.Rbq4DusBrtF8/extraction_results/ + let mut extraction_output_folder = archive_path.to_path_buf(); + // Remove the name of the extracted archive + assert!(extraction_output_folder.pop()); + // Add the suffix "results" + extraction_output_folder.push("extraction_results"); + + let command = Command::Decompress { + files: vec![archive_path.to_owned()], + output_folder: Some(extraction_output_folder.clone()), + }; + run(command, &oof::Flags::default()).expect("Failed to extract"); + + fs::read_dir(extraction_output_folder) + .unwrap() + .map(Result::unwrap) + .map(|entry| entry.path()) + .collect() +} + +fn compare_paths(original: &[PathBuf], extracted: &[PathBuf]) { + assert_eq!(original.len(), extracted.len()); + for (original, extracted) in original.iter().zip(extracted) { + assert_eq!(original.file_name(), extracted.file_name()); + } +} + +fn compare_file_contents(extracted: &[PathBuf], contents: &[FileContent]) { + for (extracted_path, expected_content) in extracted.iter().zip(contents) { + let read_content = fs::read(extracted_path).expect("Failed to read from file"); + assert_eq!(&read_content, expected_content); + } +}