use std::path::PathBuf;
use std::time::Duration;
use rustc_hash::FxHashMap;
use crate::feature::{ParsedFeature, extract_tags};
#[derive(Debug, Clone)]
pub struct ScenarioExecution {
pub feature_name: String,
pub feature_path: PathBuf,
pub name: String,
pub tags: Vec<String>,
pub steps: Vec<ScenarioStep>,
pub location: String,
pub example_values: Option<FxHashMap<String, String>>,
}
#[derive(Debug, Clone)]
pub struct ScenarioStep {
pub keyword: String,
pub text: String,
pub table: Option<crate::data_table::DataTable>,
pub docstring: Option<String>,
pub line: usize,
}
pub fn expand_feature(feature: &ParsedFeature) -> Vec<ScenarioExecution> {
let mut scenarios = Vec::new();
let feature_tags = extract_tags(&feature.feature.tags);
let background_steps: Vec<ScenarioStep> = feature
.feature
.background
.as_ref()
.map(|bg| bg.steps.iter().map(gherkin_step_to_scenario_step).collect())
.unwrap_or_default();
for scenario in &feature.feature.scenarios {
let scenario_tags: Vec<String> = feature_tags
.iter()
.chain(extract_tags(&scenario.tags).iter())
.cloned()
.collect();
if scenario.examples.is_empty() {
let mut steps = background_steps.clone();
steps.extend(scenario.steps.iter().map(gherkin_step_to_scenario_step));
scenarios.push(ScenarioExecution {
feature_name: feature.feature.name.clone(),
feature_path: feature.path.clone(),
name: scenario.name.clone(),
tags: scenario_tags,
steps,
location: format!("{}:{}", feature.path.display(), scenario.position.line),
example_values: None,
});
} else {
for example in &scenario.examples {
let example_tags: Vec<String> = scenario_tags
.iter()
.chain(extract_tags(&example.tags).iter())
.cloned()
.collect();
if let Some(table) = &example.table {
if table.rows.len() < 2 {
continue;
}
let headers = &table.rows[0];
for (row_idx, row) in table.rows[1..].iter().enumerate() {
let mut values: FxHashMap<String, String> = FxHashMap::default();
for (i, header) in headers.iter().enumerate() {
if let Some(val) = row.get(i) {
values.insert(header.clone(), val.clone());
}
}
let mut steps = background_steps.clone();
steps.extend(scenario.steps.iter().map(|s| {
let mut step = gherkin_step_to_scenario_step(s);
step.text = substitute_placeholders(&step.text, &values);
if let Some(table) = &mut step.table {
for row in table.iter_mut() {
for cell in row.iter_mut() {
*cell = substitute_placeholders(cell, &values);
}
}
}
if let Some(ds) = &mut step.docstring {
*ds = substitute_placeholders(ds, &values);
}
step
}));
scenarios.push(ScenarioExecution {
feature_name: feature.feature.name.clone(),
feature_path: feature.path.clone(),
name: if let Some(ref ex_name) = example.name {
format!("{} ({} #{})", scenario.name, ex_name, row_idx + 1)
} else {
format!("{} (Example #{})", scenario.name, row_idx + 1)
},
tags: example_tags.clone(),
steps,
location: format!("{}:{}", feature.path.display(), scenario.position.line),
example_values: Some(values),
});
}
}
}
}
}
for rule in &feature.feature.rules {
let rule_background: Vec<ScenarioStep> = rule
.background
.as_ref()
.map(|bg| bg.steps.iter().map(gherkin_step_to_scenario_step).collect())
.unwrap_or_default();
for scenario in &rule.scenarios {
let scenario_tags: Vec<String> = feature_tags
.iter()
.chain(extract_tags(&scenario.tags).iter())
.cloned()
.collect();
let mut steps = background_steps.clone();
steps.extend(rule_background.clone());
steps.extend(scenario.steps.iter().map(gherkin_step_to_scenario_step));
scenarios.push(ScenarioExecution {
feature_name: feature.feature.name.clone(),
feature_path: feature.path.clone(),
name: format!("{} > {}", rule.name, scenario.name),
tags: scenario_tags,
steps,
location: format!("{}:{}", feature.path.display(), scenario.position.line),
example_values: None,
});
}
}
scenarios
}
fn gherkin_step_to_scenario_step(step: &gherkin::Step) -> ScenarioStep {
ScenarioStep {
keyword: step.keyword.clone(),
text: step.value.clone(),
table: step.table.as_ref().map(crate::feature::table_to_vec),
docstring: step.docstring.clone(),
line: step.position.line,
}
}
fn substitute_placeholders(text: &str, values: &FxHashMap<String, String>) -> String {
let mut result = text.to_string();
for (key, val) in values {
result = result.replace(&format!("<{key}>"), val);
}
result
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub enum StepStatus {
Passed,
Failed,
Skipped,
Undefined,
Pending,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub enum ScenarioStatus {
Passed,
Failed,
Skipped,
Undefined,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct StepResult {
pub keyword: String,
pub text: String,
pub status: StepStatus,
pub duration: Duration,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ScenarioResult {
pub feature_name: String,
pub feature_path: String,
pub scenario_name: String,
pub status: ScenarioStatus,
pub steps: Vec<StepResult>,
pub duration: Duration,
pub attempt: u32,
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip)]
pub failure_screenshot: Option<Vec<u8>>,
}
impl ScenarioResult {
pub fn should_retry(&self, max_retries: u32) -> bool {
self.status == ScenarioStatus::Failed && self.attempt < max_retries
}
}