1pub mod coverage;
12
13pub use fallow_engine::health::scoring;
16
17use std::process::ExitCode;
18use std::time::Instant;
19
20use colored::Colorize;
21use fallow_config::OutputFormat;
22use fallow_engine::health::{
23 HealthExecutionOptions, HealthGateOptions, HealthGroupResolver, HealthPipelineInputs,
24 HealthScopeInputs, HealthSeams, HealthSharedParseData, HealthSort, RuntimeCoverageSeamInput,
25 execute_health_inner, validate_health_churn_file,
26};
27
28use crate::check::{get_changed_files, resolve_workspace_scope};
29use crate::error::emit_error;
30use crate::report;
31use crate::report::OwnershipResolver;
32
33#[derive(Clone, clap::ValueEnum)]
35pub enum SortBy {
36 Severity,
37 Cyclomatic,
38 Cognitive,
39 Lines,
40}
41
42impl From<SortBy> for HealthSort {
43 fn from(sort: SortBy) -> Self {
44 match sort {
45 SortBy::Severity => Self::Severity,
46 SortBy::Cyclomatic => Self::Cyclomatic,
47 SortBy::Cognitive => Self::Cognitive,
48 SortBy::Lines => Self::Lines,
49 }
50 }
51}
52
53pub type HealthOptions<'a> = HealthExecutionOptions<'a>;
54
55impl HealthGroupResolver for OwnershipResolver {
56 fn mode_label(&self) -> &'static str {
57 OwnershipResolver::mode_label(self)
58 }
59
60 fn resolve_with_rule(&self, rel_path: &std::path::Path) -> (String, Option<String>) {
61 OwnershipResolver::resolve_with_rule(self, rel_path)
62 }
63
64 fn section_owners_of(&self, rel_path: &std::path::Path) -> Option<&[String]> {
65 OwnershipResolver::section_owners_of(self, rel_path)
66 }
67}
68
69fn health_diff_index<'a>(opts: &HealthOptions<'a>) -> Option<&'a fallow_output::DiffIndex> {
72 match opts.diff_index {
73 Some(index) => Some(index),
74 None if opts.use_shared_diff_index => crate::report::ci::diff_filter::shared_diff_index(),
75 None => None,
76 }
77}
78
79fn build_health_group_resolver(
81 opts: &HealthOptions<'_>,
82 config: &fallow_config::ResolvedConfig,
83) -> Result<Option<OwnershipResolver>, ExitCode> {
84 crate::runtime_support::build_ownership_resolver_for_mode(
85 opts.group_by,
86 opts.root,
87 config.codeowners.as_deref(),
88 opts.output,
89 )
90}
91
92fn record_health_telemetry(report: &fallow_output::HealthReport, coverage_gaps_has_findings: bool) {
96 if coverage_gaps_has_findings && report.findings.is_empty() {
97 crate::telemetry::note_findings_present(true);
98 } else {
99 crate::telemetry::note_result_count(report.findings.len());
100 }
101 crate::telemetry::note_analysis_scale(
102 Some(report.summary.files_analyzed),
103 Some(report.summary.functions_analyzed),
104 );
105}
106
107fn health_seams<'a>() -> HealthSeams<'a> {
110 HealthSeams {
111 runtime_coverage_analyzer: &runtime_coverage_seam,
112 note_graph_structure: &|module_count, edge_count| {
113 crate::telemetry::note_graph_structure_counts(module_count, edge_count);
114 },
115 }
116}
117
118#[expect(
122 clippy::needless_pass_by_value,
123 reason = "by-value input matches the engine RuntimeCoverageAnalyzer seam signature"
124)]
125fn runtime_coverage_seam(
126 options: &fallow_engine::health::RuntimeCoverageOptions,
127 input: RuntimeCoverageSeamInput<'_>,
128) -> Result<fallow_output::RuntimeCoverageReport, ExitCode> {
129 coverage::analyze(
130 options,
131 &coverage::RuntimeCoverageAnalysisInput {
132 root: input.root,
133 modules: input.modules,
134 analysis_output: input.analysis_output,
135 istanbul_coverage: input.istanbul_coverage,
136 file_paths: input.file_paths,
137 ignore_set: input.ignore_set,
138 changed_files: input.changed_files,
139 ws_roots: input.ws_roots,
140 top: input.top,
141 codeowners_path: input.codeowners_path,
142 quiet: input.quiet,
143 output: input.output,
144 },
145 )
146}
147
148fn build_health_scope_inputs<'a>(
151 opts: &HealthOptions<'a>,
152 config: &fallow_config::ResolvedConfig,
153) -> Result<HealthScopeInputs<'a, OwnershipResolver>, ExitCode> {
154 let changed_files = opts
155 .changed_since
156 .and_then(|git_ref| get_changed_files(opts.root, git_ref));
157 let diff_index = health_diff_index(opts);
158 let ws_roots = resolve_workspace_scope(
159 opts.root,
160 opts.workspace,
161 opts.changed_workspaces,
162 opts.output,
163 )?;
164 let group_resolver = build_health_group_resolver(opts, config)?;
165 Ok(HealthScopeInputs {
166 changed_files,
167 diff_index,
168 ws_roots,
169 group_resolver,
170 })
171}
172
173fn load_health_config(
176 opts: &HealthOptions<'_>,
177) -> Result<(fallow_config::ResolvedConfig, f64), ExitCode> {
178 fallow_engine::health::validate_coverage_root_absolute(opts.coverage_inputs.coverage_root)
179 .map_err(|e| emit_error(&e, 2, opts.output))?;
180 validate_health_churn_file(opts)?;
181 let t = Instant::now();
182 let config = crate::load_config_for_analysis(
183 opts.root,
184 opts.config_path,
185 crate::ConfigLoadOptions {
186 output: opts.output,
187 no_cache: opts.no_cache,
188 threads: opts.threads,
189 production_override: opts
190 .production_override
191 .or_else(|| opts.production.then_some(true)),
192 quiet: opts.quiet,
193 },
194 fallow_config::ProductionAnalysis::Health,
195 )?;
196 let config_ms = t.elapsed().as_secs_f64() * 1000.0;
197 Ok((config, config_ms))
198}
199
200pub fn execute_health_with_shared_parse(
204 opts: &HealthOptions<'_>,
205 shared: HealthSharedParseData,
206) -> Result<HealthResult, ExitCode> {
207 let (config, config_ms) = load_health_config(opts)?;
208 let scope_inputs = build_health_scope_inputs(opts, &config)?;
209 let workspace_diagnostics = fallow_config::workspace_diagnostics_for(&config.root);
210 let workspaces = shared.workspaces;
211 let seams = health_seams();
212 let result = execute_health_inner(
213 opts,
214 HealthPipelineInputs {
215 config,
216 files: shared.files,
217 modules: shared.modules,
218 config_ms,
219 discover_ms: 0.0,
220 parse_ms: 0.0,
221 parse_cpu_ms: 0.0,
222 shared_parse: true,
223 pre_computed_analysis: shared.analysis_output,
224 dead_code_results: shared.dead_code_results,
225 styling_artifacts: None,
226 pre_computed_duplication: None,
227 workspaces,
228 workspace_diagnostics,
229 },
230 scope_inputs,
231 &seams,
232 )?;
233 record_health_telemetry(&result.report, result.coverage_gaps_has_findings);
234 Ok(result)
235}
236
237pub fn execute_health(opts: &HealthOptions<'_>) -> Result<HealthResult, ExitCode> {
238 let (config, config_ms) = load_health_config(opts)?;
239
240 let t = Instant::now();
241 let session = fallow_engine::session::AnalysisSession::from_resolved_config(config);
242 let discover_ms = t.elapsed().as_secs_f64() * 1000.0;
243 let parts = session.parsed_parts_uncached(true);
244 let pre_computed_analysis =
245 fallow_engine::health::should_precompute_dead_code_analysis(opts, session.config())
246 .then(|| session.analyze_dead_code_with_parsed_modules(&parts.modules))
247 .transpose()
248 .map_err(|e| emit_error(&format!("analysis failed: {e}"), 2, opts.output))?;
249 let config = parts.config;
250 let files = parts.files;
251 let modules = parts.modules;
252 let workspaces = parts.workspaces;
253 let workspace_diagnostics = parts.workspace_diagnostics;
254 let parse_ms = parts.parse_ms;
255 let parse_cpu_ms = parts.parse_cpu_ms;
256
257 let scope_inputs = build_health_scope_inputs(opts, &config)?;
258 let seams = health_seams();
259 let result = execute_health_inner(
260 opts,
261 HealthPipelineInputs {
262 config,
263 files,
264 modules,
265 config_ms,
266 discover_ms,
267 parse_ms,
268 parse_cpu_ms,
269 shared_parse: false,
270 dead_code_results: None,
271 styling_artifacts: None,
272 pre_computed_analysis,
273 pre_computed_duplication: None,
274 workspaces,
275 workspace_diagnostics,
276 },
277 scope_inputs,
278 &seams,
279 )?;
280 record_health_telemetry(&result.report, result.coverage_gaps_has_findings);
281 Ok(result)
282}
283
284pub fn run_health(opts: &HealthOptions<'_>) -> ExitCode {
285 let result = match execute_health(opts) {
286 Ok(r) => r,
287 Err(code) => return code,
288 };
289 if let Some(ref timings) = result.timings {
290 report::print_health_performance(timings, opts.output);
291 }
292 print_health_result(
293 &result,
294 HealthPrintOptions {
295 quiet: opts.quiet,
296 explain: opts.explain,
297 gates: opts.gates,
298 summary: opts.summary,
299 summary_heading: true,
300 show_explain_tip: true,
301 skip_score_and_trend: false,
302 css_requested: opts.css,
303 },
304 )
305}
306
307pub type HealthResult =
309 fallow_engine::health::HealthAnalysisResult<crate::report::OwnershipResolver>;
310
311#[derive(Clone, Copy)]
331pub struct HealthPrintOptions {
332 pub quiet: bool,
333 pub explain: bool,
334 pub gates: HealthGateOptions,
335 pub summary: bool,
336 pub summary_heading: bool,
337 pub show_explain_tip: bool,
338 pub skip_score_and_trend: bool,
339 pub css_requested: bool,
343}
344
345pub fn print_health_result(result: &HealthResult, options: HealthPrintOptions) -> ExitCode {
346 let ctx = health_report_context(result, options);
347 let report_code = report::print_health_report(
348 &result.report,
349 result.grouping.as_ref(),
350 result.group_resolver.as_ref(),
351 &ctx,
352 result.config.output,
353 );
354 if report_code != ExitCode::SUCCESS {
355 return report_code;
356 }
357
358 if options.gates.report_only {
359 return ExitCode::SUCCESS;
360 }
361
362 if health_exit_gate_failed(result, options) {
363 return ExitCode::from(1);
364 }
365 if result.should_fail_on_coverage_gaps && result.coverage_gaps_has_findings {
366 return ExitCode::from(1);
367 }
368 maybe_print_score_gate_note(result, options);
369
370 ExitCode::SUCCESS
371}
372
373fn health_report_context(
374 result: &HealthResult,
375 options: HealthPrintOptions,
376) -> report::ReportContext<'_> {
377 report::ReportContext {
378 root: &result.config.root,
379 rules: &result.config.rules,
380 elapsed: result.elapsed,
381 quiet: options.quiet,
382 explain: options.explain,
383 group_by: None,
384 top: None,
385 summary: options.summary,
386 summary_heading: options.summary_heading,
387 show_explain_tip: options.show_explain_tip,
388 baseline_matched: None,
389 config_fixable: false,
390 skip_score_and_trend: options.skip_score_and_trend,
391 css_requested: options.css_requested,
392 }
393}
394
395fn health_exit_gate_failed(result: &HealthResult, options: HealthPrintOptions) -> bool {
396 score_gate_failed(result, options)
397 || findings_gate_failed(result, options)
398 || has_failing_runtime_coverage(result)
399}
400
401fn score_gate_failed(result: &HealthResult, options: HealthPrintOptions) -> bool {
402 let Some(threshold) = options.gates.min_score else {
403 return false;
404 };
405 let Some(ref hs) = result.report.health_score else {
406 return false;
407 };
408 if hs.score >= threshold {
409 return false;
410 }
411
412 if !options.quiet {
413 eprintln!(
414 "Health score {:.1} ({}) is below minimum threshold {:.0}",
415 hs.score, hs.grade, threshold
416 );
417 }
418 true
419}
420
421fn findings_gate_failed(result: &HealthResult, options: HealthPrintOptions) -> bool {
422 if let Some(min_sev) = options.gates.min_severity {
423 result.report.findings.iter().any(|f| f.severity >= min_sev)
424 } else if options.gates.min_score.is_none() {
425 !result.report.findings.is_empty()
426 } else {
427 false
428 }
429}
430
431fn has_failing_runtime_coverage(result: &HealthResult) -> bool {
432 result
433 .report
434 .runtime_coverage
435 .as_ref()
436 .is_some_and(|report| report.findings.iter().any(is_failing_runtime_coverage))
437}
438
439fn is_failing_runtime_coverage(finding: &fallow_output::RuntimeCoverageFinding) -> bool {
440 matches!(
441 finding.verdict,
442 fallow_output::RuntimeCoverageVerdict::SafeToDelete
443 | fallow_output::RuntimeCoverageVerdict::ReviewRequired
444 | fallow_output::RuntimeCoverageVerdict::LowTraffic
445 )
446}
447
448fn maybe_print_score_gate_note(result: &HealthResult, options: HealthPrintOptions) {
449 if options.gates.min_score.is_none()
450 || options.gates.min_severity.is_some()
451 || options.quiet
452 || result.report.findings.is_empty()
453 || !matches!(result.config.output, OutputFormat::Human)
454 {
455 return;
456 }
457
458 {
459 eprintln!(
460 "{}",
461 "Findings above are informational: --min-score gates on the score, not on findings."
462 .dimmed()
463 );
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use fallow_config::{FallowConfig, OutputFormat};
471 use fallow_output::{ComplexityViolation, ExceededThreshold, FindingSeverity};
472 use std::path::PathBuf;
473 use std::time::Duration;
474
475 fn make_finding(name: &str, exceeded: ExceededThreshold) -> ComplexityViolation {
476 ComplexityViolation {
477 path: PathBuf::from("/project/src/a.ts"),
478 name: name.to_string(),
479 line: 1,
480 col: 0,
481 cyclomatic: match exceeded {
482 ExceededThreshold::Cyclomatic
483 | ExceededThreshold::Both
484 | ExceededThreshold::CyclomaticCrap
485 | ExceededThreshold::All => 25,
486 _ => 8,
487 },
488 cognitive: match exceeded {
489 ExceededThreshold::Cognitive
490 | ExceededThreshold::Both
491 | ExceededThreshold::CognitiveCrap
492 | ExceededThreshold::All => 20,
493 _ => 5,
494 },
495 line_count: 10,
496 param_count: 0,
497 react_hook_count: 0,
498 react_jsx_max_depth: 0,
499 react_prop_count: 0,
500 react_hook_profile: None,
501 exceeded,
502 severity: FindingSeverity::Moderate,
503 crap: exceeded.includes_crap().then_some(30.0),
504 coverage_pct: None,
505 coverage_tier: None,
506 coverage_source: None,
507 inherited_from: None,
508 component_rollup: None,
509 contributions: Vec::new(),
510 effective_thresholds: None,
511 threshold_source: None,
512 }
513 }
514
515 fn test_resolved_config() -> fallow_config::ResolvedConfig {
516 FallowConfig::default().resolve(
517 PathBuf::from("/project"),
518 OutputFormat::Json,
519 1,
520 true,
521 true,
522 None,
523 )
524 }
525
526 fn fx_summary(
527 tracked: usize,
528 hit: usize,
529 unhit: usize,
530 untracked: usize,
531 ) -> fallow_output::RuntimeCoverageSummary {
532 #[expect(
533 clippy::cast_precision_loss,
534 reason = "test fixture totals are tiny, f64 precision is fine"
535 )]
536 let coverage_percent = if tracked == 0 {
537 0.0
538 } else {
539 (hit as f64 / tracked as f64) * 100.0
540 };
541 fallow_output::RuntimeCoverageSummary {
542 data_source: fallow_output::RuntimeCoverageDataSource::Local,
543 last_received_at: None,
544 functions_tracked: tracked,
545 functions_hit: hit,
546 functions_unhit: unhit,
547 functions_untracked: untracked,
548 coverage_percent,
549 trace_count: 512,
550 period_days: 7,
551 deployments_seen: 2,
552 capture_quality: None,
553 }
554 }
555
556 fn fx_evidence(
557 static_status: &str,
558 test_coverage: &str,
559 v8_tracking: &str,
560 ) -> fallow_output::RuntimeCoverageEvidence {
561 fallow_output::RuntimeCoverageEvidence {
562 static_status: static_status.to_owned(),
563 test_coverage: test_coverage.to_owned(),
564 v8_tracking: v8_tracking.to_owned(),
565 untracked_reason: None,
566 observation_days: 7,
567 deployments_observed: 2,
568 }
569 }
570
571 fn fx_health_score(score: f64, grade: &'static str) -> fallow_output::HealthScore {
572 fallow_output::HealthScore {
573 formula_version: 2,
574 score,
575 grade,
576 penalties: fallow_output::HealthScorePenalties {
577 dead_files: None,
578 dead_exports: None,
579 complexity: 0.0,
580 p90_complexity: 0.0,
581 maintainability: None,
582 hotspots: None,
583 unused_deps: None,
584 circular_deps: None,
585 unit_size: None,
586 coupling: None,
587 duplication: None,
588 prop_drilling: None,
589 },
590 }
591 }
592
593 fn fx_gate_result(
594 findings: Vec<fallow_output::HealthFinding>,
595 score: Option<fallow_output::HealthScore>,
596 ) -> HealthResult {
597 HealthResult {
598 report: fallow_output::HealthReport {
599 findings,
600 health_score: score,
601 ..fallow_output::HealthReport::default()
602 },
603 grouping: None,
604 group_resolver: None,
605 config: test_resolved_config(),
606 workspace_diagnostics: Vec::new(),
607 elapsed: Duration::default(),
608 timings: None,
609 coverage_gaps_has_findings: false,
610 should_fail_on_coverage_gaps: false,
611 }
612 }
613
614 fn moderate_finding() -> fallow_output::HealthFinding {
615 make_finding("moderate", ExceededThreshold::Cyclomatic).into()
616 }
617
618 fn critical_finding() -> fallow_output::HealthFinding {
619 let mut v = make_finding("critical", ExceededThreshold::All);
620 v.severity = FindingSeverity::Critical;
621 v.into()
622 }
623
624 fn gate_exit(
626 result: &HealthResult,
627 min_score: Option<f64>,
628 min_severity: Option<FindingSeverity>,
629 report_only: bool,
630 ) -> ExitCode {
631 print_health_result(
632 result,
633 HealthPrintOptions {
634 quiet: true,
635 explain: false,
636 gates: HealthGateOptions {
637 min_score,
638 min_severity,
639 report_only,
640 },
641 summary: false,
642 summary_heading: true,
643 show_explain_tip: true,
644 skip_score_and_trend: false,
645 css_requested: false,
646 },
647 )
648 }
649
650 #[test]
651 fn plain_health_with_findings_fails() {
652 let result = fx_gate_result(vec![moderate_finding()], Some(fx_health_score(87.5, "A")));
653 assert_eq!(gate_exit(&result, None, None, false), ExitCode::from(1));
654 }
655
656 #[test]
657 fn plain_health_with_no_findings_succeeds() {
658 let result = fx_gate_result(vec![], Some(fx_health_score(100.0, "A")));
659 assert_eq!(gate_exit(&result, None, None, false), ExitCode::SUCCESS);
660 }
661
662 #[test]
663 fn min_score_zero_never_fails_even_with_findings() {
664 let result = fx_gate_result(vec![moderate_finding()], Some(fx_health_score(50.0, "D")));
665 assert_eq!(
666 gate_exit(&result, Some(0.0), None, false),
667 ExitCode::SUCCESS
668 );
669 }
670
671 #[test]
672 fn min_score_passing_demotes_findings_to_informational() {
673 let result = fx_gate_result(vec![moderate_finding()], Some(fx_health_score(87.5, "A")));
674 assert_eq!(
675 gate_exit(&result, Some(80.0), None, false),
676 ExitCode::SUCCESS
677 );
678 }
679
680 #[test]
681 fn min_score_below_threshold_fails() {
682 let result = fx_gate_result(vec![moderate_finding()], Some(fx_health_score(50.0, "D")));
683 assert_eq!(
684 gate_exit(&result, Some(80.0), None, false),
685 ExitCode::from(1)
686 );
687 }
688
689 #[test]
690 fn min_severity_gates_on_severity_independent_of_min_score() {
691 let only_moderate =
692 fx_gate_result(vec![moderate_finding()], Some(fx_health_score(87.5, "A")));
693 assert_eq!(
694 gate_exit(&only_moderate, None, Some(FindingSeverity::Critical), false),
695 ExitCode::SUCCESS,
696 );
697 let with_critical = fx_gate_result(
698 vec![moderate_finding(), critical_finding()],
699 Some(fx_health_score(87.5, "A")),
700 );
701 assert_eq!(
702 gate_exit(&with_critical, None, Some(FindingSeverity::Critical), false),
703 ExitCode::from(1),
704 );
705 }
706
707 #[test]
708 fn min_score_and_min_severity_compose_as_or() {
709 let pass = fx_gate_result(vec![moderate_finding()], Some(fx_health_score(87.5, "A")));
710 assert_eq!(
711 gate_exit(&pass, Some(80.0), Some(FindingSeverity::Critical), false),
712 ExitCode::SUCCESS,
713 );
714 let low_score = fx_gate_result(vec![moderate_finding()], Some(fx_health_score(50.0, "D")));
715 assert_eq!(
716 gate_exit(
717 &low_score,
718 Some(80.0),
719 Some(FindingSeverity::Critical),
720 false
721 ),
722 ExitCode::from(1),
723 );
724 let critical = fx_gate_result(vec![critical_finding()], Some(fx_health_score(87.5, "A")));
725 assert_eq!(
726 gate_exit(
727 &critical,
728 Some(80.0),
729 Some(FindingSeverity::Critical),
730 false
731 ),
732 ExitCode::from(1),
733 );
734 }
735
736 #[test]
737 fn report_only_never_fails_on_findings_or_low_score() {
738 let result = fx_gate_result(
739 vec![moderate_finding(), critical_finding()],
740 Some(fx_health_score(10.0, "F")),
741 );
742 assert_eq!(gate_exit(&result, None, None, true), ExitCode::SUCCESS);
743 }
744
745 #[test]
746 fn runtime_coverage_gate_independent_of_min_score() {
747 let result = fx_low_traffic_runtime_result();
748 assert_eq!(
749 gate_exit(&result, Some(0.0), None, false),
750 ExitCode::from(1)
751 );
752 assert_eq!(gate_exit(&result, None, None, true), ExitCode::SUCCESS);
753 }
754
755 fn fx_low_traffic_runtime_result() -> HealthResult {
756 HealthResult {
757 report: fallow_output::HealthReport {
758 runtime_coverage: Some(fallow_output::RuntimeCoverageReport {
759 schema_version: fallow_output::RuntimeCoverageSchemaVersion::V1,
760 verdict: fallow_output::RuntimeCoverageReportVerdict::ColdCodeDetected,
761 signals: Vec::new(),
762 summary: fx_summary(1, 0, 1, 0),
763 findings: vec![fallow_output::RuntimeCoverageFinding {
764 id: "fallow:prod:lowtraffic".to_owned(),
765 stable_id: None,
766 path: PathBuf::from("/project/src/cold.ts"),
767 function: "coldPath".to_owned(),
768 line: 14,
769 verdict: fallow_output::RuntimeCoverageVerdict::LowTraffic,
770 invocations: Some(1),
771 confidence: fallow_output::RuntimeCoverageConfidence::Low,
772 evidence: fx_evidence("used", "not_covered", "tracked"),
773 actions: vec![],
774 source_hash: None,
775 discriminators: None,
776 }],
777 hot_paths: vec![],
778 blast_radius: vec![],
779 importance: vec![],
780 watermark: None,
781 warnings: vec![],
782 actionable: true,
783 actionability_reason: None,
784 actionability_verdict: None,
785 provenance: fallow_output::RuntimeCoverageProvenance::default(),
786 }),
787 ..fallow_output::HealthReport::default()
788 },
789 grouping: None,
790 group_resolver: None,
791 config: test_resolved_config(),
792 workspace_diagnostics: Vec::new(),
793 elapsed: Duration::default(),
794 timings: None,
795 coverage_gaps_has_findings: false,
796 should_fail_on_coverage_gaps: false,
797 }
798 }
799
800 #[test]
801 fn print_health_result_fails_on_low_traffic_runtime_coverage() {
802 let result = fx_low_traffic_runtime_result();
803
804 assert_eq!(
805 print_health_result(
806 &result,
807 HealthPrintOptions {
808 quiet: true,
809 explain: false,
810 gates: HealthGateOptions::default(),
811 summary: false,
812 summary_heading: true,
813 show_explain_tip: true,
814 skip_score_and_trend: false,
815 css_requested: false,
816 },
817 ),
818 ExitCode::from(1),
819 );
820 }
821}