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