Skip to main content

rstest_bdd/execution/
error.rs

1//! Error types for step execution failures in BDD scenarios.
2//!
3//! This module defines the [`ExecutionError`] hierarchy used during BDD step
4//! execution. These errors are produced by the [`execute_step`] function and
5//! propagate through the generated scenario code to provide structured failure
6//! information.
7//!
8//! # Error Hierarchy
9//!
10//! The [`ExecutionError`] enum distinguishes between control flow signals and
11//! genuine failures:
12//!
13//! | Variant | Source | Typical Cause |
14//! |---------|--------|---------------|
15//! | [`Skip`][ExecutionError::Skip] | Step handler calls `rstest_bdd::skip!()` | Incomplete implementation, conditional logic |
16//! | [`StepNotFound`][ExecutionError::StepNotFound] | Registry lookup failure | Missing step definition (direct API use only) |
17//! | [`MissingFixtures`][ExecutionError::MissingFixtures] | Fixture validation | Required fixture not in context |
18//! | [`HandlerFailed`][ExecutionError::HandlerFailed] | Step handler returns `Err` | Assertion failure, runtime error |
19//!
20//! # Relationship to `StepKeyword`
21//!
22//! Several variants include [`StepKeyword`] to identify which Gherkin keyword
23//! (Given, When, Then, And, But, or star) triggered the error. This context
24//! helps locate failures in feature files.
25//!
26//! # Arc Usage
27//!
28//! The [`MissingFixtures`] variant wraps its details in [`Arc`] to keep the
29//! `Result<T, ExecutionError>` type compact. This avoids inflating the size of
30//! `Ok` variants with rarely-needed diagnostic data. Access the details through
31//! pattern matching or the [`MissingFixturesDetails`] struct.
32//!
33//! # Matching and Inspection
34//!
35//! Use [`is_skip()`][ExecutionError::is_skip] to distinguish control flow from
36//! failures:
37//!
38//! ```
39//! use rstest_bdd::execution::ExecutionError;
40//!
41//! fn handle_result(error: &ExecutionError) {
42//!     if error.is_skip() {
43//!         // Mark scenario as skipped, not failed
44//!         println!("Skipped: {:?}", error.skip_message());
45//!     } else {
46//!         // Treat as test failure
47//!         panic!("{error}");
48//!     }
49//! }
50//! ```
51//!
52//! For detailed inspection, pattern match on the variants:
53//!
54//! ```
55//! use rstest_bdd::execution::ExecutionError;
56//!
57//! fn inspect_error(error: &ExecutionError) {
58//!     match error {
59//!         ExecutionError::Skip { message } => {
60//!             println!("Skip reason: {message:?}");
61//!         }
62//!         ExecutionError::StepNotFound { keyword, text, .. } => {
63//!             println!("No step matches '{} {text}'", keyword.as_str());
64//!         }
65//!         ExecutionError::MissingFixtures(details) => {
66//!             println!("Missing: {:?}", details.missing);
67//!         }
68//!         ExecutionError::HandlerFailed { error, .. } => {
69//!             println!("Handler error: {error}");
70//!         }
71//!         _ => println!("Unknown error variant"),
72//!     }
73//! }
74//! ```
75//!
76//! # Propagation and Logging
77//!
78//! Generated scenario code calls [`execute_step`] for each step and checks the
79//! result. Skip errors are extracted via `__rstest_bdd_extract_skip_message`
80//! and recorded for reporting. Non-skip errors cause an immediate panic with
81//! the [`Display`] output, which includes localized messages via the i18n
82//! system.
83//!
84//! Callers using the step registry directly should:
85//!
86//! 1. Check [`is_skip()`][ExecutionError::is_skip] to separate skips from failures
87//! 2. Use [`skip_message()`][ExecutionError::skip_message] to extract optional skip reasons
88//! 3. Use [`Display`] or [`format_with_loader()`][ExecutionError::format_with_loader]
89//!    for user-facing messages
90//! 4. Access [`std::error::Error::source()`] on `HandlerFailed` to inspect the
91//!    underlying [`StepError`]
92//!
93//! [`execute_step`]: crate::execution::execute_step
94//! [`Display`]: std::fmt::Display
95
96use std::sync::Arc;
97
98use crate::{StepError, StepKeyword};
99
100/// Error type for step execution failures.
101///
102/// This enum captures all failure modes during step execution, distinguishing
103/// between control flow signals (skip requests) and actual errors (missing steps,
104/// fixture validation failures, handler errors).
105///
106/// # Variants
107///
108/// - [`Skip`][Self::Skip]: Control flow signal indicating the step requested
109///   skipping. This is not an error condition but a deliberate execution path.
110/// - [`StepNotFound`][Self::StepNotFound]: The step pattern was not found in
111///   the registry.
112/// - [`MissingFixtures`][Self::MissingFixtures]: Required fixtures were not
113///   available in the context.
114/// - [`HandlerFailed`][Self::HandlerFailed]: The step handler returned an error.
115///
116/// # Examples
117///
118/// ```
119/// use rstest_bdd::execution::ExecutionError;
120///
121/// let error = ExecutionError::Skip { message: Some("not implemented yet".into()) };
122/// assert!(error.is_skip());
123/// assert_eq!(error.skip_message(), Some("not implemented yet"));
124/// ```
125#[derive(Debug, Clone)]
126#[non_exhaustive]
127pub enum ExecutionError {
128    /// Step requested to skip execution.
129    Skip {
130        /// Optional message explaining why the step was skipped.
131        message: Option<String>,
132    },
133    /// Step pattern not found in the registry.
134    StepNotFound {
135        /// Zero-based step index.
136        index: usize,
137        /// The step keyword (Given, When, Then, etc.).
138        keyword: StepKeyword,
139        /// The step text that was not found.
140        text: String,
141        /// Path to the feature file.
142        feature_path: String,
143        /// Name of the scenario.
144        scenario_name: String,
145    },
146    /// Required fixtures missing from context.
147    ///
148    /// The details are wrapped in `Arc` to reduce the size of `Result<T, ExecutionError>`.
149    MissingFixtures(Arc<MissingFixturesDetails>),
150    /// Step handler returned an error.
151    HandlerFailed {
152        /// Zero-based step index.
153        index: usize,
154        /// The step keyword (Given, When, Then, etc.).
155        keyword: StepKeyword,
156        /// The step text.
157        text: String,
158        /// The error returned by the handler, wrapped in Arc for Clone.
159        error: Arc<StepError>,
160        /// Path to the feature file.
161        feature_path: String,
162        /// Name of the scenario.
163        scenario_name: String,
164    },
165}
166
167/// Details about missing fixture errors.
168///
169/// This struct is separated from `ExecutionError::MissingFixtures` to allow
170/// wrapping in `Arc`, reducing the overall size of `Result<T, ExecutionError>`.
171#[derive(Debug, Clone)]
172pub struct MissingFixturesDetails {
173    /// The step definition's pattern (e.g., `"a user named {name}"`).
174    pub step_pattern: String,
175    /// Source location of the step definition (`file:line`).
176    pub step_location: String,
177    /// List of all required fixture names.
178    pub required: Vec<&'static str>,
179    /// List of missing fixture names.
180    pub missing: Vec<&'static str>,
181    /// List of available fixture names in the context.
182    pub available: Vec<String>,
183    /// Path to the feature file.
184    pub feature_path: String,
185    /// Name of the scenario.
186    pub scenario_name: String,
187}
188
189impl ExecutionError {
190    /// Returns `true` if this error represents a skip request.
191    ///
192    /// Skip requests are control flow signals, not actual errors. Use this
193    /// method to distinguish between errors that should fail a test and
194    /// skip signals that should mark the test as skipped.
195    ///
196    /// # Examples
197    ///
198    /// ```
199    /// use rstest_bdd::execution::ExecutionError;
200    ///
201    /// let skip = ExecutionError::Skip { message: None };
202    /// assert!(skip.is_skip());
203    ///
204    /// let not_found = ExecutionError::StepNotFound {
205    ///     index: 0,
206    ///     keyword: rstest_bdd::StepKeyword::Given,
207    ///     text: "missing".into(),
208    ///     feature_path: "test.feature".into(),
209    ///     scenario_name: "test".into(),
210    /// };
211    /// assert!(!not_found.is_skip());
212    /// ```
213    #[must_use]
214    pub fn is_skip(&self) -> bool {
215        matches!(self, Self::Skip { .. })
216    }
217
218    /// Returns the skip message if this is a skip error.
219    ///
220    /// Returns `None` if this is not a skip error, or if the skip has no
221    /// message.
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// use rstest_bdd::execution::ExecutionError;
227    ///
228    /// let skip_with_msg = ExecutionError::Skip { message: Some("reason".into()) };
229    /// assert_eq!(skip_with_msg.skip_message(), Some("reason"));
230    ///
231    /// let skip_no_msg = ExecutionError::Skip { message: None };
232    /// assert_eq!(skip_no_msg.skip_message(), None);
233    ///
234    /// let not_skip = ExecutionError::StepNotFound {
235    ///     index: 0,
236    ///     keyword: rstest_bdd::StepKeyword::Given,
237    ///     text: "missing".into(),
238    ///     feature_path: "test.feature".into(),
239    ///     scenario_name: "test".into(),
240    /// };
241    /// assert_eq!(not_skip.skip_message(), None);
242    /// ```
243    #[must_use]
244    pub fn skip_message(&self) -> Option<&str> {
245        match self {
246            Self::Skip { message } => message.as_deref(),
247            _ => None,
248        }
249    }
250}
251
252impl ExecutionError {
253    /// Render the error message using the provided Fluent loader.
254    ///
255    /// This allows formatting the error using a specific locale loader rather than
256    /// the global default. This is useful when you need consistent locale handling
257    /// across nested error types.
258    ///
259    /// # Examples
260    ///
261    /// ```
262    /// use i18n_embed::fluent::fluent_language_loader;
263    /// use unic_langid::langid;
264    /// use rstest_bdd::execution::ExecutionError;
265    ///
266    /// let loader = {
267    ///     use i18n_embed::LanguageLoader;
268    ///     use rstest_bdd::Localizations;
269    ///     let loader = fluent_language_loader!();
270    ///     i18n_embed::select(&loader, &Localizations, &[langid!("en-US")])
271    ///         .expect("en-US locale should always be available");
272    ///     loader
273    /// };
274    /// let error = ExecutionError::Skip { message: Some("not implemented".into()) };
275    /// let message = error.format_with_loader(&loader);
276    /// assert!(message.contains("skipped"));
277    /// assert!(message.contains("not implemented"));
278    /// ```
279    #[must_use]
280    pub fn format_with_loader(&self, loader: &crate::FluentLanguageLoader) -> String {
281        match self {
282            Self::Skip { message } => {
283                crate::localization::message_with_loader(loader, "execution-error-skip", |args| {
284                    args.set(
285                        "has_message",
286                        if message.is_some() { "yes" } else { "no" }.to_string(),
287                    );
288                    args.set("message", message.clone().unwrap_or_default());
289                })
290            }
291            Self::StepNotFound {
292                index,
293                keyword,
294                text,
295                feature_path,
296                scenario_name,
297            } => crate::localization::message_with_loader(
298                loader,
299                "execution-error-step-not-found",
300                |args| {
301                    args.set("index", index.to_string());
302                    args.set("keyword", keyword.as_str().to_string());
303                    args.set("text", text.clone());
304                    args.set("feature_path", feature_path.clone());
305                    args.set("scenario_name", scenario_name.clone());
306                },
307            ),
308            Self::MissingFixtures(details) => crate::localization::message_with_loader(
309                loader,
310                "execution-error-missing-fixtures",
311                |args| {
312                    args.set("step_pattern", details.step_pattern.clone());
313                    args.set("step_location", details.step_location.clone());
314                    args.set("required", details.required.join(", "));
315                    args.set("missing", details.missing.join(", "));
316                    args.set("available", details.available.join(", "));
317                    args.set("feature_path", details.feature_path.clone());
318                    args.set("scenario_name", details.scenario_name.clone());
319                },
320            ),
321            Self::HandlerFailed {
322                index,
323                keyword,
324                text,
325                error,
326                feature_path,
327                scenario_name,
328            } => crate::localization::message_with_loader(
329                loader,
330                "execution-error-handler-failed",
331                |args| {
332                    args.set("index", index.to_string());
333                    args.set("keyword", keyword.as_str().to_string());
334                    args.set("text", text.clone());
335                    args.set("error", error.format_with_loader(loader));
336                    args.set("feature_path", feature_path.clone());
337                    args.set("scenario_name", scenario_name.clone());
338                },
339            ),
340        }
341    }
342}
343
344impl std::fmt::Display for ExecutionError {
345    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346        let message = crate::localization::with_loader(|loader| self.format_with_loader(loader));
347        f.write_str(&message)
348    }
349}
350
351impl std::error::Error for ExecutionError {
352    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
353        match self {
354            Self::HandlerFailed { error, .. } => Some(error.as_ref()),
355            _ => None,
356        }
357    }
358}