use chrono::{DateTime, Utc};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::collections::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,
}
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)
}
static VIOLATIONS: Lazy<Mutex<VecDeque<ServerConformanceViolation>>> =
Lazy::new(|| Mutex::new(VecDeque::with_capacity(effective_buffer_size())));
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(violation: ServerConformanceViolation) {
TOTAL_SEEN.fetch_add(1, Ordering::Relaxed);
let cap = effective_buffer_size();
let mut buf = VIOLATIONS.lock();
while buf.len() >= cap {
buf.pop_front();
}
buf.push_back(violation);
}
pub fn snapshot() -> Vec<ServerConformanceViolation> {
let buf = VIOLATIONS.lock();
buf.iter().rev().cloned().collect()
}
pub fn len() -> usize {
VIOLATIONS.lock().len()
}
pub fn total_seen() -> u64 {
TOTAL_SEEN.load(Ordering::Relaxed)
}
pub fn clear() {
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(),
}
}
#[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"),
}
}
}
}