From 28ac497a826b50f228f8a4c4348b69833cb8e867 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Oct 2024 19:36:09 +0200 Subject: [PATCH] Cleanup outputs; update tests --- Cargo.lock | 1 + lychee-bin/Cargo.toml | 1 + lychee-bin/src/commands/check.rs | 2 +- lychee-bin/src/formatters/response/color.rs | 26 +++---- lychee-bin/src/formatters/response/plain.rs | 6 +- lychee-bin/src/formatters/stats/compact.rs | 76 ++++++++++++++++++++- lychee-bin/src/formatters/stats/detailed.rs | 68 +++++++++++++++++- lychee-bin/src/formatters/stats/markdown.rs | 6 +- lychee-bin/tests/cli.rs | 14 ++-- lychee-lib/src/types/status.rs | 9 +-- 10 files changed, 173 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b017be20a..cf228157df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2539,6 +2539,7 @@ dependencies = [ "tokio-stream", "toml", "tracing-subscriber", + "url", "uuid", "wiremock", ] diff --git a/lychee-bin/Cargo.toml b/lychee-bin/Cargo.toml index f7b6952076..4342112422 100644 --- a/lychee-bin/Cargo.toml +++ b/lychee-bin/Cargo.toml @@ -54,6 +54,7 @@ tabled = "0.16.0" tokio = { version = "1.40.0", features = ["full"] } tokio-stream = "0.1.16" toml = "0.8.19" +url = "2.5.2" [dev-dependencies] assert_cmd = "2.0.16" diff --git a/lychee-bin/src/commands/check.rs b/lychee-bin/src/commands/check.rs index 1ec3328cfb..23d0c2ac76 100644 --- a/lychee-bin/src/commands/check.rs +++ b/lychee-bin/src/commands/check.rs @@ -399,7 +399,7 @@ mod tests { assert!(!buf.is_empty()); let buf = String::from_utf8_lossy(&buf); - assert_eq!(buf, "[200] http://127.0.0.1/ | Cached: OK (cached)\n"); + assert_eq!(buf, "[200] http://127.0.0.1/ | OK (cached)\n"); } #[tokio::test] diff --git a/lychee-bin/src/formatters/response/color.rs b/lychee-bin/src/formatters/response/color.rs index 98ec884e3f..1349171dcd 100644 --- a/lychee-bin/src/formatters/response/color.rs +++ b/lychee-bin/src/formatters/response/color.rs @@ -72,6 +72,13 @@ mod tests { use super::*; use http::StatusCode; use lychee_lib::{ErrorKind, Status, Uri}; + use pretty_assertions::assert_eq; + + #[cfg(test)] + /// Helper function to strip ANSI color codes for tests + fn strip_ansi_codes(s: &str) -> String { + console::strip_ansi_codes(s).to_string() + } // Helper function to create a ResponseBody with a given status and URI fn mock_response_body(status: Status, uri: &str) -> ResponseBody { @@ -91,10 +98,8 @@ mod tests { fn test_format_response_with_ok_status() { let formatter = ColorFormatter; let body = mock_response_body(Status::Ok(StatusCode::OK), "https://example.com"); - assert_eq!( - formatter.format_response(&body), - "\u{1b}[38;5;2m\u{1b}[1m [200]\u{1b}[0m https://example.com/" - ); + let formatted_response = strip_ansi_codes(&formatter.format_response(&body)); + assert_eq!(formatted_response, " [200] https://example.com/"); } #[test] @@ -104,10 +109,8 @@ mod tests { Status::Error(ErrorKind::InvalidUrlHost), "https://example.com/404", ); - assert_eq!( - formatter.format_response(&body), - "\u{1b}[38;5;197m [ERROR]\u{1b}[0m https://example.com/404" - ); + let formatted_response = strip_ansi_codes(&formatter.format_response(&body)); + assert_eq!(formatted_response, " [ERROR] https://example.com/404"); } #[test] @@ -116,7 +119,7 @@ mod tests { let long_uri = "https://example.com/some/very/long/path/to/a/resource/that/exceeds/normal/lengths"; let body = mock_response_body(Status::Ok(StatusCode::OK), long_uri); - let formatted_response = formatter.format_response(&body); + let formatted_response = strip_ansi_codes(&formatter.format_response(&body)); assert!(formatted_response.contains(long_uri)); } @@ -128,11 +131,10 @@ mod tests { "https://example.com/404", ); - let response = formatter.format_detailed_response(&body); - + let response = strip_ansi_codes(&formatter.format_detailed_response(&body)); assert_eq!( response, - "\u{1b}[38;5;197m [ERROR]\u{1b}[0m [ERROR] https://example.com/404 | URL is missing a host" + " [ERROR] https://example.com/404 | URL is missing a host" ); } } diff --git a/lychee-bin/src/formatters/response/plain.rs b/lychee-bin/src/formatters/response/plain.rs index 447d86e51c..3cc5f7a459 100644 --- a/lychee-bin/src/formatters/response/plain.rs +++ b/lychee-bin/src/formatters/response/plain.rs @@ -14,7 +14,7 @@ pub(crate) struct PlainFormatter; impl ResponseFormatter for PlainFormatter { fn format_response(&self, body: &ResponseBody) -> String { - body.to_string() + format!("[{}] {}", body.status.code_as_string(), body) } } @@ -59,7 +59,6 @@ mod plain_tests { fn test_format_response_with_excluded_status() { let formatter = PlainFormatter; let body = mock_response_body(Status::Excluded, "https://example.com/not-checked"); - assert_eq!(formatter.format_response(&body), body.to_string()); assert_eq!( formatter.format_response(&body), "[EXCLUDED] https://example.com/not-checked | Excluded" @@ -73,7 +72,6 @@ mod plain_tests { Status::Redirected(StatusCode::MOVED_PERMANENTLY), "https://example.com/redirect", ); - assert_eq!(formatter.format_response(&body), body.to_string()); assert_eq!( formatter.format_response(&body), "[301] https://example.com/redirect | Redirect (301 Moved Permanently): Moved Permanently" @@ -87,8 +85,6 @@ mod plain_tests { Status::UnknownStatusCode(StatusCode::from_u16(999).unwrap()), "https://example.com/unknown", ); - assert_eq!(formatter.format_response(&body), body.to_string()); - // Check the actual string representation of the status code assert_eq!( formatter.format_response(&body), "[999] https://example.com/unknown | Unknown status (999 )" diff --git a/lychee-bin/src/formatters/stats/compact.rs b/lychee-bin/src/formatters/stats/compact.rs index c2e98a9b83..c76914d2bc 100644 --- a/lychee-bin/src/formatters/stats/compact.rs +++ b/lychee-bin/src/formatters/stats/compact.rs @@ -42,7 +42,8 @@ impl Display for CompactResponseStats { for response in responses { writeln!( f, - "{}", + "[{}] {}", + response.status.code_as_string(), response_formatter.format_detailed_response(response) )?; } @@ -110,3 +111,76 @@ impl StatsFormatter for Compact { Ok(Some(compact.to_string())) } } + +#[cfg(test)] +mod tests { + use crate::formatters::stats::StatsFormatter; + use crate::{options::OutputMode, stats::ResponseStats}; + use http::StatusCode; + use lychee_lib::{InputSource, ResponseBody, Status, Uri}; + use std::collections::{HashMap, HashSet}; + use url::Url; + + use super::*; + + #[test] + fn test_formatter() { + // A couple of dummy successes + let mut success_map: HashMap> = HashMap::new(); + + success_map.insert( + InputSource::RemoteUrl(Box::new(Url::parse("https://example.com").unwrap())), + HashSet::from_iter(vec![ResponseBody { + uri: Uri::from(Url::parse("https://example.com").unwrap()), + status: Status::Ok(StatusCode::OK), + }]), + ); + + let err1 = ResponseBody { + uri: Uri::try_from("https://github.com/mre/idiomatic-rust-doesnt-exist-man").unwrap(), + status: Status::Ok(StatusCode::NOT_FOUND), + }; + + let err2 = ResponseBody { + uri: Uri::try_from("https://github.com/mre/boom").unwrap(), + status: Status::Ok(StatusCode::INTERNAL_SERVER_ERROR), + }; + + let mut fail_map: HashMap> = HashMap::new(); + let source = InputSource::RemoteUrl(Box::new(Url::parse("https://example.com").unwrap())); + fail_map.insert(source, HashSet::from_iter(vec![err1, err2])); + + let stats = ResponseStats { + total: 1, + successful: 1, + errors: 0, + unknown: 0, + excludes: 0, + timeouts: 0, + duration_secs: 0, + fail_map, + suggestion_map: HashMap::default(), + unsupported: 0, + redirects: 0, + cached: 0, + success_map, + excluded_map: HashMap::default(), + detailed_stats: false, + }; + + let formatter = Compact::new(OutputMode::Plain); + + let result = formatter.format(stats).unwrap().unwrap(); + + println!("{result}"); + + assert!(result.contains("🔍 1 Total")); + assert!(result.contains("✅ 1 OK")); + assert!(result.contains("🚫 0 Errors")); + + assert!(result.contains("[https://example.com/]:")); + assert!(result + .contains("https://github.com/mre/idiomatic-rust-doesnt-exist-man | 404 Not Found")); + assert!(result.contains("https://github.com/mre/boom | 500 Internal Server Error")); + } +} diff --git a/lychee-bin/src/formatters/stats/detailed.rs b/lychee-bin/src/formatters/stats/detailed.rs index 6e4037ca25..ae30d160b5 100644 --- a/lychee-bin/src/formatters/stats/detailed.rs +++ b/lychee-bin/src/formatters/stats/detailed.rs @@ -55,7 +55,11 @@ impl Display for DetailedResponseStats { write!(f, "\n\nErrors in {source}")?; for response in responses { - write!(f, "\n{}", response_formatter.format_response(response))?; + write!( + f, + "\n{}", + response_formatter.format_detailed_response(response) + )?; if let Some(suggestions) = &stats.suggestion_map.get(source) { writeln!(f, "\nSuggestions in {source}")?; @@ -89,3 +93,65 @@ impl StatsFormatter for Detailed { Ok(Some(detailed.to_string())) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::options::OutputMode; + use http::StatusCode; + use lychee_lib::{InputSource, ResponseBody, Status, Uri}; + use std::collections::{HashMap, HashSet}; + use url::Url; + + #[test] + fn test_detailed_formatter_github_404() { + let err1 = ResponseBody { + uri: Uri::try_from("https://github.com/mre/idiomatic-rust-doesnt-exist-man").unwrap(), + status: Status::Ok(StatusCode::NOT_FOUND), + }; + + let err2 = ResponseBody { + uri: Uri::try_from("https://github.com/mre/boom").unwrap(), + status: Status::Ok(StatusCode::INTERNAL_SERVER_ERROR), + }; + + let mut fail_map: HashMap> = HashMap::new(); + let source = InputSource::RemoteUrl(Box::new(Url::parse("https://example.com").unwrap())); + fail_map.insert(source, HashSet::from_iter(vec![err1, err2])); + + let stats = ResponseStats { + total: 2, + successful: 0, + errors: 2, + unknown: 0, + excludes: 0, + timeouts: 0, + duration_secs: 0, + unsupported: 0, + redirects: 0, + cached: 0, + suggestion_map: HashMap::default(), + success_map: HashMap::default(), + fail_map, + excluded_map: HashMap::default(), + detailed_stats: true, + }; + + let formatter = Detailed::new(OutputMode::Plain); + let result = formatter.format(stats).unwrap().unwrap(); + + // Check for the presence of expected content + assert!(result.contains("📝 Summary")); + assert!(result.contains("🔍 Total............2")); + assert!(result.contains("✅ Successful.......0")); + assert!(result.contains("⏳ Timeouts.........0")); + assert!(result.contains("🔀 Redirected.......0")); + assert!(result.contains("👻 Excluded.........0")); + assert!(result.contains("❓ Unknown..........0")); + assert!(result.contains("🚫 Errors...........2")); + assert!(result.contains("Errors in https://example.com/")); + assert!(result + .contains("https://github.com/mre/idiomatic-rust-doesnt-exist-man | 404 Not Found")); + assert!(result.contains("https://github.com/mre/boom | 500 Internal Server Error")); + } +} diff --git a/lychee-bin/src/formatters/stats/markdown.rs b/lychee-bin/src/formatters/stats/markdown.rs index 1c650953b4..e4cf11666b 100644 --- a/lychee-bin/src/formatters/stats/markdown.rs +++ b/lychee-bin/src/formatters/stats/markdown.rs @@ -186,7 +186,7 @@ mod tests { let markdown = markdown_response(&response).unwrap(); assert_eq!( markdown, - "* [200] [http://example.com/](http://example.com/) | Cached: OK (cached)" + "* [200] [http://example.com/](http://example.com/) | OK (cached)" ); } @@ -199,7 +199,7 @@ mod tests { let markdown = markdown_response(&response).unwrap(); assert_eq!( markdown, - "* [400] [http://example.com/](http://example.com/) | Cached: Error (cached)" + "* [400] [http://example.com/](http://example.com/) | Error (cached)" ); } @@ -253,7 +253,7 @@ mod tests { ### Errors in stdin -* [404] [http://127.0.0.1/](http://127.0.0.1/) | Cached: Error (cached) +* [404] [http://127.0.0.1/](http://127.0.0.1/) | Error (cached) ## Suggestions per input diff --git a/lychee-bin/tests/cli.rs b/lychee-bin/tests/cli.rs index 67a8ff9ec8..1188f9af81 100644 --- a/lychee-bin/tests/cli.rs +++ b/lychee-bin/tests/cli.rs @@ -868,7 +868,7 @@ mod cli { .assert() .stderr(contains(format!("[200] {}/\n", mock_server_ok.uri()))) .stderr(contains(format!( - "[404] {}/ | Failed: Network error: Not Found\n", + "[404] {}/ | Network error: Not Found\n", mock_server_err.uri() ))); @@ -881,11 +881,11 @@ mod cli { test_cmd .assert() .stderr(contains(format!( - "[200] {}/ | Cached: OK (cached)\n", + "[200] {}/ | OK (cached)\n", mock_server_ok.uri() ))) .stderr(contains(format!( - "[404] {}/ | Cached: Error (cached)\n", + "[404] {}/ | Error (cached)\n", mock_server_err.uri() ))); @@ -933,11 +933,11 @@ mod cli { .failure() .code(2) .stdout(contains(format!( - "[418] {}/ | Failed: Network error: I\'m a teapot", + "[418] {}/ | Network error: I\'m a teapot", mock_server_teapot.uri() ))) .stdout(contains(format!( - "[500] {}/ | Failed: Network error: Internal Server Error", + "[500] {}/ | Network error: Internal Server Error", mock_server_server_error.uri() ))); @@ -956,11 +956,11 @@ mod cli { .assert() .success() .stderr(contains(format!( - "[418] {}/ | Cached: OK (cached)", + "[418] {}/ | OK (cached)", mock_server_teapot.uri() ))) .stderr(contains(format!( - "[500] {}/ | Cached: OK (cached)", + "[500] {}/ | OK (cached)", mock_server_server_error.uri() ))); diff --git a/lychee-lib/src/types/status.rs b/lychee-lib/src/types/status.rs index 976a52e628..39ef79b0b2 100644 --- a/lychee-lib/src/types/status.rs +++ b/lychee-lib/src/types/status.rs @@ -45,7 +45,7 @@ pub enum Status { impl Display for Status { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Status::Ok(code) => write!(f, "OK ({code})"), + Status::Ok(code) => write!(f, "{code}"), Status::Redirected(code) => write!(f, "Redirect ({code})"), Status::UnknownStatusCode(code) => write!(f, "Unknown status ({code})"), Status::Excluded => f.write_str("Excluded"), @@ -53,7 +53,7 @@ impl Display for Status { Status::Timeout(None) => f.write_str("Timeout"), Status::Unsupported(e) => write!(f, "Unsupported: {e}"), Status::Error(e) => write!(f, "{e}"), - Status::Cached(status) => write!(f, "Cached: {status}"), + Status::Cached(status) => write!(f, "{status}"), } } } @@ -310,10 +310,7 @@ mod tests { fn test_status_serialization() { let status_ok = Status::Ok(StatusCode::from_u16(200).unwrap()); let serialized_with_code = serde_json::to_string(&status_ok).unwrap(); - assert_eq!( - "{\"text\":\"OK (200 OK)\",\"code\":200}", - serialized_with_code - ); + assert_eq!("{\"text\":\"200 OK\",\"code\":200}", serialized_with_code); let status_timeout = Status::Timeout(None); let serialized_without_code = serde_json::to_string(&status_timeout).unwrap();