fallow_cli/health_types/
coverage.rs1use std::path::{Path, PathBuf};
2
3use fallow_types::output_health::{
4 UntestedExportAction, UntestedExportActionType, UntestedFileAction, UntestedFileActionType,
5};
6use fallow_types::serde_path;
7
8#[derive(Debug, Clone, serde::Serialize)]
10#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
11pub struct UntestedFile {
12 #[serde(serialize_with = "serde_path::serialize")]
14 pub path: PathBuf,
15 pub value_export_count: usize,
17}
18
19#[derive(Debug, Clone, serde::Serialize)]
21#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
22pub struct UntestedExport {
23 #[serde(serialize_with = "serde_path::serialize")]
25 pub path: PathBuf,
26 pub export_name: String,
28 pub line: u32,
30 pub col: u32,
32}
33
34#[derive(Debug, Clone, serde::Serialize)]
41#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
42pub struct UntestedFileFinding {
43 #[serde(flatten)]
45 pub file: UntestedFile,
46 pub actions: Vec<UntestedFileAction>,
49}
50
51impl UntestedFileFinding {
52 #[must_use]
57 pub fn with_actions(file: UntestedFile, root: &Path) -> Self {
58 let display_path = relative_display(&file.path, root);
59 let actions = vec![
60 UntestedFileAction {
61 kind: UntestedFileActionType::AddTests,
62 auto_fixable: false,
63 description: format!("Add test coverage for `{display_path}`"),
64 note: Some("No test dependency path reaches this runtime file".to_string()),
65 comment: None,
66 },
67 UntestedFileAction {
68 kind: UntestedFileActionType::SuppressFile,
69 auto_fixable: false,
70 description: format!("Suppress coverage gap reporting for `{display_path}`"),
71 note: None,
72 comment: Some("// fallow-ignore-file coverage-gaps".to_string()),
73 },
74 ];
75 Self { file, actions }
76 }
77}
78
79#[derive(Debug, Clone, serde::Serialize)]
83#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
84pub struct UntestedExportFinding {
85 #[serde(flatten)]
87 pub export: UntestedExport,
88 pub actions: Vec<UntestedExportAction>,
91}
92
93impl UntestedExportFinding {
94 #[must_use]
96 pub fn with_actions(export: UntestedExport, root: &Path) -> Self {
97 let display_path = relative_display(&export.path, root);
98 let export_name = export.export_name.clone();
99 let actions = vec![
100 UntestedExportAction {
101 kind: UntestedExportActionType::AddTestImport,
102 auto_fixable: false,
103 description: format!("Import and test `{export_name}` from `{display_path}`"),
104 note: Some(
105 "This export is runtime-reachable but no test-reachable module references it"
106 .to_string(),
107 ),
108 comment: None,
109 },
110 UntestedExportAction {
111 kind: UntestedExportActionType::SuppressFile,
112 auto_fixable: false,
113 description: format!("Suppress coverage gap reporting for `{display_path}`"),
114 note: None,
115 comment: Some("// fallow-ignore-file coverage-gaps".to_string()),
116 },
117 ];
118 Self { export, actions }
119 }
120}
121
122fn relative_display(path: &Path, root: &Path) -> String {
123 path.strip_prefix(root)
124 .unwrap_or(path)
125 .display()
126 .to_string()
127}
128
129#[derive(Debug, Clone, Default, serde::Serialize)]
131#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
132pub struct CoverageGapSummary {
133 pub runtime_files: usize,
135 pub covered_files: usize,
137 pub file_coverage_pct: f64,
139 pub untested_files: usize,
141 pub untested_exports: usize,
143}
144
145#[derive(Debug, Clone, Default, serde::Serialize)]
148#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
149pub struct CoverageGaps {
150 pub summary: CoverageGapSummary,
152 #[serde(default, skip_serializing_if = "Vec::is_empty")]
155 #[cfg_attr(feature = "schema", schemars(default))]
156 pub files: Vec<UntestedFileFinding>,
157 #[serde(default, skip_serializing_if = "Vec::is_empty")]
160 #[cfg_attr(feature = "schema", schemars(default))]
161 pub exports: Vec<UntestedExportFinding>,
162}
163
164impl CoverageGaps {
165 #[must_use]
166 pub fn is_empty(&self) -> bool {
167 self.files.is_empty() && self.exports.is_empty()
168 }
169}