1use std::time::Duration;
2
3use serde::Deserialize;
4use serde::Serialize;
5use ts_rs::TS;
6
7use super::SourceLocation;
8
9pub type SpanId = u64;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
12#[cfg_attr(
13 feature = "ts-export",
14 ts(export, export_to = "../../../viewer/src/types/")
15)]
16#[serde(rename_all = "kebab-case")]
17pub enum FnCallKind {
18 User,
19 Bif,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
23#[cfg_attr(
24 feature = "ts-export",
25 ts(export, export_to = "../../../viewer/src/types/")
26)]
27#[serde(rename_all = "kebab-case")]
28pub enum MarkerEvalKind {
29 Skip,
30 Run,
31 Flaky,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
35#[cfg_attr(
36 feature = "ts-export",
37 ts(export, export_to = "../../../viewer/src/types/")
38)]
39#[serde(rename_all = "kebab-case")]
40pub enum MarkerEvalModifier {
41 If,
42 Unless,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
46#[cfg_attr(
47 feature = "ts-export",
48 ts(export, export_to = "../../../viewer/src/types/")
49)]
50#[serde(rename_all = "kebab-case")]
51pub enum MarkerEvalDecision {
52 Pass,
54 Mark,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
60#[cfg_attr(
61 feature = "ts-export",
62 ts(export, export_to = "../../../viewer/src/types/")
63)]
64#[serde(tag = "shape", rename_all = "kebab-case")]
65pub enum MarkerEvalDetail {
66 Unconditional,
67 Bare {
68 value: String,
69 met: bool,
70 },
71 PureMatch {
72 value: String,
73 pattern: String,
74 is_regex: bool,
75 met: bool,
76 },
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, TS)]
80#[cfg_attr(
81 feature = "ts-export",
82 ts(export, export_to = "../../../viewer/src/types/")
83)]
84#[serde(tag = "kind", rename_all = "kebab-case")]
85pub enum SpanKind {
86 Test {
87 name: String,
88 },
89 EffectSetup {
90 effect: String,
91 overlay: Vec<(String, String)>,
92 alias: Option<String>,
93 dep_sources: Vec<(String, String)>,
98 marker: String,
102 is_reuse: bool,
105 },
106 EffectCleanup {
107 effect: String,
108 alias: Option<String>,
109 setup_span: SpanId,
115 marker: String,
117 is_deferred: bool,
120 },
121 ShellBlock {
122 shell: String,
123 },
124 MultiMatch {
131 shell: String,
132 },
133 CleanupBlock,
134 FnCall {
135 name: String,
136 args: Vec<(String, String)>,
137 result: Option<String>,
138 callee_kind: FnCallKind,
139 is_pure: bool,
140 },
141 Markers,
144 MarkerEval {
150 marker_kind: MarkerEvalKind,
151 modifier: MarkerEvalModifier,
152 decision: MarkerEvalDecision,
153 },
154}
155
156impl SpanKind {
157 pub fn kind_str(&self) -> &'static str {
161 match self {
162 SpanKind::Test { .. } => "test",
163 SpanKind::EffectSetup { .. } => "effect-setup",
164 SpanKind::EffectCleanup { .. } => "effect-cleanup",
165 SpanKind::ShellBlock { .. } => "shell-block",
166 SpanKind::MultiMatch { .. } => "multi-match",
167 SpanKind::CleanupBlock => "cleanup-block",
168 SpanKind::FnCall { .. } => "fn-call",
169 SpanKind::Markers => "markers",
170 SpanKind::MarkerEval { .. } => "marker-eval",
171 }
172 }
173
174 pub fn frame_data(&self) -> (Option<String>, Vec<(String, String)>) {
177 match self {
178 SpanKind::CleanupBlock => (None, Vec::new()),
179 SpanKind::Test { name } => (Some(name.clone()), Vec::new()),
180 SpanKind::EffectSetup {
181 effect, overlay, ..
182 } => (Some(effect.clone()), overlay.clone()),
183 SpanKind::EffectCleanup { effect, .. } => (Some(effect.clone()), Vec::new()),
184 SpanKind::ShellBlock { shell } => (Some(shell.clone()), Vec::new()),
185 SpanKind::MultiMatch { shell } => (Some(shell.clone()), Vec::new()),
186 SpanKind::FnCall { name, args, .. } => (Some(name.clone()), args.clone()),
187 SpanKind::Markers => (None, Vec::new()),
188 SpanKind::MarkerEval { .. } => (None, Vec::new()),
189 }
190 }
191
192 pub fn frame_alias(&self) -> Option<String> {
195 match self {
196 SpanKind::EffectSetup { alias, .. } => alias.clone(),
197 SpanKind::EffectCleanup { alias, .. } => alias.clone(),
198 _ => None,
199 }
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn markers_span_kind_serialises_as_kebab_kind() {
209 let kind = SpanKind::Markers;
210 let v = serde_json::to_value(&kind).unwrap();
211 assert_eq!(v, serde_json::json!({ "kind": "markers" }));
212 }
213
214 #[test]
215 fn marker_eval_span_kind_serialises_payload() {
216 let kind = SpanKind::MarkerEval {
217 marker_kind: MarkerEvalKind::Skip,
218 modifier: MarkerEvalModifier::If,
219 decision: MarkerEvalDecision::Mark,
220 };
221 let v = serde_json::to_value(&kind).unwrap();
222 assert_eq!(v["kind"], serde_json::json!("marker-eval"));
223 assert_eq!(v["marker_kind"], serde_json::json!("skip"));
224 assert_eq!(v["modifier"], serde_json::json!("if"));
225 assert_eq!(v["decision"], serde_json::json!("mark"));
226 }
227
228 #[test]
229 fn fn_call_span_serializes_callee_kind_and_is_pure() {
230 let span = SpanKind::FnCall {
231 name: "trim".into(),
232 args: vec![("$0".into(), " hi ".into())],
233 result: Some("hi".into()),
234 callee_kind: FnCallKind::Bif,
235 is_pure: true,
236 };
237 let json = serde_json::to_value(&span).unwrap();
238 assert_eq!(json["kind"], "fn-call");
239 assert_eq!(json["name"], "trim");
240 assert_eq!(json["callee_kind"], "bif");
241 assert_eq!(json["is_pure"], true);
242 }
243
244 #[test]
245 fn multi_match_span_kind_serialises() {
246 let kind = SpanKind::MultiMatch {
247 shell: "default".into(),
248 };
249 let v = serde_json::to_value(&kind).unwrap();
250 assert_eq!(v["kind"], serde_json::json!("multi-match"));
251 assert_eq!(v["shell"], serde_json::json!("default"));
252 }
253
254 #[test]
255 fn effect_setup_span_kind_serialises_dep_sources() {
256 let kind = SpanKind::EffectSetup {
257 effect: "Api".into(),
258 overlay: vec![("DB_PORT".into(), "5432".into())],
259 alias: Some("Api".into()),
260 dep_sources: vec![("DB_PORT".into(), "Db".into())],
261 marker: "marker-1".into(),
262 is_reuse: false,
263 };
264 let json = serde_json::to_value(&kind).unwrap();
265 assert_eq!(json["kind"], "effect-setup");
266 assert_eq!(json["dep_sources"][0][0], "DB_PORT");
267 assert_eq!(json["dep_sources"][0][1], "Db");
268 }
269
270 #[test]
271 fn multi_match_span_kind_str_and_frame_data() {
272 let kind = SpanKind::MultiMatch {
273 shell: "default".into(),
274 };
275 assert_eq!(kind.kind_str(), "multi-match");
276 let (name, args) = kind.frame_data();
277 assert_eq!(name.as_deref(), Some("default"));
278 assert!(args.is_empty());
279 assert_eq!(kind.frame_alias(), None);
280 }
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, TS)]
284#[cfg_attr(
285 feature = "ts-export",
286 ts(export, export_to = "../../../viewer/src/types/")
287)]
288pub struct Span {
289 pub id: SpanId,
290 #[serde(flatten)]
291 pub kind: SpanKind,
292 pub parent: Option<SpanId>,
293 #[serde(with = "super::ts_duration_ms")]
294 #[ts(as = "f64")]
295 pub start_ts: Duration,
296 #[serde(with = "super::ts_duration_ms_opt")]
297 #[ts(as = "Option<f64>")]
298 pub end_ts: Option<Duration>,
299 pub location: Option<SourceLocation>,
300}