use std::collections::{HashMap, HashSet};
use lychee_lib::{CacheStatus, InputSource, Response, ResponseBody, Status};
use serde::Serialize;
#[derive(Default, Serialize)]
pub(crate) struct ResponseStats {
pub(crate) total: usize,
pub(crate) successful: usize,
pub(crate) failures: usize,
pub(crate) unknown: usize,
pub(crate) timeouts: usize,
pub(crate) redirects: usize,
pub(crate) excludes: usize,
pub(crate) errors: usize,
pub(crate) cached: usize,
pub(crate) fail_map: HashMap<InputSource, HashSet<ResponseBody>>,
}
impl ResponseStats {
#[inline]
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn add(&mut self, response: Response) {
let Response(source, ResponseBody { ref status, .. }) = response;
if status.is_unsupported() {
return;
}
self.total += 1;
match status {
Status::Ok(_) => self.successful += 1,
Status::Error(_) => self.failures += 1,
Status::UnknownStatusCode(_) => self.unknown += 1,
Status::Timeout(_) => self.timeouts += 1,
Status::Redirected(_) => self.redirects += 1,
Status::Excluded => self.excludes += 1,
Status::Unsupported(_) => (), Status::Cached(_) => self.cached += 1,
}
if let Status::Cached(cache_status) = status {
match cache_status {
CacheStatus::Ok(_) => self.successful += 1,
CacheStatus::Error(_) => self.failures += 1,
CacheStatus::Excluded | CacheStatus::Unsupported => self.excludes += 1,
}
}
if matches!(
status,
Status::Error(_)
| Status::Timeout(_)
| Status::Redirected(_)
| Status::Cached(CacheStatus::Error(_))
) {
let fail = self.fail_map.entry(source).or_default();
fail.insert(response.1);
};
}
#[inline]
pub(crate) const fn is_success(&self) -> bool {
self.total == self.successful + self.excludes
}
#[inline]
pub(crate) const fn is_empty(&self) -> bool {
self.total == 0
}
}
#[cfg(test)]
mod tests {
use std::collections::{HashMap, HashSet};
use http::StatusCode;
use lychee_lib::{ClientBuilder, InputSource, Response, ResponseBody, Status, Uri};
use reqwest::Url;
use wiremock::{matchers::path, Mock, MockServer, ResponseTemplate};
use super::ResponseStats;
fn website(url: &str) -> Uri {
Uri::from(Url::parse(url).expect("Expected valid Website URI"))
}
async fn get_mock_status_response<S>(status_code: S) -> Response
where
S: Into<StatusCode>,
{
let mock_server = MockServer::start().await;
let template = ResponseTemplate::new(status_code.into());
Mock::given(path("/"))
.respond_with(template)
.mount(&mock_server)
.await;
ClientBuilder::default()
.client()
.unwrap()
.check(mock_server.uri())
.await
.unwrap()
}
#[test]
fn test_stats_is_empty() {
let mut stats = ResponseStats::new();
assert!(stats.is_empty());
stats.add(Response(
InputSource::Stdin,
ResponseBody {
uri: website("https://example.com/ok"),
status: Status::Ok(StatusCode::OK),
},
));
assert!(!stats.is_empty());
}
#[tokio::test]
async fn test_stats() {
let status_codes = [
StatusCode::OK,
StatusCode::PERMANENT_REDIRECT,
StatusCode::BAD_GATEWAY,
];
let mut stats = ResponseStats::new();
for status in &status_codes {
stats.add(get_mock_status_response(status).await);
}
let mut expected_map: HashMap<InputSource, HashSet<ResponseBody>> = HashMap::new();
for status in &status_codes {
if status.is_server_error() || status.is_client_error() || status.is_redirection() {
let Response(source, response_body) = get_mock_status_response(status).await;
let entry = expected_map.entry(source).or_default();
entry.insert(response_body);
}
}
assert_eq!(stats.fail_map, expected_map);
}
}