Skip to content

Commit b4571be

Browse files
committed
Auto merge of rust-lang#109729 - fortanix:raoul/bugfix_libtest_json_synchronization, r=pietroalbini
Ensure test library issues json string line-by-line rust-lang#108659 introduces a custom test display implementation. It does so by using libtest to output json. The stdout is read line by line and parsed. The code trims the line read and checks whether it starts with a `{` and ends with a `}`. Unfortunately, there is a race condition in how json data is written to stdout. The `write_message` function calls `self.out.write_all` repeatedly to write a buffer that contains (partial) json data, or a new line. There is no lock around the `self.out.write_all` functions. Similarly, the `write_message` function itself is called with only partial json data. As these functions are called from concurrent threads, this may result in json data ending up on the same stdout line. This PR avoids this by buffering the complete json data before issuing a single `self.out.write_all`. (rust-lang#109484 implemented a partial fix for this issue; it only avoids that failed json parsing would result in a panic.) cc: `@jethrogb,` `@pietroalbini`
2 parents a368898 + a18b750 commit b4571be

File tree

1 file changed

+62
-70
lines changed

1 file changed

+62
-70
lines changed

library/test/src/formatters/json.rs

+62-70
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,10 @@ impl<T: Write> JsonFormatter<T> {
1818
}
1919

2020
fn writeln_message(&mut self, s: &str) -> io::Result<()> {
21-
assert!(!s.contains('\n'));
22-
23-
self.out.write_all(s.as_ref())?;
24-
self.out.write_all(b"\n")
25-
}
26-
27-
fn write_message(&mut self, s: &str) -> io::Result<()> {
28-
assert!(!s.contains('\n'));
21+
// self.out will take a lock, but that lock is released when write_all returns. This
22+
// results in a race condition and json output may not end with a new line. We avoid this
23+
// by issuing `write_all` calls line-by-line.
24+
assert_eq!(s.chars().last(), Some('\n'));
2925

3026
self.out.write_all(s.as_ref())
3127
}
@@ -34,34 +30,35 @@ impl<T: Write> JsonFormatter<T> {
3430
&mut self,
3531
ty: &str,
3632
name: &str,
37-
evt: &str,
33+
event: &str,
3834
exec_time: Option<&time::TestExecTime>,
3935
stdout: Option<Cow<'_, str>>,
4036
extra: Option<&str>,
4137
) -> io::Result<()> {
4238
// A doc test's name includes a filename which must be escaped for correct json.
43-
self.write_message(&format!(
44-
r#"{{ "type": "{}", "name": "{}", "event": "{}""#,
45-
ty,
46-
EscapedString(name),
47-
evt
48-
))?;
49-
if let Some(exec_time) = exec_time {
50-
self.write_message(&format!(r#", "exec_time": {}"#, exec_time.0.as_secs_f64()))?;
51-
}
52-
if let Some(stdout) = stdout {
53-
self.write_message(&format!(r#", "stdout": "{}""#, EscapedString(stdout)))?;
54-
}
55-
if let Some(extra) = extra {
56-
self.write_message(&format!(r#", {extra}"#))?;
57-
}
58-
self.writeln_message(" }")
39+
let name = EscapedString(name);
40+
let exec_time_json = if let Some(exec_time) = exec_time {
41+
format!(r#", "exec_time": {}"#, exec_time.0.as_secs_f64())
42+
} else {
43+
String::from("")
44+
};
45+
let stdout_json = if let Some(stdout) = stdout {
46+
format!(r#", "stdout": "{}""#, EscapedString(stdout))
47+
} else {
48+
String::from("")
49+
};
50+
let extra_json =
51+
if let Some(extra) = extra { format!(r#", {extra}"#) } else { String::from("") };
52+
let newline = "\n";
53+
54+
self.writeln_message(&format!(
55+
r#"{{ "type": "{ty}", "name": "{name}", "event": "{event}"{exec_time_json}{stdout_json}{extra_json} }}{newline}"#))
5956
}
6057
}
6158

6259
impl<T: Write> OutputFormatter for JsonFormatter<T> {
6360
fn write_discovery_start(&mut self) -> io::Result<()> {
64-
self.writeln_message(&format!(r#"{{ "type": "suite", "event": "discovery" }}"#))
61+
self.writeln_message(concat!(r#"{ "type": "suite", "event": "discovery" }"#, "\n"))
6562
}
6663

6764
fn write_test_discovered(&mut self, desc: &TestDesc, test_type: &str) -> io::Result<()> {
@@ -77,21 +74,24 @@ impl<T: Write> OutputFormatter for JsonFormatter<T> {
7774
..
7875
} = desc;
7976

77+
let name = EscapedString(name.as_slice());
78+
let ignore_message = ignore_message.unwrap_or("");
79+
let source_path = EscapedString(source_file);
80+
let newline = "\n";
81+
8082
self.writeln_message(&format!(
81-
r#"{{ "type": "{test_type}", "event": "discovered", "name": "{}", "ignore": {ignore}, "ignore_message": "{}", "source_path": "{}", "start_line": {start_line}, "start_col": {start_col}, "end_line": {end_line}, "end_col": {end_col} }}"#,
82-
EscapedString(name.as_slice()),
83-
ignore_message.unwrap_or(""),
84-
EscapedString(source_file),
83+
r#"{{ "type": "{test_type}", "event": "discovered", "name": "{name}", "ignore": {ignore}, "ignore_message": "{ignore_message}", "source_path": "{source_path}", "start_line": {start_line}, "start_col": {start_col}, "end_line": {end_line}, "end_col": {end_col} }}{newline}"#
8584
))
8685
}
8786

8887
fn write_discovery_finish(&mut self, state: &ConsoleTestDiscoveryState) -> io::Result<()> {
8988
let ConsoleTestDiscoveryState { tests, benchmarks, ignored, .. } = state;
9089

9190
let total = tests + benchmarks;
91+
let newline = "\n";
9292
self.writeln_message(&format!(
93-
r#"{{ "type": "suite", "event": "completed", "tests": {tests}, "benchmarks": {benchmarks}, "total": {total}, "ignored": {ignored} }}"#
94-
))
93+
r#"{{ "type": "suite", "event": "completed", "tests": {tests}, "benchmarks": {benchmarks}, "total": {total}, "ignored": {ignored} }}{newline}"#
94+
))
9595
}
9696

9797
fn write_run_start(&mut self, test_count: usize, shuffle_seed: Option<u64>) -> io::Result<()> {
@@ -100,15 +100,17 @@ impl<T: Write> OutputFormatter for JsonFormatter<T> {
100100
} else {
101101
String::new()
102102
};
103+
let newline = "\n";
103104
self.writeln_message(&format!(
104-
r#"{{ "type": "suite", "event": "started", "test_count": {test_count}{shuffle_seed_json} }}"#
105-
))
105+
r#"{{ "type": "suite", "event": "started", "test_count": {test_count}{shuffle_seed_json} }}{newline}"#
106+
))
106107
}
107108

108109
fn write_test_start(&mut self, desc: &TestDesc) -> io::Result<()> {
110+
let name = EscapedString(desc.name.as_slice());
111+
let newline = "\n";
109112
self.writeln_message(&format!(
110-
r#"{{ "type": "test", "event": "started", "name": "{}" }}"#,
111-
EscapedString(desc.name.as_slice())
113+
r#"{{ "type": "test", "event": "started", "name": "{name}" }}{newline}"#
112114
))
113115
}
114116

@@ -173,53 +175,43 @@ impl<T: Write> OutputFormatter for JsonFormatter<T> {
173175
} else {
174176
format!(r#", "mib_per_second": {}"#, bs.mb_s)
175177
};
178+
let name = EscapedString(desc.name.as_slice());
176179

177-
let line = format!(
180+
self.writeln_message(&format!(
178181
"{{ \"type\": \"bench\", \
179-
\"name\": \"{}\", \
180-
\"median\": {}, \
181-
\"deviation\": {}{} }}",
182-
EscapedString(desc.name.as_slice()),
183-
median,
184-
deviation,
185-
mbps
186-
);
187-
188-
self.writeln_message(&line)
182+
\"name\": \"{name}\", \
183+
\"median\": {median}, \
184+
\"deviation\": {deviation}{mbps} }}\n",
185+
))
189186
}
190187
}
191188
}
192189

193190
fn write_timeout(&mut self, desc: &TestDesc) -> io::Result<()> {
191+
let name = EscapedString(desc.name.as_slice());
192+
let newline = "\n";
194193
self.writeln_message(&format!(
195-
r#"{{ "type": "test", "event": "timeout", "name": "{}" }}"#,
196-
EscapedString(desc.name.as_slice())
194+
r#"{{ "type": "test", "event": "timeout", "name": "{name}" }}{newline}"#,
197195
))
198196
}
199197

200198
fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result<bool> {
201-
self.write_message(&format!(
202-
"{{ \"type\": \"suite\", \
203-
\"event\": \"{}\", \
204-
\"passed\": {}, \
205-
\"failed\": {}, \
206-
\"ignored\": {}, \
207-
\"measured\": {}, \
208-
\"filtered_out\": {}",
209-
if state.failed == 0 { "ok" } else { "failed" },
210-
state.passed,
211-
state.failed,
212-
state.ignored,
213-
state.measured,
214-
state.filtered_out,
215-
))?;
216-
217-
if let Some(ref exec_time) = state.exec_time {
218-
let time_str = format!(", \"exec_time\": {}", exec_time.0.as_secs_f64());
219-
self.write_message(&time_str)?;
220-
}
199+
let event = if state.failed == 0 { "ok" } else { "failed" };
200+
let passed = state.passed;
201+
let failed = state.failed;
202+
let ignored = state.ignored;
203+
let measured = state.measured;
204+
let filtered_out = state.filtered_out;
205+
let exec_time_json = if let Some(ref exec_time) = state.exec_time {
206+
format!(r#", "exec_time": {}"#, exec_time.0.as_secs_f64())
207+
} else {
208+
String::from("")
209+
};
210+
let newline = "\n";
221211

222-
self.writeln_message(" }")?;
212+
self.writeln_message(&format!(
213+
r#"{{ "type": "suite", "event": "{event}", "passed": {passed}, "failed": {failed}, "ignored": {ignored}, "measured": {measured}, "filtered_out": {filtered_out}{exec_time_json} }}{newline}"#
214+
))?;
223215

224216
Ok(state.failed == 0)
225217
}

0 commit comments

Comments
 (0)