ferridriver_test/
tracing.rs1use std::borrow::Cow;
23use std::io::Write;
24use std::path::Path;
25use std::time::{SystemTime, UNIX_EPOCH};
26
27use serde::Serialize;
28
29pub use ferridriver_config::test::TraceMode;
30
31use crate::model::TestStep;
32
33#[derive(Serialize)]
38#[serde(tag = "type")]
39enum TraceEvent<'a> {
40 #[serde(rename = "context-options")]
41 ContextOptions {
42 #[serde(rename = "browserName")]
43 browser_name: &'static str,
44 platform: &'static str,
45 #[serde(rename = "wallTime")]
46 wall_time: u64,
47 #[serde(rename = "sdkLanguage")]
48 sdk_language: &'static str,
49 },
50 #[serde(rename = "before")]
51 Before {
52 #[serde(rename = "callId")]
53 call_id: Cow<'a, str>,
54 #[serde(rename = "startTime")]
55 start_time: u64,
56 class: &'static str,
57 method: Cow<'a, str>,
58 title: Cow<'a, str>,
59 #[serde(rename = "parentId", skip_serializing_if = "Option::is_none")]
60 parent_id: Option<Cow<'a, str>>,
61 },
62 #[serde(rename = "after")]
63 After {
64 #[serde(rename = "callId")]
65 call_id: Cow<'a, str>,
66 #[serde(rename = "endTime")]
67 end_time: u64,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 error: Option<&'a str>,
70 },
71}
72
73fn count_events(steps: &[TestStep]) -> usize {
75 steps.iter().map(|s| 2 + count_events(&s.steps)).sum()
76}
77
78pub struct TraceRecorder<'a> {
80 events: Vec<TraceEvent<'a>>,
81 call_counter: u32,
82 wall_time: u64,
83}
84
85impl<'a> TraceRecorder<'a> {
86 #[must_use]
88 pub fn for_steps(steps: &[TestStep]) -> Self {
89 let capacity = 1 + count_events(steps); let wall_time = SystemTime::now()
91 .duration_since(UNIX_EPOCH)
92 .unwrap_or_default()
93 .as_millis() as u64;
94
95 let mut events = Vec::with_capacity(capacity);
96 events.push(TraceEvent::ContextOptions {
97 browser_name: "chromium",
98 platform: std::env::consts::OS,
99 wall_time,
100 sdk_language: "rust",
101 });
102
103 Self {
104 events,
105 call_counter: 0,
106 wall_time,
107 }
108 }
109
110 pub fn record_step(&mut self, step: &'a TestStep, parent_id: Option<Cow<'a, str>>) {
112 self.call_counter += 1;
113 let call_id: Cow<'a, str> = Cow::Owned(format!("s{}", self.call_counter));
114
115 self.events.push(TraceEvent::Before {
116 call_id: call_id.clone(),
117 start_time: self.wall_time.saturating_sub(step.duration.as_millis() as u64),
118 class: "Test",
119 method: Cow::Owned(step.category.to_string()),
120 title: Cow::Borrowed(&step.title),
121 parent_id,
122 });
123
124 for child in &step.steps {
126 self.record_step(child, Some(call_id.clone()));
127 }
128
129 self.events.push(TraceEvent::After {
130 call_id,
131 end_time: self.wall_time,
132 error: step.error.as_deref(),
133 });
134 }
135
136 pub fn record_steps(&mut self, steps: &'a [TestStep]) {
138 for step in steps {
139 self.record_step(step, None);
140 }
141 }
142
143 pub fn into_zip_bytes(self) -> Result<Vec<u8>, String> {
153 let mut buf = Vec::with_capacity(256 + self.events.len() * 128);
154 let cursor = std::io::Cursor::new(&mut buf);
155 let mut zip = zip::ZipWriter::new(cursor);
156
157 let options = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
158
159 zip
160 .start_file("test.trace", options)
161 .map_err(|e| format!("zip start_file: {e}"))?;
162
163 for event in &self.events {
164 serde_json::to_writer(&mut zip, event).map_err(|e| format!("serialize trace event: {e}"))?;
165 zip.write_all(b"\n").map_err(|e| format!("write newline: {e}"))?;
166 }
167
168 zip.finish().map_err(|e| format!("zip finish: {e}"))?;
169 Ok(buf)
170 }
171}
172
173pub fn write_trace_file(path: &Path, data: &[u8]) -> ferridriver::error::Result<()> {
179 if let Some(parent) = path.parent() {
180 std::fs::create_dir_all(parent)?;
181 }
182 std::fs::write(path, data)?;
183 Ok(())
184}