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 {
70 "✅"
71 } else {
72 "❌"
73 }
74 }
75
76 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#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct SafetyReport {
89 pub stage: PipelineStage,
91 pub checks: Vec<CheckResult>,
93 pub passed: bool,
95 pub total_duration: Duration,
97 pub timestamp: chrono::DateTime<chrono::Utc>,
99}
100
101impl SafetyReport {
102 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 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 pub fn merge(&mut self, other: SafetyReport) {
124 for check in other.checks {
125 self.add_check(check);
126 }
127 }
128
129 pub fn failed_checks(&self) -> Vec<&CheckResult> {
131 self.checks.iter().filter(|c| !c.passed).collect()
132 }
133
134 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 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 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 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 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 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}