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}