Skip to main content

rstest_bdd/registry/
mod.rs

1//! Step registration and lookup.
2//! This module defines the `Step` record, the `step!` macro for registration,
3//! and the global registry used to find steps by keyword and pattern or by
4//! placeholder matching.
5
6use 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/// Represents a single step definition registered with the framework.
26#[derive(Debug)]
27pub struct Step {
28    /// The step keyword, e.g. `Given` or `When`.
29    pub keyword: StepKeyword,
30    /// Pattern text used to match a Gherkin step.
31    pub pattern: &'static StepPattern,
32    /// Function pointer executed when the step is invoked (sync mode).
33    pub run: StepFn,
34    /// Function pointer executed when the step is invoked (async mode).
35    ///
36    /// For sync step definitions, this wraps the result in an immediately-ready
37    /// future, enabling mixed sync and async steps within async scenarios.
38    pub run_async: AsyncStepFn,
39    /// Whether the step has a native sync body, a native async body, or both.
40    pub execution_mode: StepExecutionMode,
41    /// Names of fixtures this step requires.
42    pub fixtures: &'static [&'static str],
43    /// Source file where the step is defined.
44    pub file: &'static str,
45    /// Line number within the source file.
46    pub line: u32,
47}
48
49/// Register a step definition with the global registry.
50///
51/// This macro accepts both sync and async handler function pointers. The async
52/// handler wraps the sync result in an immediately-ready future for sync step
53/// definitions, enabling unified execution in async scenarios.
54///
55/// # Forms
56///
57/// The macro supports two forms:
58///
59/// ## 5-argument form (explicit async handler)
60///
61/// ```ignore
62/// step!(keyword, pattern, sync_handler, async_handler, fixtures);
63/// // With explicit execution mode:
64/// step!(
65///     keyword, pattern, sync_handler, async_handler, fixtures,
66///     mode = StepExecutionMode::Async
67/// );
68/// ```
69///
70/// ## 4-argument form (auto-generated async handler)
71///
72/// ```ignore
73/// step!(keyword, pattern, sync_handler, &fixtures);
74/// // With explicit execution mode:
75/// step!(
76///     keyword, pattern, sync_handler, &fixtures,
77///     mode = StepExecutionMode::Sync
78/// );
79/// ```
80///
81/// The 4-argument form automatically generates an async wrapper that delegates
82/// to the sync handler via an immediately-ready future. This provides backward
83/// compatibility for call sites that only define sync handlers.
84///
85/// When the `mode` parameter is omitted, both forms default to
86/// [`StepExecutionMode::Both`].
87#[macro_export]
88macro_rules! step {
89    // Internal arm: 5 arguments with pre-compiled pattern reference
90    (@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    // Internal arm: 4 arguments with pre-compiled pattern reference (auto-generate async)
107    (@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    // Public arm: 4 arguments (auto-generate async handler for backward compatibility)
133    // This arm MUST come before the 5-argument arm because Rust macro matching
134    // is greedy and would otherwise try to parse fixtures as an async_handler path.
135    ($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    // Public arm: 4 arguments defaulting to Both.
148    ($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    // Public arm: 5 arguments (explicit async handler)
161    ($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    // Public arm: 5 arguments defaulting to Both.
175    ($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
220// Tracks step invocations for the lifetime of the current process only. The
221// data is not persisted across binaries, keeping usage bookkeeping lightweight
222// and ephemeral.
223static 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    // Compute the hash as if the key were (keyword, pattern.as_str()) because
242    // StepPattern hashing is by its inner text.
243    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    // Fast path: exact pattern match
259    if let Some(step) = resolve_exact_step(keyword, text.as_str().into()) {
260        return Some(step);
261    }
262
263    // Find the most specific matching step directly via iterator
264    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
274/// Compute the specificity score for a step, logging any errors.
275fn 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/// Look up a registered step by keyword and pattern.
286#[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/// Find a registered step whose pattern matches the provided text.
295#[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/// Look up a registered async step by keyword and pattern.
304///
305/// Returns the async step function pointer for use in async scenario execution.
306/// The async wrapper returns an immediately-ready future for sync step
307/// definitions.
308#[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/// Find a registered async step whose pattern matches the provided text.
317///
318/// Returns the async step function pointer for use in async scenario execution.
319/// The async wrapper returns an immediately-ready future for sync step
320/// definitions.
321#[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/// Find a registered step and return its full metadata.
330///
331/// Unlike [`find_step`], this function returns the entire [`Step`] struct,
332/// providing access to the step's required fixtures, source location, and
333/// other metadata. This is useful for fixture validation and error reporting.
334///
335/// # Examples
336///
337/// ```ignore
338/// use rstest_bdd::{find_step_with_metadata, StepKeyword, StepText};
339///
340/// if let Some(step) = find_step_with_metadata(StepKeyword::Given, StepText::from("a value")) {
341///     println!("Step requires fixtures: {:?}", step.fixtures);
342///     // Invoke the step function
343///     let result = (step.run)(&mut ctx, text, None, None);
344/// }
345/// ```
346#[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/// Return registered steps that were never executed.
354#[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/// Group step definitions that share a keyword and pattern.
366#[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/// Serialize the registry to a JSON array.
379///
380/// Each entry records the step keyword, pattern, source location, and whether
381/// the step has been executed. The JSON is intended for consumption by
382/// diagnostic tooling such as `cargo bdd`.
383///
384/// # Errors
385///
386/// Returns an error if serialization fails.
387///
388/// # Examples
389///
390/// ```
391/// use rstest_bdd::dump_registry;
392///
393/// let json = dump_registry().expect("serialize registry");
394/// assert!(json.contains("\"steps\""));
395/// ```
396#[cfg(feature = "diagnostics")]
397pub fn dump_registry() -> serde_json::Result<String> {
398    diagnostics::dump_registry()
399}