cucumber/writer/
basic.rs

1// Copyright (c) 2018-2024  Brendan Molloy <brendan@bbqsrc.net>,
2//                          Ilya Solovyiov <ilya.solovyiov@gmail.com>,
3//                          Kai Ren <tyranron@gmail.com>
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8// option. This file may not be copied, modified, or distributed
9// except according to those terms.
10
11//! Default [`Writer`] implementation.
12
13use std::{
14    borrow::Cow,
15    cmp, env,
16    fmt::{Debug, Display},
17    io,
18    str::FromStr,
19};
20
21use derive_more::{Deref, DerefMut};
22use itertools::Itertools as _;
23use once_cell::sync::Lazy;
24use regex::CaptureLocations;
25use smart_default::SmartDefault;
26
27use crate::{
28    cli::Colored,
29    event::{self, Info, Retries},
30    parser, step,
31    writer::{
32        self,
33        out::{Styles, WriteStrExt as _},
34        Ext as _, Verbosity,
35    },
36    Event, World, Writer,
37};
38
39/// CLI options of a [`Basic`] [`Writer`].
40#[derive(clap::Args, Clone, Copy, Debug, SmartDefault)]
41#[group(skip)]
42pub struct Cli {
43    /// Verbosity of an output.
44    ///
45    /// `-v` is default verbosity, `-vv` additionally outputs world on failed
46    /// steps, `-vvv` additionally outputs step's doc string (if present).
47    #[arg(short, action = clap::ArgAction::Count, global = true)]
48    pub verbose: u8,
49
50    /// Coloring policy for a console output.
51    #[arg(
52        long,
53        value_name = "auto|always|never",
54        default_value = "auto",
55        global = true
56    )]
57    #[default(Coloring::Auto)]
58    pub color: Coloring,
59}
60
61impl Colored for Cli {
62    fn coloring(&self) -> Coloring {
63        self.color
64    }
65}
66
67/// Possible policies of a [`console`] output coloring.
68#[derive(Clone, Copy, Debug)]
69pub enum Coloring {
70    /// Letting [`console::colors_enabled()`] to decide, whether output should
71    /// be colored.
72    Auto,
73
74    /// Forcing of a colored output.
75    Always,
76
77    /// Forcing of a non-colored output.
78    Never,
79}
80
81impl FromStr for Coloring {
82    type Err = &'static str;
83
84    fn from_str(s: &str) -> Result<Self, Self::Err> {
85        match s.to_ascii_lowercase().as_str() {
86            "auto" => Ok(Self::Auto),
87            "always" => Ok(Self::Always),
88            "never" => Ok(Self::Never),
89            _ => Err("possible options: auto, always, never"),
90        }
91    }
92}
93
94/// Default [`Writer`] implementation outputting to an [`io::Write`] implementor
95/// ([`io::Stdout`] by default).
96///
97/// Pretty-prints with colors if terminal was successfully detected, otherwise
98/// has simple output. Useful for running tests with CI tools.
99///
100/// # Ordering
101///
102/// This [`Writer`] isn't [`Normalized`] by itself, so should be wrapped into
103/// a [`writer::Normalize`], otherwise will produce output [`Event`]s in a
104/// broken order.
105///
106/// [`Normalized`]: writer::Normalized
107/// [`Runner`]: crate::runner::Runner
108/// [`Scenario`]: gherkin::Scenario
109#[derive(Clone, Debug, Deref, DerefMut)]
110pub struct Basic<Out: io::Write = io::Stdout> {
111    /// [`io::Write`] implementor to write the output into.
112    #[deref]
113    #[deref_mut]
114    output: Out,
115
116    /// [`Styles`] for terminal output.
117    styles: Styles,
118
119    /// Current indentation that events are outputted with.
120    indent: usize,
121
122    /// Number of lines to clear.
123    lines_to_clear: usize,
124
125    /// Buffer to be re-output after [`clear_last_lines_if_term_present()`][0].
126    ///
127    /// [0]: Self::clear_last_lines_if_term_present
128    re_output_after_clear: String,
129
130    /// [`Verbosity`] of this [`Writer`].
131    verbosity: Verbosity,
132}
133
134impl<W, Out> Writer<W> for Basic<Out>
135where
136    W: World + Debug,
137    Out: io::Write,
138{
139    type Cli = Cli;
140
141    async fn handle_event(
142        &mut self,
143        ev: parser::Result<Event<event::Cucumber<W>>>,
144        opts: &Self::Cli,
145    ) {
146        use event::{Cucumber, Feature};
147
148        self.apply_cli(*opts);
149
150        match ev.map(Event::into_inner) {
151            Err(err) => self.parsing_failed(&err),
152            Ok(
153                Cucumber::Started
154                | Cucumber::ParsingFinished { .. }
155                | Cucumber::Finished,
156            ) => Ok(()),
157            Ok(Cucumber::Feature(f, ev)) => match ev {
158                Feature::Started => self.feature_started(&f),
159                Feature::Scenario(sc, ev) => self.scenario(&f, &sc, &ev),
160                Feature::Rule(r, ev) => self.rule(&f, &r, ev),
161                Feature::Finished => Ok(()),
162            },
163        }
164        .unwrap_or_else(|e| panic!("failed to write into terminal: {e}"));
165    }
166}
167
168impl<W, Val, Out> writer::Arbitrary<W, Val> for Basic<Out>
169where
170    W: World + Debug,
171    Val: AsRef<str>,
172    Out: io::Write,
173{
174    async fn write(&mut self, val: Val) {
175        self.write_line(val.as_ref())
176            .unwrap_or_else(|e| panic!("failed to write: {e}"));
177    }
178}
179
180impl<O: io::Write> writer::NonTransforming for Basic<O> {}
181
182impl Basic {
183    /// Creates a new [`Normalized`] [`Basic`] [`Writer`] outputting to
184    /// [`io::Stdout`].
185    ///
186    /// [`Normalized`]: writer::Normalized
187    #[must_use]
188    pub fn stdout<W>() -> writer::Normalize<W, Self> {
189        Self::new(io::stdout(), Coloring::Auto, Verbosity::Default)
190    }
191}
192
193impl<Out: io::Write> Basic<Out> {
194    /// Creates a new [`Normalized`] [`Basic`] [`Writer`] outputting to the
195    /// given `output`.
196    ///
197    /// [`Normalized`]: writer::Normalized
198    #[must_use]
199    pub fn new<W>(
200        output: Out,
201        color: Coloring,
202        verbosity: impl Into<Verbosity>,
203    ) -> writer::Normalize<W, Self> {
204        Self::raw(output, color, verbosity).normalized()
205    }
206
207    /// Creates a new non-[`Normalized`] [`Basic`] [`Writer`] outputting to the
208    /// given `output`.
209    ///
210    /// Use it only if you know what you're doing. Otherwise, consider using
211    /// [`Basic::new()`] which creates an already [`Normalized`] version of a
212    /// [`Basic`] [`Writer`].
213    ///
214    /// [`Normalized`]: writer::Normalized
215    #[must_use]
216    pub fn raw(
217        output: Out,
218        color: Coloring,
219        verbosity: impl Into<Verbosity>,
220    ) -> Self {
221        let mut basic = Self {
222            output,
223            styles: Styles::new(),
224            indent: 0,
225            lines_to_clear: 0,
226            re_output_after_clear: String::new(),
227            verbosity: verbosity.into(),
228        };
229        basic.apply_cli(Cli {
230            verbose: u8::from(basic.verbosity) + 1,
231            color,
232        });
233        basic
234    }
235
236    /// Applies the given [`Cli`] options to this [`Basic`] [`Writer`].
237    pub fn apply_cli(&mut self, cli: Cli) {
238        match cli.verbose {
239            0 => {}
240            1 => self.verbosity = Verbosity::Default,
241            2 => self.verbosity = Verbosity::ShowWorld,
242            _ => self.verbosity = Verbosity::ShowWorldAndDocString,
243        };
244        self.styles.apply_coloring(cli.color);
245    }
246
247    /// Clears last `n` lines if [`Coloring`] is enabled.
248    fn clear_last_lines_if_term_present(&mut self) -> io::Result<()> {
249        if self.styles.is_present && self.lines_to_clear > 0 {
250            self.output.clear_last_lines(self.lines_to_clear)?;
251            self.output.write_str(&self.re_output_after_clear)?;
252            self.re_output_after_clear.clear();
253            self.lines_to_clear = 0;
254        }
255        Ok(())
256    }
257
258    /// Outputs the parsing `error` encountered while parsing some [`Feature`].
259    ///
260    /// [`Feature`]: gherkin::Feature
261    pub(crate) fn parsing_failed(
262        &mut self,
263        error: impl Display,
264    ) -> io::Result<()> {
265        self.output
266            .write_line(&self.styles.err(format!("Failed to parse: {error}")))
267    }
268
269    /// Outputs the [started] [`Feature`].
270    ///
271    /// [started]: event::Feature::Started
272    /// [`Feature`]: gherkin::Feature
273    pub(crate) fn feature_started(
274        &mut self,
275        feature: &gherkin::Feature,
276    ) -> io::Result<()> {
277        let out = format!("{}: {}", feature.keyword, feature.name);
278        self.output.write_line(&self.styles.ok(out))
279    }
280
281    /// Outputs the [`Rule`]'s [started]/[scenario]/[finished] event.
282    ///
283    /// [finished]: event::Rule::Finished
284    /// [scenario]: event::Rule::Scenario
285    /// [started]: event::Rule::Started
286    /// [`Rule`]: gherkin::Rule
287    pub(crate) fn rule<W: Debug>(
288        &mut self,
289        feat: &gherkin::Feature,
290        rule: &gherkin::Rule,
291        ev: event::Rule<W>,
292    ) -> io::Result<()> {
293        use event::Rule;
294
295        match ev {
296            Rule::Started => {
297                self.rule_started(rule)?;
298            }
299            Rule::Scenario(sc, ev) => {
300                self.scenario(feat, &sc, &ev)?;
301            }
302            Rule::Finished => {
303                self.indent = self.indent.saturating_sub(2);
304            }
305        }
306        Ok(())
307    }
308
309    /// Outputs the [started] [`Rule`].
310    ///
311    /// [started]: event::Rule::Started
312    /// [`Rule`]: gherkin::Rule
313    pub(crate) fn rule_started(
314        &mut self,
315        rule: &gherkin::Rule,
316    ) -> io::Result<()> {
317        let out = format!(
318            "{indent}{}: {}",
319            rule.keyword,
320            rule.name,
321            indent = " ".repeat(self.indent)
322        );
323        self.indent += 2;
324        self.output.write_line(&self.styles.ok(out))
325    }
326
327    /// Outputs the [`Scenario`]'s [started]/[background]/[step] event.
328    ///
329    /// [background]: event::Scenario::Background
330    /// [started]: event::Scenario::Started
331    /// [step]: event::Step
332    /// [`Scenario`]: gherkin::Scenario
333    pub(crate) fn scenario<W: Debug>(
334        &mut self,
335        feat: &gherkin::Feature,
336        scenario: &gherkin::Scenario,
337        ev: &event::RetryableScenario<W>,
338    ) -> io::Result<()> {
339        use event::{Hook, Scenario};
340
341        let retries = ev.retries;
342        match &ev.event {
343            Scenario::Started => {
344                self.scenario_started(scenario, retries)?;
345            }
346            Scenario::Hook(_, Hook::Started) => {
347                self.indent += 4;
348            }
349            Scenario::Hook(which, Hook::Failed(world, info)) => {
350                self.hook_failed(
351                    feat,
352                    scenario,
353                    *which,
354                    retries,
355                    world.as_ref(),
356                    info,
357                )?;
358                self.indent = self.indent.saturating_sub(4);
359            }
360            Scenario::Hook(_, Hook::Passed) => {
361                self.indent = self.indent.saturating_sub(4);
362            }
363            Scenario::Background(bg, ev) => {
364                self.background(feat, scenario, bg, ev, retries)?;
365            }
366            Scenario::Step(st, ev) => {
367                self.step(feat, scenario, st, ev, retries)?;
368            }
369            Scenario::Finished => {
370                self.indent = self.indent.saturating_sub(2);
371            }
372            Scenario::Log(msg) => self.emit_log(msg)?,
373        }
374        Ok(())
375    }
376
377    /// Outputs the [`event::Scenario::Log`].
378    pub(crate) fn emit_log(&mut self, msg: impl AsRef<str>) -> io::Result<()> {
379        self.lines_to_clear += self.styles.lines_count(msg.as_ref());
380        self.re_output_after_clear.push_str(msg.as_ref());
381        self.output.write_str(msg)
382    }
383
384    /// Outputs the [failed] [`Scenario`]'s hook.
385    ///
386    /// [failed]: event::Hook::Failed
387    /// [`Scenario`]: gherkin::Scenario
388    pub(crate) fn hook_failed<W: Debug>(
389        &mut self,
390        feat: &gherkin::Feature,
391        sc: &gherkin::Scenario,
392        which: event::HookType,
393        retries: Option<Retries>,
394        world: Option<&W>,
395        info: &Info,
396    ) -> io::Result<()> {
397        self.clear_last_lines_if_term_present()?;
398
399        let style = |s| {
400            if retries.filter(|r| r.left > 0).is_some() {
401                self.styles.bright().retry(s)
402            } else {
403                self.styles.err(s)
404            }
405        };
406
407        self.output.write_line(&style(format!(
408            "{indent}✘  Scenario's {which} hook failed {}:{}:{}\n\
409             {indent}   Captured output: {}{}",
410            feat.path
411                .as_ref()
412                .and_then(|p| p.to_str().map(trim_path))
413                .unwrap_or(&feat.name),
414            sc.position.line,
415            sc.position.col,
416            format_str_with_indent(
417                coerce_error(info),
418                self.indent.saturating_sub(3) + 3
419            ),
420            world
421                .map(|w| format_str_with_indent(
422                    format!("{w:#?}"),
423                    self.indent.saturating_sub(3) + 3,
424                ))
425                .filter(|_| self.verbosity.shows_world())
426                .unwrap_or_default(),
427            indent = " ".repeat(self.indent.saturating_sub(3)),
428        )))
429    }
430
431    /// Outputs the [started] [`Scenario`].
432    ///
433    /// [started]: event::Scenario::Started
434    /// [`Scenario`]: gherkin::Scenario
435    pub(crate) fn scenario_started(
436        &mut self,
437        scenario: &gherkin::Scenario,
438        retries: Option<Retries>,
439    ) -> io::Result<()> {
440        self.indent += 2;
441
442        if let Some(retries) = retries.filter(|r| r.current > 0) {
443            let out = format!(
444                "{}{}: {} | Retry attempt: {}/{}",
445                " ".repeat(self.indent),
446                scenario.keyword,
447                scenario.name,
448                retries.current,
449                retries.left + retries.current,
450            );
451            self.output.write_line(&self.styles.retry(out))
452        } else {
453            let out = format!(
454                "{}{}: {}",
455                " ".repeat(self.indent),
456                scenario.keyword,
457                scenario.name,
458            );
459            self.output.write_line(&self.styles.ok(out))
460        }
461    }
462
463    /// Outputs the [`Step`]'s [started]/[passed]/[skipped]/[failed] event.
464    ///
465    /// [failed]: event::Step::Failed
466    /// [passed]: event::Step::Passed
467    /// [skipped]: event::Step::Skipped
468    /// [started]: event::Step::Started
469    /// [`Step`]: gherkin::Step
470    pub(crate) fn step<W: Debug>(
471        &mut self,
472        feat: &gherkin::Feature,
473        sc: &gherkin::Scenario,
474        step: &gherkin::Step,
475        ev: &event::Step<W>,
476        retries: Option<Retries>,
477    ) -> io::Result<()> {
478        use event::Step;
479
480        match ev {
481            Step::Started => {
482                self.step_started(step)?;
483            }
484            Step::Passed(captures, _) => {
485                self.step_passed(sc, step, captures, retries)?;
486                self.indent = self.indent.saturating_sub(4);
487            }
488            Step::Skipped => {
489                self.step_skipped(feat, step)?;
490                self.indent = self.indent.saturating_sub(4);
491            }
492            Step::Failed(c, loc, w, i) => {
493                self.step_failed(
494                    feat,
495                    step,
496                    c.as_ref(),
497                    *loc,
498                    retries,
499                    w.as_ref(),
500                    i,
501                )?;
502                self.indent = self.indent.saturating_sub(4);
503            }
504        }
505        Ok(())
506    }
507
508    /// Outputs the [started] [`Step`].
509    ///
510    /// The [`Step`] is printed only if [`Coloring`] is enabled and gets
511    /// overwritten by later [passed]/[skipped]/[failed] events.
512    ///
513    /// [failed]: event::Step::Failed
514    /// [passed]: event::Step::Passed
515    /// [skipped]: event::Step::Skipped
516    /// [started]: event::Step::Started
517    /// [`Step`]: gherkin::Step
518    pub(crate) fn step_started(
519        &mut self,
520        step: &gherkin::Step,
521    ) -> io::Result<()> {
522        self.indent += 4;
523        if self.styles.is_present {
524            let out = format!(
525                "{indent}{}{}{}{}",
526                step.keyword,
527                step.value,
528                step.docstring
529                    .as_ref()
530                    .and_then(|doc| self.verbosity.shows_docstring().then(
531                        || {
532                            format_str_with_indent(
533                                doc,
534                                self.indent.saturating_sub(3) + 3,
535                            )
536                        }
537                    ))
538                    .unwrap_or_default(),
539                step.table
540                    .as_ref()
541                    .map(|t| format_table(t, self.indent))
542                    .unwrap_or_default(),
543                indent = " ".repeat(self.indent),
544            );
545            self.lines_to_clear += self.styles.lines_count(&out);
546            self.output.write_line(&out)?;
547        }
548        Ok(())
549    }
550
551    /// Outputs the [passed] [`Step`].
552    ///
553    /// [passed]: event::Step::Passed
554    /// [`Step`]: gherkin::Step
555    pub(crate) fn step_passed(
556        &mut self,
557        scenario: &gherkin::Scenario,
558        step: &gherkin::Step,
559        captures: &CaptureLocations,
560        retries: Option<Retries>,
561    ) -> io::Result<()> {
562        self.clear_last_lines_if_term_present()?;
563
564        let style = |s| {
565            if retries.filter(|r| r.current > 0).is_some()
566                && scenario.steps.last().filter(|st| *st != step).is_some()
567            {
568                self.styles.retry(s)
569            } else {
570                self.styles.ok(s)
571            }
572        };
573
574        let step_keyword = style(format!("✔  {}", step.keyword));
575        let step_value = format_captures(
576            &step.value,
577            captures,
578            |v| style(v.to_owned()),
579            |v| style(self.styles.bold(v).to_string()),
580        );
581        let doc_str = style(
582            step.docstring
583                .as_ref()
584                .and_then(|doc| {
585                    self.verbosity.shows_docstring().then(|| {
586                        format_str_with_indent(
587                            doc,
588                            self.indent.saturating_sub(3) + 3,
589                        )
590                    })
591                })
592                .unwrap_or_default(),
593        );
594        let step_table = style(
595            step.table
596                .as_ref()
597                .map(|t| format_table(t, self.indent))
598                .unwrap_or_default(),
599        );
600
601        self.output.write_line(&style(format!(
602            "{indent}{step_keyword}{step_value}{doc_str}{step_table}",
603            indent = " ".repeat(self.indent.saturating_sub(3)),
604        )))
605    }
606
607    /// Outputs the [skipped] [`Step`].
608    ///
609    /// [skipped]: event::Step::Skipped
610    /// [`Step`]: gherkin::Step
611    pub(crate) fn step_skipped(
612        &mut self,
613        feat: &gherkin::Feature,
614        step: &gherkin::Step,
615    ) -> io::Result<()> {
616        self.clear_last_lines_if_term_present()?;
617        self.output.write_line(&self.styles.skipped(format!(
618            "{indent}?  {}{}{}{}\n\
619             {indent}   Step skipped: {}:{}:{}",
620            step.keyword,
621            step.value,
622            step.docstring
623                .as_ref()
624                .and_then(|doc| self.verbosity.shows_docstring().then(|| {
625                    format_str_with_indent(
626                        doc,
627                        self.indent.saturating_sub(3) + 3,
628                    )
629                }))
630                .unwrap_or_default(),
631            step.table
632                .as_ref()
633                .map(|t| format_table(t, self.indent))
634                .unwrap_or_default(),
635            feat.path
636                .as_ref()
637                .and_then(|p| p.to_str().map(trim_path))
638                .unwrap_or(&feat.name),
639            step.position.line,
640            step.position.col,
641            indent = " ".repeat(self.indent.saturating_sub(3)),
642        )))
643    }
644
645    /// Outputs the [failed] [`Step`].
646    ///
647    /// [failed]: event::Step::Failed
648    /// [`Step`]: gherkin::Step
649    #[allow(clippy::too_many_arguments)] // TODO: Needs refactoring.
650    pub(crate) fn step_failed<W: Debug>(
651        &mut self,
652        feat: &gherkin::Feature,
653        step: &gherkin::Step,
654        captures: Option<&CaptureLocations>,
655        loc: Option<step::Location>,
656        retries: Option<Retries>,
657        world: Option<&W>,
658        err: &event::StepError,
659    ) -> io::Result<()> {
660        self.clear_last_lines_if_term_present()?;
661
662        let style = |s| {
663            if retries
664                .filter(|r| {
665                    r.left > 0 && !matches!(err, event::StepError::NotFound)
666                })
667                .is_some()
668            {
669                self.styles.bright().retry(s)
670            } else {
671                self.styles.err(s)
672            }
673        };
674
675        let indent = " ".repeat(self.indent.saturating_sub(3));
676
677        let step_keyword = style(format!("{indent}✘  {}", step.keyword));
678        let step_value = captures.map_or_else(
679            || style(step.value.clone()),
680            |capts| {
681                format_captures(
682                    &step.value,
683                    capts,
684                    |v| style(v.to_owned()),
685                    |v| style(self.styles.bold(v).to_string()),
686                )
687                .into()
688            },
689        );
690
691        let diagnostics = style(format!(
692            "{}{}\n\
693             {indent}   Step failed:\n\
694             {indent}   Defined: {}:{}:{}{}{}{}",
695            step.docstring
696                .as_ref()
697                .and_then(|doc| self.verbosity.shows_docstring().then(|| {
698                    format_str_with_indent(
699                        doc,
700                        self.indent.saturating_sub(3) + 3,
701                    )
702                }))
703                .unwrap_or_default(),
704            step.table
705                .as_ref()
706                .map(|t| format_table(t, self.indent))
707                .unwrap_or_default(),
708            feat.path
709                .as_ref()
710                .and_then(|p| p.to_str().map(trim_path))
711                .unwrap_or(&feat.name),
712            step.position.line,
713            step.position.col,
714            loc.map(|l| format!(
715                "\n{indent}   Matched: {}:{}:{}",
716                l.path, l.line, l.column,
717            ))
718            .unwrap_or_default(),
719            format_str_with_indent(
720                err.to_string(),
721                self.indent.saturating_sub(3) + 3,
722            ),
723            world
724                .map(|w| format_str_with_indent(
725                    format!("{w:#?}"),
726                    self.indent.saturating_sub(3) + 3,
727                ))
728                .filter(|_| self.verbosity.shows_world())
729                .unwrap_or_default(),
730        ));
731
732        self.output
733            .write_line(&format!("{step_keyword}{step_value}{diagnostics}"))
734    }
735
736    /// Outputs the [`Background`] [`Step`]'s
737    /// [started]/[passed]/[skipped]/[failed] event.
738    ///
739    /// [failed]: event::Step::Failed
740    /// [passed]: event::Step::Passed
741    /// [skipped]: event::Step::Skipped
742    /// [started]: event::Step::Started
743    /// [`Background`]: gherkin::Background
744    /// [`Step`]: gherkin::Step
745    pub(crate) fn background<W: Debug>(
746        &mut self,
747        feat: &gherkin::Feature,
748        sc: &gherkin::Scenario,
749        bg: &gherkin::Step,
750        ev: &event::Step<W>,
751        retries: Option<Retries>,
752    ) -> io::Result<()> {
753        use event::Step;
754
755        match ev {
756            Step::Started => {
757                self.bg_step_started(bg)?;
758            }
759            Step::Passed(captures, _) => {
760                self.bg_step_passed(sc, bg, captures, retries)?;
761                self.indent = self.indent.saturating_sub(4);
762            }
763            Step::Skipped => {
764                self.bg_step_skipped(feat, bg)?;
765                self.indent = self.indent.saturating_sub(4);
766            }
767            Step::Failed(c, loc, w, i) => {
768                self.bg_step_failed(
769                    feat,
770                    bg,
771                    c.as_ref(),
772                    *loc,
773                    retries,
774                    w.as_ref(),
775                    i,
776                )?;
777                self.indent = self.indent.saturating_sub(4);
778            }
779        }
780        Ok(())
781    }
782
783    /// Outputs the [started] [`Background`] [`Step`].
784    ///
785    /// The [`Step`] is printed only if [`Coloring`] is enabled and gets
786    /// overwritten by later [passed]/[skipped]/[failed] events.
787    ///
788    /// [failed]: event::Step::Failed
789    /// [passed]: event::Step::Passed
790    /// [skipped]: event::Step::Skipped
791    /// [started]: event::Step::Started
792    /// [`Background`]: gherkin::Background
793    /// [`Step`]: gherkin::Step
794    pub(crate) fn bg_step_started(
795        &mut self,
796        step: &gherkin::Step,
797    ) -> io::Result<()> {
798        self.indent += 4;
799        if self.styles.is_present {
800            let out = format!(
801                "{indent}> {}{}{}{}",
802                step.keyword,
803                step.value,
804                step.docstring
805                    .as_ref()
806                    .and_then(|doc| self.verbosity.shows_docstring().then(
807                        || {
808                            format_str_with_indent(
809                                doc,
810                                self.indent.saturating_sub(3) + 3,
811                            )
812                        }
813                    ))
814                    .unwrap_or_default(),
815                step.table
816                    .as_ref()
817                    .map(|t| format_table(t, self.indent))
818                    .unwrap_or_default(),
819                indent = " ".repeat(self.indent.saturating_sub(2)),
820            );
821            self.lines_to_clear += self.styles.lines_count(&out);
822            self.output.write_line(&out)?;
823        }
824        Ok(())
825    }
826
827    /// Outputs the [passed] [`Background`] [`Step`].
828    ///
829    /// [passed]: event::Step::Passed
830    /// [`Background`]: gherkin::Background
831    /// [`Step`]: gherkin::Step
832    pub(crate) fn bg_step_passed(
833        &mut self,
834        scenario: &gherkin::Scenario,
835        step: &gherkin::Step,
836        captures: &CaptureLocations,
837        retries: Option<Retries>,
838    ) -> io::Result<()> {
839        self.clear_last_lines_if_term_present()?;
840
841        let style = |s| {
842            if retries.filter(|r| r.current > 0).is_some()
843                && scenario.steps.last().filter(|st| *st != step).is_some()
844            {
845                self.styles.retry(s)
846            } else {
847                self.styles.ok(s)
848            }
849        };
850
851        let indent = " ".repeat(self.indent.saturating_sub(3));
852
853        let step_keyword = style(format!("{indent}✔> {}", step.keyword));
854        let step_value = format_captures(
855            &step.value,
856            captures,
857            |v| style(v.to_owned()),
858            |v| style(self.styles.bold(v).to_string()),
859        );
860        let doc_str = style(
861            step.docstring
862                .as_ref()
863                .and_then(|doc| {
864                    self.verbosity.shows_docstring().then(|| {
865                        format_str_with_indent(
866                            doc,
867                            self.indent.saturating_sub(3) + 3,
868                        )
869                    })
870                })
871                .unwrap_or_default(),
872        );
873        let step_table = style(
874            step.table
875                .as_ref()
876                .map(|t| format_table(t, self.indent))
877                .unwrap_or_default(),
878        );
879
880        self.output.write_line(&style(format!(
881            "{step_keyword}{step_value}{doc_str}{step_table}",
882        )))
883    }
884
885    /// Outputs the [skipped] [`Background`] [`Step`].
886    ///
887    /// [skipped]: event::Step::Skipped
888    /// [`Background`]: gherkin::Background
889    /// [`Step`]: gherkin::Step
890    pub(crate) fn bg_step_skipped(
891        &mut self,
892        feat: &gherkin::Feature,
893        step: &gherkin::Step,
894    ) -> io::Result<()> {
895        self.clear_last_lines_if_term_present()?;
896        self.output.write_line(&self.styles.skipped(format!(
897            "{indent}?> {}{}{}{}\n\
898             {indent}   Background step failed: {}:{}:{}",
899            step.keyword,
900            step.value,
901            step.docstring
902                .as_ref()
903                .and_then(|doc| self.verbosity.shows_docstring().then(|| {
904                    format_str_with_indent(
905                        doc,
906                        self.indent.saturating_sub(3) + 3,
907                    )
908                }))
909                .unwrap_or_default(),
910            step.table
911                .as_ref()
912                .map(|t| format_table(t, self.indent))
913                .unwrap_or_default(),
914            feat.path
915                .as_ref()
916                .and_then(|p| p.to_str().map(trim_path))
917                .unwrap_or(&feat.name),
918            step.position.line,
919            step.position.col,
920            indent = " ".repeat(self.indent.saturating_sub(3)),
921        )))
922    }
923
924    /// Outputs the [failed] [`Background`] [`Step`].
925    ///
926    /// [failed]: event::Step::Failed
927    /// [`Background`]: gherkin::Background
928    /// [`Step`]: gherkin::Step
929    #[allow(clippy::too_many_arguments)] // TODO: Needs refactoring.
930    pub(crate) fn bg_step_failed<W: Debug>(
931        &mut self,
932        feat: &gherkin::Feature,
933        step: &gherkin::Step,
934        captures: Option<&CaptureLocations>,
935        loc: Option<step::Location>,
936        retries: Option<Retries>,
937        world: Option<&W>,
938        err: &event::StepError,
939    ) -> io::Result<()> {
940        self.clear_last_lines_if_term_present()?;
941
942        let style = |s| {
943            if retries
944                .filter(|r| {
945                    r.left > 0 && !matches!(err, event::StepError::NotFound)
946                })
947                .is_some()
948            {
949                self.styles.bright().retry(s)
950            } else {
951                self.styles.err(s)
952            }
953        };
954
955        let indent = " ".repeat(self.indent.saturating_sub(3));
956        let step_keyword = style(format!("{indent}✘> {}", step.keyword));
957        let step_value = captures.map_or_else(
958            || style(step.value.clone()),
959            |capts| {
960                format_captures(
961                    &step.value,
962                    capts,
963                    |v| style(v.to_owned()),
964                    |v| style(self.styles.bold(v).to_string()),
965                )
966                .into()
967            },
968        );
969
970        let diagnostics = style(format!(
971            "{}{}\n\
972             {indent}   Step failed:\n\
973             {indent}   Defined: {}:{}:{}{}{}{}",
974            step.docstring
975                .as_ref()
976                .and_then(|doc| self.verbosity.shows_docstring().then(|| {
977                    format_str_with_indent(
978                        doc,
979                        self.indent.saturating_sub(3) + 3,
980                    )
981                }))
982                .unwrap_or_default(),
983            step.table
984                .as_ref()
985                .map(|t| format_table(t, self.indent))
986                .unwrap_or_default(),
987            feat.path
988                .as_ref()
989                .and_then(|p| p.to_str().map(trim_path))
990                .unwrap_or(&feat.name),
991            step.position.line,
992            step.position.col,
993            loc.map(|l| format!(
994                "\n{indent}   Matched: {}:{}:{}",
995                l.path, l.line, l.column,
996            ))
997            .unwrap_or_default(),
998            format_str_with_indent(
999                err.to_string(),
1000                self.indent.saturating_sub(3) + 3,
1001            ),
1002            world
1003                .map(|w| format_str_with_indent(
1004                    format!("{w:#?}"),
1005                    self.indent.saturating_sub(3) + 3,
1006                ))
1007                .filter(|_| self.verbosity.shows_world())
1008                .unwrap_or_default(),
1009        ));
1010
1011        self.output
1012            .write_line(&format!("{step_keyword}{step_value}{diagnostics}"))
1013    }
1014}
1015
1016/// Tries to coerce [`catch_unwind()`] output to [`String`].
1017///
1018/// [`catch_unwind()`]: std::panic::catch_unwind()
1019#[must_use]
1020pub(crate) fn coerce_error(err: &Info) -> Cow<'static, str> {
1021    err.downcast_ref::<String>()
1022        .map(|s| s.clone().into())
1023        .or_else(|| err.downcast_ref::<&str>().map(|s| s.to_owned().into()))
1024        .unwrap_or_else(|| "(Could not resolve panic payload)".into())
1025}
1026
1027/// Formats the given [`str`] by adding `indent`s to each line to prettify the
1028/// output.
1029fn format_str_with_indent(str: impl AsRef<str>, indent: usize) -> String {
1030    let str = str
1031        .as_ref()
1032        .lines()
1033        .map(|line| format!("{}{line}", " ".repeat(indent)))
1034        .join("\n");
1035    (!str.is_empty())
1036        .then(|| format!("\n{str}"))
1037        .unwrap_or_default()
1038}
1039
1040/// Formats the given [`gherkin::Table`] and adds `indent`s to each line to
1041/// prettify the output.
1042fn format_table(table: &gherkin::Table, indent: usize) -> String {
1043    use std::fmt::Write as _;
1044
1045    let max_row_len = table
1046        .rows
1047        .iter()
1048        .fold(None, |mut acc: Option<Vec<_>>, row| {
1049            // False Positive: Due to mut borrowing.
1050            #[allow(clippy::option_if_let_else)] // false positive
1051            if let Some(existing_len) = acc.as_mut() {
1052                for (cell, max_len) in row.iter().zip(existing_len) {
1053                    *max_len = cmp::max(*max_len, cell.len());
1054                }
1055            } else {
1056                acc = Some(row.iter().map(String::len).collect::<Vec<_>>());
1057            }
1058            acc
1059        })
1060        .unwrap_or_default();
1061
1062    let mut table = table
1063        .rows
1064        .iter()
1065        .map(|row| {
1066            row.iter().zip(&max_row_len).fold(
1067                String::new(),
1068                |mut out, (cell, len)| {
1069                    _ = write!(out, "| {cell:len$} ");
1070                    out
1071                },
1072            )
1073        })
1074        .map(|row| format!("{}{row}", " ".repeat(indent + 1)))
1075        .join("|\n");
1076
1077    if !table.is_empty() {
1078        table.insert(0, '\n');
1079        table.push('|');
1080    }
1081
1082    table
1083}
1084
1085/// Formats `value`s in the given `captures` with the provided `accent` style
1086/// and with the `default` style anything else.
1087fn format_captures<D, A>(
1088    value: impl AsRef<str>,
1089    captures: &CaptureLocations,
1090    default: D,
1091    accent: A,
1092) -> String
1093where
1094    D: for<'a> Fn(&'a str) -> Cow<'a, str>,
1095    A: for<'a> Fn(&'a str) -> Cow<'a, str>,
1096{
1097    // PANIC: Slicing is OK here, as all indices are obtained from the source
1098    //        string.
1099    #![allow(clippy::string_slice)] // intentional
1100
1101    let value = value.as_ref();
1102
1103    let (mut formatted, end) = (1..captures.len())
1104        .filter_map(|group| captures.get(group))
1105        .fold(
1106            (String::with_capacity(value.len()), 0),
1107            |(mut str, old), (start, end)| {
1108                // Ignore nested groups.
1109                if old > start {
1110                    return (str, old);
1111                }
1112
1113                str.push_str(&default(&value[old..start]));
1114                str.push_str(&accent(&value[start..end]));
1115                (str, end)
1116            },
1117        );
1118    formatted.push_str(&default(&value[end..value.len()]));
1119
1120    formatted
1121}
1122
1123/// Trims start of the path if it matches the current project directory.
1124pub(crate) fn trim_path(path: &str) -> &str {
1125    /// Path of the current project directory.
1126    static CURRENT_DIR: Lazy<String> = Lazy::new(|| {
1127        env::var("CARGO_WORKSPACE_DIR")
1128            .or_else(|_| env::var("CARGO_MANIFEST_DIR"))
1129            .unwrap_or_else(|_| {
1130                env::current_dir()
1131                    .map(|path| path.display().to_string())
1132                    .unwrap_or_default()
1133            })
1134    });
1135
1136    path.trim_start_matches(&**CURRENT_DIR)
1137        .trim_start_matches('/')
1138        .trim_start_matches('\\')
1139}