leviso_cheat_guard/
lib.rs

1//! # leviso-cheat-guard
2//!
3//! Runtime macros for cheat-aware error handling in non-test contexts.
4//!
5//! This crate provides `macro_rules!` macros that work like `anyhow::bail!` and
6//! `anyhow::ensure!` but include cheat documentation in error messages.
7//!
8//! ## Why This Exists
9//!
10//! The `leviso-cheat-test` crate provides proc-macro attributes for `#[test]` functions.
11//! But install-tests uses a custom `Step` trait, not standard tests. This crate
12//! provides macros that can be used inside any function to document cheat vectors
13//! and fail with informative messages.
14//!
15//! ## Macros
16//!
17//! - [`cheat_bail!`] - Like `bail!()` but with cheat documentation
18//! - [`cheat_ensure!`] - Like `ensure!()` but with cheat documentation
19//! - [`cheat_check!`] - Check a condition and add to StepResult with cheat metadata
20//!
21//! ## Example
22//!
23//! ```rust,ignore
24//! use leviso_cheat_guard::cheat_bail;
25//!
26//! fn partition_disk(console: &mut Console) -> Result<()> {
27//!     let output = console.exec("sfdisk /dev/vda", timeout)?;
28//!
29//!     if !output.contains("vda1") {
30//!         cheat_bail!(
31//!             protects = "Disk is partitioned correctly",
32//!             severity = "CRITICAL",
33//!             cheats = ["Accept exit code without verification", "Skip partition check"],
34//!             consequence = "No partitions, installation fails silently",
35//!             "Partition vda1 not found after sfdisk"
36//!         );
37//!     }
38//!
39//!     Ok(())
40//! }
41//! ```
42
43// Re-export proc-macros for convenience
44pub use leviso_cheat_test::{cheat_aware, cheat_canary, cheat_reviewed};
45
46/// Bail with cheat-aware error message.
47///
48/// Like `anyhow::bail!()` but includes cheat documentation in the error.
49///
50/// # Arguments
51///
52/// - `protects` - What user scenario this check protects
53/// - `severity` - "CRITICAL", "HIGH", "MEDIUM", or "LOW"
54/// - `cheats` - Array of ways this check could be cheated
55/// - `consequence` - What users experience if cheated
56/// - Format string and args for the actual error message
57///
58/// # Example
59///
60/// ```rust,ignore
61/// if !partition_exists {
62///     cheat_bail!(
63///         protects = "Disk partitioning works",
64///         severity = "CRITICAL",
65///         cheats = ["Return Ok without checking", "Increase timeout"],
66///         consequence = "No partitions, installation fails",
67///         "Partition {} not found", "vda1"
68///     );
69/// }
70/// ```
71#[macro_export]
72macro_rules! cheat_bail {
73    (
74        protects = $protects:expr,
75        severity = $severity:expr,
76        cheats = [$($cheat:expr),+ $(,)?],
77        consequence = $consequence:expr,
78        $($arg:tt)*
79    ) => {{
80        let cheats_list: &[&str] = &[$($cheat),+];
81        let cheats_formatted: String = cheats_list
82            .iter()
83            .enumerate()
84            .map(|(i, c)| format!("  {}. {}", i + 1, c))
85            .collect::<Vec<_>>()
86            .join("\n");
87
88        let error_msg = format!($($arg)*);
89
90        anyhow::bail!(
91            "\n{border}\n\
92             === CHEAT-GUARDED FAILURE ===\n\
93             {border}\n\n\
94             PROTECTS: {protects}\n\
95             SEVERITY: {severity}\n\n\
96             CHEAT VECTORS:\n\
97             {cheats}\n\n\
98             USER CONSEQUENCE:\n\
99             {consequence}\n\n\
100             ERROR:\n\
101             {error}\n\
102             {border}\n",
103            border = "=".repeat(70),
104            protects = $protects,
105            severity = $severity,
106            cheats = cheats_formatted,
107            consequence = $consequence,
108            error = error_msg
109        );
110    }};
111}
112
113/// Ensure a condition with cheat-aware error message.
114///
115/// Like `anyhow::ensure!()` but includes cheat documentation if the condition is false.
116///
117/// # Example
118///
119/// ```rust,ignore
120/// cheat_ensure!(
121///     partition_exists,
122///     protects = "Disk partitioning works",
123///     severity = "CRITICAL",
124///     cheats = ["Skip verification", "Accept any output"],
125///     consequence = "Installation fails",
126///     "Partition {} not found", "vda1"
127/// );
128/// ```
129#[macro_export]
130macro_rules! cheat_ensure {
131    (
132        $cond:expr,
133        protects = $protects:expr,
134        severity = $severity:expr,
135        cheats = [$($cheat:expr),+ $(,)?],
136        consequence = $consequence:expr,
137        $($arg:tt)*
138    ) => {{
139        if !($cond) {
140            $crate::cheat_bail!(
141                protects = $protects,
142                severity = $severity,
143                cheats = [$($cheat),+],
144                consequence = $consequence,
145                $($arg)*
146            );
147        }
148    }};
149}
150
151/// Check a condition and record result with cheat metadata.
152///
153/// This is for the install-tests `StepResult` pattern. It checks a condition,
154/// adds a CheckResult to the StepResult, and documents the cheat vectors.
155///
156/// # Example
157///
158/// ```rust,ignore
159/// let mut result = StepResult::new(4, "Partition Disk");
160///
161/// cheat_check!(
162///     result,
163///     name = "Partition table created",
164///     condition = output.contains("vda1"),
165///     protects = "Disk has correct partitions",
166///     severity = "CRITICAL",
167///     cheats = ["Accept any output", "Skip verification"],
168///     consequence = "No partitions, installation fails",
169///     expected = "Partition vda1 exists",
170///     actual = format!("sfdisk output: {}", output)
171/// );
172/// ```
173#[macro_export]
174macro_rules! cheat_check {
175    (
176        $result:expr,
177        name = $name:expr,
178        condition = $cond:expr,
179        protects = $protects:expr,
180        severity = $severity:expr,
181        cheats = [$($cheat:expr),+ $(,)?],
182        consequence = $consequence:expr,
183        expected = $expected:expr,
184        actual = $actual:expr
185    ) => {{
186        let cheats_list: &[&str] = &[$($cheat),+];
187        let _cheats_formatted: String = cheats_list
188            .iter()
189            .enumerate()
190            .map(|(i, c)| format!("  {}. {}", i + 1, c))
191            .collect::<Vec<_>>()
192            .join("\n");
193
194        // Print what this check protects (visible in test output)
195        println!("    checking: {} (protects: {})", $name, $protects);
196
197        if $cond {
198            $result.add_check($name, $crate::CheckResult::Pass($expected.to_string()));
199        } else {
200            // Print cheat vectors on failure
201            eprintln!("\n{}", "=".repeat(60));
202            eprintln!("CHEAT-GUARDED CHECK FAILED: {}", $name);
203            eprintln!("{}", "=".repeat(60));
204            eprintln!("PROTECTS: {}", $protects);
205            eprintln!("SEVERITY: {}", $severity);
206            eprintln!("CHEATS:");
207            eprintln!("{}", _cheats_formatted);
208            eprintln!("CONSEQUENCE: {}", $consequence);
209            eprintln!("{}", "=".repeat(60));
210
211            $result.add_check($name, $crate::CheckResult::Fail {
212                expected: $expected.to_string(),
213                actual: $actual.to_string(),
214            });
215        }
216    }};
217}
218
219/// CheckResult for use with cheat_check! macro.
220/// Mirrors the install-tests CheckResult enum.
221#[derive(Debug, Clone)]
222pub enum CheckResult {
223    Pass(String),
224    Fail { expected: String, actual: String },
225}
226
227impl CheckResult {
228    pub fn passed(&self) -> bool {
229        matches!(self, CheckResult::Pass(_))
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use anyhow::Result;
236
237    #[test]
238    fn test_cheat_ensure_passes() -> Result<()> {
239        cheat_ensure!(
240            true,
241            protects = "Test passes",
242            severity = "LOW",
243            cheats = ["None"],
244            consequence = "Test fails",
245            "This should not trigger"
246        );
247        Ok(())
248    }
249
250    #[test]
251    fn test_cheat_bail_format() {
252        let result: Result<()> = (|| {
253            cheat_bail!(
254                protects = "Test scenario",
255                severity = "CRITICAL",
256                cheats = ["Cheat 1", "Cheat 2"],
257                consequence = "Bad things happen",
258                "Error: {} not found",
259                "thing"
260            );
261        })();
262
263        let err = result.unwrap_err();
264        let msg = err.to_string();
265        assert!(msg.contains("PROTECTS: Test scenario"));
266        assert!(msg.contains("SEVERITY: CRITICAL"));
267        assert!(msg.contains("1. Cheat 1"));
268        assert!(msg.contains("2. Cheat 2"));
269        assert!(msg.contains("Error: thing not found"));
270    }
271}