use std::borrow::Cow;
use std::io::Write;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use serde::Serialize;
pub use ferridriver_config::test::TraceMode;
use crate::model::TestStep;
#[derive(Serialize)]
#[serde(tag = "type")]
enum TraceEvent<'a> {
#[serde(rename = "context-options")]
ContextOptions {
#[serde(rename = "browserName")]
browser_name: &'static str,
platform: &'static str,
#[serde(rename = "wallTime")]
wall_time: u64,
#[serde(rename = "sdkLanguage")]
sdk_language: &'static str,
},
#[serde(rename = "before")]
Before {
#[serde(rename = "callId")]
call_id: Cow<'a, str>,
#[serde(rename = "startTime")]
start_time: u64,
class: &'static str,
method: Cow<'a, str>,
title: Cow<'a, str>,
#[serde(rename = "parentId", skip_serializing_if = "Option::is_none")]
parent_id: Option<Cow<'a, str>>,
},
#[serde(rename = "after")]
After {
#[serde(rename = "callId")]
call_id: Cow<'a, str>,
#[serde(rename = "endTime")]
end_time: u64,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<&'a str>,
},
}
fn count_events(steps: &[TestStep]) -> usize {
steps.iter().map(|s| 2 + count_events(&s.steps)).sum()
}
pub struct TraceRecorder<'a> {
events: Vec<TraceEvent<'a>>,
call_counter: u32,
wall_time: u64,
}
impl<'a> TraceRecorder<'a> {
#[must_use]
pub fn for_steps(steps: &[TestStep]) -> Self {
let capacity = 1 + count_events(steps); let wall_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
let mut events = Vec::with_capacity(capacity);
events.push(TraceEvent::ContextOptions {
browser_name: "chromium",
platform: std::env::consts::OS,
wall_time,
sdk_language: "rust",
});
Self {
events,
call_counter: 0,
wall_time,
}
}
pub fn record_step(&mut self, step: &'a TestStep, parent_id: Option<Cow<'a, str>>) {
self.call_counter += 1;
let call_id: Cow<'a, str> = Cow::Owned(format!("s{}", self.call_counter));
self.events.push(TraceEvent::Before {
call_id: call_id.clone(),
start_time: self.wall_time.saturating_sub(step.duration.as_millis() as u64),
class: "Test",
method: Cow::Owned(step.category.to_string()),
title: Cow::Borrowed(&step.title),
parent_id,
});
for child in &step.steps {
self.record_step(child, Some(call_id.clone()));
}
self.events.push(TraceEvent::After {
call_id,
end_time: self.wall_time,
error: step.error.as_deref(),
});
}
pub fn record_steps(&mut self, steps: &'a [TestStep]) {
for step in steps {
self.record_step(step, None);
}
}
pub fn into_zip_bytes(self) -> Result<Vec<u8>, String> {
let mut buf = Vec::with_capacity(256 + self.events.len() * 128);
let cursor = std::io::Cursor::new(&mut buf);
let mut zip = zip::ZipWriter::new(cursor);
let options = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip
.start_file("test.trace", options)
.map_err(|e| format!("zip start_file: {e}"))?;
for event in &self.events {
serde_json::to_writer(&mut zip, event).map_err(|e| format!("serialize trace event: {e}"))?;
zip.write_all(b"\n").map_err(|e| format!("write newline: {e}"))?;
}
zip.finish().map_err(|e| format!("zip finish: {e}"))?;
Ok(buf)
}
}
pub fn write_trace_file(path: &Path, data: &[u8]) -> ferridriver::error::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, data)?;
Ok(())
}