use std::fs::OpenOptions;
use std::io::Write;
use std::panic;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Instant;
pub struct ExampleTracker {
example_name: String,
methods_tested: Vec<String>,
service_type: Option<String>,
start_time: Instant,
status: Arc<Mutex<TrackingStatus>>,
}
#[derive(Debug, Clone)]
enum TrackingStatus {
Running,
Success,
Failed(String),
}
impl ExampleTracker {
pub fn new(example_name: impl Into<String>) -> Self {
Self {
example_name: example_name.into(),
methods_tested: Vec::new(),
service_type: None,
start_time: Instant::now(),
status: Arc::new(Mutex::new(TrackingStatus::Running)),
}
}
pub fn methods(mut self, methods: &[&str]) -> Self {
self.methods_tested = methods.iter().map(|s| s.to_string()).collect();
self
}
pub fn service_type(mut self, service_type: impl Into<String>) -> Self {
self.service_type = Some(service_type.into());
self
}
pub fn start(self) -> Self {
let status = Arc::clone(&self.status);
let example_name = self.example_name.clone();
let old_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic".to_string()
};
let location = if let Some(loc) = panic_info.location() {
format!(" at {}:{}", loc.file(), loc.line())
} else {
String::new()
};
let error_msg = format!("Panic: {}{}", msg, location);
if let Ok(mut status) = status.lock() {
*status = TrackingStatus::Failed(error_msg.clone());
}
tracing::error!(example = %example_name, error = %error_msg, "Example panicked");
old_hook(panic_info);
}));
tracing::info!(example = %self.example_name, "Example tracking started");
self
}
pub fn success(&self) {
if let Ok(mut status) = self.status.lock() {
*status = TrackingStatus::Success;
}
}
pub fn fail(&self, error: impl Into<String>) {
if let Ok(mut status) = self.status.lock() {
*status = TrackingStatus::Failed(error.into());
}
}
fn csv_path() -> PathBuf {
let mut path = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
if path.ends_with("examples") {
path = path
.parent()
.and_then(|p| p.parent())
.and_then(|p| p.parent())
.unwrap_or(&path)
.to_path_buf();
}
path.join("example_runs.csv")
}
fn log_to_csv(&self) {
let duration_ms = self.start_time.elapsed().as_millis();
let status = self.status.lock().ok();
let (status_str, error_msg) = match status.as_deref() {
Some(TrackingStatus::Success) => ("success", String::new()),
Some(TrackingStatus::Failed(msg)) => ("failed", msg.clone()),
Some(TrackingStatus::Running) => {
("success", String::new())
}
None => ("unknown", "Failed to acquire status lock".to_string()),
};
let timestamp = chrono::Utc::now().to_rfc3339();
let methods = self.methods_tested.join(";");
let service_type = self.service_type.as_deref().unwrap_or("");
let git_commit = std::process::Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.unwrap_or_default();
let error_escaped = error_msg.replace('"', "\"\"");
let csv_line = format!(
"{},{},{},{},{},{},\"{}\",{}\n",
self.example_name,
timestamp,
status_str,
duration_ms,
methods,
service_type,
error_escaped,
git_commit
);
let csv_path = Self::csv_path();
match OpenOptions::new().create(true).append(true).open(&csv_path) {
Ok(mut file) => {
if let Err(e) = file.write_all(csv_line.as_bytes()) {
eprintln!("Failed to write to tracking CSV: {}", e);
} else {
tracing::info!(
example = %self.example_name,
status = %status_str,
duration_ms = %duration_ms,
csv_path = %csv_path.display(),
"Example run logged"
);
}
}
Err(e) => {
eprintln!("Failed to open tracking CSV at {:?}: {}", csv_path, e);
}
}
}
}
impl Drop for ExampleTracker {
fn drop(&mut self) {
if let Ok(mut status) = self.status.lock() {
if matches!(*status, TrackingStatus::Running) {
*status = TrackingStatus::Failed(
"Example exited without explicit success() - likely returned early via Result::Err".to_string()
);
}
}
self.log_to_csv();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tracker_creation() {
let tracker = ExampleTracker::new("test_example")
.methods(&["method1", "method2"])
.service_type("TestClient");
assert_eq!(tracker.example_name, "test_example");
assert_eq!(tracker.methods_tested, vec!["method1", "method2"]);
assert_eq!(tracker.service_type, Some("TestClient".to_string()));
}
#[test]
fn test_csv_path() {
let path = ExampleTracker::csv_path();
assert!(path.ends_with("example_runs.csv"));
}
}