use std::path::{Path, PathBuf};
use fallow_types::output_health::{
UntestedExportAction, UntestedExportActionType, UntestedFileAction, UntestedFileActionType,
};
#[derive(Debug, Clone, serde::Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct UntestedFile {
pub path: PathBuf,
pub value_export_count: usize,
}
#[derive(Debug, Clone, serde::Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct UntestedExport {
pub path: PathBuf,
pub export_name: String,
pub line: u32,
pub col: u32,
}
#[derive(Debug, Clone, serde::Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct UntestedFileFinding {
#[serde(flatten)]
pub file: UntestedFile,
pub actions: Vec<UntestedFileAction>,
}
impl UntestedFileFinding {
#[must_use]
pub fn with_actions(file: UntestedFile, root: &Path) -> Self {
let display_path = relative_display(&file.path, root);
let actions = vec![
UntestedFileAction {
kind: UntestedFileActionType::AddTests,
auto_fixable: false,
description: format!("Add test coverage for `{display_path}`"),
note: Some("No test dependency path reaches this runtime file".to_string()),
comment: None,
},
UntestedFileAction {
kind: UntestedFileActionType::SuppressFile,
auto_fixable: false,
description: format!("Suppress coverage gap reporting for `{display_path}`"),
note: None,
comment: Some("// fallow-ignore-file coverage-gaps".to_string()),
},
];
Self { file, actions }
}
}
#[derive(Debug, Clone, serde::Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct UntestedExportFinding {
#[serde(flatten)]
pub export: UntestedExport,
pub actions: Vec<UntestedExportAction>,
}
impl UntestedExportFinding {
#[must_use]
pub fn with_actions(export: UntestedExport, root: &Path) -> Self {
let display_path = relative_display(&export.path, root);
let export_name = export.export_name.clone();
let actions = vec![
UntestedExportAction {
kind: UntestedExportActionType::AddTestImport,
auto_fixable: false,
description: format!("Import and test `{export_name}` from `{display_path}`"),
note: Some(
"This export is runtime-reachable but no test-reachable module references it"
.to_string(),
),
comment: None,
},
UntestedExportAction {
kind: UntestedExportActionType::SuppressFile,
auto_fixable: false,
description: format!("Suppress coverage gap reporting for `{display_path}`"),
note: None,
comment: Some("// fallow-ignore-file coverage-gaps".to_string()),
},
];
Self { export, actions }
}
}
fn relative_display(path: &Path, root: &Path) -> String {
path.strip_prefix(root)
.unwrap_or(path)
.display()
.to_string()
}
#[derive(Debug, Clone, Default, serde::Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct CoverageGapSummary {
pub runtime_files: usize,
pub covered_files: usize,
pub file_coverage_pct: f64,
pub untested_files: usize,
pub untested_exports: usize,
}
#[derive(Debug, Clone, Default, serde::Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct CoverageGaps {
pub summary: CoverageGapSummary,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[cfg_attr(feature = "schema", schemars(default))]
pub files: Vec<UntestedFileFinding>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[cfg_attr(feature = "schema", schemars(default))]
pub exports: Vec<UntestedExportFinding>,
}
impl CoverageGaps {
#[must_use]
pub fn is_empty(&self) -> bool {
self.files.is_empty() && self.exports.is_empty()
}
}