1use 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#[derive(clap::Args, Clone, Copy, Debug, SmartDefault)]
41#[group(skip)]
42pub struct Cli {
43 #[arg(short, action = clap::ArgAction::Count, global = true)]
48 pub verbose: u8,
49
50 #[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#[derive(Clone, Copy, Debug)]
69pub enum Coloring {
70 Auto,
73
74 Always,
76
77 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#[derive(Clone, Debug, Deref, DerefMut)]
110pub struct Basic<Out: io::Write = io::Stdout> {
111 #[deref]
113 #[deref_mut]
114 output: Out,
115
116 styles: Styles,
118
119 indent: usize,
121
122 lines_to_clear: usize,
124
125 re_output_after_clear: String,
129
130 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 #[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 #[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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[allow(clippy::too_many_arguments)] 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 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 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 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 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 #[allow(clippy::too_many_arguments)] 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#[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
1027fn 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
1040fn 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 #[allow(clippy::option_if_let_else)] 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
1085fn 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 #![allow(clippy::string_slice)] 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 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
1123pub(crate) fn trim_path(path: &str) -> &str {
1125 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}