2024-06-14 17:47:52 +00:00
|
|
|
use anyhow::Result;
|
2024-09-09 16:33:18 +00:00
|
|
|
use console::Style;
|
2023-06-01 16:31:41 +00:00
|
|
|
use std::{
|
|
|
|
|
fmt::{self, Display},
|
2025-05-09 12:23:08 +00:00
|
|
|
sync::LazyLock,
|
2023-06-01 16:31:41 +00:00
|
|
|
time::Duration,
|
|
|
|
|
};
|
2021-11-17 23:44:48 +00:00
|
|
|
|
2025-05-24 16:23:23 +00:00
|
|
|
use crate::formatters::color::{BOLD_GREEN, BOLD_PINK, BOLD_YELLOW, DIM, NORMAL, color};
|
2024-06-14 17:47:52 +00:00
|
|
|
use crate::{formatters::get_response_formatter, options, stats::ResponseStats};
|
2021-11-17 23:44:48 +00:00
|
|
|
|
2022-04-25 17:19:36 +00:00
|
|
|
use super::StatsFormatter;
|
2021-11-17 23:44:48 +00:00
|
|
|
|
2024-06-14 17:47:52 +00:00
|
|
|
struct CompactResponseStats {
|
|
|
|
|
stats: ResponseStats,
|
|
|
|
|
mode: options::OutputMode,
|
|
|
|
|
}
|
2021-11-17 23:44:48 +00:00
|
|
|
|
|
|
|
|
impl Display for CompactResponseStats {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
2024-06-14 17:47:52 +00:00
|
|
|
let stats = &self.stats;
|
2021-11-17 23:44:48 +00:00
|
|
|
|
2024-11-08 08:02:33 +00:00
|
|
|
if !stats.error_map.is_empty() {
|
|
|
|
|
let input = if stats.error_map.len() == 1 {
|
2022-02-12 09:51:52 +00:00
|
|
|
"input"
|
|
|
|
|
} else {
|
|
|
|
|
"inputs"
|
|
|
|
|
};
|
|
|
|
|
|
2021-11-17 23:44:48 +00:00
|
|
|
color!(
|
|
|
|
|
f,
|
|
|
|
|
BOLD_PINK,
|
2022-02-12 09:51:52 +00:00
|
|
|
"Issues found in {} {input}. Find details below.\n\n",
|
2024-11-08 08:02:33 +00:00
|
|
|
stats.error_map.len()
|
2021-11-17 23:44:48 +00:00
|
|
|
)?;
|
|
|
|
|
}
|
2024-06-14 17:47:52 +00:00
|
|
|
|
|
|
|
|
let response_formatter = get_response_formatter(&self.mode);
|
|
|
|
|
|
2025-02-14 23:10:59 +00:00
|
|
|
for (source, responses) in super::sort_stat_map(&stats.error_map) {
|
2021-12-16 17:45:52 +00:00
|
|
|
color!(f, BOLD_YELLOW, "[{}]:\n", source)?;
|
2021-11-17 23:44:48 +00:00
|
|
|
for response in responses {
|
2024-11-06 23:22:50 +00:00
|
|
|
writeln!(
|
|
|
|
|
f,
|
|
|
|
|
"{}",
|
|
|
|
|
response_formatter.format_detailed_response(response)
|
|
|
|
|
)?;
|
2021-11-17 23:44:48 +00:00
|
|
|
}
|
2023-03-27 22:45:06 +00:00
|
|
|
|
2025-02-14 23:10:59 +00:00
|
|
|
if let Some(suggestions) = stats.suggestion_map.get(source) {
|
|
|
|
|
// Sort suggestions
|
|
|
|
|
let mut sorted_suggestions: Vec<_> = suggestions.iter().collect();
|
|
|
|
|
sorted_suggestions.sort_by(|a, b| {
|
|
|
|
|
let (a, b) = (a.to_string().to_lowercase(), b.to_string().to_lowercase());
|
|
|
|
|
human_sort::compare(&a, &b)
|
|
|
|
|
});
|
|
|
|
|
|
2023-03-27 22:45:06 +00:00
|
|
|
writeln!(f, "\n\u{2139} Suggestions")?;
|
2025-02-14 23:10:59 +00:00
|
|
|
for suggestion in sorted_suggestions {
|
2023-03-27 22:45:06 +00:00
|
|
|
writeln!(f, "{suggestion}")?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-17 23:44:48 +00:00
|
|
|
writeln!(f)?;
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-09 16:33:18 +00:00
|
|
|
color!(f, NORMAL, "🔍 {} Total", stats.total)?;
|
2023-06-01 16:31:41 +00:00
|
|
|
|
|
|
|
|
// show duration (in a human readable format), e.g. 2m 30s
|
|
|
|
|
let duration = Duration::from_secs(stats.duration_secs);
|
|
|
|
|
color!(f, DIM, " (in {})", humantime::format_duration(duration))?;
|
|
|
|
|
|
2024-09-09 16:33:18 +00:00
|
|
|
color!(f, BOLD_GREEN, " ✅ {} OK", stats.successful)?;
|
2021-11-17 23:44:48 +00:00
|
|
|
|
2022-12-20 09:43:01 +00:00
|
|
|
let total_errors = stats.errors;
|
2021-11-17 23:44:48 +00:00
|
|
|
|
|
|
|
|
let err_str = if total_errors == 1 { "Error" } else { "Errors" };
|
2024-09-09 16:33:18 +00:00
|
|
|
color!(f, BOLD_PINK, " 🚫 {} {}", total_errors, err_str)?;
|
|
|
|
|
|
|
|
|
|
write_if_any(stats.unknown, "❓", "Unknown", &BOLD_PINK, f)?;
|
|
|
|
|
write_if_any(stats.excludes, "👻", "Excluded", &BOLD_YELLOW, f)?;
|
|
|
|
|
write_if_any(stats.timeouts, "⏳", "Timeouts", &BOLD_YELLOW, f)?;
|
2025-06-20 15:52:15 +00:00
|
|
|
write_if_any(stats.unsupported, "⛔", "Unsupported", &BOLD_YELLOW, f)?;
|
2024-09-09 16:33:18 +00:00
|
|
|
|
2021-11-17 23:44:48 +00:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-09 16:33:18 +00:00
|
|
|
fn write_if_any(
|
|
|
|
|
value: usize,
|
|
|
|
|
symbol: &str,
|
|
|
|
|
text: &str,
|
2025-05-09 12:23:08 +00:00
|
|
|
style: &LazyLock<Style>,
|
2024-09-09 16:33:18 +00:00
|
|
|
f: &mut fmt::Formatter<'_>,
|
|
|
|
|
) -> Result<(), fmt::Error> {
|
|
|
|
|
if value > 0 {
|
|
|
|
|
color!(f, style, " {} {} {}", symbol, value, text)?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 17:47:52 +00:00
|
|
|
pub(crate) struct Compact {
|
|
|
|
|
mode: options::OutputMode,
|
|
|
|
|
}
|
2021-11-17 23:44:48 +00:00
|
|
|
|
|
|
|
|
impl Compact {
|
2024-06-14 17:47:52 +00:00
|
|
|
pub(crate) const fn new(mode: options::OutputMode) -> Self {
|
|
|
|
|
Self { mode }
|
2021-11-17 23:44:48 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-25 17:19:36 +00:00
|
|
|
impl StatsFormatter for Compact {
|
2024-06-14 17:47:52 +00:00
|
|
|
fn format(&self, stats: ResponseStats) -> Result<Option<String>> {
|
|
|
|
|
let compact = CompactResponseStats {
|
|
|
|
|
stats,
|
|
|
|
|
mode: self.mode.clone(),
|
|
|
|
|
};
|
2022-04-25 17:19:36 +00:00
|
|
|
Ok(Some(compact.to_string()))
|
2021-11-17 23:44:48 +00:00
|
|
|
}
|
|
|
|
|
}
|
2024-11-06 23:22:50 +00:00
|
|
|
|
|
|
|
|
#[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<InputSource, HashSet<ResponseBody>> = 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),
|
|
|
|
|
};
|
|
|
|
|
|
2024-11-08 08:02:33 +00:00
|
|
|
let mut error_map: HashMap<InputSource, HashSet<ResponseBody>> = HashMap::new();
|
2024-11-06 23:22:50 +00:00
|
|
|
let source = InputSource::RemoteUrl(Box::new(Url::parse("https://example.com").unwrap()));
|
2024-11-08 08:02:33 +00:00
|
|
|
error_map.insert(source, HashSet::from_iter(vec![err1, err2]));
|
2024-11-06 23:22:50 +00:00
|
|
|
|
|
|
|
|
let stats = ResponseStats {
|
|
|
|
|
total: 1,
|
|
|
|
|
successful: 1,
|
|
|
|
|
errors: 2,
|
|
|
|
|
unknown: 0,
|
|
|
|
|
excludes: 0,
|
|
|
|
|
timeouts: 0,
|
|
|
|
|
duration_secs: 0,
|
2024-11-08 08:02:33 +00:00
|
|
|
error_map,
|
2024-11-06 23:22:50 +00:00
|
|
|
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("🚫 2 Errors"));
|
|
|
|
|
|
|
|
|
|
assert!(result.contains("[https://example.com/]:"));
|
2025-05-24 16:23:23 +00:00
|
|
|
assert!(
|
|
|
|
|
result
|
|
|
|
|
.contains("https://github.com/mre/idiomatic-rust-doesnt-exist-man | 404 Not Found")
|
|
|
|
|
);
|
2024-11-06 23:22:50 +00:00
|
|
|
assert!(result.contains("https://github.com/mre/boom | 500 Internal Server Error"));
|
|
|
|
|
}
|
|
|
|
|
}
|