use std::{hash::Hash, mem};
use derive_more::with_trait::Deref;
use either::Either;
use linked_hash_map::LinkedHashMap;
use crate::{
Event, World, Writer,
event::{self, Metadata, Retries, Source},
parser, writer,
};
#[derive(Debug, Deref)]
pub struct Normalize<World, Writer> {
#[deref]
writer: Writer,
queue: CucumberQueue<World>,
}
impl<World, Writer: Clone> Clone for Normalize<World, Writer> {
fn clone(&self) -> Self {
Self { writer: self.writer.clone(), queue: self.queue.clone() }
}
}
impl<W, Writer> Normalize<W, Writer> {
#[must_use]
pub fn new(writer: Writer) -> Self {
Self { writer, queue: CucumberQueue::new(Metadata::new(())) }
}
#[must_use]
pub const fn inner_writer(&self) -> &Writer {
&self.writer
}
}
impl<World, Wr: Writer<World>> Writer<World> for Normalize<World, Wr> {
type Cli = Wr::Cli;
async fn handle_event(
&mut self,
event: parser::Result<Event<event::Cucumber<World>>>,
cli: &Self::Cli,
) {
use event::{Cucumber, Feature, Rule};
if self.queue.is_finished_and_emitted() {
self.writer.handle_event(event, cli).await;
return;
}
match event.map(Event::split) {
res @ (Err(_)
| Ok((
Cucumber::Started | Cucumber::ParsingFinished { .. },
_,
))) => {
self.writer
.handle_event(res.map(|(ev, meta)| meta.insert(ev)), cli)
.await;
}
Ok((Cucumber::Finished, meta)) => self.queue.finished(meta),
Ok((Cucumber::Feature(f, ev), meta)) => match ev {
Feature::Started => self.queue.new_feature(meta.wrap(f)),
Feature::Scenario(s, ev) => {
self.queue.insert_scenario_event(
&f,
None,
s,
meta.wrap(ev),
);
}
Feature::Finished => self.queue.feature_finished(meta.wrap(&f)),
Feature::Rule(r, ev) => match ev {
Rule::Started => self.queue.new_rule(&f, meta.wrap(r)),
Rule::Scenario(s, ev) => {
self.queue.insert_scenario_event(
&f,
Some(r),
s,
meta.wrap(ev),
);
}
Rule::Finished => {
self.queue.rule_finished(&f, meta.wrap(r));
}
},
},
}
while let Some(feature_to_remove) =
self.queue.emit((), &mut self.writer, cli).await
{
self.queue.remove(&feature_to_remove);
}
if let Some(meta) = self.queue.state.take_to_emit() {
self.writer
.handle_event(Ok(meta.wrap(Cucumber::Finished)), cli)
.await;
}
}
}
#[warn(clippy::missing_trait_methods)]
impl<W, Wr, Val> writer::Arbitrary<W, Val> for Normalize<W, Wr>
where
Wr: writer::Arbitrary<W, Val>,
{
async fn write(&mut self, val: Val) {
self.writer.write(val).await;
}
}
#[warn(clippy::missing_trait_methods)]
impl<W, Wr> writer::Stats<W> for Normalize<W, Wr>
where
Wr: writer::Stats<W>,
Self: Writer<W>,
{
fn passed_steps(&self) -> usize {
self.writer.passed_steps()
}
fn skipped_steps(&self) -> usize {
self.writer.skipped_steps()
}
fn failed_steps(&self) -> usize {
self.writer.failed_steps()
}
fn retried_steps(&self) -> usize {
self.writer.retried_steps()
}
fn parsing_errors(&self) -> usize {
self.writer.parsing_errors()
}
fn hook_errors(&self) -> usize {
self.writer.hook_errors()
}
fn execution_has_failed(&self) -> bool {
self.writer.execution_has_failed()
}
}
#[warn(clippy::missing_trait_methods)]
impl<W, Wr: writer::NonTransforming> writer::NonTransforming
for Normalize<W, Wr>
{
}
pub trait Normalized {}
impl<World, Writer> Normalized for Normalize<World, Writer> {}
#[derive(Clone, Copy, Debug, Deref)]
pub struct AssertNormalized<W: ?Sized>(W);
impl<Writer> AssertNormalized<Writer> {
#[must_use]
pub const fn new(writer: Writer) -> Self {
Self(writer)
}
}
#[warn(clippy::missing_trait_methods)]
impl<W: World, Wr: Writer<W> + ?Sized> Writer<W> for AssertNormalized<Wr> {
type Cli = Wr::Cli;
async fn handle_event(
&mut self,
event: parser::Result<Event<event::Cucumber<W>>>,
cli: &Self::Cli,
) {
self.0.handle_event(event, cli).await;
}
}
#[warn(clippy::missing_trait_methods)]
impl<W, Wr, Val> writer::Arbitrary<W, Val> for AssertNormalized<Wr>
where
W: World,
Wr: writer::Arbitrary<W, Val> + ?Sized,
{
async fn write(&mut self, val: Val) {
self.0.write(val).await;
}
}
#[warn(clippy::missing_trait_methods)]
impl<W, Wr> writer::Stats<W> for AssertNormalized<Wr>
where
Wr: writer::Stats<W>,
Self: Writer<W>,
{
fn passed_steps(&self) -> usize {
self.0.passed_steps()
}
fn skipped_steps(&self) -> usize {
self.0.skipped_steps()
}
fn failed_steps(&self) -> usize {
self.0.failed_steps()
}
fn retried_steps(&self) -> usize {
self.0.retried_steps()
}
fn parsing_errors(&self) -> usize {
self.0.parsing_errors()
}
fn hook_errors(&self) -> usize {
self.0.hook_errors()
}
fn execution_has_failed(&self) -> bool {
self.0.execution_has_failed()
}
}
#[warn(clippy::missing_trait_methods)]
impl<Wr: writer::NonTransforming> writer::NonTransforming
for AssertNormalized<Wr>
{
}
#[warn(clippy::missing_trait_methods)]
impl<Writer> Normalized for AssertNormalized<Writer> {}
#[derive(Clone, Debug)]
struct Queue<K: Eq + Hash, V> {
fifo: LinkedHashMap<K, V>,
initial: Option<Metadata>,
state: FinishedState,
}
impl<K: Eq + Hash, V> Queue<K, V> {
fn new(initial: Metadata) -> Self {
Self {
fifo: LinkedHashMap::new(),
initial: Some(initial),
state: FinishedState::NotFinished,
}
}
const fn finished(&mut self, meta: Metadata) {
self.state = FinishedState::FinishedButNotEmitted(meta);
}
const fn is_finished_and_emitted(&self) -> bool {
matches!(self.state, FinishedState::FinishedAndEmitted)
}
fn remove(&mut self, key: &K) {
drop(self.fifo.remove(key));
}
}
#[derive(Clone, Copy, Debug)]
enum FinishedState {
NotFinished,
FinishedButNotEmitted(Metadata),
FinishedAndEmitted,
}
impl FinishedState {
const fn take_to_emit(&mut self) -> Option<Metadata> {
let current = mem::replace(self, Self::FinishedAndEmitted);
if let Self::FinishedButNotEmitted(meta) = current {
Some(meta)
} else {
*self = current;
None
}
}
}
trait Emitter<World> {
type Current;
type Emitted;
type EmittedPath;
fn current_item(self) -> Option<Self::Current>;
fn emit<W: Writer<World>>(
self,
path: Self::EmittedPath,
writer: &mut W,
cli: &W::Cli,
) -> impl Future<Output = Option<Self::Emitted>>;
}
type CucumberQueue<World> =
Queue<Source<gherkin::Feature>, FeatureQueue<World>>;
impl<World> CucumberQueue<World> {
fn new_feature(&mut self, feat: Event<Source<gherkin::Feature>>) {
let (feat, meta) = feat.split();
drop(self.fifo.insert(feat, FeatureQueue::new(meta)));
}
fn feature_finished(&mut self, feat: Event<&Source<gherkin::Feature>>) {
let (feat, meta) = feat.split();
self.fifo
.get_mut(feat)
.unwrap_or_else(|| panic!("no `Feature: {}`", feat.name))
.finished(meta);
}
fn new_rule(
&mut self,
feat: &Source<gherkin::Feature>,
rule: Event<Source<gherkin::Rule>>,
) {
self.fifo
.get_mut(feat)
.unwrap_or_else(|| panic!("no `Feature: {}`", feat.name))
.new_rule(rule);
}
fn rule_finished(
&mut self,
feat: &Source<gherkin::Feature>,
rule: Event<Source<gherkin::Rule>>,
) {
self.fifo
.get_mut(feat)
.unwrap_or_else(|| panic!("no `Feature: {}`", feat.name))
.rule_finished(rule);
}
fn insert_scenario_event(
&mut self,
feat: &Source<gherkin::Feature>,
rule: Option<Source<gherkin::Rule>>,
scenario: Source<gherkin::Scenario>,
event: Event<event::RetryableScenario<World>>,
) {
self.fifo
.get_mut(feat)
.unwrap_or_else(|| panic!("no `Feature: {}`", feat.name))
.insert_scenario_event(rule, scenario, event.retries, event);
}
}
impl<'me, World> Emitter<World> for &'me mut CucumberQueue<World> {
type Current = (Source<gherkin::Feature>, &'me mut FeatureQueue<World>);
type Emitted = Source<gherkin::Feature>;
type EmittedPath = ();
fn current_item(self) -> Option<Self::Current> {
self.fifo.iter_mut().next().map(|(f, ev)| (f.clone(), ev))
}
async fn emit<W: Writer<World>>(
self,
(): (),
writer: &mut W,
cli: &W::Cli,
) -> Option<Self::Emitted> {
if let Some((f, events)) = self.current_item() {
if let Some(meta) = events.initial.take() {
writer
.handle_event(
Ok(meta
.wrap(event::Cucumber::feature_started(f.clone()))),
cli,
)
.await;
}
while let Some(scenario_or_rule_to_remove) =
events.emit(f.clone(), writer, cli).await
{
events.remove(&scenario_or_rule_to_remove);
}
if let Some(meta) = events.state.take_to_emit() {
writer
.handle_event(
Ok(meta.wrap(event::Cucumber::feature_finished(
f.clone(),
))),
cli,
)
.await;
return Some(f.clone());
}
}
None
}
}
type RuleOrScenario =
Either<Source<gherkin::Rule>, (Source<gherkin::Scenario>, Option<Retries>)>;
type RuleOrScenarioQueue<World> =
Either<RulesQueue<World>, ScenariosQueue<World>>;
type NextRuleOrScenario<'events, World> = Either<
(Source<gherkin::Rule>, &'events mut RulesQueue<World>),
(Source<gherkin::Scenario>, &'events mut ScenariosQueue<World>),
>;
type FeatureQueue<World> = Queue<RuleOrScenario, RuleOrScenarioQueue<World>>;
impl<World> FeatureQueue<World> {
fn new_rule(&mut self, rule: Event<Source<gherkin::Rule>>) {
let (rule, meta) = rule.split();
drop(
self.fifo.insert(
Either::Left(rule),
Either::Left(RulesQueue::new(meta)),
),
);
}
fn rule_finished(&mut self, rule: Event<Source<gherkin::Rule>>) {
let (rule, meta) = rule.split();
match self.fifo.get_mut(&Either::Left(rule)) {
Some(Either::Left(ev)) => {
ev.finished(meta);
}
Some(Either::Right(_)) | None => unreachable!(),
}
}
fn insert_scenario_event(
&mut self,
rule: Option<Source<gherkin::Rule>>,
scenario: Source<gherkin::Scenario>,
retries: Option<Retries>,
ev: Event<event::RetryableScenario<World>>,
) {
if let Some(r) = rule {
match self
.fifo
.get_mut(&Either::Left(r.clone()))
.unwrap_or_else(|| panic!("no `Rule: {}`", r.name))
{
Either::Left(rules) => rules
.fifo
.entry((scenario, retries))
.or_insert_with(ScenariosQueue::new)
.0
.push(ev),
Either::Right(_) => unreachable!(),
}
} else {
match self
.fifo
.entry(Either::Right((scenario, retries)))
.or_insert_with(|| Either::Right(ScenariosQueue::new()))
{
Either::Right(events) => events.0.push(ev),
Either::Left(_) => unreachable!(),
}
}
}
}
impl<'me, World> Emitter<World> for &'me mut FeatureQueue<World> {
type Current = NextRuleOrScenario<'me, World>;
type Emitted = RuleOrScenario;
type EmittedPath = Source<gherkin::Feature>;
fn current_item(self) -> Option<Self::Current> {
Some(match self.fifo.iter_mut().next()? {
(Either::Left(rule), Either::Left(events)) => {
Either::Left((rule.clone(), events))
}
(Either::Right((scenario, _)), Either::Right(events)) => {
Either::Right((scenario.clone(), events))
}
_ => unreachable!(),
})
}
async fn emit<W: Writer<World>>(
self,
path: Self::EmittedPath,
writer: &mut W,
cli: &W::Cli,
) -> Option<Self::Emitted> {
match self.current_item()? {
Either::Left((rule, events)) => {
events.emit((path, rule), writer, cli).await.map(Either::Left)
}
Either::Right((scenario, events)) => events
.emit((path, None, scenario), writer, cli)
.await
.map(Either::Right),
}
}
}
type RulesQueue<World> =
Queue<(Source<gherkin::Scenario>, Option<Retries>), ScenariosQueue<World>>;
impl<'me, World> Emitter<World> for &'me mut RulesQueue<World> {
type Current = (Source<gherkin::Scenario>, &'me mut ScenariosQueue<World>);
type Emitted = Source<gherkin::Rule>;
type EmittedPath = (Source<gherkin::Feature>, Source<gherkin::Rule>);
fn current_item(self) -> Option<Self::Current> {
self.fifo.iter_mut().next().map(|((sc, _), ev)| (sc.clone(), ev))
}
async fn emit<W: Writer<World>>(
self,
(feature, rule): Self::EmittedPath,
writer: &mut W,
cli: &W::Cli,
) -> Option<Self::Emitted> {
if let Some(meta) = self.initial.take() {
writer
.handle_event(
Ok(meta.wrap(event::Cucumber::rule_started(
feature.clone(),
rule.clone(),
))),
cli,
)
.await;
}
while let Some((scenario, events)) = self.current_item() {
if let Some(should_be_removed) = events
.emit(
(feature.clone(), Some(rule.clone()), scenario),
writer,
cli,
)
.await
{
self.remove(&should_be_removed);
} else {
break;
}
}
if let Some(meta) = self.state.take_to_emit() {
writer
.handle_event(
Ok(meta.wrap(event::Cucumber::rule_finished(
feature,
rule.clone(),
))),
cli,
)
.await;
return Some(rule);
}
None
}
}
#[derive(Debug)]
struct ScenariosQueue<World>(Vec<Event<event::RetryableScenario<World>>>);
impl<World> Clone for ScenariosQueue<World> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<World> ScenariosQueue<World> {
const fn new() -> Self {
Self(Vec::new())
}
}
impl<World> Emitter<World> for &mut ScenariosQueue<World> {
type Current = Event<event::RetryableScenario<World>>;
type Emitted = (Source<gherkin::Scenario>, Option<Retries>);
type EmittedPath = (
Source<gherkin::Feature>,
Option<Source<gherkin::Rule>>,
Source<gherkin::Scenario>,
);
fn current_item(self) -> Option<Self::Current> {
(!self.0.is_empty()).then(|| self.0.remove(0))
}
async fn emit<W: Writer<World>>(
self,
(feature, rule, scenario): Self::EmittedPath,
writer: &mut W,
cli: &W::Cli,
) -> Option<Self::Emitted> {
while let Some((ev, meta)) = self.current_item().map(Event::split) {
let should_be_removed =
matches!(ev.event, event::Scenario::Finished)
.then(|| ev.retries);
let ev = meta.wrap(event::Cucumber::scenario(
feature.clone(),
rule.clone(),
scenario.clone(),
ev,
));
writer.handle_event(Ok(ev), cli).await;
if let Some(retries) = should_be_removed {
return Some((scenario.clone(), retries));
}
}
None
}
}