1use std::process::ExitCode;
6
7use anyhow::{Context, Result, bail};
8
9use crate::cargo_package_resolver::CargoPackageResolver;
10use crate::cli::Args;
11use crate::config::Config;
12use crate::coverage_index::CoverageIndex;
13use crate::default_scorer::DefaultScorer;
14use crate::llvm_cov_provider::LlvmCovProvider;
15use crate::package_context::PackageContext;
16use crate::project_report::ProjectReport;
17use crate::source_function::SourceFunction;
18use crate::source_function_discovery::SourceFunctionDiscovery;
19use crate::stdout_reporter::StdoutReporter;
20use crate::traits::coverage_provider::CoverageProvider;
21use crate::traits::function_discovery::FunctionDiscovery;
22use crate::traits::package_resolver::PackageResolver;
23use crate::traits::reporter::Reporter;
24use crate::traits::scorer::Scorer;
25
26pub struct App {
27 resolver: Box<dyn PackageResolver>,
28 discovery: Box<dyn FunctionDiscovery>,
29 coverage_provider: Box<dyn CoverageProvider>,
30 scorer: Box<dyn Scorer>,
31 reporter: Box<dyn Reporter>,
32 config: Config,
33}
34
35impl App {
36 #[must_use]
37 pub fn new(config: Config) -> Self {
38 Self {
39 resolver: Box::new(CargoPackageResolver::new()),
40 discovery: Box::new(SourceFunctionDiscovery::new()),
41 coverage_provider: Box::new(LlvmCovProvider::new()),
42 scorer: Box::new(DefaultScorer::new()),
43 reporter: Box::new(StdoutReporter::new()),
44 config,
45 }
46 }
47
48 #[must_use]
49 pub fn with_deps(
50 resolver: Box<dyn PackageResolver>,
51 discovery: Box<dyn FunctionDiscovery>,
52 coverage_provider: Box<dyn CoverageProvider>,
53 scorer: Box<dyn Scorer>,
54 reporter: Box<dyn Reporter>,
55 config: Config,
56 ) -> Self {
57 Self {
58 resolver,
59 discovery,
60 coverage_provider,
61 scorer,
62 reporter,
63 config,
64 }
65 }
66
67 pub fn run(&self) -> Result<ExitCode> {
68 let packages = self.resolver.resolve(&self.config)?;
69 let coverage_records = self.coverage_provider.provide(&self.config, &packages)?;
70
71 let functions = self.discover_all_functions(&packages)?;
72 if functions.is_empty() {
73 bail!("no Rust functions were discovered in the selected packages");
74 }
75
76 if coverage_records.is_empty() {
77 bail!("coverage file did not contain any function records");
78 }
79 let coverage_index = CoverageIndex::from_records(coverage_records);
80
81 let matched_count = functions
82 .iter()
83 .filter(|function| coverage_index.match_function(function).is_some())
84 .count();
85 if matched_count == 0 {
86 bail!(
87 "coverage data could not be matched to any discovered function by file path and line"
88 );
89 }
90
91 let reports = self
92 .scorer
93 .score_functions(functions, &coverage_index, &self.config);
94 let metrics = self.scorer.project_metrics(&reports, &self.config);
95
96 let report_data = ProjectReport {
97 scope_name: packages
98 .iter()
99 .map(|package| package.name.clone())
100 .collect::<Vec<_>>()
101 .join(", "),
102 total_functions: metrics.total_functions,
103 crappy_functions: metrics.crappy_functions,
104 crappy_percent: metrics.crappy_percent,
105 verdict: metrics.verdict,
106 functions: reports,
107 };
108
109 self.reporter.render(&report_data, &self.config);
110
111 Ok(report_data.exit_code(&self.config))
112 }
113
114 fn discover_all_functions(&self, packages: &[PackageContext]) -> Result<Vec<SourceFunction>> {
115 let mut functions = Vec::new();
116 for package in packages {
117 let mut package_functions = self.discovery.discover(package).with_context(|| {
118 format!("failed to discover functions in package {}", package.name)
119 })?;
120 functions.append(&mut package_functions);
121 }
122 Ok(functions)
123 }
124}
125
126pub fn run(args: Args) -> Result<ExitCode> {
127 App::new(Config::from_args(args)).run()
128}