From 0033e8381f597a349fa551d3f1a96bdfcb544bba Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sun, 15 Mar 2020 22:41:47 -0700 Subject: [PATCH] Test `imdl torrent verify` output - Test all individual `FileError` variants - Test terminal colors - Test multiple and single file torrents type: testing --- Cargo.lock | 41 +++++ Cargo.toml | 1 + src/env.rs | 2 - src/error.rs | 10 +- src/file_error.rs | 4 +- src/output_stream.rs | 4 + src/status.rs | 9 +- src/step.rs | 2 +- src/subcommand/torrent/create.rs | 6 +- src/subcommand/torrent/verify.rs | 283 ++++++++++++++++++++++++++++++- src/test_env.rs | 18 ++ src/test_env_builder.rs | 9 +- 12 files changed, 355 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a66b77..a5544dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -316,6 +316,7 @@ dependencies = [ "console", "globset", "indicatif", + "indoc", "lazy_static", "libc", "md5", @@ -349,6 +350,29 @@ dependencies = [ "regex", ] +[[package]] +name = "indoc" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9553c1e16c114b8b77ebeb329e5f2876eed62a8d51178c8bc6bff0d65f98f8" +dependencies = [ + "indoc-impl", + "proc-macro-hack", +] + +[[package]] +name = "indoc-impl" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b714fc08d0961716390977cdff1536234415ac37b509e34e5a983def8340fb75" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", + "unindent", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -504,6 +528,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.9" @@ -908,6 +943,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +[[package]] +name = "unindent" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63f18aa3b0e35fed5a0048f029558b1518095ffe2a0a31fb87c93dece93a4993" + [[package]] name = "update-readme" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index d0cdfdc..a3fd164 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ version = "0.3.0" features = ["default", "wrap_help"] [dev-dependencies] +indoc = "0.3.4" temptree = "0.0.0" [workspace] diff --git a/src/env.rs b/src/env.rs index 20433d3..59a3fd2 100644 --- a/src/env.rs +++ b/src/env.rs @@ -78,8 +78,6 @@ impl Env { ) .ok(); - error.print_body(self).ok(); - if let Some(lint) = error.lint() { writeln!( &mut self.err, diff --git a/src/error.rs b/src/error.rs index fc91728..1c00226 100644 --- a/src/error.rs +++ b/src/error.rs @@ -120,7 +120,7 @@ pub(crate) enum Error { ))] Unstable { feature: &'static str }, #[snafu(display("Torrent verification failed."))] - Verify { status: Status }, + Verify, } impl Error { @@ -137,14 +137,6 @@ impl Error { message: message.into(), } } - - pub(crate) fn print_body(&self, env: &mut Env) -> Result<()> { - if let Self::Verify { status } = self { - status.print_body(env)?; - } - - Ok(()) - } } impl From for Error { diff --git a/src/file_error.rs b/src/file_error.rs index f9778a6..b28c7fa 100644 --- a/src/file_error.rs +++ b/src/file_error.rs @@ -73,8 +73,8 @@ impl Display for FileError { Self::Io(io_error) => write!(f, "{}", io_error), Self::Missing => write!(f, "File missing"), Self::Directory => write!(f, "Expected file but found directory"), - Self::Surfeit(difference) => write!(f, "Extra bytes: {}", difference), - Self::Dearth(difference) => write!(f, "Missing bytes: {}", difference), + Self::Surfeit(difference) => write!(f, "{} too long", difference), + Self::Dearth(difference) => write!(f, "{} too short", difference), Self::Md5 { actual, expected } => write!( f, "MD5 checksum mismatch: {} (expected {})", diff --git a/src/output_stream.rs b/src/output_stream.rs index 84f5cf2..a5fe7eb 100644 --- a/src/output_stream.rs +++ b/src/output_stream.rs @@ -49,6 +49,10 @@ impl OutputStream { self.style } + pub(crate) fn is_styled_term(&self) -> bool { + self.is_styled() && self.is_term() + } + pub(crate) fn style(&self) -> Style { Style::from_active(self.style) } diff --git a/src/status.rs b/src/status.rs index b19afdc..1742870 100644 --- a/src/status.rs +++ b/src/status.rs @@ -49,7 +49,7 @@ impl Status { } } - pub(crate) fn print_body(&self, env: &mut Env) -> Result<()> { + pub(crate) fn print(&self, env: &mut Env) -> Result<()> { match self { Self::Single { error, .. } => { if let Some(error) = error { @@ -70,13 +70,6 @@ impl Status { error.println(env.err_mut()).context(error::Stderr)?; } } - - errln!( - env, - "{}/{} files corrupted.", - files.iter().filter(|file| file.is_bad()).count(), - files.len(), - )?; } } diff --git a/src/step.rs b/src/step.rs index a80bfc3..0115bce 100644 --- a/src/step.rs +++ b/src/step.rs @@ -15,7 +15,7 @@ pub(crate) trait Step { dim.suffix() )?; - err!(env, "{}{} ", message.prefix(), self.symbol())?; + err!(env, "{} {}", self.symbol(), message.prefix())?; self.write_message(env.err_mut()).context(error::Stderr)?; diff --git a/src/subcommand/torrent/create.rs b/src/subcommand/torrent/create.rs index f4989b7..348d7ad 100644 --- a/src/subcommand/torrent/create.rs +++ b/src/subcommand/torrent/create.rs @@ -208,7 +208,7 @@ impl Create { CreateStep::Searching.print(env)?; - let spinner = if env.err().is_styled() { + let spinner = if env.err().is_styled_term() { let style = ProgressStyle::default_spinner() .template("{spinner:.green} {msg:.bold}…") .tick_chars(consts::TICK_CHARS); @@ -300,7 +300,7 @@ impl Create { CreateStep::Hashing.print(env)?; - let progress_bar = if env.err().is_styled() { + let progress_bar = if env.err().is_styled_term() { let style = ProgressStyle::default_bar() .template( "{spinner:.green} ⟪{elapsed_precise}⟫ ⟦{bar:40.cyan}⟧ \ @@ -380,7 +380,7 @@ impl Create { let status = metainfo.verify(&input, None)?; if !status.good() { - return Err(Error::Verify { status }); + return Err(Error::Verify); } } diff --git a/src/subcommand/torrent/verify.rs b/src/subcommand/torrent/verify.rs index 7988ca2..936fdc6 100644 --- a/src/subcommand/torrent/verify.rs +++ b/src/subcommand/torrent/verify.rs @@ -45,7 +45,7 @@ impl Verify { metainfo_path.parent().unwrap().join(&metainfo.info.name) }; - let progress_bar = if env.err().is_styled() { + let progress_bar = if env.err().is_styled_term() { let style = ProgressStyle::default_bar() .template( "{spinner:.green} ⟪{elapsed_precise}⟫ ⟦{bar:40.cyan}⟧ \ @@ -63,6 +63,8 @@ impl Verify { let status = metainfo.verify(&base, progress_bar)?; + status.print(env)?; + if status.good() { errln!( env, @@ -70,7 +72,7 @@ impl Verify { )?; Ok(()) } else { - Err(Error::Verify { status }) + Err(Error::Verify) } } } @@ -79,6 +81,8 @@ impl Verify { mod tests { use super::*; + use pretty_assertions::assert_eq; + #[test] fn require_metainfo_argument() { let mut env = test_env! { @@ -173,13 +177,22 @@ mod tests { tree: {}, }; - assert_matches!(verify_env.run(), Err(Error::Verify { .. })); + assert_matches!(verify_env.status(), Err(EXIT_FAILURE)); - let want = format!( - "[1/2] \u{1F4BE} Loading metainfo from `{}`…\n[2/2] \u{1F9EE} Verifying pieces from `{}`…\n", - torrent.display(), - create_env.resolve("foo").display() - ); + let want = [ + &format!( + "[1/2] \u{1F4BE} Loading metainfo from `{}`…", + torrent.display() + ), + &format!( + "[2/2] \u{1F9EE} Verifying pieces from `{}`…", + create_env.resolve("foo").display() + ), + "Pieces corrupted.", + "error: Torrent verification failed.", + "", + ] + .join("\n"); assert_eq!(verify_env.err(), want); assert_eq!(verify_env.out(), ""); @@ -243,4 +256,258 @@ mod tests { Ok(()) } + + #[test] + fn output_multiple() -> Result<()> { + let mut create_env = test_env! { + args: [ + "torrent", + "create", + "--input", + "foo", + "--announce", + "https://bar", + "--md5", + ], + tree: { + foo: { + a: "abc", + d: "efg", + h: "ijk", + l: "mno", + p: "qrs", + t: "uvw", + }, + }, + }; + + create_env.run()?; + + let torrent = create_env.resolve("foo.torrent"); + + create_env.write("foo/a", "xyz"); + create_env.write("foo/d", "efgg"); + create_env.write("foo/h", "ik"); + create_env.remove_file("foo/l"); + create_env.remove_file("foo/p"); + create_env.create_dir("foo/p"); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let metadata = create_env.metadata("foo/t"); + let mut permissions = metadata.permissions(); + permissions.set_mode(0); + create_env.set_permissions("foo/t", permissions); + } + + let mut verify_env = test_env! { + args: [ + "torrent", + "verify", + "--input", + &torrent, + ], + tree: {}, + }; + + assert_matches!(verify_env.status(), Err(EXIT_FAILURE)); + + let want = [ + &format!( + "[1/2] \u{1F4BE} Loading metainfo from `{}`…", + torrent.display() + ), + &format!( + "[2/2] \u{1F9EE} Verifying pieces from `{}`…", + create_env.resolve("foo").display() + ), + "a: MD5 checksum mismatch: d16fb36f911f878998c136191af705e (expected \ + 90150983cd24fb0d6963f7d28e17f72)", + "d: 1 byte too long", + "h: 1 byte too short", + "l: File missing", + "p: Expected file but found directory", + #[cfg(unix)] + "t: Permission denied (os error 13)", + "Pieces corrupted.", + "error: Torrent verification failed.", + "", + ] + .join("\n"); + + assert_eq!(verify_env.err(), want); + assert_eq!(verify_env.out(), ""); + + Ok(()) + } + + #[test] + fn output_color() -> Result<()> { + let mut create_env = test_env! { + args: [ + "torrent", + "create", + "--input", + "foo", + "--announce", + "https://bar", + "--md5", + ], + tree: { + foo: { + a: "abc", + d: "efg", + h: "ijk", + l: "mno", + p: "qrs", + t: "uvw", + }, + }, + }; + + create_env.run()?; + + let torrent = create_env.resolve("foo.torrent"); + + create_env.write("foo/a", "xyz"); + create_env.write("foo/d", "efgg"); + create_env.write("foo/h", "ik"); + create_env.remove_file("foo/l"); + create_env.remove_file("foo/p"); + create_env.create_dir("foo/p"); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let metadata = create_env.metadata("foo/t"); + let mut permissions = metadata.permissions(); + permissions.set_mode(0); + create_env.set_permissions("foo/t", permissions); + } + + let mut verify_env = test_env! { + args: [ + "torrent", + "verify", + "--input", + &torrent, + ], + err_style: true, + tree: {}, + }; + + assert_matches!(verify_env.status(), Err(EXIT_FAILURE)); + + let style = Style::active(); + + fn error(path: &str, message: &str) -> String { + let style = Style::active(); + format!( + "{}{}:{} {}", + style.message().prefix(), + path, + style.message().suffix(), + style.error().paint(message) + ) + } + + let want = [ + &format!( + "{} \u{1F4BE} {}", + style.dim().paint("[1/2]"), + style + .message() + .paint(format!("Loading metainfo from `{}`…", torrent.display())) + ), + &format!( + "{} \u{1F9EE} {}", + style.dim().paint("[2/2]"), + style.message().paint(format!( + "Verifying pieces from `{}`…", + create_env.resolve("foo").display() + )) + ), + &error( + "a", + "MD5 checksum mismatch: d16fb36f911f878998c136191af705e (expected \ + 90150983cd24fb0d6963f7d28e17f72)", + ), + &error("d", "1 byte too long"), + &error("h", "1 byte too short"), + &error("l", "File missing"), + &error("p", "Expected file but found directory"), + #[cfg(unix)] + &error("t", "Permission denied (os error 13)"), + "Pieces corrupted.", + &format!( + "{}{}", + style.error().paint("error"), + style.message().paint(": Torrent verification failed.") + ), + "", + ] + .join("\n"); + + assert_eq!(verify_env.err(), want); + assert_eq!(verify_env.out(), ""); + + Ok(()) + } + + #[test] + fn output_single() -> Result<()> { + let mut create_env = test_env! { + args: [ + "torrent", + "create", + "--input", + "foo", + "--announce", + "https://bar", + ], + tree: { + foo: "abc", + }, + }; + + create_env.run()?; + + let torrent = create_env.resolve("foo.torrent"); + + create_env.write("foo", "abcxyz"); + + let mut verify_env = test_env! { + args: [ + "torrent", + "verify", + "--input", + &torrent, + ], + tree: {}, + }; + + assert_matches!(verify_env.status(), Err(EXIT_FAILURE)); + + let want = [ + &format!( + "[1/2] \u{1F4BE} Loading metainfo from `{}`…", + torrent.display() + ), + &format!( + "[2/2] \u{1F9EE} Verifying pieces from `{}`…", + create_env.resolve("foo").display() + ), + "3 bytes too long", + "Pieces corrupted.", + "error: Torrent verification failed.", + "", + ] + .join("\n"); + + assert_eq!(verify_env.err(), want); + assert_eq!(verify_env.out(), ""); + + Ok(()) + } } diff --git a/src/test_env.rs b/src/test_env.rs index b3e33ec..b851772 100644 --- a/src/test_env.rs +++ b/src/test_env.rs @@ -4,6 +4,7 @@ macro_rules! test_env { { args: [$($arg:expr),* $(,)?], $(cwd: $cwd:expr,)? + $(err_style: $err_style:expr,)? tree: { $($tree:tt)* } $(,)? @@ -13,6 +14,7 @@ macro_rules! test_env { TestEnvBuilder::new() $(.current_dir(tempdir.path().join($cwd)))? + $(.err_style($err_style))? .tempdir(tempdir) .arg("imdl") $(.arg($arg))* @@ -55,6 +57,22 @@ impl TestEnv { fs::write(self.env.resolve(path), bytes.as_ref()).unwrap(); } + pub(crate) fn remove_file(&self, path: impl AsRef) { + fs::remove_file(self.env.resolve(path)).unwrap(); + } + + pub(crate) fn create_dir(&self, path: impl AsRef) { + fs::create_dir(self.env.resolve(path)).unwrap(); + } + + pub(crate) fn metadata(&self, path: impl AsRef) -> fs::Metadata { + fs::metadata(self.env.resolve(path)).unwrap() + } + + pub(crate) fn set_permissions(&self, path: impl AsRef, permissions: fs::Permissions) { + fs::set_permissions(self.env.resolve(path), permissions).unwrap(); + } + pub(crate) fn load_metainfo(&self, filename: impl AsRef) -> Metainfo { Metainfo::load(self.env.resolve(filename.as_ref())).unwrap() } diff --git a/src/test_env_builder.rs b/src/test_env_builder.rs index 8315c1e..f5df9b1 100644 --- a/src/test_env_builder.rs +++ b/src/test_env_builder.rs @@ -6,6 +6,7 @@ pub(crate) struct TestEnvBuilder { out_is_term: bool, tempdir: Option, use_color: bool, + err_style: bool, } impl TestEnvBuilder { @@ -16,6 +17,7 @@ impl TestEnvBuilder { out_is_term: false, tempdir: None, use_color: false, + err_style: false, } } @@ -24,6 +26,11 @@ impl TestEnvBuilder { self } + pub(crate) fn err_style(mut self, err_style: bool) -> Self { + self.err_style = err_style; + self + } + pub(crate) fn arg(mut self, arg: impl Into) -> Self { self.args.push(arg.into()); self @@ -64,7 +71,7 @@ impl TestEnvBuilder { self.out_is_term, ); - let err_stream = OutputStream::new(Box::new(err.clone()), false, false); + let err_stream = OutputStream::new(Box::new(err.clone()), self.err_style, false); let env = Env::new(current_dir, self.args, out_stream, err_stream);