1use std::fmt;
2use std::sync::Arc;
3use std::time::Duration;
4
5pub type OnEvent = Arc<dyn Fn(&Event) + Send + Sync>;
13
14#[derive(Debug)]
15pub struct ScenarioResult {
16 pub name: String,
17 pub events: Vec<Event>,
18 pub duration: Duration,
19 pub outcome: Outcome,
20}
21
22#[derive(Debug)]
23pub struct Event {
24 pub description: String,
25 pub kind: EventKind,
26 pub outcome: Outcome,
27 pub duration: Duration,
28 pub stdout: String,
31 pub stderr: String,
33}
34
35impl Event {
36 pub fn bare(
39 description: String,
40 kind: EventKind,
41 outcome: Outcome,
42 duration: Duration,
43 ) -> Self {
44 Self {
45 description,
46 kind,
47 outcome,
48 duration,
49 stdout: String::new(),
50 stderr: String::new(),
51 }
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum EventKind {
57 Step,
58 Assertion,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum Outcome {
63 Passed,
64 Failed(String),
65 Skipped,
66}
67
68impl Outcome {
69 pub fn is_pass(&self) -> bool {
70 matches!(self, Outcome::Passed)
71 }
72
73 pub fn is_fail(&self) -> bool {
74 matches!(self, Outcome::Failed(_))
75 }
76}
77
78impl ScenarioResult {
79 pub fn passed(&self) -> bool {
80 self.outcome.is_pass()
81 }
82
83 pub fn failure_summary(&self) -> Option<String> {
88 let Outcome::Failed(top) = &self.outcome else {
89 return None;
90 };
91 let one_line = |s: &str| -> String {
92 let first = s.lines().next().unwrap_or("").trim();
93 if first.chars().count() > 100 {
94 format!("{}…", first.chars().take(99).collect::<String>())
95 } else {
96 first.to_string()
97 }
98 };
99 for ev in &self.events {
102 if let Outcome::Failed(msg) = &ev.outcome {
103 return Some(format!("{}: {}", ev.description, one_line(msg)));
104 }
105 }
106 Some(one_line(top))
107 }
108}
109
110impl fmt::Display for ScenarioResult {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 let icon = match &self.outcome {
113 Outcome::Passed => "PASS",
114 Outcome::Failed(_) => "FAIL",
115 Outcome::Skipped => "SKIP",
116 };
117 writeln!(
118 f,
119 "{icon} {} ({:.1}s)",
120 self.name,
121 self.duration.as_secs_f64()
122 )?;
123
124 if self.events.is_empty()
126 && let Outcome::Failed(msg) = &self.outcome
127 {
128 writeln!(f, " {msg}")?;
129 }
130
131 for event in &self.events {
132 let mark = match &event.outcome {
133 Outcome::Passed => " ok ",
134 Outcome::Failed(_) => "FAIL",
135 Outcome::Skipped => "skip",
136 };
137 let kind_label = match event.kind {
138 EventKind::Step => "step",
139 EventKind::Assertion => "assert",
140 };
141 write!(
142 f,
143 " [{mark}] {kind_label}: {} ({:.1}s)",
144 event.description,
145 event.duration.as_secs_f64()
146 )?;
147 if let Outcome::Failed(msg) = &event.outcome {
148 write!(f, "\n {msg}")?;
149 }
150 writeln!(f)?;
151
152 if matches!(event.outcome, Outcome::Passed) {
156 render_output(f, "stdout", &event.stdout)?;
157 render_output(f, "stderr", &event.stderr)?;
158 }
159 }
160
161 Ok(())
162 }
163}
164
165fn render_output(f: &mut fmt::Formatter<'_>, label: &str, text: &str) -> fmt::Result {
166 let trimmed = text.trim_end_matches('\n');
167 if trimmed.is_empty() {
168 return Ok(());
169 }
170 writeln!(f, " [{label}]")?;
171 for line in trimmed.lines() {
172 writeln!(f, " | {line}")?;
173 }
174 Ok(())
175}