use std::path::Path;
use crate::baseline::{self as generic, BaselineConfig, Fingerprintable};
use super::{AuditResult, BrokenReference};
const BASELINE_KEY: &str = "docs";
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DocsBaselineMetadata {
pub docs_scanned: usize,
pub undocumented_features: usize,
}
struct DocsFinding<'a>(&'a BrokenReference);
impl Fingerprintable for DocsFinding<'_> {
fn fingerprint(&self) -> String {
format!("{}::{}", self.0.doc, self.0.claim)
}
fn description(&self) -> String {
self.0.action.clone()
}
fn context_label(&self) -> String {
self.0.doc.clone()
}
}
pub type DocsBaseline = generic::Baseline<DocsBaselineMetadata>;
pub type BaselineComparison = generic::Comparison;
pub fn save_baseline(
source_path: &Path,
result: &AuditResult,
) -> crate::error::Result<std::path::PathBuf> {
let config = BaselineConfig::new(source_path, BASELINE_KEY);
let metadata = DocsBaselineMetadata {
docs_scanned: result.summary.docs_scanned,
undocumented_features: result.summary.undocumented_features,
};
let items: Vec<DocsFinding> = result.broken_references.iter().map(DocsFinding).collect();
generic::save(&config, &result.component_id, &items, metadata)
}
pub fn load_baseline(source_path: &Path) -> Option<DocsBaseline> {
let config = BaselineConfig::new(source_path, BASELINE_KEY);
generic::load::<DocsBaselineMetadata>(&config)
.ok()
.flatten()
}
pub fn compare(result: &AuditResult, baseline: &DocsBaseline) -> BaselineComparison {
let items: Vec<DocsFinding> = result.broken_references.iter().map(DocsFinding).collect();
generic::compare(&items, baseline)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::code_audit::docs_audit::{AlignmentSummary, ClaimConfidence};
fn make_broken_ref(doc: &str, claim: &str) -> BrokenReference {
BrokenReference {
doc: doc.to_string(),
line: 10,
claim: claim.to_string(),
confidence: ClaimConfidence::Real,
doc_context: None,
action: "Stale reference".to_string(),
}
}
fn make_result(broken: Vec<BrokenReference>) -> AuditResult {
AuditResult {
component_id: "test".to_string(),
baseline_ref: None,
summary: AlignmentSummary {
docs_scanned: 5,
priority_docs: 0,
broken_references: broken.len(),
unchanged_docs: 5,
total_features: 0,
documented_features: 0,
undocumented_features: 0,
},
changed_files: vec![],
priority_docs: vec![],
broken_references: broken,
undocumented_features: vec![],
detected_features: vec![],
}
}
#[test]
fn save_and_load_roundtrips() {
let dir = tempfile::tempdir().unwrap();
let result = make_result(vec![
make_broken_ref("api.md", "file path `src/old.rs`"),
make_broken_ref("guide.md", "class `OldClass`"),
]);
save_baseline(dir.path(), &result).unwrap();
let loaded = load_baseline(dir.path()).unwrap();
assert_eq!(loaded.context_id, "test");
assert_eq!(loaded.item_count, 2);
assert_eq!(loaded.metadata.docs_scanned, 5);
}
#[test]
fn compare_detects_new_broken_ref() {
let dir = tempfile::tempdir().unwrap();
let original = make_result(vec![make_broken_ref("api.md", "file path `src/old.rs`")]);
save_baseline(dir.path(), &original).unwrap();
let baseline = load_baseline(dir.path()).unwrap();
let current = make_result(vec![
make_broken_ref("api.md", "file path `src/old.rs`"),
make_broken_ref("guide.md", "file path `src/removed.rs`"),
]);
let comparison = compare(¤t, &baseline);
assert!(comparison.drift_increased);
assert_eq!(comparison.new_items.len(), 1);
}
#[test]
fn compare_detects_resolved_ref() {
let dir = tempfile::tempdir().unwrap();
let original = make_result(vec![
make_broken_ref("api.md", "file path `src/old.rs`"),
make_broken_ref("guide.md", "class `OldClass`"),
]);
save_baseline(dir.path(), &original).unwrap();
let baseline = load_baseline(dir.path()).unwrap();
let current = make_result(vec![make_broken_ref("api.md", "file path `src/old.rs`")]);
let comparison = compare(¤t, &baseline);
assert!(!comparison.drift_increased);
assert_eq!(comparison.resolved_fingerprints.len(), 1);
}
#[test]
fn fingerprint_ignores_line_number() {
let ref1 = BrokenReference {
doc: "api.md".to_string(),
line: 10,
claim: "file path `src/old.rs`".to_string(),
confidence: ClaimConfidence::Real,
doc_context: None,
action: "Stale".to_string(),
};
let ref2 = BrokenReference {
doc: "api.md".to_string(),
line: 42, claim: "file path `src/old.rs`".to_string(),
confidence: ClaimConfidence::Real,
doc_context: None,
action: "Stale".to_string(),
};
assert_eq!(
DocsFinding(&ref1).fingerprint(),
DocsFinding(&ref2).fingerprint()
);
}
}