1use std::collections::VecDeque;
2use std::env;
3use std::fmt;
4use std::num::NonZeroUsize;
5use std::sync::{Arc, Mutex};
6
7use chrome_for_testing_manager::{DriverOutputLine, DriverOutputListener, DriverOutputSource};
8use rootcause::Report;
9use rootcause::handlers::{
10 AttachmentFormattingPlacement, AttachmentFormattingStyle, AttachmentHandler, FormattingFunction,
11};
12use rootcause::report_attachment::ReportAttachment;
13
14use crate::BrowserTestError;
15use crate::env::env_flag_enabled;
16
17pub(crate) const DEFAULT_BROWSER_DRIVER_OUTPUT_ENV: &str = "BROWSER_TEST_DRIVER_OUTPUT";
19
20pub(crate) const DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES_ENV: &str =
22 "BROWSER_TEST_DRIVER_OUTPUT_TAIL_LINES";
23
24pub(crate) const DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES: usize = 200;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum DriverOutputConfig {
30 Disabled,
32
33 TailLines(usize),
37
38 FromEnv,
41}
42
43#[deprecated(since = "0.1.0", note = "use DriverOutputConfig instead")]
47pub type BrowserDriverOutputConfig = DriverOutputConfig;
48
49impl DriverOutputConfig {
50 #[must_use]
52 pub const fn disabled() -> Self {
53 Self::Disabled
54 }
55
56 #[must_use]
60 pub const fn tail_lines(tail_lines: usize) -> Self {
61 Self::TailLines(tail_lines)
62 }
63
64 #[must_use]
68 pub const fn new(tail_lines: usize) -> Self {
69 Self::tail_lines(tail_lines)
70 }
71
72 #[must_use]
74 pub const fn from_env() -> Self {
75 Self::FromEnv
76 }
77}
78
79#[derive(Debug, Clone)]
81pub(crate) struct DriverOutputCapture {
82 inner: Arc<Mutex<BrowserDriverOutputState>>,
83}
84
85#[derive(Debug)]
86struct BrowserDriverOutputState {
87 tail_capacity: NonZeroUsize,
88 total_lines: usize,
89 tail_lines: VecDeque<DriverOutputLine>,
90}
91
92impl DriverOutputCapture {
93 #[must_use]
97 pub(crate) fn new(tail_lines: NonZeroUsize) -> Self {
98 Self {
99 inner: Arc::new(Mutex::new(BrowserDriverOutputState {
100 tail_capacity: tail_lines,
101 total_lines: 0,
102 tail_lines: VecDeque::with_capacity(tail_lines.get()),
103 })),
104 }
105 }
106
107 #[must_use]
113 pub(crate) fn snapshot(&self) -> DriverOutputSnapshot {
114 let state = self
115 .inner
116 .lock()
117 .expect("browser driver output capture mutex should not be poisoned");
118 DriverOutputSnapshot {
119 total_lines: state.total_lines,
120 tail_capacity: state.tail_capacity.get(),
121 tail_lines: state.tail_lines.iter().cloned().collect(),
122 }
123 }
124
125 pub(crate) fn listener(&self) -> DriverOutputListener {
126 let capture = self.clone();
127 DriverOutputListener::new(move |line| {
128 capture.push(line);
129 })
130 }
131
132 pub(crate) fn push(&self, line: DriverOutputLine) {
133 let mut state = self
134 .inner
135 .lock()
136 .expect("browser driver output capture mutex should not be poisoned");
137 state.total_lines += 1;
138
139 while state.tail_lines.len() >= state.tail_capacity.get() {
140 state.tail_lines.pop_front();
141 }
142 state.tail_lines.push_back(line);
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
148pub(crate) struct DriverOutputSnapshot {
149 pub(crate) total_lines: usize,
151
152 pub(crate) tail_capacity: usize,
154
155 pub(crate) tail_lines: Vec<DriverOutputLine>,
157}
158
159impl DriverOutputSnapshot {
160 #[must_use]
162 pub(crate) fn is_empty(&self) -> bool {
163 self.tail_lines.is_empty()
164 }
165}
166
167#[derive(Debug, Clone)]
168struct DriverOutputAttachment {
169 snapshot: DriverOutputSnapshot,
170}
171
172struct DriverOutputAttachmentHandler;
173
174impl AttachmentHandler<DriverOutputAttachment> for DriverOutputAttachmentHandler {
175 fn display(value: &DriverOutputAttachment, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176 writeln!(formatter, "Recent browser driver output")?;
177 writeln!(
178 formatter,
179 "note: output comes from one shared browser-driver process; parallel tests may interleave lines."
180 )?;
181 if value.snapshot.total_lines > value.snapshot.tail_lines.len() {
182 writeln!(
183 formatter,
184 "showing the last {} of {} captured line(s).",
185 value.snapshot.tail_lines.len(),
186 value.snapshot.total_lines,
187 )?;
188 }
189
190 for line in &value.snapshot.tail_lines {
191 writeln!(
192 formatter,
193 "[{} {}] {}",
194 line.sequence,
195 source_label(line.source),
196 line.line,
197 )?;
198 }
199
200 Ok(())
201 }
202
203 fn debug(value: &DriverOutputAttachment, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
204 Self::display(value, formatter)
205 }
206
207 fn preferred_formatting_style(
208 _value: &DriverOutputAttachment,
209 _report_formatting: FormattingFunction,
210 ) -> AttachmentFormattingStyle {
211 AttachmentFormattingStyle {
212 placement: AttachmentFormattingPlacement::Appendix {
213 appendix_name: "Recent browser driver output",
214 },
215 function: FormattingFunction::Display,
216 priority: 5,
217 }
218 }
219}
220
221pub(crate) fn attach_browser_driver_output(
222 report: &mut Report<BrowserTestError>,
223 capture: Option<&DriverOutputCapture>,
224) {
225 let Some(snapshot) = capture.map(DriverOutputCapture::snapshot) else {
226 return;
227 };
228 if snapshot.is_empty() {
229 return;
230 }
231
232 report.attachments_mut().push(
233 ReportAttachment::new_custom::<DriverOutputAttachmentHandler>(DriverOutputAttachment {
234 snapshot,
235 })
236 .into_dynamic(),
237 );
238}
239
240pub(crate) fn attach_browser_driver_output_to_result(
241 result: Result<(), Report<BrowserTestError>>,
242 capture: Option<&DriverOutputCapture>,
243) -> Result<(), Report<BrowserTestError>> {
244 result.map_err(|mut err| {
245 attach_browser_driver_output(&mut err, capture);
246 err
247 })
248}
249
250pub(crate) fn browser_driver_output_config_from_env() -> DriverOutputConfig {
251 if !env_flag_enabled(DEFAULT_BROWSER_DRIVER_OUTPUT_ENV) {
252 return DriverOutputConfig::Disabled;
253 }
254
255 let tail_lines = env::var_os(DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES_ENV).map_or(
256 DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES,
257 |value| {
258 let value = value.to_string_lossy();
259 match value.trim().parse::<usize>() {
260 Ok(tail_lines) => tail_lines,
261 Err(err) => {
262 tracing::warn!(
263 env_var = DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES_ENV,
264 value = %value,
265 error = %err,
266 fallback = DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES,
267 "invalid browser driver output tail-line setting"
268 );
269 DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES
270 }
271 }
272 },
273 );
274
275 if tail_lines == 0 {
276 DriverOutputConfig::Disabled
277 } else {
278 DriverOutputConfig::TailLines(tail_lines)
279 }
280}
281
282fn source_label(source: DriverOutputSource) -> &'static str {
283 match source {
284 DriverOutputSource::Stdout => "stdout",
285 DriverOutputSource::Stderr => "stderr",
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use assertr::prelude::*;
293
294 #[allow(deprecated)]
295 #[test]
296 fn deprecated_browser_driver_output_config_alias_matches_driver_output_config() {
297 let config: BrowserDriverOutputConfig = DriverOutputConfig::tail_lines(3);
298
299 assert_that!(config).is_equal_to(DriverOutputConfig::TailLines(3));
300 }
301
302 #[test]
303 fn capture_retains_bounded_tail_and_total_count() {
304 let capture = DriverOutputCapture::new(
305 NonZeroUsize::new(2).expect("literal tail capacity should be non-zero"),
306 );
307
308 capture.push(line(DriverOutputSource::Stdout, 0, "one"));
309 capture.push(line(DriverOutputSource::Stderr, 1, "two"));
310 capture.push(line(DriverOutputSource::Stdout, 2, "three"));
311
312 let snapshot = capture.snapshot();
313
314 assert_that!(snapshot.total_lines).is_equal_to(3);
315 assert_that!(snapshot.tail_capacity).is_equal_to(2);
316 assert_that!(snapshot.tail_lines.len()).is_equal_to(2);
317 assert_that!(&snapshot.tail_lines[0].line).is_equal_to("two");
318 assert_that!(&snapshot.tail_lines[1].line).is_equal_to("three");
319 assert_that!(snapshot.tail_lines[0].source).is_equal_to(DriverOutputSource::Stderr);
320 }
321
322 #[test]
323 fn browser_driver_output_is_rootcause_attachment_not_child_error() {
324 let capture = DriverOutputCapture::new(
325 NonZeroUsize::new(5).expect("literal tail capacity should be non-zero"),
326 );
327 capture.push(line(DriverOutputSource::Stdout, 0, "Starting ChromeDriver"));
328 let mut report = Report::new(BrowserTestError::RunTest {
329 test_name: "login".to_owned(),
330 });
331 let initial_attachment_count = report.attachments().len();
332
333 attach_browser_driver_output(&mut report, Some(&capture));
334
335 assert_that!(report.attachments().len()).is_equal_to(initial_attachment_count + 1);
336 assert_that!(report.children().len()).is_equal_to(0);
337 assert_that!(report.to_string()).contains("Recent browser driver output");
338 assert_that!(report.to_string()).contains("parallel tests may interleave lines");
339 }
340
341 fn line(source: DriverOutputSource, sequence: u64, line: &str) -> DriverOutputLine {
342 DriverOutputLine {
343 source,
344 sequence,
345 line: line.to_owned(),
346 }
347 }
348}