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}