1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7#[serde(tag = "status", rename_all = "snake_case")]
8pub enum Outcome {
9 Passed,
11 Failed {
13 message: String,
15 },
16 Skipped {
18 reason: Option<String>,
20 },
21 Error {
23 message: String,
25 },
26 XFail {
28 reason: Option<String>,
30 },
31 XPass,
33}
34
35impl Outcome {
36 pub fn is_success(&self) -> bool {
38 matches!(
39 self,
40 Outcome::Passed | Outcome::Skipped { .. } | Outcome::XFail { .. }
41 )
42 }
43
44 pub fn is_failure(&self) -> bool {
46 matches!(self, Outcome::Failed { .. } | Outcome::XPass)
47 }
48
49 pub fn is_error(&self) -> bool {
51 matches!(self, Outcome::Error { .. })
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57#[serde(tag = "event", rename_all = "snake_case")]
58pub enum TestEvent {
59 TestStart {
61 node_id: String,
63 },
64
65 TestFinish {
67 node_id: String,
69 outcome: Outcome,
71 duration_ms: u64,
73 stdout: Option<String>,
75 stderr: Option<String>,
77 },
78
79 CollectionStart,
81
82 ItemCollected {
84 node_id: String,
86 file_path: String,
88 line_number: Option<u32>,
90 markers: Vec<String>,
92 },
93
94 CollectionFinish {
96 count: usize,
98 },
99
100 SessionStart {
102 test_count: usize,
104 },
105
106 SessionFinish {
108 exit_code: i32,
110 },
111
112 Warning {
114 message: String,
116 location: Option<String>,
118 },
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
123pub struct LogEvent {
124 pub level: LogLevel,
126 pub message: String,
128 pub module: Option<String>,
130 pub timestamp_ms: u64,
132}
133
134#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
136#[serde(rename_all = "lowercase")]
137pub enum LogLevel {
138 Debug,
139 Info,
140 Warning,
141 Error,
142 Critical,
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn outcome_classification() {
151 assert!(Outcome::Passed.is_success());
152 assert!(Outcome::Skipped { reason: None }.is_success());
153 assert!(Outcome::XFail { reason: None }.is_success());
154
155 assert!(Outcome::Failed {
156 message: "".to_string()
157 }
158 .is_failure());
159 assert!(Outcome::XPass.is_failure());
160
161 assert!(Outcome::Error {
162 message: "".to_string()
163 }
164 .is_error());
165 }
166
167 #[test]
168 fn test_event_roundtrip() {
169 let events = vec![
170 TestEvent::TestStart {
171 node_id: "test_foo.py::test_bar".to_string(),
172 },
173 TestEvent::TestFinish {
174 node_id: "test_foo.py::test_bar".to_string(),
175 outcome: Outcome::Passed,
176 duration_ms: 42,
177 stdout: Some("output".to_string()),
178 stderr: None,
179 },
180 TestEvent::CollectionStart,
181 TestEvent::ItemCollected {
182 node_id: "test_foo.py::test_bar".to_string(),
183 file_path: "test_foo.py".to_string(),
184 line_number: Some(10),
185 markers: vec!["slow".to_string()],
186 },
187 TestEvent::CollectionFinish { count: 100 },
188 TestEvent::SessionStart { test_count: 50 },
189 TestEvent::SessionFinish { exit_code: 0 },
190 TestEvent::Warning {
191 message: "Deprecated API".to_string(),
192 location: Some("test_foo.py:20".to_string()),
193 },
194 ];
195
196 for event in events {
197 let encoded = rmp_serde::to_vec(&event).unwrap();
198 let decoded: TestEvent = rmp_serde::from_slice(&encoded).unwrap();
199 assert_eq!(event, decoded);
200 }
201 }
202}