use super::types::{PipelineEvent, TimestampedEvent};
use anyhow::{Context, Result};
use std::io::Write;
use std::path::{Path, PathBuf};
pub fn event_log_path(playbook_path: &Path) -> PathBuf {
let stem = playbook_path.file_stem().unwrap_or_default().to_string_lossy();
playbook_path.with_file_name(format!("{}.events.jsonl", stem))
}
pub fn generate_run_id() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default();
let seed = now.as_nanos() ^ (std::process::id() as u128);
format!("r-{:012x}", seed & 0xFFFF_FFFF_FFFF)
}
pub fn now_iso8601() -> String {
chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
}
pub fn append_event(playbook_path: &Path, event: PipelineEvent) -> Result<()> {
let path = event_log_path(playbook_path);
let timestamped = TimestampedEvent { ts: now_iso8601(), event };
let json = serde_json::to_string(×tamped).context("failed to serialize event")?;
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.with_context(|| format!("failed to open event log: {}", path.display()))?;
writeln!(file, "{}", json).context("failed to write event")?;
Ok(())
}
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::*;
#[test]
fn test_PB005_event_log_path() {
let path = event_log_path(Path::new("/tmp/pipeline.yaml"));
assert_eq!(path, PathBuf::from("/tmp/pipeline.events.jsonl"));
}
#[test]
fn test_PB005_generate_run_id_format() {
let id = generate_run_id();
assert!(id.starts_with("r-"));
assert!(id.len() > 2);
}
#[test]
fn test_PB005_generate_run_id_unique() {
let id1 = generate_run_id();
std::thread::sleep(std::time::Duration::from_millis(1));
let id2 = generate_run_id();
assert_ne!(id1, id2);
}
#[test]
fn test_PB005_append_event() {
let dir = tempfile::tempdir().expect("tempdir creation failed");
let playbook_path = dir.path().join("test.yaml");
append_event(
&playbook_path,
PipelineEvent::RunStarted {
playbook: "test".to_string(),
run_id: "r-abc123".to_string(),
batuta_version: "0.6.5".to_string(),
},
)
.expect("unexpected failure");
append_event(
&playbook_path,
PipelineEvent::StageCached {
stage: "hello".to_string(),
cache_key: "blake3:key".to_string(),
reason: "cache_key matches".to_string(),
},
)
.expect("unexpected failure");
let log_path = event_log_path(&playbook_path);
let content = std::fs::read_to_string(&log_path).expect("fs read failed");
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 2);
let event1: TimestampedEvent =
serde_json::from_str(lines[0]).expect("json deserialize failed");
assert!(matches!(event1.event, PipelineEvent::RunStarted { .. }));
let event2: TimestampedEvent =
serde_json::from_str(lines[1]).expect("json deserialize failed");
assert!(matches!(event2.event, PipelineEvent::StageCached { .. }));
}
#[test]
fn test_PB005_now_iso8601_format() {
let ts = now_iso8601();
assert!(ts.contains('T'));
assert!(ts.ends_with('Z'));
}
}