#![cfg(feature = "demo")]
#[cfg(test)]
mod export_integration_tests {
use anyhow::Result;
use chrono::Utc;
use pmat::demo::export::*;
use pmat::demo::Hotspot;
use pmat::models::churn::{ChurnSummary, CodeChurnAnalysis, FileChurnMetrics};
use pmat::models::dag::{DependencyGraph, Edge, EdgeType, NodeInfo, NodeType};
use pmat::services::complexity::{
ComplexityMetrics, ComplexityReport, ComplexitySummary, FileComplexityMetrics,
FunctionComplexity,
};
use pmat::services::deep_context::{CacheStats, ContextMetadata};
use rustc_hash::FxHashMap;
use std::collections::HashMap;
use tempfile::TempDir;
fn create_test_export_report() -> ExportReport {
let mut mermaid_graphs = HashMap::new();
mermaid_graphs.insert(
"main".to_string(),
"graph TD\n A --> B\n B --> C".to_string(),
);
let fixed_timestamp =
chrono::TimeZone::with_ymd_and_hms(&Utc, 2025, 1, 1, 12, 0, 0).unwrap();
ExportReport {
repository: "test-repo".to_string(),
timestamp: fixed_timestamp,
metadata: ContextMetadata {
generated_at: Utc::now(),
tool_version: "1.0.0".to_string(),
project_root: std::path::PathBuf::from("test-repo"),
cache_stats: CacheStats {
hit_rate: 0.0,
memory_efficiency: 0.0,
time_saved_ms: 0,
},
analysis_duration: std::time::Duration::from_millis(1500),
},
ast_contexts: vec![],
dependency_graph: DependencyGraph::new(),
complexity_analysis: ComplexityAnalysis {
hotspots: vec![
Hotspot {
file: "main.rs::main".to_string(),
complexity: 15,
churn_score: 20,
},
Hotspot {
file: "lib.rs::process".to_string(),
complexity: 12,
churn_score: 18,
},
],
total_files: 10,
average_complexity: 5.5,
technical_debt_hours: 8,
},
churn_analysis: Some(create_test_churn_analysis()),
satd_analysis: None,
dead_code_results: None,
cross_references: vec![],
quality_scorecard: None,
defect_summary: None,
mermaid_graphs,
summary: ProjectSummary {
total_nodes: 15,
total_edges: 22,
analyzed_files: 10,
analysis_time_ms: 1500,
},
}
}
fn create_test_churn_analysis() -> CodeChurnAnalysis {
CodeChurnAnalysis {
generated_at: Utc::now(),
period_days: 30,
repository_root: std::path::PathBuf::from("test-repo"),
files: vec![
FileChurnMetrics {
path: std::path::PathBuf::from("src/main.rs"),
relative_path: "src/main.rs".to_string(),
commit_count: 45,
unique_authors: vec![],
additions: 100,
deletions: 50,
churn_score: 0.85,
last_modified: Utc::now(),
first_seen: Utc::now(),
},
FileChurnMetrics {
path: std::path::PathBuf::from("src/lib.rs"),
relative_path: "src/lib.rs".to_string(),
commit_count: 38,
unique_authors: vec![],
additions: 80,
deletions: 30,
churn_score: 0.72,
last_modified: Utc::now(),
first_seen: Utc::now(),
},
],
summary: ChurnSummary {
total_commits: 83,
total_files_changed: 2,
hotspot_files: vec![],
stable_files: vec![],
author_contributions: HashMap::new(),
mean_churn_score: 0.0,
variance_churn_score: 0.0,
stddev_churn_score: 0.0,
},
}
}
#[test]
fn test_markdown_export() {
let report = create_test_export_report();
let exporter = MarkdownExporter;
let result = exporter.export(&report).unwrap();
assert!(result.contains("# Analysis: test-repo"));
assert!(result.contains("Generated: 2025-01-01 12:00 UTC"));
assert!(result.contains("## Summary"));
assert!(result.contains("**Analyzed Files**: 10"));
assert!(result.contains("**Average Complexity**: 5.50"));
assert!(result.contains("**Technical Debt**: 8 hours"));
assert!(result.contains("**Analysis Time**: 1500ms"));
assert!(result.contains("## Dependency Graph"));
assert!(result.contains("```mermaid"));
assert!(result.contains("graph TD"));
assert!(result.contains("A --> B"));
assert!(result.contains("B --> C"));
assert!(result.contains("## Complexity Hotspots"));
assert!(result.contains("| main.rs::main | 15 | 20 |"));
assert!(result.contains("| lib.rs::process | 12 | 18 |"));
assert!(result.contains("## Code Churn Analysis (Last 30 Days)"));
assert!(result.contains("| src/main.rs | 0.85 | 45 |"));
assert!(result.contains("| src/lib.rs | 0.72 | 38 |"));
assert!(result.contains("<details>"));
assert!(result.contains("<summary>Raw Data</summary>"));
assert!(result.contains("```json"));
}
#[test]
fn test_json_export_pretty() {
let report = create_test_export_report();
let exporter = JsonExporter::new(true);
let result = exporter.export(&report).unwrap();
assert!(result.contains('\n'));
assert!(result.contains(" "));
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["repository"], "test-repo");
assert_eq!(parsed["complexity_analysis"]["total_files"], 10);
assert_eq!(parsed["complexity_analysis"]["average_complexity"], 5.5);
assert_eq!(parsed["summary"]["total_nodes"], 15);
}
#[test]
fn test_json_export_compact() {
let report = create_test_export_report();
let exporter = JsonExporter::new(false);
let result = exporter.export(&report).unwrap();
let newline_count = result.matches('\n').count();
assert_eq!(newline_count, 0);
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["repository"], "test-repo");
}
#[test]
fn test_sarif_export() {
let report = create_test_export_report();
let exporter = SarifExporter;
let result = exporter.export(&report).unwrap();
let sarif: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(sarif["version"], "2.1.0");
assert!(sarif["$schema"]
.as_str()
.unwrap()
.contains("sarif-schema-2.1.0.json"));
let driver = &sarif["runs"][0]["tool"]["driver"];
assert_eq!(driver["name"], "pmat");
assert!(driver["informationUri"]
.as_str()
.unwrap()
.contains("github.com"));
let rules = driver["rules"].as_array().unwrap();
assert_eq!(rules.len(), 1);
assert_eq!(rules[0]["id"], "COMPLEXITY001");
assert_eq!(rules[0]["name"], "HighComplexity");
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0]["ruleId"], "COMPLEXITY001");
assert_eq!(results[0]["level"], "warning"); assert!(results[0]["message"]["text"]
.as_str()
.unwrap()
.contains("main.rs::main"));
assert!(results[0]["message"]["text"]
.as_str()
.unwrap()
.contains("complexity of 15"));
}
#[test]
fn test_export_service() {
let service = ExportService::new();
let report = create_test_export_report();
let formats = service.supported_formats();
assert!(formats.contains(&"markdown"));
assert!(formats.contains(&"json"));
assert!(formats.contains(&"sarif"));
let markdown_result = service.export("markdown", &report).unwrap();
assert!(markdown_result.contains("# Analysis: test-repo"));
let json_result = service.export("json", &report).unwrap();
let _: serde_json::Value = serde_json::from_str(&json_result).unwrap();
let sarif_result = service.export("sarif", &report).unwrap();
let sarif: serde_json::Value = serde_json::from_str(&sarif_result).unwrap();
assert_eq!(sarif["version"], "2.1.0");
let unsupported_result = service.export("xml", &report);
assert!(unsupported_result.is_err());
}
#[test]
fn test_export_service_save_to_file() -> Result<()> {
let temp_dir = TempDir::new()?;
let service = ExportService::new();
let report = create_test_export_report();
let md_path = temp_dir.path().join("report.md");
service.save_to_file("markdown", &report, &md_path)?;
assert!(md_path.exists());
let md_content = std::fs::read_to_string(&md_path)?;
assert!(md_content.contains("# Analysis: test-repo"));
let json_path = temp_dir.path().join("report.json");
service.save_to_file("json", &report, &json_path)?;
assert!(json_path.exists());
let json_content = std::fs::read_to_string(&json_path)?;
let _: serde_json::Value = serde_json::from_str(&json_content)?;
let sarif_path = temp_dir.path().join("report.sarif");
service.save_to_file("sarif", &report, &sarif_path)?;
assert!(sarif_path.exists());
Ok(())
}
#[test]
fn test_create_export_report() {
let mut nodes = FxHashMap::default();
nodes.insert(
"node1".to_string(),
NodeInfo {
id: "node1".to_string(),
label: "Module A".to_string(),
node_type: NodeType::Module,
file_path: "src/module_a.rs".to_string(),
line_number: 1,
complexity: 5,
metadata: FxHashMap::default(),
},
);
nodes.insert(
"node2".to_string(),
NodeInfo {
id: "node2".to_string(),
label: "Module B".to_string(),
node_type: NodeType::Module,
file_path: "src/module_b.rs".to_string(),
line_number: 1,
complexity: 8,
metadata: FxHashMap::default(),
},
);
let edges = vec![Edge {
from: "node1".to_string(),
to: "node2".to_string(),
edge_type: EdgeType::Imports,
weight: 1,
}];
let dag = DependencyGraph { nodes, edges };
let complexity_report = ComplexityReport {
summary: ComplexitySummary {
total_files: 2,
total_functions: 4,
median_cyclomatic: 5.0,
median_cognitive: 7.0,
max_cyclomatic: 15,
max_cognitive: 20,
p90_cyclomatic: 12,
p90_cognitive: 18,
technical_debt_hours: 4.5,
},
violations: vec![],
hotspots: vec![],
files: vec![FileComplexityMetrics {
path: "src/main.rs".to_string(),
total_complexity: ComplexityMetrics::new(15, 20, 3, 50),
functions: vec![FunctionComplexity {
name: "main".to_string(),
line_start: 1,
line_end: 50,
metrics: ComplexityMetrics::new(15, 20, 3, 50),
}],
classes: vec![],
}],
};
let churn_analysis = CodeChurnAnalysis {
generated_at: Utc::now(),
period_days: 30,
repository_root: std::path::PathBuf::from("/test/repo"),
files: vec![FileChurnMetrics {
path: std::path::PathBuf::from("/test/repo/src/main.rs"),
relative_path: "src/main.rs".to_string(),
commit_count: 25,
unique_authors: vec![
"author1".to_string(),
"author2".to_string(),
"author3".to_string(),
],
additions: 500,
deletions: 200,
churn_score: 0.75,
last_modified: Utc::now(),
first_seen: Utc::now(),
}],
summary: ChurnSummary {
total_commits: 100,
total_files_changed: 20,
hotspot_files: vec![std::path::PathBuf::from("src/main.rs")],
stable_files: vec![std::path::PathBuf::from("src/config.rs")],
author_contributions: HashMap::new(),
mean_churn_score: 0.0,
variance_churn_score: 0.0,
stddev_churn_score: 0.0,
},
};
let report = create_export_report(
"test-repo",
&dag,
Some(&complexity_report),
Some(&churn_analysis),
"graph TD\n A --> B",
1234,
);
assert_eq!(report.repository, "test-repo");
assert_eq!(
report.mermaid_graphs.get("main").unwrap(),
"graph TD\n A --> B"
);
assert_eq!(report.complexity_analysis.total_files, 1);
assert_eq!(report.complexity_analysis.average_complexity, 5.0);
assert_eq!(report.complexity_analysis.technical_debt_hours, 4);
assert_eq!(report.summary.total_nodes, 2);
assert_eq!(report.summary.total_edges, 1);
assert_eq!(report.summary.analysis_time_ms, 1234);
assert_eq!(report.complexity_analysis.hotspots.len(), 1);
assert_eq!(
report.complexity_analysis.hotspots[0].file,
"src/main.rs::main"
);
assert_eq!(report.complexity_analysis.hotspots[0].complexity, 15);
assert!(report.churn_analysis.is_some());
let churn = report.churn_analysis.unwrap();
assert_eq!(churn.period_days, 30);
assert_eq!(churn.files.len(), 1);
assert_eq!(churn.files[0].relative_path, "src/main.rs");
assert_eq!(churn.files[0].churn_score, 0.75);
}
#[test]
fn test_export_without_optional_data() {
let dag = DependencyGraph {
nodes: FxHashMap::default(),
edges: vec![],
};
let report = create_export_report("minimal-repo", &dag, None, None, "graph TD", 100);
assert_eq!(report.repository, "minimal-repo");
assert_eq!(report.complexity_analysis.hotspots.len(), 0);
assert_eq!(report.complexity_analysis.total_files, 0);
assert_eq!(report.complexity_analysis.average_complexity, 0.0);
assert!(report.churn_analysis.is_none());
assert_eq!(report.summary.analyzed_files, 0);
}
}