use crate::snippets::types::{Snippet, SnippetStatus, ValidationResult};
use std::collections::BTreeMap;
use std::sync::Mutex;
pub(super) const FAILURE_PROGRESS_STRIDE: usize = 25;
pub(super) const FAILURE_MESSAGE_PREVIEW_CHARS: usize = 400;
#[derive(Clone, Copy, Default)]
struct LanguageTally {
completed: usize,
failed: usize,
unavailable: usize,
invoked: usize,
}
pub(super) struct FailureReporter {
totals: BTreeMap<crate::snippets::types::Language, usize>,
tallies: Mutex<BTreeMap<crate::snippets::types::Language, LanguageTally>>,
span: tracing::Span,
}
impl FailureReporter {
pub(super) fn new(snippets: &[Snippet]) -> Self {
let mut totals = BTreeMap::new();
for snippet in snippets {
*totals.entry(snippet.language).or_insert(0_usize) += 1;
}
Self {
totals,
tallies: Mutex::new(BTreeMap::new()),
span: tracing::Span::current(),
}
}
pub(super) fn record_toolchain_start(&self, language: crate::snippets::types::Language, timeout_secs: u64) {
let Ok(mut tallies) = self.tallies.lock() else {
return;
};
let tally = tallies.entry(language).or_default();
tally.invoked += 1;
let first = tally.invoked == 1;
drop(tallies);
if !first {
return;
}
let snippet_count = self.totals.get(&language).copied().unwrap_or(0);
self.span.in_scope(|| {
tracing::info!(
language = %language,
snippet_count = snippet_count,
timeout_secs = timeout_secs,
"Starting per-snippet validation"
);
});
}
pub(super) fn invoked_by_language(&self) -> BTreeMap<crate::snippets::types::Language, usize> {
let Ok(tallies) = self.tallies.lock() else {
return BTreeMap::new();
};
tallies
.iter()
.filter(|(_, tally)| tally.invoked > 0)
.map(|(language, tally)| (*language, tally.invoked))
.collect()
}
pub(super) fn record(&self, result: &ValidationResult) {
let language = result.snippet.language;
let failed = matches!(result.status, SnippetStatus::Fail | SnippetStatus::Error);
let unavailable = matches!(result.status, SnippetStatus::Unavailable);
let Ok(mut tallies) = self.tallies.lock() else {
return;
};
let tally = tallies.entry(language).or_default();
tally.completed += 1;
if failed {
tally.failed += 1;
}
if unavailable {
tally.unavailable += 1;
}
let tally = *tally;
drop(tallies);
let snippet_count = self.totals.get(&language).copied().unwrap_or(tally.completed);
self.span.in_scope(|| {
if failed && tally.failed == 1 {
tracing::warn!(
language = %language,
path = %result.snippet.source_origin.path.display(),
line = result.snippet.source_origin.line,
snippet_count = snippet_count,
error = %failure_preview(result.message.as_deref()),
"First snippet validation failure for this language"
);
} else if unavailable && tally.unavailable == 1 {
tracing::warn!(
language = %language,
path = %result.snippet.source_origin.path.display(),
line = result.snippet.source_origin.line,
snippet_count = snippet_count,
unresolved_dependency = result.unresolved_dependency,
error = %failure_preview(result.message.as_deref()),
"First snippet validation unavailability for this language"
);
} else if failed && tally.failed % FAILURE_PROGRESS_STRIDE == 0 {
tracing::warn!(
language = %language,
failed = tally.failed,
completed = tally.completed,
snippet_count = snippet_count,
"Snippet validation failures accumulating"
);
}
if tally.completed < snippet_count {
return;
}
if tally.failed > 0 {
tracing::warn!(
language = %language,
failed = tally.failed,
unavailable = tally.unavailable,
snippet_count = snippet_count,
"Finished snippet validation for this language with failures"
);
} else if tally.unavailable > 0 {
tracing::warn!(
language = %language,
unavailable = tally.unavailable,
snippet_count = snippet_count,
"Finished snippet validation for this language with every result unvalidated"
);
} else {
tracing::debug!(
language = %language,
snippet_count = snippet_count,
"Finished snippet validation for this language"
);
}
});
}
}
pub(super) fn failure_preview(message: Option<&str>) -> String {
let joined = message
.unwrap_or_default()
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join(" | ");
if joined.is_empty() {
return "<no validator output>".to_string();
}
match joined.char_indices().nth(FAILURE_MESSAGE_PREVIEW_CHARS) {
Some((index, _)) => format!("{}...", &joined[..index]),
None => joined,
}
}