pub const COMPACT_MESSAGE_CAP: usize = 512;
pub const COMPACT_HEAD_CHARS: usize = 320;
pub const COMPACT_TAIL_CHARS: usize = 128;
#[must_use]
pub fn compact_error_message(message: &str) -> String {
let total = message.chars().count();
if total <= COMPACT_MESSAGE_CAP {
return message.to_owned();
}
let head: String = message.chars().take(COMPACT_HEAD_CHARS).collect();
let tail: String = {
let skip = total - COMPACT_TAIL_CHARS;
message.chars().skip(skip).collect()
};
let elided = total - COMPACT_HEAD_CHARS - COMPACT_TAIL_CHARS;
format!("{head} [... {elided} chars elided ...] {tail}")
}
#[derive(Debug, Default)]
pub struct FailureTracker {
last: Option<(String, String)>,
count: u32,
}
impl FailureTracker {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn record_success(&mut self) {
self.last = None;
self.count = 0;
}
pub fn content_for_failure(&mut self, tool: &str, full_message: &str) -> String {
let same = self.last.as_ref().is_some_and(|(last_tool, last_message)| {
last_tool == tool && last_message == full_message
});
if same {
self.count += 1;
format!(
"tool `{tool}` has failed {count} consecutive times with the same error; \
the full error is recorded in the run log",
count = self.count
)
} else {
self.last = Some((tool.to_owned(), full_message.to_owned()));
self.count = 1;
compact_error_message(full_message)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn short_messages_are_unchanged() {
let message = "connection refused";
assert_eq!(compact_error_message(message), message);
let at_cap = "x".repeat(COMPACT_MESSAGE_CAP);
assert_eq!(compact_error_message(&at_cap), at_cap);
}
#[test]
fn long_messages_keep_head_and_tail() {
let head_part = "H".repeat(COMPACT_HEAD_CHARS);
let middle = "M".repeat(1000);
let tail_part = "T".repeat(COMPACT_TAIL_CHARS);
let message = format!("{head_part}{middle}{tail_part}");
let compacted = compact_error_message(&message);
assert!(compacted.starts_with(&head_part));
assert!(compacted.ends_with(&tail_part));
assert!(compacted.contains("[... 1000 chars elided ...]"));
}
#[test]
fn truncation_is_char_safe() {
let total = COMPACT_MESSAGE_CAP + 100;
let elided = total - COMPACT_HEAD_CHARS - COMPACT_TAIL_CHARS;
let message = "\u{00e9}".repeat(total);
let compacted = compact_error_message(&message);
assert!(compacted.contains(&format!("[... {elided} chars elided ...]")));
}
#[test]
fn repeats_collapse_and_streaks_reset() {
let mut tracker = FailureTracker::new();
let first = tracker.content_for_failure("search", "boom");
assert_eq!(first, "boom");
let second = tracker.content_for_failure("search", "boom");
assert!(second.contains("2 consecutive times"), "{second}");
let third = tracker.content_for_failure("search", "boom");
assert!(third.contains("3 consecutive times"), "{third}");
let different = tracker.content_for_failure("search", "bang");
assert_eq!(different, "bang");
tracker.record_success();
let after_success = tracker.content_for_failure("search", "bang");
assert_eq!(after_success, "bang");
}
#[test]
fn repeats_are_per_tool() {
let mut tracker = FailureTracker::new();
let _ = tracker.content_for_failure("search", "boom");
let other_tool = tracker.content_for_failure("fetch", "boom");
assert_eq!(other_tool, "boom");
}
}