1#![allow(
2 clippy::missing_errors_doc,
3 clippy::missing_panics_doc,
4 clippy::must_use_candidate,
5 clippy::doc_markdown,
6 clippy::module_name_repetitions,
7 clippy::cast_possible_truncation,
8 clippy::cast_precision_loss,
9 clippy::cast_sign_loss,
10 clippy::redundant_closure_for_method_calls,
11 clippy::implicit_clone,
12 clippy::too_many_lines,
13 clippy::uninlined_format_args,
14 clippy::type_complexity,
15 clippy::unnecessary_map_or,
16 clippy::match_same_arms,
17 clippy::should_implement_trait,
18 clippy::unnecessary_wraps,
19 clippy::unused_async,
20 clippy::items_after_statements,
21 clippy::needless_pass_by_value,
22 clippy::single_match_else,
23 clippy::vec_init_then_push,
24 clippy::from_over_into,
25 clippy::single_char_pattern,
26 clippy::ptr_arg,
27 clippy::unnecessary_sort_by,
28 clippy::collapsible_match,
29 clippy::if_same_then_else,
30 clippy::single_match
31)]
32#![allow(unused_imports)]
34extern crate self as ferridriver_bdd;
75
76pub use ferridriver_bdd_macros::{after, before, given, param_type, step, then, when};
78
79pub use inventory;
81
82pub mod data_table;
83pub mod executor;
84pub mod expression;
85pub mod feature;
86pub mod filter;
87pub mod hook;
88pub mod js;
89pub mod param_type;
90pub mod registry;
91pub mod scenario;
93pub mod snippet;
94pub mod step;
95pub mod steps;
96pub mod translate;
97pub mod world;
98
99pub mod prelude {
101 pub use crate::step::{DataTable, StepError, StepParam};
102 pub use crate::step_err;
103 pub use crate::world::BrowserWorld;
104
105 pub use ferridriver_bdd_macros::{after, before, given, param_type, step, then, when};
107
108 pub use ferridriver::Page;
110}
111
112#[macro_export]
114macro_rules! step_err {
115 ($($arg:tt)*) => {
116 $crate::step::StepError::from(format!($($arg)*))
117 };
118}
119
120#[macro_export]
150macro_rules! bdd_main {
151 () => {
152 fn main() {
153 $crate::run_bdd_harness();
154 }
155 };
156}
157
158pub fn run_bdd_harness() {
164 ferridriver_test::logging::init_from_env();
165
166 let rt = tokio::runtime::Builder::new_multi_thread()
167 .enable_all()
168 .build()
169 .expect("failed to build tokio runtime");
170
171 let exit_code = rt.block_on(async {
172 let overrides = ferridriver_test::parse_common_cli_args();
173 let config = ferridriver_test::config::resolve_config(&overrides).unwrap_or_else(|e| {
174 eprintln!("config error: {e}");
175 std::process::exit(1);
176 });
177 run_bdd_with(config, overrides).await
178 });
179
180 std::process::exit(exit_code);
181}
182
183pub async fn run_bdd_with(
192 mut config: ferridriver_test::config::TestConfig,
193 overrides: ferridriver_test::config::CliOverrides,
194) -> i32 {
195 use std::sync::Arc;
196
197 let feature_patterns = std::env::var("FERRIDRIVER_FEATURES")
200 .ok()
201 .map(|s| s.split(',').map(String::from).collect::<Vec<_>>())
202 .unwrap_or_else(|| vec!["features/**/*.feature".to_string()]);
203
204 if config.features.is_empty() {
205 config.features = feature_patterns;
206 }
207
208 if let Ok(tags) = std::env::var("FERRIDRIVER_TAGS") {
209 if config.tags.is_none() {
210 config.tags = Some(tags);
211 }
212 }
213
214 if let Some(ref tags) = overrides.bdd_tags {
216 config.tags = Some(tags.clone());
217 }
218 if overrides.bdd_dry_run {
219 config.dry_run = true;
220 }
221 if overrides.bdd_fail_fast {
222 config.fail_fast = true;
223 }
224 if let Some(t) = overrides.bdd_step_timeout {
225 config.timeout = t;
226 }
227 if overrides.bdd_strict {
228 config.strict = true;
229 }
230 if let Some(ref order) = overrides.bdd_order {
231 config.order = order.clone();
232 }
233 if overrides.bdd_language.is_some() {
234 config.language = overrides.bdd_language.clone();
235 }
236 if let Some(s) = overrides.world_parameters.as_deref() {
237 match serde_json::from_str::<serde_json::Value>(s) {
238 Ok(v) => config.world_parameters = v,
239 Err(e) => {
240 eprintln!("--world-parameters: invalid JSON: {e}");
241 return 1;
242 },
243 }
244 }
245
246 let feature_set = match feature::FeatureSet::discover_and_parse(&config.features, &config.test_ignore) {
247 Ok(fs) => fs,
248 Err(e) => {
249 eprintln!("feature discovery error: {e}");
250 return 1;
251 },
252 };
253
254 if feature_set.features.is_empty() {
255 eprintln!("no feature files found matching: {:?}", config.features);
256 return 0;
257 }
258
259 let js_globs: Vec<String> = if overrides.bdd_steps.is_empty() {
264 config.steps.clone()
265 } else {
266 overrides.bdd_steps.clone()
267 };
268 let extensions: Vec<String> = if overrides.extensions.is_empty() {
269 std::env::var("FERRIDRIVER_EXTENSIONS")
270 .ok()
271 .map(|s| {
272 s.split(',')
273 .map(str::trim)
274 .filter(|s| !s.is_empty())
275 .map(String::from)
276 .collect()
277 })
278 .unwrap_or_default()
279 } else {
280 overrides.extensions.clone()
281 };
282 let plan = if js_globs.is_empty() && extensions.is_empty() {
283 let registry = Arc::new(registry::StepRegistry::build());
284 translate::translate_features(&feature_set, registry, &config)
285 } else {
286 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
287 let bundle = match js::bundle_steps_with(&js_globs, &extensions, &cwd).await {
290 Ok(b) => b,
291 Err(e) => {
292 eprintln!("step bundle error: {e}");
293 return 1;
294 },
295 };
296 js::translate_features_js(&feature_set, &config, bundle, cwd)
297 };
298
299 if plan.total_tests == 0 {
300 eprintln!("no scenarios found");
301 return 0;
302 }
303
304 config.has_bdd = true;
305 let mut runner = ferridriver_test::runner::TestRunner::new(config, overrides);
306 runner.run(plan).await
307}