1use crate::pattern::StepPattern;
7use crate::placeholder::extract_placeholders;
8use crate::types::{AsyncStepFn, PatternStr, StepExecutionMode, StepFn, StepKeyword, StepText};
9use hashbrown::{HashMap, HashSet};
10use inventory::iter;
11use rstest_bdd_patterns::SpecificityScore;
12use std::hash::{BuildHasher, Hash, Hasher};
13use std::sync::{LazyLock, Mutex};
14
15mod async_lookup;
16mod bypassed;
17#[cfg(feature = "diagnostics")]
18pub(crate) mod diagnostics;
19
20pub use async_lookup::{
21 find_step_async_with_mode, find_step_with_mode, lookup_step_async_with_mode,
22};
23pub use bypassed::{record_bypassed_steps, record_bypassed_steps_with_tags};
24
25#[derive(Debug)]
27pub struct Step {
28 pub keyword: StepKeyword,
30 pub pattern: &'static StepPattern,
32 pub run: StepFn,
34 pub run_async: AsyncStepFn,
39 pub execution_mode: StepExecutionMode,
41 pub fixtures: &'static [&'static str],
43 pub file: &'static str,
45 pub line: u32,
47}
48
49#[macro_export]
88macro_rules! step {
89 (@pattern $keyword:expr, $pattern:expr, $handler:path, $async_handler:path, $fixtures:expr, $mode:expr) => {
91 const _: () = {
92 $crate::submit! {
93 $crate::Step {
94 keyword: $keyword,
95 pattern: $pattern,
96 run: $handler,
97 run_async: $async_handler,
98 execution_mode: $mode,
99 fixtures: $fixtures,
100 file: file!(),
101 line: line!(),
102 }
103 }
104 };
105 };
106 (@pattern $keyword:expr, $pattern:expr, $handler:path, $fixtures:expr, $mode:expr) => {
108 const _: () = {
109 fn __rstest_bdd_auto_async<'ctx, 'fixtures>(
110 ctx: &'ctx mut $crate::StepContext<'fixtures>,
111 text: &'ctx str,
112 docstring: ::core::option::Option<&'ctx str>,
113 table: ::core::option::Option<&'ctx [&'ctx [&'ctx str]]>,
114 ) -> $crate::StepFuture<'ctx> {
115 ::std::boxed::Box::pin(::std::future::ready($handler(ctx, text, docstring, table)))
116 }
117
118 $crate::submit! {
119 $crate::Step {
120 keyword: $keyword,
121 pattern: $pattern,
122 run: $handler,
123 run_async: __rstest_bdd_auto_async,
124 execution_mode: $mode,
125 fixtures: $fixtures,
126 file: file!(),
127 line: line!(),
128 }
129 }
130 };
131 };
132 ($keyword:expr, $pattern:expr, $handler:path, & $fixtures:expr, mode = $mode:expr $(,)?) => {
136 const _: () = {
137 static PATTERN: $crate::StepPattern = $crate::StepPattern::new($pattern);
138 $crate::step!(
139 @pattern $keyword,
140 &PATTERN,
141 $handler,
142 &$fixtures,
143 $mode
144 );
145 };
146 };
147 ($keyword:expr, $pattern:expr, $handler:path, & $fixtures:expr) => {
149 const _: () = {
150 static PATTERN: $crate::StepPattern = $crate::StepPattern::new($pattern);
151 $crate::step!(
152 @pattern $keyword,
153 &PATTERN,
154 $handler,
155 &$fixtures,
156 $crate::StepExecutionMode::Both
157 );
158 };
159 };
160 ($keyword:expr, $pattern:expr, $handler:path, $async_handler:path, $fixtures:expr, mode = $mode:expr $(,)?) => {
162 const _: () = {
163 static PATTERN: $crate::StepPattern = $crate::StepPattern::new($pattern);
164 $crate::step!(
165 @pattern $keyword,
166 &PATTERN,
167 $handler,
168 $async_handler,
169 $fixtures,
170 $mode
171 );
172 };
173 };
174 ($keyword:expr, $pattern:expr, $handler:path, $async_handler:path, $fixtures:expr) => {
176 const _: () = {
177 static PATTERN: $crate::StepPattern = $crate::StepPattern::new($pattern);
178 $crate::step!(
179 @pattern $keyword,
180 &PATTERN,
181 $handler,
182 $async_handler,
183 $fixtures,
184 $crate::StepExecutionMode::Both
185 );
186 };
187 };
188}
189
190inventory::collect!(Step);
191
192type StepKey = (StepKeyword, &'static StepPattern);
193
194static STEP_MAP: LazyLock<HashMap<StepKey, &'static Step>> = LazyLock::new(|| {
195 let steps: Vec<_> = iter::<Step>.into_iter().collect();
196 let mut map = HashMap::with_capacity(steps.len());
197 for step in steps {
198 step.pattern.compile().unwrap_or_else(|e| {
199 panic!(
200 "invalid step pattern '{}' at {}:{}: {e}",
201 step.pattern.as_str(),
202 step.file,
203 step.line
204 )
205 });
206 let key = (step.keyword, step.pattern);
207 assert!(
208 !map.contains_key(&key),
209 "duplicate step for '{}' + '{}' defined at {}:{}",
210 step.keyword.as_str(),
211 step.pattern.as_str(),
212 step.file,
213 step.line
214 );
215 map.insert(key, step);
216 }
217 map
218});
219
220static USED_STEPS: LazyLock<Mutex<HashSet<StepKey>>> = LazyLock::new(|| Mutex::new(HashSet::new()));
224
225fn mark_used(key: StepKey) {
226 USED_STEPS
227 .lock()
228 .unwrap_or_else(std::sync::PoisonError::into_inner)
229 .insert(key);
230}
231
232fn all_steps() -> Vec<&'static Step> {
233 iter::<Step>.into_iter().collect()
234}
235
236fn step_by_key(key: StepKey) -> Option<&'static Step> {
237 STEP_MAP.get(&key).copied()
238}
239
240fn resolve_exact_step(keyword: StepKeyword, pattern: PatternStr<'_>) -> Option<&'static Step> {
241 let build = STEP_MAP.hasher();
244 let mut state = build.build_hasher();
245 keyword.hash(&mut state);
246 pattern.as_str().hash(&mut state);
247 let hash = state.finish();
248
249 STEP_MAP
250 .raw_entry()
251 .from_hash(hash, |(kw, pat)| {
252 *kw == keyword && pat.as_str() == pattern.as_str()
253 })
254 .map(|(_, step)| *step)
255}
256
257fn resolve_step(keyword: StepKeyword, text: StepText<'_>) -> Option<&'static Step> {
258 if let Some(step) = resolve_exact_step(keyword, text.as_str().into()) {
260 return Some(step);
261 }
262
263 iter::<Step>
265 .into_iter()
266 .filter(|step| step.keyword == keyword && extract_placeholders(step.pattern, text).is_ok())
267 .max_by(|a, b| {
268 let a_spec = step_specificity(a);
269 let b_spec = step_specificity(b);
270 a_spec.cmp(&b_spec)
271 })
272}
273
274fn step_specificity(step: &Step) -> SpecificityScore {
276 step.pattern.specificity().unwrap_or_else(|e| {
277 log::warn!(
278 "specificity calculation failed for pattern '{}': {e}",
279 step.pattern.as_str()
280 );
281 SpecificityScore::default()
282 })
283}
284
285#[must_use]
287pub fn lookup_step(keyword: StepKeyword, pattern: PatternStr<'_>) -> Option<StepFn> {
288 resolve_exact_step(keyword, pattern).map(|step| {
289 mark_used((step.keyword, step.pattern));
290 step.run
291 })
292}
293
294#[must_use]
296pub fn find_step(keyword: StepKeyword, text: StepText<'_>) -> Option<StepFn> {
297 resolve_step(keyword, text).map(|step| {
298 mark_used((step.keyword, step.pattern));
299 step.run
300 })
301}
302
303#[must_use]
309pub fn lookup_step_async(keyword: StepKeyword, pattern: PatternStr<'_>) -> Option<AsyncStepFn> {
310 resolve_exact_step(keyword, pattern).map(|step| {
311 mark_used((step.keyword, step.pattern));
312 step.run_async
313 })
314}
315
316#[must_use]
322pub fn find_step_async(keyword: StepKeyword, text: StepText<'_>) -> Option<AsyncStepFn> {
323 resolve_step(keyword, text).map(|step| {
324 mark_used((step.keyword, step.pattern));
325 step.run_async
326 })
327}
328
329#[must_use]
347pub fn find_step_with_metadata(keyword: StepKeyword, text: StepText<'_>) -> Option<&'static Step> {
348 resolve_step(keyword, text).inspect(|step| {
349 mark_used((step.keyword, step.pattern));
350 })
351}
352
353#[must_use]
355pub fn unused_steps() -> Vec<&'static Step> {
356 let used = USED_STEPS
357 .lock()
358 .unwrap_or_else(std::sync::PoisonError::into_inner);
359 all_steps()
360 .into_iter()
361 .filter(|s| !used.contains(&(s.keyword, s.pattern)))
362 .collect()
363}
364
365#[must_use]
367pub fn duplicate_steps() -> Vec<Vec<&'static Step>> {
368 let mut groups: HashMap<StepKey, Vec<&'static Step>> = HashMap::new();
369 for step in all_steps() {
370 groups
371 .entry((step.keyword, step.pattern))
372 .or_default()
373 .push(step);
374 }
375 groups.into_values().filter(|v| v.len() > 1).collect()
376}
377
378#[cfg(feature = "diagnostics")]
397pub fn dump_registry() -> serde_json::Result<String> {
398 diagnostics::dump_registry()
399}