Skip to content

Commit 49f8e8d

Browse files
committed
feat(json)!: Add CaseMessage
The aim is to allow multiple failures
1 parent f3968c3 commit 49f8e8d

7 files changed

Lines changed: 207 additions & 9 deletions

File tree

DESIGN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Decisions
3737
- Terse and pretty progress indicators are too nebulous to render (see their notifiers)
3838
- There is likely not enough value add in the failure message
3939
- This puts more of a burden on custom test harnesses for their implementation than is strictly needed
40+
- Report failures separate from test-complete so we can have multiple
4041

4142
### Prior Art
4243

crates/libtest-json/event.schema.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,19 @@
6767
"event"
6868
]
6969
},
70+
{
71+
"type": "object",
72+
"properties": {
73+
"event": {
74+
"type": "string",
75+
"const": "case_message"
76+
}
77+
},
78+
"$ref": "#/$defs/CaseMessage",
79+
"required": [
80+
"event"
81+
]
82+
},
7083
{
7184
"type": "object",
7285
"properties": {
@@ -207,6 +220,37 @@
207220
"failed"
208221
]
209222
},
223+
"CaseMessage": {
224+
"type": "object",
225+
"properties": {
226+
"name": {
227+
"type": "string"
228+
},
229+
"status": {
230+
"$ref": "#/$defs/RunStatus"
231+
},
232+
"message": {
233+
"type": [
234+
"string",
235+
"null"
236+
]
237+
},
238+
"elapsed_s": {
239+
"anyOf": [
240+
{
241+
"$ref": "#/$defs/Elapsed"
242+
},
243+
{
244+
"type": "null"
245+
}
246+
]
247+
}
248+
},
249+
"required": [
250+
"name",
251+
"status"
252+
]
253+
},
210254
"CaseComplete": {
211255
"type": "object",
212256
"properties": {

crates/libtest-json/src/event.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub enum Event {
99
DiscoverComplete(DiscoverComplete),
1010
RunStart(RunStart),
1111
CaseStart(CaseStart),
12+
CaseMessage(CaseMessage),
1213
CaseComplete(CaseComplete),
1314
RunComplete(RunComplete),
1415
}
@@ -22,6 +23,7 @@ impl Event {
2223
Self::DiscoverComplete(event) => event.to_jsonline(),
2324
Self::RunStart(event) => event.to_jsonline(),
2425
Self::CaseStart(event) => event.to_jsonline(),
26+
Self::CaseMessage(event) => event.to_jsonline(),
2527
Self::CaseComplete(event) => event.to_jsonline(),
2628
Self::RunComplete(event) => event.to_jsonline(),
2729
}
@@ -58,6 +60,12 @@ impl From<CaseStart> for Event {
5860
}
5961
}
6062

63+
impl From<CaseMessage> for Event {
64+
fn from(inner: CaseMessage) -> Self {
65+
Self::CaseMessage(inner)
66+
}
67+
}
68+
6169
impl From<CaseComplete> for Event {
6270
fn from(inner: CaseComplete) -> Self {
6371
Self::CaseComplete(inner)
@@ -292,6 +300,67 @@ impl CaseStart {
292300
}
293301
}
294302

303+
#[derive(Clone, Debug)]
304+
#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
305+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
306+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
307+
pub struct CaseMessage {
308+
pub name: String,
309+
pub status: RunStatus,
310+
#[cfg_attr(
311+
feature = "serde",
312+
serde(default, skip_serializing_if = "Option::is_none")
313+
)]
314+
pub message: Option<String>,
315+
#[cfg_attr(
316+
feature = "serde",
317+
serde(default, skip_serializing_if = "Option::is_none")
318+
)]
319+
pub elapsed_s: Option<Elapsed>,
320+
}
321+
322+
impl CaseMessage {
323+
#[cfg(feature = "json")]
324+
pub fn to_jsonline(&self) -> String {
325+
use json_write::JsonWrite as _;
326+
327+
let mut buffer = String::new();
328+
buffer.open_object().unwrap();
329+
330+
buffer.key("event").unwrap();
331+
buffer.keyval_sep().unwrap();
332+
buffer.value("case_message").unwrap();
333+
334+
buffer.val_sep().unwrap();
335+
buffer.key("name").unwrap();
336+
buffer.keyval_sep().unwrap();
337+
buffer.value(&self.name).unwrap();
338+
339+
buffer.val_sep().unwrap();
340+
buffer.key("status").unwrap();
341+
buffer.keyval_sep().unwrap();
342+
buffer.value(self.status.as_str()).unwrap();
343+
344+
if let Some(message) = &self.message {
345+
buffer.val_sep().unwrap();
346+
buffer.key("message").unwrap();
347+
buffer.keyval_sep().unwrap();
348+
buffer.value(message).unwrap();
349+
}
350+
351+
if let Some(elapsed_s) = self.elapsed_s {
352+
buffer.val_sep().unwrap();
353+
buffer.key("elapsed_s").unwrap();
354+
buffer.keyval_sep().unwrap();
355+
buffer.value(String::from(elapsed_s)).unwrap();
356+
}
357+
358+
buffer.close_object().unwrap();
359+
360+
buffer
361+
}
362+
}
363+
295364
#[derive(Clone, Debug)]
296365
#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
297366
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]

crates/libtest-json/tests/roundtrip.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,31 @@ fn case_start() {
101101
);
102102
}
103103

104+
#[test]
105+
fn case_message() {
106+
t(
107+
libtest_json::event::CaseMessage {
108+
name: "Hello\tworld!".to_owned(),
109+
status: libtest_json::RunStatus::Failed,
110+
message: None,
111+
elapsed_s: None,
112+
},
113+
str![[r#"{"event":"case_message","name":"Hello\tworld!","status":"failed"}"#]],
114+
);
115+
116+
t(
117+
libtest_json::event::CaseMessage {
118+
name: "Hello\tworld!".to_owned(),
119+
status: libtest_json::RunStatus::Ignored,
120+
message: Some("This\tfailed".to_owned()),
121+
elapsed_s: Some(libtest_json::Elapsed(Default::default())),
122+
},
123+
str![[
124+
r#"{"event":"case_message","name":"Hello\tworld!","status":"ignored","message":"This\tfailed","elapsed_s":"0"}"#
125+
]],
126+
);
127+
}
128+
104129
#[test]
105130
fn case_complete() {
106131
t(

crates/libtest2-harness/src/notify/pretty.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ impl<W: std::io::Write> super::Notifier for PrettyRunNotifier<W> {
5151
self.writer.flush()?;
5252
}
5353
}
54+
Event::CaseMessage(_) => {}
5455
Event::CaseComplete(inner) => {
5556
let status = self.summary.get_status(&inner.name);
5657
let (s, style) = match status {

crates/libtest2-harness/src/notify/summary.rs

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::event::CaseComplete;
1+
use super::event::CaseMessage;
22
use super::Event;
33
use super::RunStatus;
44
use super::FAILED;
@@ -11,14 +11,14 @@ pub(crate) struct Summary {
1111
/// filter-in pattern or by `--skip` arguments).
1212
num_filtered_out: usize,
1313

14-
status: std::collections::HashMap<String, CaseComplete>,
14+
status: std::collections::HashMap<String, CaseStatus>,
1515
elapsed_s: Option<super::Elapsed>,
1616
}
1717

1818
impl Summary {
1919
pub(crate) fn get_status(&self, name: &str) -> Option<RunStatus> {
20-
let event = self.status.get(name)?;
21-
event.status
20+
let status = self.status.get(name)?;
21+
find_run_status(status)
2222
}
2323

2424
pub(crate) fn write_start(&self, writer: &mut dyn std::io::Write) -> std::io::Result<()> {
@@ -34,12 +34,27 @@ impl Summary {
3434
let mut num_failed = 0;
3535
let mut num_ignored = 0;
3636
let mut failures = std::collections::BTreeMap::new();
37-
for event in self.status.values() {
38-
match event.status {
37+
for (name, case_status) in &self.status {
38+
let mut status = find_run_status(case_status);
39+
if !case_status.started {
40+
// Even override `Ignored`
41+
status = Some(RunStatus::Failed);
42+
failures.insert(name, Some("test found that never started"));
43+
}
44+
if !case_status.completed {
45+
// Even override `Ignored`
46+
status = Some(RunStatus::Failed);
47+
failures.insert(name, Some("test never completed"));
48+
}
49+
match status {
3950
Some(RunStatus::Ignored) => num_ignored += 1,
4051
Some(RunStatus::Failed) => {
4152
num_failed += 1;
42-
failures.insert(&event.name, &event.message);
53+
for event in &case_status.messages {
54+
if Some(event.status) == status {
55+
failures.insert(name, event.message.as_deref());
56+
}
57+
}
4358
}
4459
None => num_passed += 1,
4560
}
@@ -106,9 +121,30 @@ impl super::Notifier for Summary {
106121
}
107122
Event::DiscoverComplete(_) => {}
108123
Event::RunStart(_) => {}
109-
Event::CaseStart(_) => {}
124+
Event::CaseStart(inner) => {
125+
self.status.entry(inner.name).or_default().started = true;
126+
}
127+
Event::CaseMessage(inner) => {
128+
self.status
129+
.entry(inner.name.clone())
130+
.or_default()
131+
.messages
132+
.push(inner);
133+
}
110134
Event::CaseComplete(inner) => {
111-
self.status.insert(inner.name.clone(), inner);
135+
if let Some(status) = inner.status {
136+
self.status
137+
.entry(inner.name.clone())
138+
.or_default()
139+
.messages
140+
.push(CaseMessage {
141+
name: inner.name.clone(),
142+
status,
143+
message: inner.message.clone(),
144+
elapsed_s: inner.elapsed_s,
145+
});
146+
}
147+
self.status.entry(inner.name).or_default().completed = true;
112148
}
113149
Event::RunComplete(inner) => {
114150
self.elapsed_s = inner.elapsed_s;
@@ -117,3 +153,23 @@ impl super::Notifier for Summary {
117153
Ok(())
118154
}
119155
}
156+
157+
fn find_run_status(case_status: &CaseStatus) -> Option<RunStatus> {
158+
let mut status = None;
159+
for event in &case_status.messages {
160+
status = match (status, event.status) {
161+
(None, _) => Some(event.status),
162+
(Some(RunStatus::Ignored), _) => status,
163+
(_, RunStatus::Ignored) => Some(event.status),
164+
(Some(RunStatus::Failed), _) => status,
165+
}
166+
}
167+
status
168+
}
169+
170+
#[derive(Default, Clone, Debug)]
171+
struct CaseStatus {
172+
messages: Vec<CaseMessage>,
173+
started: bool,
174+
completed: bool,
175+
}

crates/libtest2-harness/src/notify/terse.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ impl<W: std::io::Write> super::Notifier for TerseListNotifier<W> {
3535
}
3636
Event::RunStart(_) => {}
3737
Event::CaseStart(_) => {}
38+
Event::CaseMessage(_) => {}
3839
Event::CaseComplete(_) => {}
3940
Event::RunComplete(_) => {}
4041
}
@@ -68,6 +69,7 @@ impl<W: std::io::Write> super::Notifier for TerseRunNotifier<W> {
6869
self.summary.write_start(&mut self.writer)?;
6970
}
7071
Event::CaseStart(_) => {}
72+
Event::CaseMessage(_) => {}
7173
Event::CaseComplete(inner) => {
7274
let status = self.summary.get_status(&inner.name);
7375
let (c, style) = match status {

0 commit comments

Comments
 (0)