1use std::{
16    fmt::Debug,
17    io, iter, mem,
18    str::FromStr,
19    time::{Duration, SystemTime},
20};
21
22use derive_more::From;
23use either::Either;
24use itertools::Itertools as _;
25use serde::Serialize;
26
27use crate::{
28    cli,
29    event::{self, Retries},
30    parser,
31    writer::{
32        self,
33        basic::{coerce_error, trim_path},
34        out::WriteStrExt as _,
35        Arbitrary, Normalize, Summarize,
36    },
37    Event, World, Writer, WriterExt as _,
38};
39
40#[derive(clap::Args, Clone, Debug, Default)]
42#[group(skip)]
43pub struct Cli {
44    #[arg(long, value_name = "json")]
46    pub format: Option<Format>,
47
48    #[arg(long)]
51    pub show_output: bool,
52
53    #[arg(long, value_name = "plain|colored", default_missing_value = "plain")]
55    pub report_time: Option<ReportTime>,
56
57    #[arg(short = 'Z')]
59    pub nightly: Option<String>,
60}
61
62#[derive(Clone, Copy, Debug)]
66pub enum Format {
67    Json,
71}
72
73impl FromStr for Format {
74    type Err = String;
75
76    fn from_str(s: &str) -> Result<Self, Self::Err> {
77        match s.to_lowercase().as_str() {
78            "json" => Ok(Self::Json),
79            s @ ("pretty" | "terse" | "junit") => {
80                Err(format!("`{s}` option is not supported yet"))
81            }
82            s => Err(format!(
83                "Unknown option `{s}`, expected `pretty` or `json`",
84            )),
85        }
86    }
87}
88
89#[derive(Clone, Copy, Debug)]
91pub enum ReportTime {
92    Plain,
94
95    Colored,
97}
98
99impl FromStr for ReportTime {
100    type Err = String;
101
102    fn from_str(s: &str) -> Result<Self, Self::Err> {
103        match s.to_lowercase().as_str() {
104            "plain" => Ok(Self::Plain),
105            "colored" => Ok(Self::Colored),
106            s => Err(format!(
107                "Unknown option `{s}`, expected `plain` or `colored`",
108            )),
109        }
110    }
111}
112
113#[derive(Debug)]
132pub struct Libtest<W, Out: io::Write = io::Stdout> {
133    output: Out,
135
136    events: Vec<parser::Result<Event<event::Cucumber<W>>>>,
146
147    parsed_all: bool,
151
152    passed: usize,
156
157    failed: usize,
161
162    retried: usize,
166
167    ignored: usize,
171
172    parsing_errors: usize,
176
177    hook_errors: usize,
181
182    features_without_path: usize,
190
191    started_at: Option<SystemTime>,
195
196    step_started_at: Option<SystemTime>,
202}
203
204impl<World, Out: Clone + io::Write> Clone for Libtest<World, Out> {
207    fn clone(&self) -> Self {
208        Self {
209            output: self.output.clone(),
210            events: self.events.clone(),
211            parsed_all: self.parsed_all,
212            passed: self.passed,
213            failed: self.failed,
214            retried: self.retried,
215            ignored: self.ignored,
216            parsing_errors: self.parsing_errors,
217            hook_errors: self.hook_errors,
218            features_without_path: self.features_without_path,
219            started_at: self.started_at,
220            step_started_at: self.step_started_at,
221        }
222    }
223}
224
225impl<W: World + Debug, Out: io::Write> Writer<W> for Libtest<W, Out> {
226    type Cli = Cli;
227
228    async fn handle_event(
229        &mut self,
230        event: parser::Result<Event<event::Cucumber<W>>>,
231        cli: &Self::Cli,
232    ) {
233        self.handle_cucumber_event(event, cli);
234    }
235}
236
237pub type Or<W, Wr> = writer::Or<
239    Wr,
240    Normalize<W, Libtest<W, io::Stdout>>,
241    fn(
242        &parser::Result<Event<event::Cucumber<W>>>,
243        &cli::Compose<<Wr as Writer<W>>::Cli, Cli>,
244    ) -> bool,
245>;
246
247pub type OrBasic<W> = Or<W, Summarize<Normalize<W, writer::Basic>>>;
249
250impl<W: Debug + World> Libtest<W, io::Stdout> {
251    #[must_use]
256    pub fn stdout() -> Normalize<W, Self> {
257        Self::new(io::stdout())
258    }
259
260    #[must_use]
266    pub fn or<AnotherWriter: Writer<W>>(
267        writer: AnotherWriter,
268    ) -> Or<W, AnotherWriter> {
269        Or::new(writer, Self::stdout(), |_, cli| {
270            !matches!(cli.right.format, Some(Format::Json))
271        })
272    }
273
274    #[must_use]
281    pub fn or_basic() -> OrBasic<W> {
282        Self::or(writer::Basic::stdout().summarized())
283    }
284}
285
286impl<W: Debug + World, Out: io::Write> Libtest<W, Out> {
287    #[must_use]
300    pub fn new(output: Out) -> Normalize<W, Self> {
301        Self::raw(output).normalized()
302    }
303
304    #[must_use]
317    pub const fn raw(output: Out) -> Self {
318        Self {
319            output,
320            events: Vec::new(),
321            parsed_all: false,
322            passed: 0,
323            failed: 0,
324            retried: 0,
325            parsing_errors: 0,
326            hook_errors: 0,
327            ignored: 0,
328            features_without_path: 0,
329            started_at: None,
330            step_started_at: None,
331        }
332    }
333
334    fn handle_cucumber_event(
344        &mut self,
345        event: parser::Result<Event<event::Cucumber<W>>>,
346        cli: &Cli,
347    ) {
348        use event::{Cucumber, Metadata};
349
350        let unite = |ev: Result<(Cucumber<W>, Metadata), _>| {
351            ev.map(|(e, m)| m.insert(e))
352        };
353
354        match (event.map(Event::split), self.parsed_all) {
355            (event @ Ok((Cucumber::ParsingFinished { .. }, _)), false) => {
356                self.parsed_all = true;
357
358                let all_events =
359                    iter::once(unite(event)).chain(mem::take(&mut self.events));
360                for ev in all_events {
361                    self.output_event(ev, cli);
362                }
363            }
364            (event, false) => self.events.push(unite(event)),
365            (event, true) => self.output_event(unite(event), cli),
366        }
367    }
368
369    fn output_event(
371        &mut self,
372        event: parser::Result<Event<event::Cucumber<W>>>,
373        cli: &Cli,
374    ) {
375        for ev in self.expand_cucumber_event(event, cli) {
376            self.output
377                .write_line(serde_json::to_string(&ev).unwrap_or_else(|e| {
378                    panic!("Failed to serialize `LibTestJsonEvent`: {e}")
379                }))
380                .unwrap_or_else(|e| panic!("Failed to write: {e}"));
381        }
382    }
383
384    fn expand_cucumber_event(
386        &mut self,
387        event: parser::Result<Event<event::Cucumber<W>>>,
388        cli: &Cli,
389    ) -> Vec<LibTestJsonEvent> {
390        use event::Cucumber;
391
392        match event.map(Event::split) {
393            Ok((Cucumber::Started, meta)) => {
394                self.started_at = Some(meta.at);
395                Vec::new()
396            }
397            Ok((
398                Cucumber::ParsingFinished {
399                    steps,
400                    parser_errors,
401                    ..
402                },
403                _,
404            )) => {
405                vec![SuiteEvent::Started {
406                    test_count: steps + parser_errors,
407                }
408                .into()]
409            }
410            Ok((Cucumber::Finished, meta)) => {
411                let exec_time = self
412                    .started_at
413                    .and_then(|started| meta.at.duration_since(started).ok())
414                    .as_ref()
415                    .map(Duration::as_secs_f64);
416
417                let failed =
418                    self.failed + self.parsing_errors + self.hook_errors;
419                let results = SuiteResults {
420                    passed: self.passed,
421                    failed,
422                    ignored: self.ignored,
423                    measured: 0,
424                    filtered_out: 0,
425                    exec_time,
426                };
427                let ev = if failed == 0 {
428                    SuiteEvent::Ok { results }
429                } else {
430                    SuiteEvent::Failed { results }
431                }
432                .into();
433
434                vec![ev]
435            }
436            Ok((Cucumber::Feature(feature, ev), meta)) => {
437                self.expand_feature_event(&feature, ev, meta, cli)
438            }
439            Err(e) => {
440                self.parsing_errors += 1;
441
442                let path = match &e {
443                    parser::Error::Parsing(e) => match &**e {
444                        gherkin::ParseFileError::Parsing { path, .. }
445                        | gherkin::ParseFileError::Reading { path, .. } => {
446                            Some(path)
447                        }
448                    },
449                    parser::Error::ExampleExpansion(e) => e.path.as_ref(),
450                };
451                let name = path.and_then(|p| p.to_str()).map_or_else(
452                    || self.parsing_errors.to_string(),
453                    |p| p.escape_default().to_string(),
454                );
455                let name = format!("Feature: Parsing {name}");
456
457                vec![
458                    TestEvent::started(name.clone()).into(),
459                    TestEvent::failed(name, None)
460                        .with_stdout(e.to_string())
461                        .into(),
462                ]
463            }
464        }
465    }
466
467    fn expand_feature_event(
469        &mut self,
470        feature: &gherkin::Feature,
471        ev: event::Feature<W>,
472        meta: event::Metadata,
473        cli: &Cli,
474    ) -> Vec<LibTestJsonEvent> {
475        use event::{Feature, Rule};
476
477        match ev {
478            Feature::Started
479            | Feature::Finished
480            | Feature::Rule(_, Rule::Started | Rule::Finished) => Vec::new(),
481            Feature::Rule(rule, Rule::Scenario(scenario, ev)) => self
482                .expand_scenario_event(
483                    feature,
484                    Some(&rule),
485                    &scenario,
486                    ev,
487                    meta,
488                    cli,
489                ),
490            Feature::Scenario(scenario, ev) => self
491                .expand_scenario_event(feature, None, &scenario, ev, meta, cli),
492        }
493    }
494
495    fn expand_scenario_event(
497        &mut self,
498        feature: &gherkin::Feature,
499        rule: Option<&gherkin::Rule>,
500        scenario: &gherkin::Scenario,
501        ev: event::RetryableScenario<W>,
502        meta: event::Metadata,
503        cli: &Cli,
504    ) -> Vec<LibTestJsonEvent> {
505        use event::Scenario;
506
507        let retries = ev.retries;
508        match ev.event {
509            Scenario::Started | Scenario::Finished => Vec::new(),
510            Scenario::Hook(ty, ev) => self.expand_hook_event(
511                feature, rule, scenario, ty, ev, retries, meta, cli,
512            ),
513            Scenario::Background(step, ev) => self.expand_step_event(
514                feature, rule, scenario, &step, ev, retries, true, meta, cli,
515            ),
516            Scenario::Step(step, ev) => self.expand_step_event(
517                feature, rule, scenario, &step, ev, retries, false, meta, cli,
518            ),
519            #[allow(clippy::print_stdout)] Scenario::Log(msg) => {
526                print!("{msg}");
527                vec![]
528            }
529        }
530    }
531
532    #[allow(clippy::too_many_arguments)] fn expand_hook_event(
535        &mut self,
536        feature: &gherkin::Feature,
537        rule: Option<&gherkin::Rule>,
538        scenario: &gherkin::Scenario,
539        hook: event::HookType,
540        ev: event::Hook<W>,
541        retries: Option<Retries>,
542        meta: event::Metadata,
543        cli: &Cli,
544    ) -> Vec<LibTestJsonEvent> {
545        match ev {
546            event::Hook::Started => {
547                self.step_started_at(meta, cli);
548                Vec::new()
549            }
550            event::Hook::Passed => Vec::new(),
551            event::Hook::Failed(world, info) => {
552                self.hook_errors += 1;
553
554                let name = self.test_case_name(
555                    feature,
556                    rule,
557                    scenario,
558                    Either::Left(hook),
559                    retries,
560                );
561
562                vec![
563                    TestEvent::started(name.clone()).into(),
564                    TestEvent::failed(name, self.step_exec_time(meta, cli))
565                        .with_stdout(format!(
566                            "{}{}",
567                            coerce_error(&info),
568                            world
569                                .map(|w| format!("\n{w:#?}"))
570                                .unwrap_or_default(),
571                        ))
572                        .into(),
573                ]
574            }
575        }
576    }
577
578    #[allow(clippy::too_many_arguments)] fn expand_step_event(
581        &mut self,
582        feature: &gherkin::Feature,
583        rule: Option<&gherkin::Rule>,
584        scenario: &gherkin::Scenario,
585        step: &gherkin::Step,
586        ev: event::Step<W>,
587        retries: Option<Retries>,
588        is_background: bool,
589        meta: event::Metadata,
590        cli: &Cli,
591    ) -> Vec<LibTestJsonEvent> {
592        use event::Step;
593
594        let name = self.test_case_name(
595            feature,
596            rule,
597            scenario,
598            Either::Right((step, is_background)),
599            retries,
600        );
601
602        let ev = match ev {
603            Step::Started => {
604                self.step_started_at(meta, cli);
605                TestEvent::started(name)
606            }
607            Step::Passed(_, loc) => {
608                self.passed += 1;
609
610                let event = TestEvent::ok(name, self.step_exec_time(meta, cli));
611                if cli.show_output {
612                    event.with_stdout(format!(
613                        "{}:{}:{} (defined){}",
614                        feature
615                            .path
616                            .as_ref()
617                            .and_then(|p| p.to_str().map(trim_path))
618                            .unwrap_or(&feature.name),
619                        step.position.line,
620                        step.position.col,
621                        loc.map(|l| format!(
622                            "\n{}:{}:{} (matched)",
623                            l.path, l.line, l.column,
624                        ))
625                        .unwrap_or_default()
626                    ))
627                } else {
628                    event
629                }
630            }
631            Step::Skipped => {
632                self.ignored += 1;
633
634                let event =
635                    TestEvent::ignored(name, self.step_exec_time(meta, cli));
636                if cli.show_output {
637                    event.with_stdout(format!(
638                        "{}:{}:{} (defined)",
639                        feature
640                            .path
641                            .as_ref()
642                            .and_then(|p| p.to_str().map(trim_path))
643                            .unwrap_or(&feature.name),
644                        step.position.line,
645                        step.position.col,
646                    ))
647                } else {
648                    event
649                }
650            }
651            Step::Failed(_, loc, world, err) => {
652                if retries.is_some_and(|r| {
653                    r.left > 0 && !matches!(err, event::StepError::NotFound)
654                }) {
655                    self.retried += 1;
656                } else {
657                    self.failed += 1;
658                }
659
660                TestEvent::failed(name, self.step_exec_time(meta, cli))
661                    .with_stdout(format!(
662                        "{}:{}:{} (defined){}\n{err}{}",
663                        feature
664                            .path
665                            .as_ref()
666                            .and_then(|p| p.to_str().map(trim_path))
667                            .unwrap_or(&feature.name),
668                        step.position.line,
669                        step.position.col,
670                        loc.map(|l| format!(
671                            "\n{}:{}:{} (matched)",
672                            l.path, l.line, l.column,
673                        ))
674                        .unwrap_or_default(),
675                        world.map(|w| format!("\n{w:#?}")).unwrap_or_default(),
676                    ))
677            }
678        };
679
680        vec![ev.into()]
681    }
682
683    fn test_case_name(
685        &mut self,
686        feature: &gherkin::Feature,
687        rule: Option<&gherkin::Rule>,
688        scenario: &gherkin::Scenario,
689        step: Either<event::HookType, (&gherkin::Step, IsBackground)>,
690        retries: Option<Retries>,
691    ) -> String {
692        let feature_name = format!(
693            "{}: {} {}",
694            feature.keyword,
695            feature.name,
696            feature
697                .path
698                .as_ref()
699                .and_then(|p| p.to_str().map(trim_path))
700                .map_or_else(
701                    || {
702                        self.features_without_path += 1;
703                        self.features_without_path.to_string()
704                    },
705                    |s| s.escape_default().to_string()
706                ),
707        );
708        let rule_name = rule
709            .as_ref()
710            .map(|r| format!("{}: {}: {}", r.position.line, r.keyword, r.name));
711        let scenario_name = format!(
712            "{}: {}: {}{}",
713            scenario.position.line,
714            scenario.keyword,
715            scenario.name,
716            retries
717                .filter(|r| r.current > 0)
718                .map(|r| format!(
719                    " | Retry attempt {}/{}",
720                    r.current,
721                    r.current + r.left,
722                ))
723                .unwrap_or_default(),
724        );
725        let step_name = match step {
726            Either::Left(hook) => format!("{hook} hook"),
727            Either::Right((step, is_bg)) => format!(
728                "{}: {} {}{}",
729                step.position.line,
730                is_bg
731                    .then(|| feature
732                        .background
733                        .as_ref()
734                        .map_or("Background", |bg| bg.keyword.as_str()))
735                    .unwrap_or_default(),
736                step.keyword,
737                step.value,
738            ),
739        };
740
741        [
742            Some(feature_name),
743            rule_name,
744            Some(scenario_name),
745            Some(step_name),
746        ]
747        .into_iter()
748        .flatten()
749        .join("::")
750    }
751
752    fn step_started_at(&mut self, meta: event::Metadata, cli: &Cli) {
756        self.step_started_at =
757            Some(meta.at).filter(|_| cli.report_time.is_some());
758    }
759
760    fn step_exec_time(
763        &mut self,
764        meta: event::Metadata,
765        cli: &Cli,
766    ) -> Option<Duration> {
767        self.step_started_at.take().and_then(|started| {
768            meta.at
769                .duration_since(started)
770                .ok()
771                .filter(|_| cli.report_time.is_some())
772        })
773    }
774}
775
776type IsBackground = bool;
781
782impl<W, O: io::Write> writer::NonTransforming for Libtest<W, O> {}
783
784impl<W, O> writer::Stats<W> for Libtest<W, O>
785where
786    O: io::Write,
787    Self: Writer<W>,
788{
789    fn passed_steps(&self) -> usize {
790        self.passed
791    }
792
793    fn skipped_steps(&self) -> usize {
794        self.ignored
795    }
796
797    fn failed_steps(&self) -> usize {
798        self.failed
799    }
800
801    fn retried_steps(&self) -> usize {
802        self.retried
803    }
804
805    fn parsing_errors(&self) -> usize {
806        self.parsing_errors
807    }
808
809    fn hook_errors(&self) -> usize {
810        self.hook_errors
811    }
812}
813
814impl<W, Val, Out> Arbitrary<W, Val> for Libtest<W, Out>
815where
816    W: World + Debug,
817    Val: AsRef<str>,
818    Out: io::Write,
819{
820    async fn write(&mut self, val: Val) {
821        self.output
822            .write_line(val.as_ref())
823            .unwrap_or_else(|e| panic!("failed to write: {e}"));
824    }
825}
826
827#[derive(Clone, Debug, From, Serialize)]
834#[serde(tag = "type", rename_all = "snake_case")]
835enum LibTestJsonEvent {
836    Suite {
838        #[serde(flatten)]
840        event: SuiteEvent,
841    },
842
843    Test {
845        #[serde(flatten)]
847        event: TestEvent,
848    },
849}
850
851#[derive(Clone, Debug, Serialize)]
853#[serde(tag = "event", rename_all = "snake_case")]
854enum SuiteEvent {
855    Started {
857        test_count: usize,
863    },
864
865    Ok {
867        #[serde(flatten)]
869        results: SuiteResults,
870    },
871
872    Failed {
874        #[serde(flatten)]
876        results: SuiteResults,
877    },
878}
879
880#[derive(Clone, Copy, Debug, Serialize)]
882struct SuiteResults {
883    passed: usize,
885
886    failed: usize,
888
889    ignored: usize,
891
892    measured: usize,
894
895    filtered_out: usize,
898
899    #[serde(skip_serializing_if = "Option::is_none")]
901    exec_time: Option<f64>,
902}
903
904#[derive(Clone, Debug, Serialize)]
906#[serde(tag = "event", rename_all = "snake_case")]
907enum TestEvent {
908    Started(TestEventInner),
910
911    Ok(TestEventInner),
913
914    Failed(TestEventInner),
916
917    Ignored(TestEventInner),
919
920    Timeout(TestEventInner),
922}
923
924impl TestEvent {
925    const fn started(name: String) -> Self {
927        Self::Started(TestEventInner::new(name))
928    }
929
930    fn ok(name: String, exec_time: Option<Duration>) -> Self {
932        Self::Ok(TestEventInner::new(name).with_exec_time(exec_time))
933    }
934
935    fn failed(name: String, exec_time: Option<Duration>) -> Self {
937        Self::Failed(TestEventInner::new(name).with_exec_time(exec_time))
938    }
939
940    fn ignored(name: String, exec_time: Option<Duration>) -> Self {
942        Self::Ignored(TestEventInner::new(name).with_exec_time(exec_time))
943    }
944
945    #[allow(dead_code)]
947    fn timeout(name: String, exec_time: Option<Duration>) -> Self {
948        Self::Timeout(TestEventInner::new(name).with_exec_time(exec_time))
949    }
950
951    fn with_stdout(self, mut stdout: String) -> Self {
953        if !stdout.ends_with('\n') {
954            stdout.push('\n');
955        }
956
957        match self {
958            Self::Started(inner) => Self::Started(inner.with_stdout(stdout)),
959            Self::Ok(inner) => Self::Ok(inner.with_stdout(stdout)),
960            Self::Failed(inner) => Self::Failed(inner.with_stdout(stdout)),
961            Self::Ignored(inner) => Self::Ignored(inner.with_stdout(stdout)),
962            Self::Timeout(inner) => Self::Timeout(inner.with_stdout(stdout)),
963        }
964    }
965}
966
967#[derive(Clone, Debug, Serialize)]
969struct TestEventInner {
970    name: String,
972
973    #[serde(skip_serializing_if = "Option::is_none")]
977    stdout: Option<String>,
978
979    #[serde(skip_serializing_if = "Option::is_none")]
986    stderr: Option<String>,
987
988    #[serde(skip_serializing_if = "Option::is_none")]
990    exec_time: Option<f64>,
991}
992
993impl TestEventInner {
994    const fn new(name: String) -> Self {
996        Self {
997            name,
998            stdout: None,
999            stderr: None,
1000            exec_time: None,
1001        }
1002    }
1003
1004    fn with_exec_time(mut self, exec_time: Option<Duration>) -> Self {
1006        self.exec_time = exec_time.as_ref().map(Duration::as_secs_f64);
1007        self
1008    }
1009
1010    fn with_stdout(mut self, stdout: String) -> Self {
1012        self.stdout = Some(stdout);
1013        self
1014    }
1015}