use chrono::{DateTime, Utc};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConformanceViolation {
pub timestamp: DateTime<Utc>,
pub method: String,
pub path: String,
pub client_ip: String,
pub status: u16,
pub reason: String,
pub category: String,
#[serde(default = "one")]
pub occurrences: u32,
}
fn one() -> u32 {
1
}
const DEFAULT_BUFFER_SIZE: usize = 256;
fn effective_buffer_size() -> usize {
let cap: usize = 64 * 1024;
std::env::var("MOCKFORGE_CONFORMANCE_BUFFER_SIZE")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.filter(|n| *n > 0)
.map(|n| n.min(cap))
.unwrap_or(DEFAULT_BUFFER_SIZE)
}
fn unique_mode_enabled() -> bool {
std::env::var("MOCKFORGE_CONFORMANCE_BUFFER_UNIQUE")
.ok()
.map(|s| matches!(s.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
.unwrap_or(false)
}
fn signature(v: &ServerConformanceViolation) -> String {
format!("{}|{}|{}|{}|{}", v.method, v.path, v.status, v.category, v.reason)
}
static VIOLATIONS: Lazy<Mutex<VecDeque<ServerConformanceViolation>>> =
Lazy::new(|| Mutex::new(VecDeque::with_capacity(effective_buffer_size())));
struct UniqueBuffer {
by_sig: HashMap<String, ServerConformanceViolation>,
order: VecDeque<String>,
}
impl UniqueBuffer {
fn new() -> Self {
Self {
by_sig: HashMap::new(),
order: VecDeque::new(),
}
}
fn record(&mut self, mut v: ServerConformanceViolation, cap: usize) {
let sig = signature(&v);
if let Some(existing) = self.by_sig.get_mut(&sig) {
existing.occurrences = existing.occurrences.saturating_add(1);
existing.timestamp = v.timestamp;
return;
}
v.occurrences = 1;
while self.order.len() >= cap {
if let Some(old) = self.order.pop_front() {
self.by_sig.remove(&old);
} else {
break;
}
}
self.order.push_back(sig.clone());
self.by_sig.insert(sig, v);
}
fn snapshot(&self) -> Vec<ServerConformanceViolation> {
self.order.iter().rev().filter_map(|s| self.by_sig.get(s).cloned()).collect()
}
fn len(&self) -> usize {
self.order.len()
}
fn clear(&mut self) {
self.by_sig.clear();
self.order.clear();
}
}
static UNIQUE_VIOLATIONS: Lazy<Mutex<UniqueBuffer>> = Lazy::new(|| Mutex::new(UniqueBuffer::new()));
static TOTAL_SEEN: AtomicU64 = AtomicU64::new(0);
static TOTAL_OK: AtomicU64 = AtomicU64::new(0);
pub fn record_ok() {
TOTAL_OK.fetch_add(1, Ordering::Relaxed);
}
pub fn total_ok() -> u64 {
TOTAL_OK.load(Ordering::Relaxed)
}
pub fn record(mut violation: ServerConformanceViolation) {
TOTAL_SEEN.fetch_add(1, Ordering::Relaxed);
let cap = effective_buffer_size();
if unique_mode_enabled() {
UNIQUE_VIOLATIONS.lock().record(violation, cap);
return;
}
if violation.occurrences == 0 {
violation.occurrences = 1;
}
let mut buf = VIOLATIONS.lock();
while buf.len() >= cap {
buf.pop_front();
}
buf.push_back(violation);
}
pub fn snapshot() -> Vec<ServerConformanceViolation> {
if unique_mode_enabled() {
UNIQUE_VIOLATIONS.lock().snapshot()
} else {
let buf = VIOLATIONS.lock();
buf.iter().rev().cloned().collect()
}
}
pub fn len() -> usize {
if unique_mode_enabled() {
UNIQUE_VIOLATIONS.lock().len()
} else {
VIOLATIONS.lock().len()
}
}
pub fn total_seen() -> u64 {
TOTAL_SEEN.load(Ordering::Relaxed)
}
pub fn clear() {
VIOLATIONS.lock().clear();
UNIQUE_VIOLATIONS.lock().clear();
TOTAL_SEEN.store(0, Ordering::Relaxed);
TOTAL_OK.store(0, Ordering::Relaxed);
}
#[cfg(test)]
mod tests {
use super::*;
fn v(method: &str, status: u16) -> ServerConformanceViolation {
ServerConformanceViolation {
timestamp: Utc::now(),
method: method.to_string(),
path: "/test".into(),
client_ip: "127.0.0.1".into(),
status,
reason: "test".into(),
category: "parameters".into(),
occurrences: 1,
}
}
#[test]
fn record_and_snapshot_in_lifo_order() {
clear();
record(v("GET", 400));
record(v("POST", 422));
let snap = snapshot();
assert_eq!(snap.len(), 2);
assert_eq!(snap[0].method, "POST");
assert_eq!(snap[1].method, "GET");
}
#[test]
fn buffer_drops_oldest_at_capacity() {
clear();
for i in 0..(DEFAULT_BUFFER_SIZE + 50) {
let mut entry = v("GET", 400);
entry.reason = format!("{i}");
record(entry);
}
assert_eq!(len(), DEFAULT_BUFFER_SIZE);
let snap = snapshot();
assert_eq!(snap[0].reason, format!("{}", DEFAULT_BUFFER_SIZE + 50 - 1));
assert_eq!(snap[DEFAULT_BUFFER_SIZE - 1].reason, format!("{}", 50));
}
#[test]
#[ignore]
fn effective_buffer_size_respects_env_var() {
let original = std::env::var("MOCKFORGE_CONFORMANCE_BUFFER_SIZE").ok();
unsafe {
std::env::set_var("MOCKFORGE_CONFORMANCE_BUFFER_SIZE", "1000");
}
assert_eq!(effective_buffer_size(), 1000);
unsafe {
std::env::set_var("MOCKFORGE_CONFORMANCE_BUFFER_SIZE", "0");
}
assert_eq!(effective_buffer_size(), DEFAULT_BUFFER_SIZE, "zero falls back to default");
unsafe {
std::env::set_var("MOCKFORGE_CONFORMANCE_BUFFER_SIZE", "garbage");
}
assert_eq!(
effective_buffer_size(),
DEFAULT_BUFFER_SIZE,
"unparsable falls back to default"
);
unsafe {
std::env::set_var("MOCKFORGE_CONFORMANCE_BUFFER_SIZE", "999999");
}
assert_eq!(effective_buffer_size(), 64 * 1024, "clamped to 64k");
unsafe {
match original {
Some(v) => std::env::set_var("MOCKFORGE_CONFORMANCE_BUFFER_SIZE", v),
None => std::env::remove_var("MOCKFORGE_CONFORMANCE_BUFFER_SIZE"),
}
}
}
#[test]
fn unique_buffer_dedups_by_signature_and_counts_occurrences() {
let mut buf = UniqueBuffer::new();
for _ in 0..10_000 {
buf.record(v("GET", 400), 256);
}
assert_eq!(buf.len(), 1);
let snap = buf.snapshot();
assert_eq!(snap.len(), 1);
assert_eq!(snap[0].occurrences, 10_000);
assert_eq!(snap[0].method, "GET");
}
#[test]
fn unique_buffer_distinguishes_distinct_signatures() {
let mut buf = UniqueBuffer::new();
for _ in 0..100 {
buf.record(v("GET", 400), 256);
buf.record(v("POST", 422), 256);
let mut other = v("GET", 400);
other.reason = "different".into();
buf.record(other, 256);
}
assert_eq!(buf.len(), 3);
let snap = buf.snapshot();
assert_eq!(snap.len(), 3);
for entry in &snap {
assert_eq!(entry.occurrences, 100, "each signature seen 100×");
}
}
#[test]
fn unique_buffer_evicts_oldest_signature_at_capacity() {
let mut buf = UniqueBuffer::new();
let cap = 4;
for i in 0..(cap + 3) {
let mut entry = v("GET", 400);
entry.reason = format!("kind-{i}");
buf.record(entry, cap);
}
assert_eq!(buf.len(), cap);
let snap = buf.snapshot();
let kinds: Vec<&str> = snap.iter().map(|e| e.reason.as_str()).collect();
assert_eq!(kinds, vec!["kind-6", "kind-5", "kind-4", "kind-3"]);
}
}