ferrous_forge/safety/
report.rs1use crate::{Error, Result};
4use console::style;
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7use tokio::fs;
8
9use super::{CheckType, PipelineStage};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct CheckResult {
14 pub check_type: CheckType,
16 pub passed: bool,
18 pub duration: Duration,
20 pub errors: Vec<String>,
22 pub suggestions: Vec<String>,
24 pub context: Vec<String>,
26}
27
28impl CheckResult {
29 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 pub fn fail(&mut self) {
43 self.passed = false;
44 }
45
46 pub fn add_error(&mut self, error: impl Into<String>) {
48 self.errors.push(error.into());
49 self.fail();
50 }
51
52 pub fn add_suggestion(&mut self, suggestion: impl Into<String>) {
54 self.suggestions.push(suggestion.into());
55 }
56
57 pub fn add_context(&mut self, context: impl Into<String>) {
59 self.context.push(context.into());
60 }
61
62 pub fn set_duration(&mut self, duration: Duration) {
64 self.duration = duration;
65 }
66
67 pub fn status_emoji(&self) -> &'static str {
69 if self.passed { "✅" } else { "❌" }
70 }
71
72 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#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct SafetyReport {
85 pub stage: PipelineStage,
87 pub checks: Vec<CheckResult>,
89 pub passed: bool,
91 pub total_duration: Duration,
93 pub timestamp: chrono::DateTime<chrono::Utc>,
95}
96
97impl SafetyReport {
98 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 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 pub fn merge(&mut self, other: SafetyReport) {
120 for check in other.checks {
121 self.add_check(check);
122 }
123 }
124
125 pub fn failed_checks(&self) -> Vec<&CheckResult> {
127 self.checks.iter().filter(|c| !c.passed).collect()
128 }
129
130 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 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 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 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 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 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}