use std::collections::VecDeque;
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LoopGuard {
pub warn_after: u32,
pub stop_after: u32,
pub window: usize,
}
impl Default for LoopGuard {
fn default() -> Self {
Self {
warn_after: 3,
stop_after: 5,
window: 30,
}
}
}
impl LoopGuard {
#[must_use]
pub const fn disabled() -> Self {
Self {
warn_after: u32::MAX,
stop_after: u32::MAX,
window: 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoopVerdict {
Ok,
Warn {
repeats: u32,
},
Stop {
repeats: u32,
},
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct LoopTracker {
recent: VecDeque<u64>,
}
impl LoopTracker {
pub(crate) fn from_saved(saved: Vec<u64>) -> Self {
Self {
recent: saved.into(),
}
}
pub(crate) fn saved(&self) -> Vec<u64> {
self.recent.iter().copied().collect()
}
pub(crate) fn record(
&mut self,
guard: &LoopGuard,
name: &str,
arguments: &serde_json::Value,
result: &str,
) -> LoopVerdict {
if guard.window == 0 {
return LoopVerdict::Ok;
}
let print = fingerprint(name, arguments, result);
self.recent.push_back(print);
while self.recent.len() > guard.window {
self.recent.pop_front();
}
let repeats = u32::try_from(self.recent.iter().filter(|seen| **seen == print).count())
.unwrap_or(u32::MAX);
if repeats >= guard.stop_after {
LoopVerdict::Stop { repeats }
} else if repeats >= guard.warn_after {
LoopVerdict::Warn { repeats }
} else {
LoopVerdict::Ok
}
}
}
fn fingerprint(name: &str, arguments: &serde_json::Value, result: &str) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
name.hash(&mut hasher);
arguments.to_string().hash(&mut hasher);
result.hash(&mut hasher);
hasher.finish()
}
pub(crate) fn warning_note(name: &str, repeats: u32) -> String {
format!(
"\n[harness] You called {name} {repeats} times with the same arguments and got the same result. Do not call it again with these arguments: change approach or answer with what you have."
)
}
#[cfg(test)]
mod tests;