diff --git a/fixtures/double-count/home.md b/fixtures/double-count/home.md new file mode 100644 index 0000000000..77ce67be1e --- /dev/null +++ b/fixtures/double-count/home.md @@ -0,0 +1,5 @@ +# Navbar + +* [Resource 1](resource-1.md) +* [Resource 2](resource-2.md) +* [Resource 3](resource-3.md) \ No newline at end of file diff --git a/fixtures/double-count/index.md b/fixtures/double-count/index.md new file mode 100644 index 0000000000..77ce67be1e --- /dev/null +++ b/fixtures/double-count/index.md @@ -0,0 +1,5 @@ +# Navbar + +* [Resource 1](resource-1.md) +* [Resource 2](resource-2.md) +* [Resource 3](resource-3.md) \ No newline at end of file diff --git a/fixtures/double-count/resource-1.md b/fixtures/double-count/resource-1.md new file mode 100644 index 0000000000..ce78c654b2 --- /dev/null +++ b/fixtures/double-count/resource-1.md @@ -0,0 +1 @@ +# First resource \ No newline at end of file diff --git a/fixtures/double-count/resource-2.md b/fixtures/double-count/resource-2.md new file mode 100644 index 0000000000..e9eec3428d --- /dev/null +++ b/fixtures/double-count/resource-2.md @@ -0,0 +1 @@ +# Second resource \ No newline at end of file diff --git a/fixtures/double-count/resource-3.md b/fixtures/double-count/resource-3.md new file mode 100644 index 0000000000..d934cdb889 --- /dev/null +++ b/fixtures/double-count/resource-3.md @@ -0,0 +1 @@ +# Third resource \ No newline at end of file diff --git a/lychee-bin/src/formatters/stats/compact.rs b/lychee-bin/src/formatters/stats/compact.rs index fb23f5f977..53ea116408 100644 --- a/lychee-bin/src/formatters/stats/compact.rs +++ b/lychee-bin/src/formatters/stats/compact.rs @@ -55,6 +55,7 @@ impl Display for CompactResponseStats { color!(f, NORMAL, "🔍 {} Total", stats.total)?; color!(f, DIM, " (in {})", format_duration(stats.duration))?; + color!(f, NORMAL, " 🔗 {} Unique", stats.unique)?; color!(f, BOLD_GREEN, " ✅ {} OK", stats.successful)?; let total_errors = stats.errors; @@ -181,7 +182,7 @@ mod tests { ℹ Suggestions https://original.dev/ --> https://suggestion.dev/ -🔍 2 Total (in 0s) ✅ 0 OK 🚫 1 Error ⏳ 1 Timeouts 🔀 1 Redirects +🔍 2 Total (in 0s) 🔗 2 Unique ✅ 0 OK 🚫 1 Error ⏳ 1 Timeouts 🔀 1 Redirects 📊 Per-host Statistics ──────────────────────────────────────────────────────────── diff --git a/lychee-bin/src/formatters/stats/detailed.rs b/lychee-bin/src/formatters/stats/detailed.rs index 96ea4f39ef..7a4edbea4f 100644 --- a/lychee-bin/src/formatters/stats/detailed.rs +++ b/lychee-bin/src/formatters/stats/detailed.rs @@ -46,6 +46,7 @@ impl Display for DetailedResponseStats { writeln!(f, "📝 Summary")?; writeln!(f, "{separator}")?; write_stat(f, "🔍 Total", stats.total, true)?; + write_stat(f, "🔗 Unique", stats.unique, true)?; write_stat(f, "✅ Successful", stats.successful, true)?; write_stat(f, "⏳ Timeouts", stats.timeouts, true)?; write_stat(f, "🔀 Redirected", stats.redirects, true)?; @@ -136,6 +137,7 @@ mod tests { "📝 Summary --------------------- 🔍 Total............2 +🔗 Unique...........2 ✅ Successful.......0 ⏳ Timeouts.........1 🔀 Redirected.......1 diff --git a/lychee-bin/src/formatters/stats/json.rs b/lychee-bin/src/formatters/stats/json.rs index fac41f7ada..fe358c573f 100644 --- a/lychee-bin/src/formatters/stats/json.rs +++ b/lychee-bin/src/formatters/stats/json.rs @@ -24,6 +24,7 @@ mod tests { const EXPECTED_JSON: &str = r#"{ "total": 2, + "unique": 2, "successful": 0, "unknown": 0, "unsupported": 0, diff --git a/lychee-bin/src/formatters/stats/markdown.rs b/lychee-bin/src/formatters/stats/markdown.rs index 992cffe3f2..243b99875a 100644 --- a/lychee-bin/src/formatters/stats/markdown.rs +++ b/lychee-bin/src/formatters/stats/markdown.rs @@ -32,6 +32,10 @@ fn stats_table(stats: &ResponseStats) -> String { status: "🔍 Total", count: stats.total, }, + StatsTableEntry { + status: "🔗 Unique", + count: stats.unique, + }, StatsTableEntry { status: "✅ Successful", count: stats.successful, @@ -244,6 +248,7 @@ mod tests { let expected = "| Status | Count | |----------------|-------| | 🔍 Total | 0 | +| 🔗 Unique | 0 | | ✅ Successful | 0 | | ⏳ Timeouts | 0 | | 🔀 Redirected | 0 | @@ -262,6 +267,7 @@ mod tests { | Status | Count | |----------------|-------| | 🔍 Total | 2 | +| 🔗 Unique | 2 | | ✅ Successful | 0 | | ⏳ Timeouts | 1 | | 🔀 Redirected | 1 | diff --git a/lychee-bin/src/formatters/stats/mod.rs b/lychee-bin/src/formatters/stats/mod.rs index 1b60f763a0..5ae56267a7 100644 --- a/lychee-bin/src/formatters/stats/mod.rs +++ b/lychee-bin/src/formatters/stats/mod.rs @@ -173,6 +173,7 @@ fn get_dummy_stats() -> OutputStats { let response_stats = ResponseStats { total: 2, + unique: 2, successful: 0, errors: 1, unknown: 0, @@ -189,6 +190,7 @@ fn get_dummy_stats() -> OutputStats { excluded_map: HashMap::default(), timeout_map, detailed_stats: true, + seen_responses: HashSet::new(), }; let host_stats = Some(HostStatsMap::from(HashMap::from([( diff --git a/lychee-bin/src/formatters/stats/response.rs b/lychee-bin/src/formatters/stats/response.rs index bf2c1fdd82..2ad3533704 100644 --- a/lychee-bin/src/formatters/stats/response.rs +++ b/lychee-bin/src/formatters/stats/response.rs @@ -6,7 +6,7 @@ use std::{ time::Duration, }; -use lychee_lib::{CacheStatus, InputSource, Response, ResponseBody, Status}; +use lychee_lib::{CacheStatus, InputSource, Response, ResponseBody, Status, Uri}; use serde::Serialize; use crate::formatters::suggestion::Suggestion; @@ -24,6 +24,8 @@ use crate::formatters::suggestion::Suggestion; pub(crate) struct ResponseStats { /// Total number of responses pub(crate) total: usize, + /// Number of unique responses (i.e. responses with a unique URI) + pub(crate) unique: usize, /// Number of successful responses pub(crate) successful: usize, /// Number of responses with an unknown status @@ -56,6 +58,9 @@ pub(crate) struct ResponseStats { pub(crate) duration: Duration, /// Also track successful and excluded responses pub(crate) detailed_stats: bool, + /// Set for counting unique responses + #[serde(skip)] + pub(crate) seen_responses: HashSet, } impl ResponseStats { @@ -65,6 +70,7 @@ impl ResponseStats { pub(crate) fn extended() -> Self { Self { detailed_stats: true, + seen_responses: HashSet::new(), ..Default::default() } } @@ -113,6 +119,10 @@ impl ResponseStats { /// Update the stats with a new response pub(crate) fn add(&mut self, response: Response) { + if self.seen_responses.insert(response.body().uri.clone()) { + self.unique += 1; + } + self.total += 1; self.increment_status_counters(response.status()); self.add_response_status(response); diff --git a/lychee-bin/tests/cli.rs b/lychee-bin/tests/cli.rs index 48c17dfdf4..4d80522858 100644 --- a/lychee-bin/tests/cli.rs +++ b/lychee-bin/tests/cli.rs @@ -3667,4 +3667,26 @@ https://lychee.cli.rs/guides/cli/#fragments-ignored .assert() .success(); } + + #[test] + fn test_no_double_count() { + let test_file_1 = fixtures_path!().join("double-count/index.md"); + let test_file_2 = fixtures_path!().join("double-count/home.md"); + + cargo_bin_cmd!() + .arg(&test_file_1) + .arg(&test_file_2) + .assert() + .success() + .stdout(contains("6 Total")) + .stdout(contains("3 Unique")); + + cargo_bin_cmd!() + .arg("--dump") + .arg(&test_file_1) + .arg(&test_file_2) + .assert() + .success() + .stdout(contains("resource-1.md").count(2)); + } }