1use std::{
14 borrow::Cow,
15 cmp, env,
16 fmt::{Debug, Display},
17 io,
18 str::FromStr,
19 sync::LazyLock,
20};
21
22use derive_more::with_trait::{Deref, DerefMut};
23use itertools::Itertools as _;
24use regex::CaptureLocations;
25use smart_default::SmartDefault;
26
27use crate::{
28 Event, World, Writer,
29 cli::Colored,
30 event::{self, Info, Retries},
31 parser, step,
32 writer::{
33 self, Ext as _, Verbosity,
34 out::{Styles, WriteStrExt as _},
35 },
36};
37
38#[derive(Clone, Copy, Debug, SmartDefault, clap::Args)]
40#[group(skip)]
41pub struct Cli {
42 #[arg(short, action = clap::ArgAction::Count, global = true)]
47 pub verbose: u8,
48
49 #[arg(
51 long,
52 value_name = "auto|always|never",
53 default_value = "auto",
54 global = true
55 )]
56 #[default(Coloring::Auto)]
57 pub color: Coloring,
58}
59
60impl Colored for Cli {
61 fn coloring(&self) -> Coloring {
62 self.color
63 }
64}
65
66#[derive(Clone, Copy, Debug)]
68pub enum Coloring {
69 Auto,
72
73 Always,
75
76 Never,
78}
79
80impl FromStr for Coloring {
81 type Err = &'static str;
82
83 fn from_str(s: &str) -> Result<Self, Self::Err> {
84 match s.to_ascii_lowercase().as_str() {
85 "auto" => Ok(Self::Auto),
86 "always" => Ok(Self::Always),
87 "never" => Ok(Self::Never),
88 _ => Err("possible options: auto, always, never"),
89 }
90 }
91}
92
93#[derive(Clone, Debug, Deref, DerefMut)]
109pub struct Basic<Out: io::Write = io::Stdout> {
110 #[deref]
112 #[deref_mut]
113 output: Out,
114
115 styles: Styles,
117
118 indent: usize,
120
121 lines_to_clear: usize,
123
124 re_output_after_clear: String,
128
129 verbosity: Verbosity,
131}
132
133impl<W, Out> Writer<W> for Basic<Out>
134where
135 W: World + Debug,
136 Out: io::Write,
137{
138 type Cli = Cli;
139
140 async fn handle_event(
141 &mut self,
142 event: parser::Result<Event<event::Cucumber<W>>>,
143 cli: &Self::Cli,
144 ) {
145 use event::{Cucumber, Feature};
146
147 self.apply_cli(*cli);
148
149 match event.map(Event::into_inner) {
150 Err(err) => self.parsing_failed(&err),
151 Ok(
152 Cucumber::Started
153 | Cucumber::ParsingFinished { .. }
154 | Cucumber::Finished,
155 ) => Ok(()),
156 Ok(Cucumber::Feature(f, ev)) => match ev {
157 Feature::Started => self.feature_started(&f),
158 Feature::Scenario(sc, ev) => self.scenario(&f, &sc, &ev),
159 Feature::Rule(r, ev) => self.rule(&f, &r, ev),
160 Feature::Finished => Ok(()),
161 },
162 }
163 .unwrap_or_else(|e| panic!("failed to write into terminal: {e}"));
164 }
165}
166
167impl<W, Val, Out> writer::Arbitrary<W, Val> for Basic<Out>
168where
169 W: World + Debug,
170 Val: AsRef<str>,
171 Out: io::Write,
172{
173 async fn write(&mut self, val: Val) {
174 self.write_line(val.as_ref())
175 .unwrap_or_else(|e| panic!("failed to write: {e}"));
176 }
177}
178
179impl<O: io::Write> writer::NonTransforming for Basic<O> {}
180
181impl Basic {
182 #[must_use]
187 pub fn stdout<W>() -> writer::Normalize<W, Self> {
188 Self::new(io::stdout(), Coloring::Auto, Verbosity::Default)
189 }
190}
191
192impl<Out: io::Write> Basic<Out> {
193 #[must_use]
198 pub fn new<W>(
199 output: Out,
200 color: Coloring,
201 verbosity: impl Into<Verbosity>,
202 ) -> writer::Normalize<W, Self> {
203 Self::raw(output, color, verbosity).normalized()
204 }
205
206 #[must_use]
215 pub fn raw(
216 output: Out,
217 color: Coloring,
218 verbosity: impl Into<Verbosity>,
219 ) -> Self {
220 let mut basic = Self {
221 output,
222 styles: Styles::new(),
223 indent: 0,
224 lines_to_clear: 0,
225 re_output_after_clear: String::new(),
226 verbosity: verbosity.into(),
227 };
228 basic.apply_cli(Cli { verbose: u8::from(basic.verbosity) + 1, color });
229 basic
230 }
231
232 pub fn apply_cli(&mut self, cli: Cli) {
234 match cli.verbose {
235 0 => {}
236 1 => self.verbosity = Verbosity::Default,
237 2 => self.verbosity = Verbosity::ShowWorld,
238 _ => self.verbosity = Verbosity::ShowWorldAndDocString,
239 }
240 self.styles.apply_coloring(cli.color);
241 }
242
243 fn clear_last_lines_if_term_present(&mut self) -> io::Result<()> {
245 if self.styles.is_present && self.lines_to_clear > 0 {
246 self.output.clear_last_lines(self.lines_to_clear)?;
247 self.output.write_str(&self.re_output_after_clear)?;
248 self.re_output_after_clear.clear();
249 self.lines_to_clear = 0;
250 }
251 Ok(())
252 }
253
254 pub(crate) fn parsing_failed(
258 &mut self,
259 error: impl Display,
260 ) -> io::Result<()> {
261 self.output
262 .write_line(self.styles.err(format!("Failed to parse: {error}")))
263 }
264
265 pub(crate) fn feature_started(
270 &mut self,
271 feature: &gherkin::Feature,
272 ) -> io::Result<()> {
273 let out = format!("{}: {}", feature.keyword, feature.name);
274 self.output.write_line(self.styles.ok(out))
275 }
276
277 pub(crate) fn rule<W: Debug>(
284 &mut self,
285 feat: &gherkin::Feature,
286 rule: &gherkin::Rule,
287 ev: event::Rule<W>,
288 ) -> io::Result<()> {
289 use event::Rule;
290
291 match ev {
292 Rule::Started => {
293 self.rule_started(rule)?;
294 }
295 Rule::Scenario(sc, ev) => {
296 self.scenario(feat, &sc, &ev)?;
297 }
298 Rule::Finished => {
299 self.indent = self.indent.saturating_sub(2);
300 }
301 }
302 Ok(())
303 }
304
305 pub(crate) fn rule_started(
310 &mut self,
311 rule: &gherkin::Rule,
312 ) -> io::Result<()> {
313 let out = format!(
314 "{indent}{}: {}",
315 rule.keyword,
316 rule.name,
317 indent = " ".repeat(self.indent)
318 );
319 self.indent += 2;
320 self.output.write_line(self.styles.ok(out))
321 }
322
323 pub(crate) fn scenario<W: Debug>(
330 &mut self,
331 feat: &gherkin::Feature,
332 scenario: &gherkin::Scenario,
333 ev: &event::RetryableScenario<W>,
334 ) -> io::Result<()> {
335 use event::{Hook, Scenario};
336
337 let retries = ev.retries;
338 match &ev.event {
339 Scenario::Started => {
340 self.scenario_started(scenario, retries)?;
341 }
342 Scenario::Hook(_, Hook::Started) => {
343 self.indent += 4;
344 }
345 Scenario::Hook(which, Hook::Failed(world, info)) => {
346 self.hook_failed(
347 feat,
348 scenario,
349 *which,
350 retries,
351 world.as_ref(),
352 info,
353 )?;
354 self.indent = self.indent.saturating_sub(4);
355 }
356 Scenario::Hook(_, Hook::Passed) => {
357 self.indent = self.indent.saturating_sub(4);
358 }
359 Scenario::Background(bg, ev) => {
360 self.background(feat, scenario, bg, ev, retries)?;
361 }
362 Scenario::Step(st, ev) => {
363 self.step(feat, scenario, st, ev, retries)?;
364 }
365 Scenario::Finished => {
366 self.indent = self.indent.saturating_sub(2);
367 }
368 Scenario::Log(msg) => self.emit_log(msg)?,
369 }
370 Ok(())
371 }
372
373 pub(crate) fn emit_log(&mut self, msg: impl AsRef<str>) -> io::Result<()> {
375 self.lines_to_clear += self.styles.lines_count(msg.as_ref());
376 self.re_output_after_clear.push_str(msg.as_ref());
377 self.output.write_str(msg)
378 }
379
380 pub(crate) fn hook_failed<W: Debug>(
385 &mut self,
386 feat: &gherkin::Feature,
387 sc: &gherkin::Scenario,
388 which: event::HookType,
389 retries: Option<Retries>,
390 world: Option<&W>,
391 info: &Info,
392 ) -> io::Result<()> {
393 self.clear_last_lines_if_term_present()?;
394
395 let style = |s| {
396 if retries.filter(|r| r.left > 0).is_some() {
397 self.styles.bright().retry(s)
398 } else {
399 self.styles.err(s)
400 }
401 };
402
403 self.output.write_line(style(format!(
404 "{indent}✘ Scenario's {which} hook failed {}:{}:{}\n\
405 {indent} Captured output: {}{}",
406 feat.path
407 .as_ref()
408 .and_then(|p| p.to_str().map(trim_path))
409 .unwrap_or(&feat.name),
410 sc.position.line,
411 sc.position.col,
412 format_str_with_indent(
413 coerce_error(info),
414 self.indent.saturating_sub(3) + 3
415 ),
416 world
417 .map(|w| format_str_with_indent(
418 format!("{w:#?}"),
419 self.indent.saturating_sub(3) + 3,
420 ))
421 .filter(|_| self.verbosity.shows_world())
422 .unwrap_or_default(),
423 indent = " ".repeat(self.indent.saturating_sub(3)),
424 )))
425 }
426
427 pub(crate) fn scenario_started(
432 &mut self,
433 scenario: &gherkin::Scenario,
434 retries: Option<Retries>,
435 ) -> io::Result<()> {
436 self.indent += 2;
437
438 if let Some(retries) = retries.filter(|r| r.current > 0) {
439 let out = format!(
440 "{}{}: {} | Retry attempt: {}/{}",
441 " ".repeat(self.indent),
442 scenario.keyword,
443 scenario.name,
444 retries.current,
445 retries.left + retries.current,
446 );
447 self.output.write_line(self.styles.retry(out))
448 } else {
449 let out = format!(
450 "{}{}: {}",
451 " ".repeat(self.indent),
452 scenario.keyword,
453 scenario.name,
454 );
455 self.output.write_line(self.styles.ok(out))
456 }
457 }
458
459 pub(crate) fn step<W: Debug>(
467 &mut self,
468 feat: &gherkin::Feature,
469 sc: &gherkin::Scenario,
470 step: &gherkin::Step,
471 ev: &event::Step<W>,
472 retries: Option<Retries>,
473 ) -> io::Result<()> {
474 use event::Step;
475
476 match ev {
477 Step::Started => {
478 self.step_started(step)?;
479 }
480 Step::Passed(captures, _) => {
481 self.step_passed(sc, step, captures, retries)?;
482 self.indent = self.indent.saturating_sub(4);
483 }
484 Step::Skipped => {
485 self.step_skipped(feat, step)?;
486 self.indent = self.indent.saturating_sub(4);
487 }
488 Step::Failed(c, loc, w, i) => {
489 self.step_failed(
490 feat,
491 step,
492 c.as_ref(),
493 *loc,
494 retries,
495 w.as_ref(),
496 i,
497 )?;
498 self.indent = self.indent.saturating_sub(4);
499 }
500 }
501 Ok(())
502 }
503
504 pub(crate) fn step_started(
515 &mut self,
516 step: &gherkin::Step,
517 ) -> io::Result<()> {
518 self.indent += 4;
519 if self.styles.is_present {
520 let out = format!(
521 "{indent}{}{}{}{}",
522 step.keyword,
523 step.value,
524 step.docstring
525 .as_ref()
526 .and_then(|doc| self.verbosity.shows_docstring().then(
527 || {
528 format_str_with_indent(
529 doc,
530 self.indent.saturating_sub(3) + 3,
531 )
532 }
533 ))
534 .unwrap_or_default(),
535 step.table
536 .as_ref()
537 .map(|t| format_table(t, self.indent))
538 .unwrap_or_default(),
539 indent = " ".repeat(self.indent),
540 );
541 self.lines_to_clear += self.styles.lines_count(&out);
542 self.output.write_line(&out)?;
543 }
544 Ok(())
545 }
546
547 pub(crate) fn step_passed(
552 &mut self,
553 scenario: &gherkin::Scenario,
554 step: &gherkin::Step,
555 captures: &CaptureLocations,
556 retries: Option<Retries>,
557 ) -> io::Result<()> {
558 self.clear_last_lines_if_term_present()?;
559
560 let style = |s| {
561 if retries.filter(|r| r.current > 0).is_some()
562 && scenario.steps.last().filter(|st| *st != step).is_some()
563 {
564 self.styles.retry(s)
565 } else {
566 self.styles.ok(s)
567 }
568 };
569
570 let step_keyword = style(format!("✔ {}", step.keyword));
571 let step_value = format_captures(
572 &step.value,
573 captures,
574 |v| style(v.to_owned()),
575 |v| style(self.styles.bold(v).to_string()),
576 );
577 let doc_str = style(
578 step.docstring
579 .as_ref()
580 .and_then(|doc| {
581 self.verbosity.shows_docstring().then(|| {
582 format_str_with_indent(
583 doc,
584 self.indent.saturating_sub(3) + 3,
585 )
586 })
587 })
588 .unwrap_or_default(),
589 );
590 let step_table = style(
591 step.table
592 .as_ref()
593 .map(|t| format_table(t, self.indent))
594 .unwrap_or_default(),
595 );
596
597 self.output.write_line(style(format!(
598 "{indent}{step_keyword}{step_value}{doc_str}{step_table}",
599 indent = " ".repeat(self.indent.saturating_sub(3)),
600 )))
601 }
602
603 pub(crate) fn step_skipped(
608 &mut self,
609 feat: &gherkin::Feature,
610 step: &gherkin::Step,
611 ) -> io::Result<()> {
612 self.clear_last_lines_if_term_present()?;
613 self.output.write_line(self.styles.skipped(format!(
614 "{indent}? {}{}{}{}\n\
615 {indent} Step skipped: {}:{}:{}",
616 step.keyword,
617 step.value,
618 step.docstring
619 .as_ref()
620 .and_then(|doc| self.verbosity.shows_docstring().then(|| {
621 format_str_with_indent(
622 doc,
623 self.indent.saturating_sub(3) + 3,
624 )
625 }))
626 .unwrap_or_default(),
627 step.table
628 .as_ref()
629 .map(|t| format_table(t, self.indent))
630 .unwrap_or_default(),
631 feat.path
632 .as_ref()
633 .and_then(|p| p.to_str().map(trim_path))
634 .unwrap_or(&feat.name),
635 step.position.line,
636 step.position.col,
637 indent = " ".repeat(self.indent.saturating_sub(3)),
638 )))
639 }
640
641 #[expect(clippy::too_many_arguments, reason = "needs refactoring")]
647 pub(crate) fn step_failed<W: Debug>(
648 &mut self,
649 feat: &gherkin::Feature,
650 step: &gherkin::Step,
651 captures: Option<&CaptureLocations>,
652 loc: Option<step::Location>,
653 retries: Option<Retries>,
654 world: Option<&W>,
655 err: &event::StepError,
656 ) -> io::Result<()> {
657 self.clear_last_lines_if_term_present()?;
658
659 let style = |s| {
660 if retries
661 .filter(|r| {
662 r.left > 0 && !matches!(err, event::StepError::NotFound)
663 })
664 .is_some()
665 {
666 self.styles.bright().retry(s)
667 } else {
668 self.styles.err(s)
669 }
670 };
671
672 let indent = " ".repeat(self.indent.saturating_sub(3));
673
674 let step_keyword = style(format!("{indent}✘ {}", step.keyword));
675 let step_value = captures.map_or_else(
676 || style(step.value.clone()),
677 |capts| {
678 format_captures(
679 &step.value,
680 capts,
681 |v| style(v.to_owned()),
682 |v| style(self.styles.bold(v).to_string()),
683 )
684 .into()
685 },
686 );
687
688 let diagnostics = style(format!(
689 "{}{}\n\
690 {indent} Step failed:\n\
691 {indent} Defined: {}:{}:{}{}{}{}",
692 step.docstring
693 .as_ref()
694 .and_then(|doc| self.verbosity.shows_docstring().then(|| {
695 format_str_with_indent(
696 doc,
697 self.indent.saturating_sub(3) + 3,
698 )
699 }))
700 .unwrap_or_default(),
701 step.table
702 .as_ref()
703 .map(|t| format_table(t, self.indent))
704 .unwrap_or_default(),
705 feat.path
706 .as_ref()
707 .and_then(|p| p.to_str().map(trim_path))
708 .unwrap_or(&feat.name),
709 step.position.line,
710 step.position.col,
711 loc.map(|l| format!(
712 "\n{indent} Matched: {}:{}:{}",
713 l.path, l.line, l.column,
714 ))
715 .unwrap_or_default(),
716 format_str_with_indent(
717 err.to_string(),
718 self.indent.saturating_sub(3) + 3,
719 ),
720 world
721 .map(|w| format_str_with_indent(
722 format!("{w:#?}"),
723 self.indent.saturating_sub(3) + 3,
724 ))
725 .filter(|_| self.verbosity.shows_world())
726 .unwrap_or_default(),
727 ));
728
729 self.output
730 .write_line(format!("{step_keyword}{step_value}{diagnostics}"))
731 }
732
733 pub(crate) fn background<W: Debug>(
743 &mut self,
744 feat: &gherkin::Feature,
745 sc: &gherkin::Scenario,
746 bg: &gherkin::Step,
747 ev: &event::Step<W>,
748 retries: Option<Retries>,
749 ) -> io::Result<()> {
750 use event::Step;
751
752 match ev {
753 Step::Started => {
754 self.bg_step_started(bg)?;
755 }
756 Step::Passed(captures, _) => {
757 self.bg_step_passed(sc, bg, captures, retries)?;
758 self.indent = self.indent.saturating_sub(4);
759 }
760 Step::Skipped => {
761 self.bg_step_skipped(feat, bg)?;
762 self.indent = self.indent.saturating_sub(4);
763 }
764 Step::Failed(c, loc, w, i) => {
765 self.bg_step_failed(
766 feat,
767 bg,
768 c.as_ref(),
769 *loc,
770 retries,
771 w.as_ref(),
772 i,
773 )?;
774 self.indent = self.indent.saturating_sub(4);
775 }
776 }
777 Ok(())
778 }
779
780 pub(crate) fn bg_step_started(
792 &mut self,
793 step: &gherkin::Step,
794 ) -> io::Result<()> {
795 self.indent += 4;
796 if self.styles.is_present {
797 let out = format!(
798 "{indent}> {}{}{}{}",
799 step.keyword,
800 step.value,
801 step.docstring
802 .as_ref()
803 .and_then(|doc| self.verbosity.shows_docstring().then(
804 || {
805 format_str_with_indent(
806 doc,
807 self.indent.saturating_sub(3) + 3,
808 )
809 }
810 ))
811 .unwrap_or_default(),
812 step.table
813 .as_ref()
814 .map(|t| format_table(t, self.indent))
815 .unwrap_or_default(),
816 indent = " ".repeat(self.indent.saturating_sub(2)),
817 );
818 self.lines_to_clear += self.styles.lines_count(&out);
819 self.output.write_line(&out)?;
820 }
821 Ok(())
822 }
823
824 pub(crate) fn bg_step_passed(
830 &mut self,
831 scenario: &gherkin::Scenario,
832 step: &gherkin::Step,
833 captures: &CaptureLocations,
834 retries: Option<Retries>,
835 ) -> io::Result<()> {
836 self.clear_last_lines_if_term_present()?;
837
838 let style = |s| {
839 if retries.filter(|r| r.current > 0).is_some()
840 && scenario.steps.last().filter(|st| *st != step).is_some()
841 {
842 self.styles.retry(s)
843 } else {
844 self.styles.ok(s)
845 }
846 };
847
848 let indent = " ".repeat(self.indent.saturating_sub(3));
849
850 let step_keyword = style(format!("{indent}✔> {}", step.keyword));
851 let step_value = format_captures(
852 &step.value,
853 captures,
854 |v| style(v.to_owned()),
855 |v| style(self.styles.bold(v).to_string()),
856 );
857 let doc_str = style(
858 step.docstring
859 .as_ref()
860 .and_then(|doc| {
861 self.verbosity.shows_docstring().then(|| {
862 format_str_with_indent(
863 doc,
864 self.indent.saturating_sub(3) + 3,
865 )
866 })
867 })
868 .unwrap_or_default(),
869 );
870 let step_table = style(
871 step.table
872 .as_ref()
873 .map(|t| format_table(t, self.indent))
874 .unwrap_or_default(),
875 );
876
877 self.output.write_line(style(format!(
878 "{step_keyword}{step_value}{doc_str}{step_table}",
879 )))
880 }
881
882 pub(crate) fn bg_step_skipped(
888 &mut self,
889 feat: &gherkin::Feature,
890 step: &gherkin::Step,
891 ) -> io::Result<()> {
892 self.clear_last_lines_if_term_present()?;
893 self.output.write_line(self.styles.skipped(format!(
894 "{indent}?> {}{}{}{}\n\
895 {indent} Background step failed: {}:{}:{}",
896 step.keyword,
897 step.value,
898 step.docstring
899 .as_ref()
900 .and_then(|doc| self.verbosity.shows_docstring().then(|| {
901 format_str_with_indent(
902 doc,
903 self.indent.saturating_sub(3) + 3,
904 )
905 }))
906 .unwrap_or_default(),
907 step.table
908 .as_ref()
909 .map(|t| format_table(t, self.indent))
910 .unwrap_or_default(),
911 feat.path
912 .as_ref()
913 .and_then(|p| p.to_str().map(trim_path))
914 .unwrap_or(&feat.name),
915 step.position.line,
916 step.position.col,
917 indent = " ".repeat(self.indent.saturating_sub(3)),
918 )))
919 }
920
921 #[expect(clippy::too_many_arguments, reason = "needs refactoring")]
928 pub(crate) fn bg_step_failed<W: Debug>(
929 &mut self,
930 feat: &gherkin::Feature,
931 step: &gherkin::Step,
932 captures: Option<&CaptureLocations>,
933 loc: Option<step::Location>,
934 retries: Option<Retries>,
935 world: Option<&W>,
936 err: &event::StepError,
937 ) -> io::Result<()> {
938 self.clear_last_lines_if_term_present()?;
939
940 let style = |s| {
941 if retries
942 .filter(|r| {
943 r.left > 0 && !matches!(err, event::StepError::NotFound)
944 })
945 .is_some()
946 {
947 self.styles.bright().retry(s)
948 } else {
949 self.styles.err(s)
950 }
951 };
952
953 let indent = " ".repeat(self.indent.saturating_sub(3));
954 let step_keyword = style(format!("{indent}✘> {}", step.keyword));
955 let step_value = captures.map_or_else(
956 || style(step.value.clone()),
957 |capts| {
958 format_captures(
959 &step.value,
960 capts,
961 |v| style(v.to_owned()),
962 |v| style(self.styles.bold(v).to_string()),
963 )
964 .into()
965 },
966 );
967
968 let diagnostics = style(format!(
969 "{}{}\n\
970 {indent} Step failed:\n\
971 {indent} Defined: {}:{}:{}{}{}{}",
972 step.docstring
973 .as_ref()
974 .and_then(|doc| self.verbosity.shows_docstring().then(|| {
975 format_str_with_indent(
976 doc,
977 self.indent.saturating_sub(3) + 3,
978 )
979 }))
980 .unwrap_or_default(),
981 step.table
982 .as_ref()
983 .map(|t| format_table(t, self.indent))
984 .unwrap_or_default(),
985 feat.path
986 .as_ref()
987 .and_then(|p| p.to_str().map(trim_path))
988 .unwrap_or(&feat.name),
989 step.position.line,
990 step.position.col,
991 loc.map(|l| format!(
992 "\n{indent} Matched: {}:{}:{}",
993 l.path, l.line, l.column,
994 ))
995 .unwrap_or_default(),
996 format_str_with_indent(
997 err.to_string(),
998 self.indent.saturating_sub(3) + 3,
999 ),
1000 world
1001 .map(|w| format_str_with_indent(
1002 format!("{w:#?}"),
1003 self.indent.saturating_sub(3) + 3,
1004 ))
1005 .filter(|_| self.verbosity.shows_world())
1006 .unwrap_or_default(),
1007 ));
1008
1009 self.output
1010 .write_line(format!("{step_keyword}{step_value}{diagnostics}"))
1011 }
1012}
1013
1014#[must_use]
1018pub(crate) fn coerce_error(err: &Info) -> Cow<'static, str> {
1019 (**err)
1020 .downcast_ref::<String>()
1021 .map(|s| s.clone().into())
1022 .or_else(|| (**err).downcast_ref::<&str>().map(|s| s.to_owned().into()))
1023 .unwrap_or_else(|| "(Could not resolve panic payload)".into())
1024}
1025
1026fn format_str_with_indent(str: impl AsRef<str>, indent: usize) -> String {
1029 let str = str
1030 .as_ref()
1031 .lines()
1032 .map(|line| format!("{}{line}", " ".repeat(indent)))
1033 .join("\n");
1034 if str.is_empty() { String::new() } else { format!("\n{str}") }
1035}
1036
1037fn format_table(table: &gherkin::Table, indent: usize) -> String {
1040 use std::fmt::Write as _;
1041
1042 let max_row_len = table
1043 .rows
1044 .iter()
1045 .fold(None, |mut acc: Option<Vec<_>>, row| {
1046 if let Some(existing_len) = acc.as_mut() {
1047 for (cell, max_len) in row.iter().zip(existing_len) {
1048 *max_len = cmp::max(*max_len, cell.len());
1049 }
1050 } else {
1051 acc = Some(row.iter().map(String::len).collect::<Vec<_>>());
1052 }
1053 acc
1054 })
1055 .unwrap_or_default();
1056
1057 let mut table = table
1058 .rows
1059 .iter()
1060 .map(|row| {
1061 row.iter().zip(&max_row_len).fold(
1062 String::new(),
1063 |mut out, (cell, len)| {
1064 _ = write!(out, "| {cell:len$} ");
1065 out
1066 },
1067 )
1068 })
1069 .map(|row| format!("{}{row}", " ".repeat(indent + 1)))
1070 .join("|\n");
1071
1072 if !table.is_empty() {
1073 table.insert(0, '\n');
1074 table.push('|');
1075 }
1076
1077 table
1078}
1079
1080fn format_captures<D, A>(
1083 value: impl AsRef<str>,
1084 captures: &CaptureLocations,
1085 default: D,
1086 accent: A,
1087) -> String
1088where
1089 D: for<'a> Fn(&'a str) -> Cow<'a, str>,
1090 A: for<'a> Fn(&'a str) -> Cow<'a, str>,
1091{
1092 #![expect( clippy::string_slice,
1094 reason = "all indices are obtained from the source string"
1095 )]
1096
1097 let value = value.as_ref();
1098
1099 let (mut formatted, end) =
1100 (1..captures.len()).filter_map(|group| captures.get(group)).fold(
1101 (String::with_capacity(value.len()), 0),
1102 |(mut str, old), (start, end)| {
1103 if old > start {
1105 return (str, old);
1106 }
1107
1108 str.push_str(&default(&value[old..start]));
1109 str.push_str(&accent(&value[start..end]));
1110 (str, end)
1111 },
1112 );
1113 formatted.push_str(&default(&value[end..value.len()]));
1114
1115 formatted
1116}
1117
1118pub(crate) fn trim_path(path: &str) -> &str {
1120 static CURRENT_DIR: LazyLock<String> = LazyLock::new(|| {
1122 env::var("CARGO_WORKSPACE_DIR")
1123 .or_else(|_| env::var("CARGO_MANIFEST_DIR"))
1124 .unwrap_or_else(|_| {
1125 env::current_dir()
1126 .map(|path| path.display().to_string())
1127 .unwrap_or_default()
1128 })
1129 });
1130
1131 path.trim_start_matches(&**CURRENT_DIR)
1132 .trim_start_matches('/')
1133 .trim_start_matches('\\')
1134}