1use std::path::Path;
4
5use fallow_output::{
6 CodeClimateIssue, CodeClimateIssueInput, CodeClimateSeverity, ComplexityViolation,
7 CoverageIntelligenceFinding, CoverageIntelligenceRecommendation, CoverageIntelligenceVerdict,
8 ExceededThreshold, FindingSeverity, HealthReport, HealthSummary, RuntimeCoverageFinding,
9 RuntimeCoverageVerdict, StylingFinding, StylingFindingSeverity, UntestedExportFinding,
10 UntestedFileFinding, build_codeclimate_issue, codeclimate_fingerprint_hash, normalize_uri,
11};
12use fallow_types::output_dead_code::EffectiveSeverity;
13
14struct HealthCodeClimateContext<'a> {
15 root: &'a Path,
16 summary: &'a HealthSummary,
20}
21
22impl HealthCodeClimateContext<'_> {
23 fn complexity_issue(&self, finding: &ComplexityViolation) -> CodeClimateIssue {
24 let path = codeclimate_path(&finding.path, self.root);
25 let check_name = complexity_check_name(finding);
26 let line_str = finding.line.to_string();
27 let fp = codeclimate_fingerprint_hash(&[check_name, &path, &line_str, &finding.name]);
28 build_codeclimate_issue(CodeClimateIssueInput {
29 check_name,
30 description: &self.complexity_description(finding),
31 severity: complexity_codeclimate_severity(finding),
32 category: "Complexity",
33 path: &path,
34 begin_line: Some(finding.line),
35 fingerprint: &fp,
36 })
37 }
38
39 fn styling_issue(&self, finding: &StylingFinding) -> CodeClimateIssue {
40 let path = codeclimate_path(Path::new(&finding.path), self.root);
41 let check_name = format!("fallow/{}", finding.code);
42 let description = format!("[{}] {}: {}", finding.code, finding.sub_kind, finding.value);
43 let line_str = finding.line.to_string();
44 let fp = codeclimate_fingerprint_hash(&[
45 &check_name,
46 &path,
47 &line_str,
48 &finding.sub_kind,
49 &finding.value,
50 ]);
51 build_codeclimate_issue(CodeClimateIssueInput {
52 check_name: &check_name,
53 description: &description,
54 severity: styling_finding_severity(finding.effective_severity),
55 category: "Style",
56 path: &path,
57 begin_line: Some(finding.line),
58 fingerprint: &fp,
59 })
60 }
61
62 fn complexity_description(&self, finding: &ComplexityViolation) -> String {
63 let thresholds = finding.resolved_thresholds(self.summary);
64 match finding.exceeded {
65 ExceededThreshold::Both => format!(
66 "'{}' has cyclomatic complexity {} (threshold: {}) and cognitive complexity {} (threshold: {})",
67 finding.name,
68 finding.cyclomatic,
69 thresholds.max_cyclomatic,
70 finding.cognitive,
71 thresholds.max_cognitive
72 ),
73 ExceededThreshold::Cyclomatic => format!(
74 "'{}' has cyclomatic complexity {} (threshold: {})",
75 finding.name, finding.cyclomatic, thresholds.max_cyclomatic
76 ),
77 ExceededThreshold::Cognitive => format!(
78 "'{}' has cognitive complexity {} (threshold: {})",
79 finding.name, finding.cognitive, thresholds.max_cognitive
80 ),
81 ExceededThreshold::Crap
82 | ExceededThreshold::CyclomaticCrap
83 | ExceededThreshold::CognitiveCrap
84 | ExceededThreshold::All => {
85 let crap = finding.crap.unwrap_or(0.0);
86 let coverage = finding
87 .coverage_pct
88 .map(|pct| format!(", coverage {pct:.0}%"))
89 .unwrap_or_default();
90 format!(
91 "'{}' has CRAP score {crap:.1} (threshold: {:.1}, cyclomatic {}{coverage})",
92 finding.name, thresholds.max_crap, finding.cyclomatic,
93 )
94 }
95 }
96 }
97
98 fn runtime_coverage_issue(&self, finding: &RuntimeCoverageFinding) -> CodeClimateIssue {
99 let path = codeclimate_path(&finding.path, self.root);
100 let check_name = runtime_coverage_check_name(finding.verdict);
101 let invocations_hint = finding.invocations.map_or_else(
102 || "untracked".to_owned(),
103 |hits| format!("{hits} invocations"),
104 );
105 let description = format!(
106 "'{}' runtime coverage verdict: {} ({})",
107 finding.function,
108 finding.verdict.human_label(),
109 invocations_hint,
110 );
111 let fp = codeclimate_fingerprint_hash(&[
112 check_name,
113 &path,
114 &finding.line.to_string(),
115 &finding.function,
116 ]);
117 build_codeclimate_issue(CodeClimateIssueInput {
118 check_name,
119 description: &description,
120 severity: runtime_coverage_severity(finding.verdict),
121 category: "Bug Risk",
122 path: &path,
123 begin_line: Some(finding.line),
124 fingerprint: &fp,
125 })
126 }
127
128 fn coverage_intelligence_issue(
129 &self,
130 finding: &CoverageIntelligenceFinding,
131 ) -> Option<CodeClimateIssue> {
132 let severity = coverage_intelligence_severity(finding.verdict)?;
133 let path = codeclimate_path(&finding.path, self.root);
134 let check_name = coverage_intelligence_check_name(finding.recommendation);
135 let identity = finding.identity.as_deref().unwrap_or("code");
136 let description = format!(
137 "'{}' coverage intelligence verdict: {} ({})",
138 identity, finding.verdict, finding.recommendation,
139 );
140 let fp = codeclimate_fingerprint_hash(&[
141 check_name,
142 &path,
143 &finding.line.to_string(),
144 identity,
145 &finding.id,
146 ]);
147 Some(build_codeclimate_issue(CodeClimateIssueInput {
148 check_name,
149 description: &description,
150 severity,
151 category: "Bug Risk",
152 path: &path,
153 begin_line: Some(finding.line),
154 fingerprint: &fp,
155 }))
156 }
157
158 fn untested_file_issue(&self, item: &UntestedFileFinding) -> CodeClimateIssue {
159 let path = codeclimate_path(&item.file.path, self.root);
160 let description = format!(
161 "File is runtime-reachable but has no test dependency path ({} value export{})",
162 item.file.value_export_count,
163 if item.file.value_export_count == 1 {
164 ""
165 } else {
166 "s"
167 },
168 );
169 let fp = codeclimate_fingerprint_hash(&["fallow/untested-file", &path]);
170 build_codeclimate_issue(CodeClimateIssueInput {
171 check_name: "fallow/untested-file",
172 description: &description,
173 severity: CodeClimateSeverity::Minor,
174 category: "Coverage",
175 path: &path,
176 begin_line: None,
177 fingerprint: &fp,
178 })
179 }
180
181 fn untested_export_issue(&self, item: &UntestedExportFinding) -> CodeClimateIssue {
182 let path = codeclimate_path(&item.export.path, self.root);
183 let description = format!(
184 "Export '{}' is runtime-reachable but never referenced by test-reachable modules",
185 item.export.export_name
186 );
187 let line_str = item.export.line.to_string();
188 let fp = codeclimate_fingerprint_hash(&[
189 "fallow/untested-export",
190 &path,
191 &line_str,
192 &item.export.export_name,
193 ]);
194 build_codeclimate_issue(CodeClimateIssueInput {
195 check_name: "fallow/untested-export",
196 description: &description,
197 severity: CodeClimateSeverity::Minor,
198 category: "Coverage",
199 path: &path,
200 begin_line: Some(item.export.line),
201 fingerprint: &fp,
202 })
203 }
204}
205
206#[must_use]
208pub fn build_health_codeclimate(report: &HealthReport, root: &Path) -> Vec<CodeClimateIssue> {
209 let mut issues = Vec::new();
210 let ctx = HealthCodeClimateContext {
211 root,
212 summary: &report.summary,
213 };
214
215 for finding in &report.findings {
216 let mut issue = ctx.complexity_issue(finding);
217 if !finding.worsened_metrics.is_empty() {
220 let class =
221 if finding.attribution == Some(fallow_output::AuditFindingAttribution::Worsened) {
222 "worsened"
223 } else {
224 "introduced"
225 };
226 issue.description = format!(
227 "{} ({class} against the base: {})",
228 issue.description,
229 fallow_output::describe_worsened_metrics(&finding.worsened_metrics)
230 );
231 }
232 if finding.gate_exemption == Some(fallow_output::AuditGateExemption::TestFile) {
233 issue.description =
234 format!("{} (test file: audit does not gate it)", issue.description);
235 }
236 issues.push(issue);
237 }
238 for finding in &report.styling_findings {
239 issues.push(ctx.styling_issue(finding));
240 }
241
242 if let Some(ref production) = report.runtime_coverage {
243 for finding in &production.findings {
244 issues.push(ctx.runtime_coverage_issue(finding));
245 }
246 }
247
248 if let Some(ref intelligence) = report.coverage_intelligence {
249 for finding in &intelligence.findings {
250 if let Some(issue) = ctx.coverage_intelligence_issue(finding) {
251 issues.push(issue);
252 }
253 }
254 }
255
256 if let Some(ref gaps) = report.coverage_gaps {
257 for item in &gaps.files {
258 issues.push(ctx.untested_file_issue(item));
259 }
260
261 for item in &gaps.exports {
262 issues.push(ctx.untested_export_issue(item));
263 }
264 }
265
266 issues
267}
268
269fn codeclimate_path(path: &Path, root: &Path) -> String {
270 normalize_uri(
271 &path
272 .strip_prefix(root)
273 .unwrap_or(path)
274 .display()
275 .to_string(),
276 )
277}
278
279const fn coverage_intelligence_check_name(
280 recommendation: CoverageIntelligenceRecommendation,
281) -> &'static str {
282 match recommendation {
283 CoverageIntelligenceRecommendation::AddTestOrSplitBeforeMerge => {
284 "fallow/coverage-intelligence-risky-change"
285 }
286 CoverageIntelligenceRecommendation::DeleteAfterConfirmingOwner => {
287 "fallow/coverage-intelligence-delete"
288 }
289 CoverageIntelligenceRecommendation::ReviewBeforeChanging => {
290 "fallow/coverage-intelligence-review"
291 }
292 CoverageIntelligenceRecommendation::RefactorCarefullyKeepBehavior => {
293 "fallow/coverage-intelligence-refactor"
294 }
295 }
296}
297
298const fn complexity_check_name(finding: &ComplexityViolation) -> &'static str {
299 match finding.exceeded {
300 ExceededThreshold::Both => "fallow/high-complexity",
301 ExceededThreshold::Cyclomatic => "fallow/high-cyclomatic-complexity",
302 ExceededThreshold::Cognitive => "fallow/high-cognitive-complexity",
303 ExceededThreshold::Crap
304 | ExceededThreshold::CyclomaticCrap
305 | ExceededThreshold::CognitiveCrap
306 | ExceededThreshold::All => "fallow/high-crap-score",
307 }
308}
309
310const fn complexity_codeclimate_severity(finding: &ComplexityViolation) -> CodeClimateSeverity {
317 match finding.effective_severity {
318 Some(EffectiveSeverity::Error) => CodeClimateSeverity::Major,
319 Some(EffectiveSeverity::Warn) => CodeClimateSeverity::Minor,
320 None => health_finding_severity(finding.severity),
321 }
322}
323
324const fn health_finding_severity(severity: FindingSeverity) -> CodeClimateSeverity {
325 match severity {
326 FindingSeverity::Critical => CodeClimateSeverity::Critical,
327 FindingSeverity::High => CodeClimateSeverity::Major,
328 FindingSeverity::Moderate => CodeClimateSeverity::Minor,
329 }
330}
331
332const fn styling_finding_severity(severity: StylingFindingSeverity) -> CodeClimateSeverity {
333 match severity {
334 StylingFindingSeverity::Error => CodeClimateSeverity::Major,
335 StylingFindingSeverity::Warn => CodeClimateSeverity::Minor,
336 }
337}
338
339const fn runtime_coverage_check_name(verdict: RuntimeCoverageVerdict) -> &'static str {
340 match verdict {
341 RuntimeCoverageVerdict::SafeToDelete => "fallow/runtime-safe-to-delete",
342 RuntimeCoverageVerdict::ReviewRequired => "fallow/runtime-review-required",
343 RuntimeCoverageVerdict::LowTraffic => "fallow/runtime-low-traffic",
344 RuntimeCoverageVerdict::CoverageUnavailable => "fallow/runtime-coverage-unavailable",
345 RuntimeCoverageVerdict::Active | RuntimeCoverageVerdict::Unknown => {
346 "fallow/runtime-coverage"
347 }
348 }
349}
350
351const fn runtime_coverage_severity(verdict: RuntimeCoverageVerdict) -> CodeClimateSeverity {
352 match verdict {
353 RuntimeCoverageVerdict::SafeToDelete => CodeClimateSeverity::Critical,
354 RuntimeCoverageVerdict::ReviewRequired => CodeClimateSeverity::Major,
355 _ => CodeClimateSeverity::Minor,
356 }
357}
358
359const fn coverage_intelligence_severity(
360 verdict: CoverageIntelligenceVerdict,
361) -> Option<CodeClimateSeverity> {
362 match verdict {
363 CoverageIntelligenceVerdict::RiskyChangeDetected
364 | CoverageIntelligenceVerdict::HighConfidenceDelete => Some(CodeClimateSeverity::Major),
365 CoverageIntelligenceVerdict::ReviewRequired
366 | CoverageIntelligenceVerdict::RefactorCarefully => Some(CodeClimateSeverity::Minor),
367 CoverageIntelligenceVerdict::Clean | CoverageIntelligenceVerdict::Unknown => None,
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use std::path::{Path, PathBuf};
374
375 use fallow_output::{
376 ComplexityViolation, ExceededThreshold, FindingSeverity, HealthReport, HealthSummary,
377 StylingFinding, StylingFindingSeverity,
378 };
379
380 use super::*;
381
382 #[test]
383 fn health_codeclimate_uses_relative_normalized_paths() {
384 let report = HealthReport {
385 summary: HealthSummary {
386 max_cyclomatic_threshold: 10,
387 max_cognitive_threshold: 8,
388 max_crap_threshold: 30.0,
389 ..HealthSummary::default()
390 },
391 findings: vec![
392 ComplexityViolation {
393 path: PathBuf::from("/root/app/[id]/page.tsx"),
394 name: "render".to_string(),
395 line: 7,
396 col: 0,
397 cyclomatic: 12,
398 standard_cyclomatic: None,
399 cognitive: 9,
400 line_count: 20,
401 param_count: 1,
402 react_hook_count: 0,
403 react_jsx_max_depth: 0,
404 react_prop_count: 0,
405 react_hook_profile: None,
406 exceeded: ExceededThreshold::Both,
407 effective_severity: None,
408 severity: FindingSeverity::High,
409 mutation: None,
410 coverage_pct: None,
411 crap: None,
412 coverage_tier: None,
413 coverage_source: None,
414 inherited_from: None,
415 component_rollup: None,
416 contributions: Vec::new(),
417 effective_thresholds: None,
418 threshold_source: None,
419 }
420 .into(),
421 ],
422 ..HealthReport::default()
423 };
424
425 let issues = build_health_codeclimate(&report, Path::new("/root"));
426
427 assert_eq!(issues.len(), 1);
428 let issue = &issues[0];
429 assert_eq!(issue.check_name, "fallow/high-complexity");
430 assert_eq!(issue.location.path, "app/%5Bid%5D/page.tsx");
431 assert_eq!(issue.location.lines.begin, 7);
432 assert_eq!(issue.severity, CodeClimateSeverity::Major);
433 }
434
435 #[test]
436 fn health_codeclimate_includes_styling_findings() {
437 let report = HealthReport {
438 styling_findings: vec![StylingFinding {
439 code: "css-selector-complexity".to_string(),
440 sub_kind: "high-specificity".to_string(),
441 path: "src/styles.css".to_string(),
442 line: 4,
443 value: "#app .card .title".to_string(),
444 effective_severity: StylingFindingSeverity::Error,
445 blast_radius: None,
446 confidence: None,
447 agent_disposition: None,
448 nearest_token: None,
449 fix_hint: None,
450 actions: Vec::new(),
451 }],
452 ..HealthReport::default()
453 };
454
455 let issues = build_health_codeclimate(&report, Path::new("/root"));
456
457 assert_eq!(issues.len(), 1);
458 let issue = &issues[0];
459 assert_eq!(issue.check_name, "fallow/css-selector-complexity");
460 assert_eq!(issue.location.path, "src/styles.css");
461 assert_eq!(issue.location.lines.begin, 4);
462 assert_eq!(issue.severity, CodeClimateSeverity::Major);
463 }
464
465 fn crap_violation(
466 effective_thresholds: Option<fallow_output::HealthEffectiveThresholds>,
467 ) -> ComplexityViolation {
468 ComplexityViolation {
469 path: PathBuf::from("/root/src/Board.astro"),
470 name: "<template>".to_string(),
471 line: 6,
472 col: 3,
473 cyclomatic: 11,
474 standard_cyclomatic: None,
475 cognitive: 4,
476 line_count: 20,
477 param_count: 0,
478 react_hook_count: 0,
479 react_jsx_max_depth: 0,
480 react_prop_count: 0,
481 react_hook_profile: None,
482 exceeded: ExceededThreshold::Crap,
483 effective_severity: None,
484 severity: FindingSeverity::Critical,
485 mutation: None,
486 coverage_pct: None,
487 crap: Some(132.0),
488 coverage_tier: None,
489 coverage_source: None,
490 inherited_from: None,
491 component_rollup: None,
492 contributions: Vec::new(),
493 threshold_source: effective_thresholds
494 .map(|_| fallow_output::ThresholdSource::Override),
495 effective_thresholds,
496 }
497 }
498
499 fn crap_report(
500 effective_thresholds: Option<fallow_output::HealthEffectiveThresholds>,
501 ) -> HealthReport {
502 HealthReport {
503 summary: HealthSummary {
504 max_crap_threshold: 30.0,
505 ..HealthSummary::default()
506 },
507 findings: vec![crap_violation(effective_thresholds).into()],
508 ..HealthReport::default()
509 }
510 }
511
512 #[test]
516 fn codeclimate_description_uses_the_override_ceiling_not_the_global_one() {
517 let report = crap_report(Some(fallow_output::HealthEffectiveThresholds {
518 max_cyclomatic: 20,
519 max_cognitive: 15,
520 max_crap: 100.0,
521 max_unit_size: 60,
522 }));
523
524 let issues = build_health_codeclimate(&report, Path::new("/root"));
525
526 assert_eq!(issues.len(), 1);
527 assert!(
528 issues[0].description.contains("threshold: 100.0"),
529 "{}",
530 issues[0].description
531 );
532 assert!(
533 !issues[0].description.contains("threshold: 30.0"),
534 "{}",
535 issues[0].description
536 );
537 }
538
539 #[test]
540 fn codeclimate_description_falls_back_to_the_global_ceiling() {
541 let issues = build_health_codeclimate(&crap_report(None), Path::new("/root"));
542
543 assert_eq!(issues.len(), 1);
544 assert!(
545 issues[0].description.contains("threshold: 30.0"),
546 "{}",
547 issues[0].description
548 );
549 }
550
551 #[test]
552 fn health_severities_reach_serialized_codeclimate_issues() {
553 for (severity, expected) in [
554 (FindingSeverity::Critical, "critical"),
555 (FindingSeverity::High, "major"),
556 (FindingSeverity::Moderate, "minor"),
557 ] {
558 let mut violation = crap_violation(None);
559 violation.severity = severity;
560 let report = HealthReport {
561 findings: vec![violation.into()],
562 ..HealthReport::default()
563 };
564 let issues = fallow_output::codeclimate_issues_to_value(&build_health_codeclimate(
565 &report,
566 Path::new("/root"),
567 ));
568 let issues = issues
569 .as_array()
570 .expect("CodeClimate output must be an array");
571 assert_eq!(issues.len(), 1);
572 assert_eq!(issues[0]["severity"], expected);
573 assert_eq!(issues[0]["check_name"], "fallow/high-crap-score");
574 }
575 }
576
577 #[test]
578 fn runtime_verdicts_reach_serialized_codeclimate_issues() {
579 use fallow_output::{
580 RuntimeCoverageConfidence, RuntimeCoverageEvidence, RuntimeCoverageReport,
581 };
582
583 for (verdict, check_name, severity) in [
584 (
585 RuntimeCoverageVerdict::SafeToDelete,
586 "fallow/runtime-safe-to-delete",
587 "critical",
588 ),
589 (
590 RuntimeCoverageVerdict::ReviewRequired,
591 "fallow/runtime-review-required",
592 "major",
593 ),
594 (
595 RuntimeCoverageVerdict::LowTraffic,
596 "fallow/runtime-low-traffic",
597 "minor",
598 ),
599 (
600 RuntimeCoverageVerdict::CoverageUnavailable,
601 "fallow/runtime-coverage-unavailable",
602 "minor",
603 ),
604 (
605 RuntimeCoverageVerdict::Active,
606 "fallow/runtime-coverage",
607 "minor",
608 ),
609 (
610 RuntimeCoverageVerdict::Unknown,
611 "fallow/runtime-coverage",
612 "minor",
613 ),
614 ] {
615 let report = HealthReport {
616 runtime_coverage: Some(RuntimeCoverageReport {
617 findings: vec![RuntimeCoverageFinding {
618 id: "runtime-fixture".to_string(),
619 stable_id: None,
620 source_hash: None,
621 path: PathBuf::from("/root/src/legacy.ts"),
622 function: "legacyHelper".to_string(),
623 line: 12,
624 verdict,
625 invocations: Some(0),
626 confidence: RuntimeCoverageConfidence::High,
627 evidence: RuntimeCoverageEvidence {
628 static_status: "unused".to_string(),
629 test_coverage: "not_covered".to_string(),
630 test_only_reference: None,
631 v8_tracking: "tracked".to_string(),
632 untracked_reason: None,
633 observation_days: 30,
634 deployments_observed: 3,
635 },
636 actions: Vec::new(),
637 discriminators: None,
638 }],
639 ..RuntimeCoverageReport::default()
640 }),
641 ..HealthReport::default()
642 };
643 let issues = fallow_output::codeclimate_issues_to_value(&build_health_codeclimate(
644 &report,
645 Path::new("/root"),
646 ));
647 let issues = issues
648 .as_array()
649 .expect("CodeClimate output must be an array");
650 assert_eq!(issues.len(), 1, "{verdict:?}");
651 assert_eq!(issues[0]["check_name"], check_name);
652 assert_eq!(issues[0]["severity"], severity);
653 assert_eq!(issues[0]["location"]["path"], "src/legacy.ts");
654 assert_eq!(issues[0]["location"]["lines"]["begin"], 12);
655 }
656 }
657
658 #[test]
659 fn coverage_intelligence_verdicts_and_recommendations_reach_serialized_issues() {
660 use fallow_output::{
661 CoverageIntelligenceConfidence, CoverageIntelligenceEvidence,
662 CoverageIntelligenceReport,
663 };
664
665 let recommendations = [
666 (
667 CoverageIntelligenceRecommendation::AddTestOrSplitBeforeMerge,
668 "fallow/coverage-intelligence-risky-change",
669 ),
670 (
671 CoverageIntelligenceRecommendation::DeleteAfterConfirmingOwner,
672 "fallow/coverage-intelligence-delete",
673 ),
674 (
675 CoverageIntelligenceRecommendation::ReviewBeforeChanging,
676 "fallow/coverage-intelligence-review",
677 ),
678 (
679 CoverageIntelligenceRecommendation::RefactorCarefullyKeepBehavior,
680 "fallow/coverage-intelligence-refactor",
681 ),
682 ];
683 for (verdict, severity) in [
684 (
685 CoverageIntelligenceVerdict::RiskyChangeDetected,
686 Some("major"),
687 ),
688 (
689 CoverageIntelligenceVerdict::HighConfidenceDelete,
690 Some("major"),
691 ),
692 (CoverageIntelligenceVerdict::ReviewRequired, Some("minor")),
693 (
694 CoverageIntelligenceVerdict::RefactorCarefully,
695 Some("minor"),
696 ),
697 (CoverageIntelligenceVerdict::Clean, None),
698 (CoverageIntelligenceVerdict::Unknown, None),
699 ] {
700 for (recommendation, check_name) in recommendations {
701 let report = HealthReport {
702 coverage_intelligence: Some(CoverageIntelligenceReport {
703 schema_version: fallow_output::CoverageIntelligenceSchemaVersion::default(),
704 verdict,
705 summary: fallow_output::CoverageIntelligenceSummary::default(),
706 findings: vec![CoverageIntelligenceFinding {
707 id: "coverage-fixture".to_string(),
708 path: PathBuf::from("/root/src/legacy.ts"),
709 identity: Some("legacyHelper".to_string()),
710 line: 12,
711 verdict,
712 signals: Vec::new(),
713 recommendation,
714 confidence: CoverageIntelligenceConfidence::High,
715 related_ids: Vec::new(),
716 evidence: CoverageIntelligenceEvidence::default(),
717 actions: Vec::new(),
718 }],
719 }),
720 ..HealthReport::default()
721 };
722 let issues = fallow_output::codeclimate_issues_to_value(&build_health_codeclimate(
723 &report,
724 Path::new("/root"),
725 ));
726 let issues = issues
727 .as_array()
728 .expect("CodeClimate output must be an array");
729 if let Some(severity) = severity {
730 assert_eq!(issues.len(), 1, "{verdict:?}");
731 assert_eq!(issues[0]["check_name"], check_name);
732 assert_eq!(issues[0]["severity"], severity);
733 assert_eq!(issues[0]["location"]["path"], "src/legacy.ts");
734 } else {
735 assert!(issues.is_empty(), "{verdict:?}");
736 }
737 }
738 }
739 }
740}