Skip to main content

rstest_bdd/
lib.rs

1//! Core library for `rstest-bdd`.
2//! This crate exposes helper utilities used by behaviour tests. It also defines
3//! the global step registry used to orchestrate behaviour-driven tests.
4
5extern crate self as rstest_bdd;
6
7pub mod config;
8pub mod execution;
9mod macros;
10mod skip;
11
12/// Returns a greeting for the library.
13///
14/// # Examples
15///
16/// ```
17/// use rstest_bdd::greet;
18///
19/// assert_eq!(greet(), "Hello from rstest-bdd!");
20/// ```
21#[must_use]
22pub fn greet() -> &'static str {
23    "Hello from rstest-bdd!"
24}
25
26#[cfg(feature = "diagnostics")]
27use ctor::ctor;
28pub use i18n_embed::fluent::FluentLanguageLoader;
29pub use inventory::{iter, submit};
30
31pub mod async_step;
32mod context;
33pub mod datatable;
34pub mod localization;
35pub mod panic_support;
36mod pattern;
37mod placeholder;
38mod registry;
39pub mod reporting;
40mod skip_helpers;
41pub mod state;
42pub mod step_args;
43mod types;
44
45#[cfg(feature = "test-support")]
46pub mod test_support;
47
48pub use context::{FixtureRef, FixtureRefMut, StepContext};
49pub use localization::{
50    LocalizationError, Localizations, current_languages, install_localization_loader,
51    select_localizations,
52};
53pub use pattern::StepPattern;
54pub use placeholder::extract_placeholders;
55#[cfg(feature = "diagnostics")]
56pub use registry::dump_registry;
57pub use registry::record_bypassed_steps;
58pub use registry::record_bypassed_steps_with_tags;
59pub use registry::{
60    Step, duplicate_steps, find_step, find_step_async, find_step_async_with_mode,
61    find_step_with_metadata, find_step_with_mode, lookup_step, lookup_step_async,
62    lookup_step_async_with_mode, unused_steps,
63};
64
65/// Whether the crate was built with the `diagnostics` feature enabled.
66#[must_use]
67pub const fn diagnostics_enabled() -> bool {
68    cfg!(feature = "diagnostics")
69}
70#[doc(hidden)]
71pub use panic_support::catch_unwind_future as __rstest_bdd_catch_unwind_future;
72#[doc(hidden)]
73pub use skip::{
74    ScopeKind as __rstest_bdd_scope_kind, SkipRequest, StepScopeGuard as __rstest_bdd_scope_guard,
75    enter_scope as __rstest_bdd_enter_scope,
76    request_current_skip as __rstest_bdd_request_current_skip,
77};
78#[doc(hidden)]
79pub use skip_helpers::{
80    __rstest_bdd_assert_scenario_detail_flag, __rstest_bdd_assert_scenario_detail_message_absent,
81    __rstest_bdd_assert_scenario_detail_message_contains,
82    __rstest_bdd_assert_step_skipped_message_absent,
83    __rstest_bdd_assert_step_skipped_message_contains, __rstest_bdd_expect_skip_flag,
84    __rstest_bdd_expect_skip_message_absent, __rstest_bdd_expect_skip_message_contains,
85    __rstest_bdd_unwrap_step_skipped,
86};
87pub use state::{ScenarioState, Slot};
88pub use step_args::{StepArgs, StepArgsError};
89pub use types::{
90    AsyncStepFn, PatternStr, PlaceholderError, PlaceholderSyntaxError, StepCtx, StepDoc,
91    StepExecution, StepExecutionMode, StepFn, StepFuture, StepKeyword, StepKeywordParseError,
92    StepPatternError, StepTable, StepText, StepTextRef, UnsupportedStepType,
93};
94
95pub use execution::{ExecutionError, MissingFixturesDetails};
96
97#[cfg(feature = "diagnostics")]
98#[ctor]
99fn dump_steps() {
100    // Only activate when explicitly enabled by the diagnostics runner.
101    if std::env::var_os("RSTEST_BDD_DUMP_STEPS").is_some()
102        && std::env::args().any(|a| a == "--dump-steps")
103    {
104        reporting::run_dump_seeds();
105        #[expect(
106            clippy::print_stdout,
107            clippy::print_stderr,
108            reason = "registry dump is written to standard streams"
109        )]
110        {
111            match dump_registry() {
112                Ok(json) => println!("{json}"),
113                Err(e) => eprintln!("failed to serialize step registry: {e}"),
114            }
115        }
116        std::process::exit(0);
117    }
118}
119
120pub use panic_support::panic_message;
121
122#[doc(hidden)]
123#[must_use]
124pub fn __rstest_bdd_payload_from_value<T: std::any::Any>(
125    value: T,
126) -> Option<Box<dyn std::any::Any>> {
127    if std::any::TypeId::of::<T>() == std::any::TypeId::of::<()>() {
128        None
129    } else {
130        Some(Box::new(value) as Box<dyn std::any::Any>)
131    }
132}
133
134/// Error type produced by step wrappers.
135///
136/// The variants categorize the possible failure modes when invoking a step.
137#[derive(Debug, Clone, PartialEq, Eq)]
138#[non_exhaustive]
139pub enum StepError {
140    /// Raised when a required fixture is absent from the [`StepContext`].
141    MissingFixture {
142        /// Name of the missing fixture.
143        name: String,
144        /// Type of the missing fixture.
145        ty: String,
146        /// Step function that requested the fixture.
147        step: String,
148    },
149    /// Raised when the invoked step function returns an [`Err`] variant.
150    ExecutionError {
151        /// Pattern text used when invoking the step.
152        pattern: String,
153        /// Name of the step function.
154        function: String,
155        /// Error message produced by the step function.
156        message: String,
157    },
158    /// Raised when the step function panics during execution.
159    PanicError {
160        /// Pattern text used when invoking the step.
161        pattern: String,
162        /// Name of the step function.
163        function: String,
164        /// Panic payload converted to a string.
165        message: String,
166    },
167}
168
169// Macro that maps `StepError` variants to their Fluent identifiers without
170// repeating localization boilerplate in each match arm.
171macro_rules! step_error_message {
172    (
173        $self:expr,
174        $loader:expr,
175        $( $variant:ident { $( $field:ident ),* } => $id:literal ),+ $(,)?
176    ) => {{
177        match $self {
178            $(
179                Self::$variant { $( $field ),* } => {
180                    $crate::localization::message_with_loader($loader, $id, |args| {
181                        $( args.set(stringify!($field), $field.clone()); )*
182                    })
183                }
184            ),+
185        }
186    }};
187}
188
189impl StepError {
190    /// Render the error message using the provided Fluent loader.
191    ///
192    /// # Examples
193    /// ```
194    /// # use rstest_bdd::StepError;
195    /// # use rstest_bdd::localization::Localizations;
196    /// # use i18n_embed::fluent::{fluent_language_loader, FluentLanguageLoader};
197    /// # use unic_langid::langid;
198    /// let loader: FluentLanguageLoader = {
199    ///     let mut loader = fluent_language_loader!();
200    ///     i18n_embed::select(&loader, &Localizations, &[langid!("en-US")]).unwrap();
201    ///     loader
202    /// };
203    /// let error = StepError::MissingFixture {
204    ///     name: "db".into(),
205    ///     ty: "Pool".into(),
206    ///     step: "Given a database".into(),
207    /// };
208    /// let message = error.format_with_loader(&loader);
209    /// // Check the key components (name, type, step) rather than exact format.
210    /// assert!(message.contains("db"));
211    /// assert!(message.contains("Pool"));
212    /// assert!(message.contains("Given a database"));
213    /// ```
214    #[must_use]
215    pub fn format_with_loader(&self, loader: &FluentLanguageLoader) -> String {
216        step_error_message!(
217            self,
218            loader,
219            MissingFixture { name, ty, step } => "step-error-missing-fixture",
220            ExecutionError { pattern, function, message } => "step-error-execution",
221            PanicError { pattern, function, message } => "step-error-panic",
222        )
223    }
224}
225
226impl std::fmt::Display for StepError {
227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228        let message = localization::with_loader(|loader| self.format_with_loader(loader));
229        f.write_str(&message)
230    }
231}
232
233impl std::error::Error for StepError {}
234
235/// Convenient alias for fallible step return values.
236///
237/// The `#[given]`, `#[when]`, and `#[then]` macros recognise this alias when
238/// determining whether a step returns a `Result<..>` or a payload value.
239pub type StepResult<T, E = StepError> = Result<T, E>;