1use crate::domain::violations::{GuardianResult, Severity, ValidationReport, Violation};
9use serde_json::Value as JsonValue;
10use std::io::Write;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum OutputFormat {
15 Human,
17 Json,
19 Junit,
21 Sarif,
23 GitHub,
25 Agent,
27}
28
29use std::str::FromStr;
30
31impl FromStr for OutputFormat {
32 type Err = String;
33
34 fn from_str(s: &str) -> Result<Self, Self::Err> {
36 match s.to_lowercase().as_str() {
37 "human" => Ok(Self::Human),
38 "json" => Ok(Self::Json),
39 "junit" => Ok(Self::Junit),
40 "sarif" => Ok(Self::Sarif),
41 "github" => Ok(Self::GitHub),
42 "agent" => Ok(Self::Agent),
43 _ => Err(format!("Unknown output format: {s}")),
44 }
45 }
46}
47
48impl OutputFormat {
49 pub fn all_formats() -> &'static [&'static str] {
51 &["human", "json", "junit", "sarif", "github", "agent"]
52 }
53
54 pub fn validate_for_context(&self, is_ci_environment: bool) -> GuardianResult<()> {
58 match (self, is_ci_environment) {
59 (Self::Human, true) => {
60 Ok(())
63 }
64 (Self::Junit | Self::GitHub | Self::Sarif, false) => {
65 Ok(())
67 }
68 _ => Ok(()),
69 }
70 }
71
72 pub fn supports_colors(&self) -> bool {
74 matches!(self, Self::Human)
75 }
76
77 pub fn is_structured(&self) -> bool {
79 matches!(self, Self::Json | Self::Sarif | Self::Junit)
80 }
81}
82
83#[derive(Debug, Clone)]
85pub struct ReportOptions {
86 pub use_colors: bool,
88 pub show_context: bool,
90 pub show_suggestions: bool,
92 pub max_violations: Option<usize>,
94 pub min_severity: Option<Severity>,
96}
97
98impl Default for ReportOptions {
99 fn default() -> Self {
100 Self {
101 use_colors: true,
102 show_context: true,
103 show_suggestions: true,
104 max_violations: None,
105 min_severity: None,
106 }
107 }
108}
109
110impl ReportOptions {
111 pub fn validate(&self) -> GuardianResult<()> {
115 if let Some(max) = self.max_violations {
117 if max == 0 {
118 return Err(crate::domain::violations::GuardianError::config(
119 "max_violations cannot be zero - this would produce empty reports",
120 ));
121 }
122 if max > 10000 {
123 return Err(crate::domain::violations::GuardianError::config(
124 "max_violations too high - consider using severity filtering instead",
125 ));
126 }
127 }
128
129 if let Some(min_severity) = self.min_severity {
131 if min_severity > Severity::Error {
132 return Err(crate::domain::violations::GuardianError::config(
133 "min_severity cannot be higher than Error",
134 ));
135 }
136 }
137
138 Ok(())
139 }
140
141 pub fn is_optimized_for(&self, format: OutputFormat) -> bool {
143 match format {
144 OutputFormat::Human => true, OutputFormat::Json | OutputFormat::Sarif => {
146 !self.use_colors && !self.show_context
148 }
149 OutputFormat::Junit => {
150 !self.use_colors
152 }
153 OutputFormat::GitHub => {
154 !self.use_colors && !self.show_suggestions
156 }
157 OutputFormat::Agent => {
158 !self.use_colors && !self.show_context && !self.show_suggestions
160 }
161 }
162 }
163
164 pub fn optimized_for(format: OutputFormat) -> Self {
166 match format {
167 OutputFormat::Human => Self::default(),
168 OutputFormat::Json | OutputFormat::Sarif => Self {
169 use_colors: false,
170 show_context: false,
171 show_suggestions: false,
172 ..Self::default()
173 },
174 OutputFormat::Junit => {
175 Self { use_colors: false, show_suggestions: false, ..Self::default() }
176 }
177 OutputFormat::GitHub => {
178 Self { use_colors: false, show_suggestions: false, ..Self::default() }
179 }
180 OutputFormat::Agent => Self {
181 use_colors: false,
182 show_context: false,
183 show_suggestions: false,
184 ..Self::default()
185 },
186 }
187 }
188}
189
190pub struct ReportFormatter {
192 options: ReportOptions,
193}
194
195impl ReportFormatter {
196 pub fn new(options: ReportOptions) -> GuardianResult<Self> {
200 options.validate()?;
201 Ok(Self { options })
202 }
203
204 pub fn with_options(options: ReportOptions) -> Self {
208 options
209 .validate()
210 .expect("ReportOptions validation failed - this indicates a programming error");
211 Self { options }
212 }
213
214 pub fn validate_capabilities(&self) -> GuardianResult<()> {
219 if self.options.use_colors && !Self::supports_ansi_colors() {
221 return Err(crate::domain::violations::GuardianError::config(
222 "Color output requested but terminal does not support ANSI colors",
223 ));
224 }
225
226 if let Some(min_severity) = self.options.min_severity {
228 if min_severity > Severity::Error {
229 return Err(crate::domain::violations::GuardianError::config(
230 "Minimum severity cannot be higher than Error",
231 ));
232 }
233 }
234
235 if let Some(max) = self.options.max_violations {
237 if max == 0 {
238 return Err(crate::domain::violations::GuardianError::config(
239 "Maximum violations cannot be zero - use filtering instead",
240 ));
241 }
242 }
243
244 Ok(())
245 }
246
247 fn supports_ansi_colors() -> bool {
249 std::env::var("NO_COLOR").is_err()
251 && (std::env::var("TERM").is_ok_and(|term| term != "dumb"))
252 }
253
254 pub fn validate_format_integrity(
258 &self,
259 report: &ValidationReport,
260 format: OutputFormat,
261 output: &str,
262 ) -> GuardianResult<()> {
263 match format {
264 OutputFormat::Json => self.validate_json_structure(output),
265 OutputFormat::Junit => self.validate_junit_structure(output),
266 OutputFormat::Sarif => self.validate_sarif_structure(output),
267 OutputFormat::Human | OutputFormat::GitHub | OutputFormat::Agent => {
268 if output.is_empty() && !report.violations.is_empty() {
270 return Err(crate::domain::violations::GuardianError::config(
271 "Non-empty report produced empty output",
272 ));
273 }
274 Ok(())
275 }
276 }
277 }
278
279 fn validate_json_structure(&self, output: &str) -> GuardianResult<()> {
281 let json: JsonValue = serde_json::from_str(output).map_err(|e| {
282 crate::domain::violations::GuardianError::config(format!("Invalid JSON structure: {e}"))
283 })?;
284
285 if json.get("violations").is_none() {
287 return Err(crate::domain::violations::GuardianError::config(
288 "JSON output missing required 'violations' field",
289 ));
290 }
291
292 if json.get("summary").is_none() {
293 return Err(crate::domain::violations::GuardianError::config(
294 "JSON output missing required 'summary' field",
295 ));
296 }
297
298 Ok(())
299 }
300
301 fn validate_junit_structure(&self, output: &str) -> GuardianResult<()> {
303 if !output.starts_with("<?xml version=\"1.0\"") {
304 return Err(crate::domain::violations::GuardianError::config(
305 "JUnit output must start with XML declaration",
306 ));
307 }
308
309 if !output.contains("<testsuite") {
310 return Err(crate::domain::violations::GuardianError::config(
311 "JUnit output must contain testsuite element",
312 ));
313 }
314
315 Ok(())
316 }
317
318 fn validate_sarif_structure(&self, output: &str) -> GuardianResult<()> {
320 let json: JsonValue = serde_json::from_str(output).map_err(|e| {
321 crate::domain::violations::GuardianError::config(format!("Invalid SARIF JSON: {e}"))
322 })?;
323
324 if json.get("version").and_then(|v| v.as_str()) != Some("2.1.0") {
325 return Err(crate::domain::violations::GuardianError::config(
326 "SARIF output must specify version 2.1.0",
327 ));
328 }
329
330 if json.get("runs").and_then(|r| r.as_array()).is_none_or(|arr| arr.is_empty()) {
331 return Err(crate::domain::violations::GuardianError::config(
332 "SARIF output must contain at least one run",
333 ));
334 }
335
336 Ok(())
337 }
338
339 pub fn format_report(
344 &self,
345 report: &ValidationReport,
346 format: OutputFormat,
347 ) -> GuardianResult<String> {
348 self.validate_capabilities()?;
350
351 let filtered_violations = self.filter_violations(&report.violations);
353
354 let output = match format {
355 OutputFormat::Human => self.format_human(report, &filtered_violations),
356 OutputFormat::Json => self.format_json(report, &filtered_violations),
357 OutputFormat::Junit => self.format_junit(report, &filtered_violations),
358 OutputFormat::Sarif => self.format_sarif(report, &filtered_violations),
359 OutputFormat::GitHub => self.format_github(report, &filtered_violations),
360 OutputFormat::Agent => self.format_agent(report, &filtered_violations),
361 }?;
362
363 self.validate_format_integrity(report, format, &output)?;
365
366 Ok(output)
367 }
368
369 pub fn write_report<W: Write>(
371 &self,
372 report: &ValidationReport,
373 format: OutputFormat,
374 mut writer: W,
375 ) -> GuardianResult<()> {
376 let formatted = self.format_report(report, format)?;
377 writer
378 .write_all(formatted.as_bytes())
379 .map_err(|e| crate::domain::violations::GuardianError::Io { source: e })?;
380 Ok(())
381 }
382
383 fn filter_violations<'a>(&self, violations: &'a [Violation]) -> Vec<&'a Violation> {
385 let mut filtered: Vec<&Violation> = violations
386 .iter()
387 .filter(|v| {
388 if let Some(min_severity) = self.options.min_severity {
390 if v.severity < min_severity {
391 return false;
392 }
393 }
394 true
395 })
396 .collect();
397
398 if let Some(max) = self.options.max_violations {
400 filtered.truncate(max);
401 }
402
403 filtered
404 }
405
406 fn format_human(
408 &self,
409 report: &ValidationReport,
410 violations: &[&Violation],
411 ) -> GuardianResult<String> {
412 let mut output = String::new();
413
414 if violations.is_empty() {
415 if self.options.use_colors {
416 output.push_str("✅ \x1b[32mNo code quality violations found\x1b[0m\n");
417 } else {
418 output.push_str("✅ No code quality violations found\n");
419 }
420 } else {
421 let icon = if report.has_errors() { "❌" } else { "⚠️" };
423 if self.options.use_colors {
424 let color = if report.has_errors() { "31" } else { "33" };
425 output.push_str(&format!(
426 "{icon} \x1b[{color}mCode Quality Violations Found\x1b[0m\n\n"
427 ));
428 } else {
429 output.push_str(&format!("{icon} Code Quality Violations Found\n\n"));
430 }
431
432 let mut by_file: std::collections::BTreeMap<&std::path::Path, Vec<&Violation>> =
434 std::collections::BTreeMap::new();
435
436 for violation in violations {
437 by_file.entry(&violation.file_path).or_default().push(violation);
438 }
439
440 for (file_path, file_violations) in by_file {
442 output.push_str(&format!("📁 {}\n", file_path.display()));
443
444 for violation in file_violations {
445 let severity_color = match violation.severity {
447 Severity::Error => "31", Severity::Warning => "33", Severity::Info => "36", };
451
452 let position = match (violation.line_number, violation.column_number) {
453 (Some(line), Some(col)) => format!("{line}:{col}"),
454 (Some(line), None) => line.to_string(),
455 _ => "?".to_string(),
456 };
457
458 if self.options.use_colors {
459 output.push_str(&format!(
460 " \x1b[{}m{}:{}\x1b[0m [\x1b[{}m{}\x1b[0m] {}\n",
461 "2", position,
463 violation.rule_id,
464 severity_color,
465 violation.severity.as_str(),
466 violation.message
467 ));
468 } else {
469 output.push_str(&format!(
470 " {}:{} [{}] {}\n",
471 position,
472 violation.rule_id,
473 violation.severity.as_str(),
474 violation.message
475 ));
476 }
477
478 if self.options.show_context {
480 if let Some(context) = &violation.context {
481 if self.options.use_colors {
482 output.push_str(&format!(" \x1b[2m│ {context}\x1b[0m\n"));
483 } else {
484 output.push_str(&format!(" │ {context}\n"));
485 }
486 }
487 }
488
489 if self.options.show_suggestions {
491 if let Some(suggestion) = &violation.suggested_fix {
492 if self.options.use_colors {
493 output.push_str(&format!(" \x1b[32m💡 {suggestion}\x1b[0m\n"));
494 } else {
495 output.push_str(&format!(" 💡 {suggestion}\n"));
496 }
497 }
498 }
499
500 output.push('\n');
501 }
502 }
503 }
504
505 output.push_str(&self.format_summary(report));
507
508 Ok(output)
509 }
510
511 fn format_json(
513 &self,
514 report: &ValidationReport,
515 violations: &[&Violation],
516 ) -> GuardianResult<String> {
517 let json_violations: Vec<JsonValue> = violations
518 .iter()
519 .map(|v| {
520 serde_json::json!({
521 "rule_id": v.rule_id,
522 "severity": v.severity.as_str(),
523 "file_path": v.file_path.display().to_string(),
524 "line_number": v.line_number,
525 "column_number": v.column_number,
526 "message": v.message,
527 "context": v.context,
528 "suggested_fix": v.suggested_fix,
529 "detected_at": v.detected_at.to_rfc3339()
530 })
531 })
532 .collect();
533
534 let json_report = serde_json::json!({
535 "violations": json_violations,
536 "summary": {
537 "total_files": report.summary.total_files,
538 "violations_by_severity": {
539 "error": report.summary.violations_by_severity.error,
540 "warning": report.summary.violations_by_severity.warning,
541 "info": report.summary.violations_by_severity.info
542 },
543 "execution_time_ms": report.summary.execution_time_ms,
544 "validated_at": report.summary.validated_at.to_rfc3339()
545 },
546 "config_fingerprint": report.config_fingerprint
547 });
548
549 serde_json::to_string_pretty(&json_report).map_err(|e| {
550 crate::domain::violations::GuardianError::config(format!(
551 "JSON serialization failed: {e}"
552 ))
553 })
554 }
555
556 fn format_junit(
558 &self,
559 report: &ValidationReport,
560 violations: &[&Violation],
561 ) -> GuardianResult<String> {
562 let mut xml = String::new();
563 xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
564
565 let total_tests = violations.len();
566 let failures = violations.iter().filter(|v| v.severity == Severity::Error).count();
567 let errors = 0; let execution_time = (report.summary.execution_time_ms as f64) / 1000.0;
569
570 xml.push_str(&format!(
571 "<testsuite name=\"rust-guardian\" tests=\"{total_tests}\" failures=\"{failures}\" errors=\"{errors}\" time=\"{execution_time:.3}\">\n"
572 ));
573
574 for violation in violations {
575 xml.push_str(&format!(
576 " <testcase classname=\"{}\" name=\"{}\">\n",
577 violation.rule_id,
578 escape_xml(&violation.file_path.display().to_string())
579 ));
580
581 if violation.severity == Severity::Error {
582 xml.push_str(&format!(
583 " <failure message=\"{}\">\n",
584 escape_xml(&violation.message)
585 ));
586 xml.push_str(&format!(
587 " File: {}:{}:{}\n",
588 violation.file_path.display(),
589 violation.line_number.unwrap_or(0),
590 violation.column_number.unwrap_or(0)
591 ));
592 if let Some(context) = &violation.context {
593 xml.push_str(&format!(" Context: {}\n", escape_xml(context)));
594 }
595 xml.push_str(" </failure>\n");
596 }
597
598 xml.push_str(" </testcase>\n");
599 }
600
601 xml.push_str("</testsuite>\n");
602 Ok(xml)
603 }
604
605 fn format_sarif(
607 &self,
608 _report: &ValidationReport,
609 violations: &[&Violation],
610 ) -> GuardianResult<String> {
611 let sarif_results: Vec<JsonValue> = violations
612 .iter()
613 .map(|v| {
614 let level = match v.severity {
615 Severity::Error => "error",
616 Severity::Warning => "warning",
617 Severity::Info => "note",
618 };
619
620 serde_json::json!({
621 "ruleId": v.rule_id,
622 "level": level,
623 "message": {
624 "text": v.message
625 },
626 "locations": [{
627 "physicalLocation": {
628 "artifactLocation": {
629 "uri": v.file_path.display().to_string()
630 },
631 "region": {
632 "startLine": v.line_number.unwrap_or(1),
633 "startColumn": v.column_number.unwrap_or(1)
634 },
635 "contextRegion": v.context.as_ref().map(|c| serde_json::json!({
636 "snippet": {
637 "text": c
638 }
639 }))
640 }
641 }]
642 })
643 })
644 .collect();
645
646 let sarif_report = serde_json::json!({
647 "version": "2.1.0",
648 "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
649 "runs": [{
650 "tool": {
651 "driver": {
652 "name": "rust-guardian",
653 "version": "0.1.0",
654 "informationUri": "https://github.com/cloudfunnels/rust-guardian"
655 }
656 },
657 "results": sarif_results
658 }]
659 });
660
661 serde_json::to_string_pretty(&sarif_report).map_err(|e| {
662 crate::domain::violations::GuardianError::config(format!(
663 "SARIF serialization failed: {e}"
664 ))
665 })
666 }
667
668 fn format_github(
670 &self,
671 _report: &ValidationReport,
672 violations: &[&Violation],
673 ) -> GuardianResult<String> {
674 let mut output = String::new();
675
676 for violation in violations {
677 let level = match violation.severity {
678 Severity::Error => "error",
679 Severity::Warning => "warning",
680 Severity::Info => "notice",
681 };
682
683 let position = match (violation.line_number, violation.column_number) {
684 (Some(line), Some(col)) => format!("line={line},col={col}"),
685 (Some(line), None) => format!("line={line}"),
686 _ => String::new(),
687 };
688
689 let position_part =
690 if position.is_empty() { String::new() } else { format!(" {position}") };
691
692 output.push_str(&format!(
693 "::{} file={},title={}{}::{}\n",
694 level,
695 violation.file_path.display(),
696 violation.rule_id,
697 position_part,
698 violation.message
699 ));
700 }
701
702 Ok(output)
703 }
704
705 fn format_agent(
707 &self,
708 _report: &ValidationReport,
709 violations: &[&Violation],
710 ) -> GuardianResult<String> {
711 let mut output = String::new();
712
713 for violation in violations {
714 let line_number = violation.line_number.unwrap_or(1);
715 let path = violation.file_path.display();
716
717 output.push_str(&format!("[{}:{}]\n{}\n\n", line_number, path, violation.message));
718 }
719
720 Ok(output)
721 }
722
723 fn format_summary(&self, report: &ValidationReport) -> String {
725 let mut summary = String::new();
726
727 let total_violations = report.summary.violations_by_severity.total();
728 let execution_time = (report.summary.execution_time_ms as f64) / 1000.0;
729
730 if self.options.use_colors {
731 summary.push_str("📊 \x1b[1mSummary:\x1b[0m ");
732 } else {
733 summary.push_str("📊 Summary: ");
734 }
735
736 if total_violations == 0 {
737 if self.options.use_colors {
738 summary.push_str(&format!(
739 "\x1b[32m0 violations\x1b[0m in {} files ({:.1}s)\n",
740 report.summary.total_files, execution_time
741 ));
742 } else {
743 summary.push_str(&format!(
744 "0 violations in {} files ({:.1}s)\n",
745 report.summary.total_files, execution_time
746 ));
747 }
748 } else {
749 let mut parts = Vec::new();
750
751 if report.summary.violations_by_severity.error > 0 {
752 let text = format!(
753 "{} error{}",
754 report.summary.violations_by_severity.error,
755 if report.summary.violations_by_severity.error == 1 { "" } else { "s" }
756 );
757 if self.options.use_colors {
758 parts.push(format!("\x1b[31m{text}\x1b[0m"));
759 } else {
760 parts.push(text);
761 }
762 }
763
764 if report.summary.violations_by_severity.warning > 0 {
765 let text = format!(
766 "{} warning{}",
767 report.summary.violations_by_severity.warning,
768 if report.summary.violations_by_severity.warning == 1 { "" } else { "s" }
769 );
770 if self.options.use_colors {
771 parts.push(format!("\x1b[33m{text}\x1b[0m"));
772 } else {
773 parts.push(text);
774 }
775 }
776
777 if report.summary.violations_by_severity.info > 0 {
778 let text = format!("{} info", report.summary.violations_by_severity.info);
779 if self.options.use_colors {
780 parts.push(format!("\x1b[36m{text}\x1b[0m"));
781 } else {
782 parts.push(text);
783 }
784 }
785
786 summary.push_str(&format!(
787 "{} in {} files ({:.1}s)\n",
788 parts.join(", "),
789 report.summary.total_files,
790 execution_time
791 ));
792 }
793
794 summary
795 }
796}
797
798impl Default for ReportFormatter {
799 fn default() -> Self {
801 Self::with_options(ReportOptions::default())
802 }
803}
804
805fn escape_xml(s: &str) -> String {
807 s.replace('&', "&")
808 .replace('<', "<")
809 .replace('>', ">")
810 .replace('"', """)
811 .replace('\'', "'")
812}
813
814#[cfg(test)]
815mod tests {
816 use super::*;
817 use std::path::PathBuf;
818 fn create_test_report() -> ValidationReport {
821 let mut report = ValidationReport::new();
822
823 report.add_violation(
824 crate::domain::violations::Violation::new(
825 "test_rule",
826 Severity::Error,
827 PathBuf::from("src/main.rs"),
828 "Test violation",
829 )
830 .with_position(42, 15)
831 .with_context("let x = unimplemented!();"),
832 );
833
834 report.set_files_analyzed(10);
835 report.set_execution_time(1200);
836
837 report
838 }
839
840 #[test]
841 fn test_human_format() {
842 let options = ReportOptions { use_colors: false, ..Default::default() };
843
844 options.validate().expect("Test options should be valid");
846
847 let formatter = ReportFormatter::with_options(options);
848
849 let report = create_test_report();
850 let output = formatter
851 .format_report(&report, OutputFormat::Human)
852 .expect("Human format should always succeed for valid reports");
853
854 formatter
856 .validate_format_integrity(&report, OutputFormat::Human, &output)
857 .expect("Human output should pass integrity validation");
858
859 assert!(output.contains("Code Quality Violations Found"));
860 assert!(output.contains("src/main.rs"));
861 assert!(output.contains("Test violation"));
862 assert!(output.contains("Summary:"));
863 }
864
865 #[test]
866 fn test_json_format() {
867 let formatter = ReportFormatter::default();
868 let report = create_test_report();
869 let output = formatter
870 .format_report(&report, OutputFormat::Json)
871 .expect("JSON format should always succeed for valid reports");
872
873 formatter
875 .validate_format_integrity(&report, OutputFormat::Json, &output)
876 .expect("JSON output should pass integrity validation");
877
878 let json: JsonValue =
879 serde_json::from_str(&output).expect("JSON output should be valid JSON");
880 assert!(json["violations"].is_array());
881 assert_eq!(json["violations"].as_array().expect("violations should be an array").len(), 1);
882 assert_eq!(json["violations"][0]["rule_id"], "test_rule");
883 assert_eq!(json["summary"]["total_files"], 10);
884 }
885
886 #[test]
887 fn test_junit_format() {
888 let formatter = ReportFormatter::default();
889 let report = create_test_report();
890 let output = formatter
891 .format_report(&report, OutputFormat::Junit)
892 .expect("JUnit format should always succeed for valid reports");
893
894 formatter
896 .validate_format_integrity(&report, OutputFormat::Junit, &output)
897 .expect("JUnit output should pass integrity validation");
898
899 assert!(output.contains("<?xml version=\"1.0\""));
900 assert!(output.contains("<testsuite"));
901 assert!(output.contains("test_rule"));
902 assert!(output.contains("<failure"));
903 }
904
905 #[test]
906 fn test_github_format() {
907 let formatter = ReportFormatter::default();
908 let report = create_test_report();
909 let output = formatter
910 .format_report(&report, OutputFormat::GitHub)
911 .expect("GitHub format should always succeed for valid reports");
912
913 formatter
915 .validate_format_integrity(&report, OutputFormat::GitHub, &output)
916 .expect("GitHub output should pass integrity validation");
917
918 assert!(output.contains("::error"));
919 assert!(output.contains("file=src/main.rs"));
920 assert!(output.contains("line=42,col=15"));
921 assert!(output.contains("Test violation"));
922 }
923
924 #[test]
925 fn test_empty_report() {
926 let options = ReportOptions { use_colors: false, ..Default::default() };
927
928 options.validate().expect("Test options should be valid");
930
931 let formatter = ReportFormatter::with_options(options);
932
933 let report = ValidationReport::new();
934 let output = formatter
935 .format_report(&report, OutputFormat::Human)
936 .expect("Human format should always succeed for empty reports");
937
938 formatter
940 .validate_format_integrity(&report, OutputFormat::Human, &output)
941 .expect("Empty report output should pass integrity validation");
942
943 assert!(output.contains("No code quality violations found"));
944 }
945
946 #[test]
947 fn test_severity_filtering() {
948 let options = ReportOptions { min_severity: Some(Severity::Error), ..Default::default() };
949
950 options.validate().expect("Severity filtering options should be valid");
952
953 let formatter = ReportFormatter::with_options(options);
954
955 let mut report = ValidationReport::new();
956 report.add_violation(crate::domain::violations::Violation::new(
957 "warning_rule",
958 Severity::Warning,
959 PathBuf::from("src/lib.rs"),
960 "Warning message",
961 ));
962 report.add_violation(crate::domain::violations::Violation::new(
963 "error_rule",
964 Severity::Error,
965 PathBuf::from("src/main.rs"),
966 "Error message",
967 ));
968
969 let output = formatter
970 .format_report(&report, OutputFormat::Json)
971 .expect("JSON format should succeed for severity filtering test");
972
973 formatter
975 .validate_format_integrity(&report, OutputFormat::Json, &output)
976 .expect("Filtered JSON output should pass integrity validation");
977
978 let json: JsonValue =
979 serde_json::from_str(&output).expect("Severity filtered JSON should be valid");
980
981 assert_eq!(
983 json["violations"].as_array().expect("filtered violations should be an array").len(),
984 1
985 );
986 assert_eq!(json["violations"][0]["rule_id"], "error_rule");
987 }
988
989 #[test]
990 fn test_domain_validation_behavior() {
991 let invalid_options = ReportOptions {
993 max_violations: Some(0), ..Default::default()
995 };
996
997 assert!(invalid_options.validate().is_err());
998
999 assert!(ReportFormatter::new(invalid_options).is_err());
1001
1002 let formatter = ReportFormatter::default();
1004
1005 let valid_json = r#"{"violations": [], "summary": {"total_files": 0}}"#;
1007 assert!(formatter.validate_json_structure(valid_json).is_ok());
1008
1009 let invalid_json = r#"{"missing_required_fields": true}"#;
1011 assert!(formatter.validate_json_structure(invalid_json).is_err());
1012
1013 assert!(OutputFormat::Human.supports_colors());
1015 assert!(!OutputFormat::Json.supports_colors());
1016 assert!(OutputFormat::Json.is_structured());
1017 assert!(!OutputFormat::Human.is_structured());
1018
1019 let human_options = ReportOptions::optimized_for(OutputFormat::Human);
1021 assert!(human_options.is_optimized_for(OutputFormat::Human));
1022
1023 let json_options = ReportOptions::optimized_for(OutputFormat::Json);
1024 assert!(json_options.is_optimized_for(OutputFormat::Json));
1025 assert!(!json_options.use_colors); }
1027}