Skip to main content

proef_core/
step.rs

1//! Lowered, engine-agnostic step and batch types (TECH-SPEC §3).
2//!
3//! A scenario lowers to an ordered list of [`LoweredStep`]s, segmented into
4//! [`StepBatch`]es of **contiguous same-engine steps** (batched maximally — splits
5//! happen only at `optional:` boundaries and engine changes, ADR-0010). Engines
6//! return a [`BatchResult`] with one [`StepOutcome`] per executed step.
7
8use std::sync::Arc;
9use std::time::Duration;
10
11use serde::{Deserialize, Serialize};
12
13use crate::engine::EngineId;
14use crate::error::EngineError;
15
16/// Anchor back to the authored `.feature` source (file, 1-based line, step text).
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct StepRef {
19    /// Feature file path as authored.
20    pub file: Arc<str>,
21    /// 1-based line of the step keyword in the feature file.
22    pub line: usize,
23    /// The full step text (keyword stripped).
24    pub text: Arc<str>,
25}
26
27/// Identifies the *kind* of a macro step (`hurl`, …). A step kind
28/// names the engine that claims it via [`crate::engine::StepKindSpec`] (ADR-0002).
29#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
30pub struct StepKindId(Arc<str>);
31
32impl StepKindId {
33    /// The kind name as written in packs (without the trailing `:`).
34    pub fn as_str(&self) -> &str {
35        &self.0
36    }
37}
38
39impl From<&str> for StepKindId {
40    fn from(s: &str) -> Self {
41        Self(Arc::from(s))
42    }
43}
44
45impl std::fmt::Display for StepKindId {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        f.write_str(&self.0)
48    }
49}
50
51/// The engine-facing payload of a lowered step.
52#[derive(Debug, Clone, PartialEq)]
53pub enum StepPayload {
54    /// Lowered hurl text (`${…}` resolved, `{{…}}` untouched) — one or more entries.
55    HurlEntries(String),
56    /// Asserts an `expect:` macro merged into the *previous* request entry
57    /// (ADR-0004): the step owns the last `lines` assert lines appended to
58    /// that entry's text. It renders no bytes of its own — the sidecar
59    /// anchors those lines so results attribute to the authored `Then`.
60    MergedAsserts {
61        /// How many assert lines this step appended to the previous entry.
62        lines: usize,
63    },
64    /// Structured payload — reserved for future non-hurl engines (ADR-0004).
65    Structured(serde_json::Value),
66}
67
68/// Finite retry policy for a step (`retry:` — infinite retries are rejected at pack
69/// load by the finite-retry lint, ADR-0007).
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71pub struct Retry {
72    /// Maximum number of retries (finite by construction).
73    pub count: u32,
74    /// Interval between attempts, in milliseconds.
75    pub interval_ms: u64,
76}
77
78/// A `when:` skip guard: the step runs unless the resolved expression is
79/// empty or a literal false (TECH-SPEC §6).
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct Guard(pub String);
82
83impl Guard {
84    /// Should the guarded step be skipped? Empty means "no condition met",
85    /// and a literal `false`/`0` (case-insensitive) skips too — an author
86    /// writing `when: ${flag}` with `flag=false` means *skip*, not "the
87    /// string is non-empty, run anyway".
88    pub fn skips(&self) -> bool {
89        let value = self.0.trim();
90        value.is_empty() || value.eq_ignore_ascii_case("false") || value == "0"
91    }
92}
93
94/// One step after macro expansion and `${…}` lowering — engine-agnostic.
95#[derive(Debug, Clone, PartialEq)]
96pub struct LoweredStep {
97    /// Anchor to the authored feature line.
98    pub step: StepRef,
99    /// Which step kind (and therefore which engine) executes this step.
100    pub kind: StepKindId,
101    /// The engine-facing payload.
102    pub payload: StepPayload,
103    /// `optional:` steps warn instead of failing (and segment the batch).
104    pub optional: bool,
105    /// Skip guard, when configured (resolved text; the runtime skips the step
106    /// per [`Guard::skips`]).
107    pub when: Option<Guard>,
108    /// Pack-step entry label (events/console), when authored.
109    pub label: Option<String>,
110    /// `saveAs:` promotions: capture name → `global` (ADR-0005).
111    pub save_as: std::collections::BTreeMap<String, String>,
112}
113
114/// A contiguous run of same-engine steps, dispatched as one unit (ADR-0002).
115#[derive(Debug, Clone, PartialEq)]
116pub struct StepBatch {
117    /// Ordinal of this batch within the *scenario* (the sidecar map's `batch`
118    /// key). Engines must select sidecar entries by this index — a per-session
119    /// counter diverges as soon as another engine's batch interleaves.
120    pub index: usize,
121    /// The engine that executes this batch.
122    pub engine: EngineId,
123    /// The steps, in authored order.
124    pub steps: Vec<LoweredStep>,
125}
126
127/// Outcome status of a step or scenario.
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
129#[serde(rename_all = "snake_case")]
130pub enum Status {
131    /// Ran and passed.
132    Passed,
133    /// Ran and failed.
134    Failed,
135    /// Not run (guard, earlier failure, filter).
136    Skipped,
137    /// An `optional:` step failed — reported as a warning, run continues.
138    Warned,
139}
140
141/// Per-step result reported by an engine.
142#[derive(Debug, Clone)]
143pub struct StepOutcome {
144    /// Anchor to the authored feature line.
145    pub step: StepRef,
146    /// Outcome status.
147    pub status: Status,
148    /// Number of attempts made (≥ 1 once the step ran).
149    pub attempts: u32,
150    /// Wall-clock duration of all attempts.
151    pub duration: Duration,
152    /// Engine-specific detail (assert message, timing breakdown, …).
153    pub detail: Option<String>,
154}
155
156/// Result of dispatching one [`StepBatch`] to an engine.
157#[derive(Debug)]
158pub struct BatchResult {
159    /// One outcome per step the engine reached.
160    pub steps: Vec<StepOutcome>,
161    /// A batch-level failure, if the batch stopped early.
162    pub error: Option<EngineError>,
163}
164
165#[cfg(test)]
166mod tests {
167    use super::Guard;
168
169    /// `when:` semantics (TECH-SPEC §6): empty and literal-false skip; any
170    /// other non-empty text runs.
171    #[test]
172    fn guard_skips_on_empty_and_literal_false() {
173        for skipping in ["", "  ", "false", "FALSE", "0"] {
174            assert!(Guard(skipping.to_owned()).skips(), "{skipping:?}");
175        }
176        for running in ["true", "yes", "1", "anything"] {
177            assert!(!Guard(running.to_owned()).skips(), "{running:?}");
178        }
179    }
180}