ferrous_forge/safety/
report.rs

1//! Safety check reporting and result handling
2
3use crate::{Error, Result};
4use console::style;
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7use tokio::fs;
8
9use super::{CheckType, PipelineStage};
10
11/// Result of a single safety check
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct CheckResult {
14    /// Type of check that was run
15    pub check_type: CheckType,
16    /// Whether the check passed
17    pub passed: bool,
18    /// Duration the check took to run
19    pub duration: Duration,
20    /// Error messages if check failed
21    pub errors: Vec<String>,
22    /// Suggestions for fixing issues
23    pub suggestions: Vec<String>,
24    /// Additional context information
25    pub context: Vec<String>,
26}
27
28impl CheckResult {
29    /// Create a new check result
30    pub fn new(check_type: CheckType) -> Self {
31        Self {
32            check_type,
33            passed: true,
34            duration: Duration::default(),
35            errors: Vec::new(),
36            suggestions: Vec::new(),
37            context: Vec::new(),
38        }
39    }
40
41    /// Mark the check as failed
42    pub fn fail(&mut self) {
43        self.passed = false;
44    }
45
46    /// Add an error message
47    pub fn add_error(&mut self, error: impl Into<String>) {
48        self.errors.push(error.into());
49        self.fail();
50    }
51
52    /// Add a suggestion
53    pub fn add_suggestion(&mut self, suggestion: impl Into<String>) {
54        self.suggestions.push(suggestion.into());
55    }
56
57    /// Add context information
58    pub fn add_context(&mut self, context: impl Into<String>) {
59        self.context.push(context.into());
60    }
61
62    /// Set the duration
63    pub fn set_duration(&mut self, duration: Duration) {
64        self.duration = duration;
65    }
66
67    /// Get a status emoji for display
68    pub fn status_emoji(&self) -> &'static str {
69        if self.passed { "✅" } else { "❌" }
70    }
71
72    /// Get a colored status for display
73    pub fn status_colored(&self) -> console::StyledObject<&'static str> {
74        if self.passed {
75            style("PASS").green().bold()
76        } else {
77            style("FAIL").red().bold()
78        }
79    }
80}
81
82/// Comprehensive safety report for a pipeline stage
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct SafetyReport {
85    /// Pipeline stage this report is for
86    pub stage: PipelineStage,
87    /// Individual check results
88    pub checks: Vec<CheckResult>,
89    /// Whether all checks passed
90    pub passed: bool,
91    /// Total duration for all checks
92    pub total_duration: Duration,
93    /// Timestamp when report was generated
94    pub timestamp: chrono::DateTime<chrono::Utc>,
95}
96
97impl SafetyReport {
98    /// Create a new safety report
99    pub fn new(stage: PipelineStage) -> Self {
100        Self {
101            stage,
102            checks: Vec::new(),
103            passed: true,
104            total_duration: Duration::default(),
105            timestamp: chrono::Utc::now(),
106        }
107    }
108
109    /// Add a check result to the report
110    pub fn add_check(&mut self, check: CheckResult) {
111        if !check.passed {
112            self.passed = false;
113        }
114        self.total_duration += check.duration;
115        self.checks.push(check);
116    }
117
118    /// Merge another report into this one
119    pub fn merge(&mut self, other: SafetyReport) {
120        for check in other.checks {
121            self.add_check(check);
122        }
123    }
124
125    /// Get failed checks
126    pub fn failed_checks(&self) -> Vec<&CheckResult> {
127        self.checks.iter().filter(|c| !c.passed).collect()
128    }
129
130    /// Get all error messages
131    pub fn all_errors(&self) -> Vec<String> {
132        self.checks
133            .iter()
134            .flat_map(|c| c.errors.iter().cloned())
135            .collect()
136    }
137
138    /// Get all suggestions
139    pub fn all_suggestions(&self) -> Vec<String> {
140        self.checks
141            .iter()
142            .flat_map(|c| c.suggestions.iter().cloned())
143            .collect()
144    }
145
146    /// Print a concise report
147    pub fn print_summary(&self) {
148        println!(
149            "🛡️  Ferrous Forge Safety Pipeline - {}\n",
150            self.stage.display_name()
151        );
152
153        for check in &self.checks {
154            println!(
155                "  {} {} ({:.2}s)",
156                check.status_emoji(),
157                check.check_type.display_name(),
158                check.duration.as_secs_f64()
159            );
160        }
161
162        println!("\nTotal time: {:.2}s", self.total_duration.as_secs_f64());
163
164        if self.passed {
165            println!("{}", style("🎉 All safety checks passed!").green().bold());
166        } else {
167            println!(
168                "{}",
169                style("🚨 Safety checks FAILED - operation blocked!")
170                    .red()
171                    .bold()
172            );
173        }
174    }
175
176    /// Print a detailed report with errors and suggestions
177    pub fn print_detailed(&self) {
178        self.print_summary();
179
180        if !self.passed {
181            let failed = self.failed_checks();
182
183            if !failed.is_empty() {
184                println!("\n{}", style("📋 Failed Checks:").red().bold());
185
186                for check in failed {
187                    println!(
188                        "\n  {} {}",
189                        style("❌").red(),
190                        style(check.check_type.display_name()).red().bold()
191                    );
192
193                    for error in &check.errors {
194                        println!("    {}", style(format!("⚠️  {}", error)).yellow());
195                    }
196
197                    if !check.suggestions.is_empty() {
198                        println!("    {}", style("💡 Suggestions:").cyan());
199                        for suggestion in &check.suggestions {
200                            println!("      • {}", style(suggestion).cyan());
201                        }
202                    }
203                }
204            }
205
206            // Show general suggestions
207            let all_suggestions = self.all_suggestions();
208            if !all_suggestions.is_empty() {
209                println!("\n{}", style("🔧 How to Fix:").cyan().bold());
210                for suggestion in all_suggestions.iter().take(5) {
211                    println!("  • {}", suggestion);
212                }
213            }
214        }
215
216        println!();
217    }
218
219    /// Save report to file for audit trail
220    pub async fn save_to_file(&self) -> Result<()> {
221        let reports_dir = crate::config::Config::config_dir_path()?.join("safety-reports");
222        fs::create_dir_all(&reports_dir).await?;
223
224        let filename = format!(
225            "{}-{}.json",
226            self.timestamp.format("%Y%m%d-%H%M%S"),
227            self.stage.name()
228        );
229
230        let report_path = reports_dir.join(filename);
231        let contents = serde_json::to_string_pretty(self)
232            .map_err(|e| Error::config(format!("Failed to serialize report: {}", e)))?;
233
234        fs::write(&report_path, contents).await?;
235        Ok(())
236    }
237}
238
239#[cfg(test)]
240#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn test_check_result_creation() {
246        let mut result = CheckResult::new(CheckType::Format);
247
248        assert!(result.passed);
249        assert!(result.errors.is_empty());
250        assert_eq!(result.check_type, CheckType::Format);
251
252        result.add_error("Format violation");
253        assert!(!result.passed);
254        assert_eq!(result.errors.len(), 1);
255    }
256
257    #[test]
258    fn test_safety_report() {
259        let mut report = SafetyReport::new(PipelineStage::PreCommit);
260
261        assert!(report.passed);
262        assert!(report.checks.is_empty());
263
264        let mut failed_check = CheckResult::new(CheckType::Clippy);
265        failed_check.add_error("Clippy error");
266
267        report.add_check(failed_check);
268
269        assert!(!report.passed);
270        assert_eq!(report.checks.len(), 1);
271        assert_eq!(report.failed_checks().len(), 1);
272    }
273
274    #[test]
275    fn test_report_merge() {
276        let mut report1 = SafetyReport::new(PipelineStage::PreCommit);
277        let mut report2 = SafetyReport::new(PipelineStage::PrePush);
278
279        let check1 = CheckResult::new(CheckType::Format);
280        let check2 = CheckResult::new(CheckType::Test);
281
282        report1.add_check(check1);
283        report2.add_check(check2);
284
285        report1.merge(report2);
286
287        assert_eq!(report1.checks.len(), 2);
288    }
289}