fallow_cli/health_types/
coverage.rs1use std::path::{Path, PathBuf};
2
3use fallow_types::output_health::{
4 UntestedExportAction, UntestedExportActionType, UntestedFileAction, UntestedFileActionType,
5};
6
7#[derive(Debug, Clone, serde::Serialize)]
9#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
10pub struct UntestedFile {
11 pub path: PathBuf,
13 pub value_export_count: usize,
15}
16
17#[derive(Debug, Clone, serde::Serialize)]
19#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
20pub struct UntestedExport {
21 pub path: PathBuf,
23 pub export_name: String,
25 pub line: u32,
27 pub col: u32,
29}
30
31#[derive(Debug, Clone, serde::Serialize)]
38#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
39pub struct UntestedFileFinding {
40 #[serde(flatten)]
42 pub file: UntestedFile,
43 pub actions: Vec<UntestedFileAction>,
46}
47
48impl UntestedFileFinding {
49 #[must_use]
54 pub fn with_actions(file: UntestedFile, root: &Path) -> Self {
55 let display_path = relative_display(&file.path, root);
56 let actions = vec![
57 UntestedFileAction {
58 kind: UntestedFileActionType::AddTests,
59 auto_fixable: false,
60 description: format!("Add test coverage for `{display_path}`"),
61 note: Some("No test dependency path reaches this runtime file".to_string()),
62 comment: None,
63 },
64 UntestedFileAction {
65 kind: UntestedFileActionType::SuppressFile,
66 auto_fixable: false,
67 description: format!("Suppress coverage gap reporting for `{display_path}`"),
68 note: None,
69 comment: Some("// fallow-ignore-file coverage-gaps".to_string()),
70 },
71 ];
72 Self { file, actions }
73 }
74}
75
76#[derive(Debug, Clone, serde::Serialize)]
80#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
81pub struct UntestedExportFinding {
82 #[serde(flatten)]
84 pub export: UntestedExport,
85 pub actions: Vec<UntestedExportAction>,
88}
89
90impl UntestedExportFinding {
91 #[must_use]
93 pub fn with_actions(export: UntestedExport, root: &Path) -> Self {
94 let display_path = relative_display(&export.path, root);
95 let export_name = export.export_name.clone();
96 let actions = vec![
97 UntestedExportAction {
98 kind: UntestedExportActionType::AddTestImport,
99 auto_fixable: false,
100 description: format!("Import and test `{export_name}` from `{display_path}`"),
101 note: Some(
102 "This export is runtime-reachable but no test-reachable module references it"
103 .to_string(),
104 ),
105 comment: None,
106 },
107 UntestedExportAction {
108 kind: UntestedExportActionType::SuppressFile,
109 auto_fixable: false,
110 description: format!("Suppress coverage gap reporting for `{display_path}`"),
111 note: None,
112 comment: Some("// fallow-ignore-file coverage-gaps".to_string()),
113 },
114 ];
115 Self { export, actions }
116 }
117}
118
119fn relative_display(path: &Path, root: &Path) -> String {
120 path.strip_prefix(root)
121 .unwrap_or(path)
122 .display()
123 .to_string()
124}
125
126#[derive(Debug, Clone, Default, serde::Serialize)]
128#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
129pub struct CoverageGapSummary {
130 pub runtime_files: usize,
132 pub covered_files: usize,
134 pub file_coverage_pct: f64,
136 pub untested_files: usize,
138 pub untested_exports: usize,
140}
141
142#[derive(Debug, Clone, Default, serde::Serialize)]
145#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
146pub struct CoverageGaps {
147 pub summary: CoverageGapSummary,
149 #[serde(skip_serializing_if = "Vec::is_empty")]
152 #[cfg_attr(feature = "schema", schemars(default))]
153 pub files: Vec<UntestedFileFinding>,
154 #[serde(skip_serializing_if = "Vec::is_empty")]
157 #[cfg_attr(feature = "schema", schemars(default))]
158 pub exports: Vec<UntestedExportFinding>,
159}
160
161impl CoverageGaps {
162 #[must_use]
163 pub fn is_empty(&self) -> bool {
164 self.files.is_empty() && self.exports.is_empty()
165 }
166}