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 cognitive: 9,
399 line_count: 20,
400 param_count: 1,
401 react_hook_count: 0,
402 react_jsx_max_depth: 0,
403 react_prop_count: 0,
404 react_hook_profile: None,
405 exceeded: ExceededThreshold::Both,
406 effective_severity: None,
407 severity: FindingSeverity::High,
408 mutation: None,
409 coverage_pct: None,
410 crap: None,
411 coverage_tier: None,
412 coverage_source: None,
413 inherited_from: None,
414 component_rollup: None,
415 contributions: Vec::new(),
416 effective_thresholds: None,
417 threshold_source: None,
418 }
419 .into(),
420 ],
421 ..HealthReport::default()
422 };
423
424 let issues = build_health_codeclimate(&report, Path::new("/root"));
425
426 assert_eq!(issues.len(), 1);
427 let issue = &issues[0];
428 assert_eq!(issue.check_name, "fallow/high-complexity");
429 assert_eq!(issue.location.path, "app/%5Bid%5D/page.tsx");
430 assert_eq!(issue.location.lines.begin, 7);
431 assert_eq!(issue.severity, CodeClimateSeverity::Major);
432 }
433
434 #[test]
435 fn health_codeclimate_includes_styling_findings() {
436 let report = HealthReport {
437 styling_findings: vec![StylingFinding {
438 code: "css-selector-complexity".to_string(),
439 sub_kind: "high-specificity".to_string(),
440 path: "src/styles.css".to_string(),
441 line: 4,
442 value: "#app .card .title".to_string(),
443 effective_severity: StylingFindingSeverity::Error,
444 blast_radius: None,
445 confidence: None,
446 agent_disposition: None,
447 nearest_token: None,
448 fix_hint: None,
449 actions: Vec::new(),
450 }],
451 ..HealthReport::default()
452 };
453
454 let issues = build_health_codeclimate(&report, Path::new("/root"));
455
456 assert_eq!(issues.len(), 1);
457 let issue = &issues[0];
458 assert_eq!(issue.check_name, "fallow/css-selector-complexity");
459 assert_eq!(issue.location.path, "src/styles.css");
460 assert_eq!(issue.location.lines.begin, 4);
461 assert_eq!(issue.severity, CodeClimateSeverity::Major);
462 }
463
464 fn crap_violation(
465 effective_thresholds: Option<fallow_output::HealthEffectiveThresholds>,
466 ) -> ComplexityViolation {
467 ComplexityViolation {
468 path: PathBuf::from("/root/src/Board.astro"),
469 name: "<template>".to_string(),
470 line: 6,
471 col: 3,
472 cyclomatic: 11,
473 cognitive: 4,
474 line_count: 20,
475 param_count: 0,
476 react_hook_count: 0,
477 react_jsx_max_depth: 0,
478 react_prop_count: 0,
479 react_hook_profile: None,
480 exceeded: ExceededThreshold::Crap,
481 effective_severity: None,
482 severity: FindingSeverity::Critical,
483 mutation: None,
484 coverage_pct: None,
485 crap: Some(132.0),
486 coverage_tier: None,
487 coverage_source: None,
488 inherited_from: None,
489 component_rollup: None,
490 contributions: Vec::new(),
491 threshold_source: effective_thresholds
492 .map(|_| fallow_output::ThresholdSource::Override),
493 effective_thresholds,
494 }
495 }
496
497 fn crap_report(
498 effective_thresholds: Option<fallow_output::HealthEffectiveThresholds>,
499 ) -> HealthReport {
500 HealthReport {
501 summary: HealthSummary {
502 max_crap_threshold: 30.0,
503 ..HealthSummary::default()
504 },
505 findings: vec![crap_violation(effective_thresholds).into()],
506 ..HealthReport::default()
507 }
508 }
509
510 #[test]
514 fn codeclimate_description_uses_the_override_ceiling_not_the_global_one() {
515 let report = crap_report(Some(fallow_output::HealthEffectiveThresholds {
516 max_cyclomatic: 20,
517 max_cognitive: 15,
518 max_crap: 100.0,
519 max_unit_size: 60,
520 }));
521
522 let issues = build_health_codeclimate(&report, Path::new("/root"));
523
524 assert_eq!(issues.len(), 1);
525 assert!(
526 issues[0].description.contains("threshold: 100.0"),
527 "{}",
528 issues[0].description
529 );
530 assert!(
531 !issues[0].description.contains("threshold: 30.0"),
532 "{}",
533 issues[0].description
534 );
535 }
536
537 #[test]
538 fn codeclimate_description_falls_back_to_the_global_ceiling() {
539 let issues = build_health_codeclimate(&crap_report(None), Path::new("/root"));
540
541 assert_eq!(issues.len(), 1);
542 assert!(
543 issues[0].description.contains("threshold: 30.0"),
544 "{}",
545 issues[0].description
546 );
547 }
548
549 #[test]
550 fn health_severities_reach_serialized_codeclimate_issues() {
551 for (severity, expected) in [
552 (FindingSeverity::Critical, "critical"),
553 (FindingSeverity::High, "major"),
554 (FindingSeverity::Moderate, "minor"),
555 ] {
556 let mut violation = crap_violation(None);
557 violation.severity = severity;
558 let report = HealthReport {
559 findings: vec![violation.into()],
560 ..HealthReport::default()
561 };
562 let issues = fallow_output::codeclimate_issues_to_value(&build_health_codeclimate(
563 &report,
564 Path::new("/root"),
565 ));
566 let issues = issues
567 .as_array()
568 .expect("CodeClimate output must be an array");
569 assert_eq!(issues.len(), 1);
570 assert_eq!(issues[0]["severity"], expected);
571 assert_eq!(issues[0]["check_name"], "fallow/high-crap-score");
572 }
573 }
574
575 #[test]
576 fn runtime_verdicts_reach_serialized_codeclimate_issues() {
577 use fallow_output::{
578 RuntimeCoverageConfidence, RuntimeCoverageEvidence, RuntimeCoverageReport,
579 };
580
581 for (verdict, check_name, severity) in [
582 (
583 RuntimeCoverageVerdict::SafeToDelete,
584 "fallow/runtime-safe-to-delete",
585 "critical",
586 ),
587 (
588 RuntimeCoverageVerdict::ReviewRequired,
589 "fallow/runtime-review-required",
590 "major",
591 ),
592 (
593 RuntimeCoverageVerdict::LowTraffic,
594 "fallow/runtime-low-traffic",
595 "minor",
596 ),
597 (
598 RuntimeCoverageVerdict::CoverageUnavailable,
599 "fallow/runtime-coverage-unavailable",
600 "minor",
601 ),
602 (
603 RuntimeCoverageVerdict::Active,
604 "fallow/runtime-coverage",
605 "minor",
606 ),
607 (
608 RuntimeCoverageVerdict::Unknown,
609 "fallow/runtime-coverage",
610 "minor",
611 ),
612 ] {
613 let report = HealthReport {
614 runtime_coverage: Some(RuntimeCoverageReport {
615 findings: vec![RuntimeCoverageFinding {
616 id: "runtime-fixture".to_string(),
617 stable_id: None,
618 source_hash: None,
619 path: PathBuf::from("/root/src/legacy.ts"),
620 function: "legacyHelper".to_string(),
621 line: 12,
622 verdict,
623 invocations: Some(0),
624 confidence: RuntimeCoverageConfidence::High,
625 evidence: RuntimeCoverageEvidence {
626 static_status: "unused".to_string(),
627 test_coverage: "not_covered".to_string(),
628 test_only_reference: None,
629 v8_tracking: "tracked".to_string(),
630 untracked_reason: None,
631 observation_days: 30,
632 deployments_observed: 3,
633 },
634 actions: Vec::new(),
635 discriminators: None,
636 }],
637 ..RuntimeCoverageReport::default()
638 }),
639 ..HealthReport::default()
640 };
641 let issues = fallow_output::codeclimate_issues_to_value(&build_health_codeclimate(
642 &report,
643 Path::new("/root"),
644 ));
645 let issues = issues
646 .as_array()
647 .expect("CodeClimate output must be an array");
648 assert_eq!(issues.len(), 1, "{verdict:?}");
649 assert_eq!(issues[0]["check_name"], check_name);
650 assert_eq!(issues[0]["severity"], severity);
651 assert_eq!(issues[0]["location"]["path"], "src/legacy.ts");
652 assert_eq!(issues[0]["location"]["lines"]["begin"], 12);
653 }
654 }
655
656 #[test]
657 fn coverage_intelligence_verdicts_and_recommendations_reach_serialized_issues() {
658 use fallow_output::{
659 CoverageIntelligenceConfidence, CoverageIntelligenceEvidence,
660 CoverageIntelligenceReport,
661 };
662
663 let recommendations = [
664 (
665 CoverageIntelligenceRecommendation::AddTestOrSplitBeforeMerge,
666 "fallow/coverage-intelligence-risky-change",
667 ),
668 (
669 CoverageIntelligenceRecommendation::DeleteAfterConfirmingOwner,
670 "fallow/coverage-intelligence-delete",
671 ),
672 (
673 CoverageIntelligenceRecommendation::ReviewBeforeChanging,
674 "fallow/coverage-intelligence-review",
675 ),
676 (
677 CoverageIntelligenceRecommendation::RefactorCarefullyKeepBehavior,
678 "fallow/coverage-intelligence-refactor",
679 ),
680 ];
681 for (verdict, severity) in [
682 (
683 CoverageIntelligenceVerdict::RiskyChangeDetected,
684 Some("major"),
685 ),
686 (
687 CoverageIntelligenceVerdict::HighConfidenceDelete,
688 Some("major"),
689 ),
690 (CoverageIntelligenceVerdict::ReviewRequired, Some("minor")),
691 (
692 CoverageIntelligenceVerdict::RefactorCarefully,
693 Some("minor"),
694 ),
695 (CoverageIntelligenceVerdict::Clean, None),
696 (CoverageIntelligenceVerdict::Unknown, None),
697 ] {
698 for (recommendation, check_name) in recommendations {
699 let report = HealthReport {
700 coverage_intelligence: Some(CoverageIntelligenceReport {
701 schema_version: fallow_output::CoverageIntelligenceSchemaVersion::default(),
702 verdict,
703 summary: fallow_output::CoverageIntelligenceSummary::default(),
704 findings: vec![CoverageIntelligenceFinding {
705 id: "coverage-fixture".to_string(),
706 path: PathBuf::from("/root/src/legacy.ts"),
707 identity: Some("legacyHelper".to_string()),
708 line: 12,
709 verdict,
710 signals: Vec::new(),
711 recommendation,
712 confidence: CoverageIntelligenceConfidence::High,
713 related_ids: Vec::new(),
714 evidence: CoverageIntelligenceEvidence::default(),
715 actions: Vec::new(),
716 }],
717 }),
718 ..HealthReport::default()
719 };
720 let issues = fallow_output::codeclimate_issues_to_value(&build_health_codeclimate(
721 &report,
722 Path::new("/root"),
723 ));
724 let issues = issues
725 .as_array()
726 .expect("CodeClimate output must be an array");
727 if let Some(severity) = severity {
728 assert_eq!(issues.len(), 1, "{verdict:?}");
729 assert_eq!(issues[0]["check_name"], check_name);
730 assert_eq!(issues[0]["severity"], severity);
731 assert_eq!(issues[0]["location"]["path"], "src/legacy.ts");
732 } else {
733 assert!(issues.is_empty(), "{verdict:?}");
734 }
735 }
736 }
737 }
738}