use serde::Serialize;
use crate::graph::KnowledgeGraph;
use crate::model::ItemType;
#[derive(Debug, Clone, Serialize)]
pub struct TypeCoverage {
pub item_type: ItemType,
pub type_name: String,
pub total: usize,
pub complete: usize,
pub incomplete: usize,
pub coverage_percent: f64,
}
#[derive(Debug, Clone, Serialize)]
pub struct IncompleteItem {
pub id: String,
pub name: String,
pub item_type: String,
pub reason: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct CoverageReport {
pub overall_coverage: f64,
pub by_type: Vec<TypeCoverage>,
pub incomplete_items: Vec<IncompleteItem>,
pub total_items: usize,
pub complete_items: usize,
}
impl CoverageReport {
pub fn generate(graph: &KnowledgeGraph) -> Self {
let mut by_type = Vec::new();
let mut incomplete_items = Vec::new();
let mut total_items = 0;
let mut complete_items = 0;
for item_type in ItemType::all() {
let items = graph.items_by_type(*item_type);
let total = items.len();
if total == 0 {
continue;
}
let mut type_complete = 0;
let mut type_incomplete = 0;
for item in items {
let is_complete = Self::check_item_complete(item, graph);
if is_complete {
type_complete += 1;
} else {
type_incomplete += 1;
incomplete_items.push(Self::create_incomplete_item(item, graph));
}
}
let coverage_percent = if total > 0 {
(type_complete as f64 / total as f64) * 100.0
} else {
100.0
};
by_type.push(TypeCoverage {
item_type: *item_type,
type_name: item_type.display_name().to_string(),
total,
complete: type_complete,
incomplete: type_incomplete,
coverage_percent,
});
total_items += total;
complete_items += type_complete;
}
let overall_coverage = if total_items > 0 {
(complete_items as f64 / total_items as f64) * 100.0
} else {
100.0
};
Self {
overall_coverage,
by_type,
incomplete_items,
total_items,
complete_items,
}
}
fn check_item_complete(item: &crate::model::Item, graph: &KnowledgeGraph) -> bool {
if item.item_type.is_root() {
return !graph.children(&item.id).is_empty();
}
if item.item_type.is_leaf() {
return !item.upstream.is_empty();
}
!item.upstream.is_empty()
}
fn create_incomplete_item(item: &crate::model::Item, graph: &KnowledgeGraph) -> IncompleteItem {
let reason = if item.item_type.is_root() && graph.children(&item.id).is_empty() {
"No downstream items defined".to_string()
} else if item.upstream.is_empty() {
format!(
"Missing parent {}",
Self::expected_parent_type(item.item_type)
)
} else {
"Incomplete traceability".to_string()
};
IncompleteItem {
id: item.id.as_str().to_string(),
name: item.name.clone(),
item_type: item.item_type.display_name().to_string(),
reason,
}
}
fn expected_parent_type(item_type: ItemType) -> &'static str {
match item_type {
ItemType::Solution => "N/A (root)",
ItemType::UseCase => "Solution",
ItemType::Scenario => "Use Case",
ItemType::SystemRequirement => "Scenario",
ItemType::SystemArchitecture => "System Requirement",
ItemType::HardwareRequirement => "System Architecture",
ItemType::SoftwareRequirement => "System Architecture",
ItemType::HardwareDetailedDesign => "Hardware Requirement",
ItemType::SoftwareDetailedDesign => "Software Requirement",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::GraphBuilder;
use crate::model::{Item, ItemBuilder, ItemId, SourceLocation, UpstreamRefs};
use std::path::PathBuf;
fn create_test_item(id: &str, item_type: ItemType) -> Item {
let source = SourceLocation::new(PathBuf::from("/repo"), format!("{}.md", id), 1);
let mut builder = ItemBuilder::new()
.id(ItemId::new_unchecked(id))
.item_type(item_type)
.name(format!("Test {}", id))
.source(source);
if item_type.requires_specification() {
builder = builder.specification("Test specification");
}
builder.build().unwrap()
}
fn create_test_item_with_upstream(
id: &str,
item_type: ItemType,
upstream: UpstreamRefs,
) -> Item {
let source = SourceLocation::new(PathBuf::from("/repo"), format!("{}.md", id), 1);
let mut builder = ItemBuilder::new()
.id(ItemId::new_unchecked(id))
.item_type(item_type)
.name(format!("Test {}", id))
.source(source)
.upstream(upstream);
if item_type.requires_specification() {
builder = builder.specification("Test specification");
}
builder.build().unwrap()
}
#[test]
fn test_coverage_report_complete() {
let sol = create_test_item("SOL-001", ItemType::Solution);
let uc = create_test_item_with_upstream(
"UC-001",
ItemType::UseCase,
UpstreamRefs {
refines: vec![ItemId::new_unchecked("SOL-001")],
..Default::default()
},
);
let graph = GraphBuilder::new()
.add_item(sol)
.add_item(uc)
.build()
.unwrap();
let report = CoverageReport::generate(&graph);
assert!(report.overall_coverage > 0.0);
}
#[test]
fn test_coverage_report_incomplete() {
let uc = create_test_item("UC-001", ItemType::UseCase);
let graph = GraphBuilder::new().add_item(uc).build().unwrap();
let report = CoverageReport::generate(&graph);
assert!(!report.incomplete_items.is_empty());
}
}