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 /// Messages from earlier, failed attempts of a step that ultimately passed
155 /// — the flaky-failure detail. Empty for a clean single-attempt step;
156 /// engine-agnostic, so any engine with retries can fill it.
157 pub attempt_details: Vec<String>,
158 /// Engine-provided command to reproduce this step alone (engine-hurl fills
159 /// it with the redacted `curl` of the failing request). Set only on failure;
160 /// `None` otherwise and for engines that offer no hint.
161 pub reproduce_hint: Option<String>,
162}
163
164/// Result of dispatching one [`StepBatch`] to an engine.
165#[derive(Debug)]
166pub struct BatchResult {
167 /// One outcome per step the engine reached.
168 pub steps: Vec<StepOutcome>,
169 /// A batch-level failure, if the batch stopped early.
170 pub error: Option<EngineError>,
171}
172
173#[cfg(test)]
174mod tests {
175 use super::Guard;
176
177 /// `when:` semantics (TECH-SPEC §6): empty and literal-false skip; any
178 /// other non-empty text runs.
179 #[test]
180 fn guard_skips_on_empty_and_literal_false() {
181 for skipping in ["", " ", "false", "FALSE", "0"] {
182 assert!(Guard(skipping.to_owned()).skips(), "{skipping:?}");
183 }
184 for running in ["true", "yes", "1", "anything"] {
185 assert!(!Guard(running.to_owned()).skips(), "{running:?}");
186 }
187 }
188}