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<()> {
226 let reports_dir = crate::config::Config::config_dir_path()?.join("safety-reports");
227 fs::create_dir_all(&reports_dir).await?;
228
229 let filename = format!(
230 "{}-{}.json",
231 self.timestamp.format("%Y%m%d-%H%M%S"),
232 self.stage.name()
233 );
234
235 let report_path = reports_dir.join(filename);
236 let contents = serde_json::to_string_pretty(self)
237 .map_err(|e| Error::config(format!("Failed to serialize report: {}", e)))?;
238
239 fs::write(&report_path, contents).await?;
240 Ok(())
241 }
242}
243
244#[cfg(test)]
245#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn test_check_result_creation() {
251 let mut result = CheckResult::new(CheckType::Format);
252
253 assert!(result.passed);
254 assert!(result.errors.is_empty());
255 assert_eq!(result.check_type, CheckType::Format);
256
257 result.add_error("Format violation");
258 assert!(!result.passed);
259 assert_eq!(result.errors.len(), 1);
260 }
261
262 #[test]
263 fn test_safety_report() {
264 let mut report = SafetyReport::new(PipelineStage::PreCommit);
265
266 assert!(report.passed);
267 assert!(report.checks.is_empty());
268
269 let mut failed_check = CheckResult::new(CheckType::Clippy);
270 failed_check.add_error("Clippy error");
271
272 report.add_check(failed_check);
273
274 assert!(!report.passed);
275 assert_eq!(report.checks.len(), 1);
276 assert_eq!(report.failed_checks().len(), 1);
277 }
278
279 #[test]
280 fn test_report_merge() {
281 let mut report1 = SafetyReport::new(PipelineStage::PreCommit);
282 let mut report2 = SafetyReport::new(PipelineStage::PrePush);
283
284 let check1 = CheckResult::new(CheckType::Format);
285 let check2 = CheckResult::new(CheckType::Test);
286
287 report1.add_check(check1);
288 report2.add_check(check2);
289
290 report1.merge(report2);
291
292 assert_eq!(report1.checks.len(), 2);
293 }
294}