#![allow(unused)]
#![cfg_attr(coverage_nightly, coverage(off))]
use crate::services::path_glob::{resolve_project_path_with_globs, ResolvedProjectPath};
include!("tools_advanced_part1.rs");
include!("tools_advanced_part2.rs");
include!("tools_advanced_part3.rs");
include!("tools_advanced_part4.rs");
#[cfg(test)]
mod part2_tests {
use super::*;
use crate::models::dead_code::{
ConfidenceLevel, DeadCodeAnalysisConfig, DeadCodeItem, DeadCodeRankingResult,
DeadCodeSummary, DeadCodeType, FileDeadCodeMetrics,
};
use crate::models::tdg::{TDGHotspot, TDGSummary};
use chrono::Utc;
use serde_json::json;
fn empty_dc_summary() -> DeadCodeSummary {
DeadCodeSummary {
total_files_analyzed: 0,
files_with_dead_code: 0,
total_dead_lines: 0,
dead_percentage: 0.0,
dead_functions: 0,
dead_classes: 0,
dead_modules: 0,
unreachable_blocks: 0,
}
}
fn tdg_summary(total: usize, critical: usize, warning: usize) -> TDGSummary {
TDGSummary {
total_files: total,
critical_files: critical,
warning_files: warning,
average_tdg: 1.5,
p95_tdg: 2.5,
p99_tdg: 3.0,
estimated_debt_hours: 42.0,
hotspots: Vec::new(),
}
}
#[test]
fn test_get_confidence_level_text_all_variants() {
assert_eq!(get_confidence_level_text(ConfidenceLevel::High), "HIGH ");
assert_eq!(
get_confidence_level_text(ConfidenceLevel::Medium),
"MEDIUM "
);
assert_eq!(get_confidence_level_text(ConfidenceLevel::Low), "LOW ");
}
#[test]
fn test_format_confidence_emoji_all_variants() {
assert_eq!(format_confidence_emoji(ConfidenceLevel::High), "🔴 High");
assert_eq!(
format_confidence_emoji(ConfidenceLevel::Medium),
"🟡 Medium"
);
assert_eq!(format_confidence_emoji(ConfidenceLevel::Low), "🟢 Low");
}
#[test]
fn test_calculate_percentage_zero_total_returns_zero() {
assert_eq!(calculate_percentage(5, 0), 0.0);
}
#[test]
fn test_calculate_percentage_basic_ratio() {
assert!((calculate_percentage(1, 4) - 25.0).abs() < 1e-10);
assert!((calculate_percentage(7, 10) - 70.0).abs() < 1e-10);
}
#[test]
fn test_calculate_dead_files_percentage_zero_total() {
let s = empty_dc_summary();
assert_eq!(calculate_dead_files_percentage(&s), 0.0);
}
#[test]
fn test_calculate_dead_files_percentage_ratio() {
let mut s = empty_dc_summary();
s.total_files_analyzed = 10;
s.files_with_dead_code = 3;
assert!((calculate_dead_files_percentage(&s) - 30.0).abs() < 1e-3);
}
#[test]
fn test_write_dead_code_header_includes_title_and_date() {
let mut out = String::new();
let ts = Utc::now();
write_dead_code_header(&mut out, &ts);
assert!(out.contains("# Dead Code Analysis Report"));
assert!(out.contains("**Analysis Date:**"));
}
#[test]
fn test_write_dead_code_metrics_has_all_fields() {
let mut s = empty_dc_summary();
s.total_dead_lines = 100;
s.dead_percentage = 12.5;
s.dead_functions = 7;
s.dead_classes = 2;
s.dead_modules = 1;
s.unreachable_blocks = 3;
let mut out = String::new();
write_dead_code_metrics(&mut out, &s);
for key in [
"Total dead lines",
"100",
"12.5%",
"Dead functions:** 7",
"Dead classes:** 2",
"Dead modules:** 1",
"Unreachable blocks:** 3",
] {
assert!(out.contains(key), "missing {key} in: {out}");
}
}
#[test]
fn test_write_dead_code_summary_section_integration() {
let mut s = empty_dc_summary();
s.total_files_analyzed = 100;
s.files_with_dead_code = 25;
let mut out = String::new();
write_dead_code_summary_section(&mut out, &s);
assert!(out.contains("## Summary"));
assert!(out.contains("Total files analyzed:** 100"));
assert!(out.contains("Files with dead code:** 25 (25.0%)"));
}
#[test]
fn test_write_dead_code_top_files_section_empty_is_no_op() {
let mut out = String::new();
write_dead_code_top_files_section(&mut out, &[]);
assert!(out.is_empty());
}
fn dc_metric(path: &str, lines: usize, conf: ConfidenceLevel) -> FileDeadCodeMetrics {
FileDeadCodeMetrics {
path: path.to_string(),
dead_lines: lines,
total_lines: 100,
dead_percentage: lines as f32,
dead_functions: 1,
dead_classes: 0,
dead_modules: 0,
unreachable_blocks: 0,
dead_score: 50.0,
confidence: conf,
items: Vec::new(),
}
}
#[test]
fn test_write_dead_code_top_files_section_populated_has_table() {
let files = vec![
dc_metric("src/a.rs", 10, ConfidenceLevel::High),
dc_metric("src/b.rs", 5, ConfidenceLevel::Medium),
];
let mut out = String::new();
write_dead_code_top_files_section(&mut out, &files);
assert!(out.contains("## Top Files with Dead Code"));
assert!(out.contains("| Rank |"));
assert!(out.contains("src/a.rs"));
assert!(out.contains("src/b.rs"));
assert!(out.contains("🔴 High"));
assert!(out.contains("🟡 Medium"));
}
#[test]
fn test_format_dead_code_as_sarif_mcp_basic_shape() {
let file = FileDeadCodeMetrics {
path: "src/x.rs".to_string(),
dead_lines: 5,
total_lines: 50,
dead_percentage: 10.0,
dead_functions: 1,
dead_classes: 0,
dead_modules: 0,
unreachable_blocks: 0,
dead_score: 30.0,
confidence: ConfidenceLevel::High,
items: vec![DeadCodeItem {
item_type: DeadCodeType::Function,
name: "unused_fn".to_string(),
line: 42,
reason: "No call sites".to_string(),
}],
};
let result = DeadCodeRankingResult {
summary: empty_dc_summary(),
ranked_files: vec![file],
analysis_timestamp: Utc::now(),
config: DeadCodeAnalysisConfig {
include_unreachable: false,
include_tests: false,
min_dead_lines: 0,
},
};
let sarif = format_dead_code_as_sarif_mcp(&result).unwrap();
let v: serde_json::Value = serde_json::from_str(&sarif).unwrap();
assert_eq!(
v.get("$schema").and_then(|s| s.as_str()),
Some("https://json.schemastore.org/sarif-2.1.0.json")
);
assert!(v.get("runs").is_some());
assert!(sarif.contains("dead-code-function"));
assert!(sarif.contains("unused_fn"));
}
#[test]
fn test_format_dead_code_as_sarif_mcp_empty_has_zero_results() {
let result = DeadCodeRankingResult {
summary: empty_dc_summary(),
ranked_files: Vec::new(),
analysis_timestamp: Utc::now(),
config: DeadCodeAnalysisConfig {
include_unreachable: false,
include_tests: false,
min_dead_lines: 0,
},
};
let sarif = format_dead_code_as_sarif_mcp(&result).unwrap();
let v: serde_json::Value = serde_json::from_str(&sarif).unwrap();
let results_len = v["runs"][0]["results"].as_array().unwrap().len();
assert_eq!(results_len, 0);
}
#[test]
fn test_format_dead_code_as_markdown_mcp_integration() {
let result = DeadCodeRankingResult {
summary: empty_dc_summary(),
ranked_files: Vec::new(),
analysis_timestamp: Utc::now(),
config: DeadCodeAnalysisConfig {
include_unreachable: false,
include_tests: false,
min_dead_lines: 0,
},
};
let out = format_dead_code_as_markdown_mcp(&result).unwrap();
assert!(out.contains("# Dead Code Analysis Report"));
assert!(out.contains("## Summary"));
}
#[test]
fn test_parse_tdg_args_valid() {
let args = parse_tdg_args(json!({
"project_path": "/tmp",
"format": "json",
"threshold": 1.5,
}))
.unwrap();
assert_eq!(args.project_path.as_deref(), Some("/tmp"));
assert_eq!(args.format.as_deref(), Some("json"));
assert_eq!(args.threshold, Some(1.5));
}
#[test]
fn test_parse_tdg_args_invalid_type_returns_err() {
let result = parse_tdg_args(json!({"threshold": "not-a-number"}));
assert!(result.is_err());
}
#[test]
fn test_extract_tdg_project_path_none_is_err() {
let args = AnalyzeTdgArgs {
project_path: None,
format: None,
threshold: None,
include_components: None,
max_results: None,
};
assert!(extract_tdg_project_path(&args).is_err());
}
#[test]
fn test_extract_tdg_project_path_empty_is_err() {
let args = AnalyzeTdgArgs {
project_path: Some(String::new()),
format: None,
threshold: None,
include_components: None,
max_results: None,
};
assert!(extract_tdg_project_path(&args).is_err());
}
#[test]
fn test_format_tdg_summary_integration_empty_hotspots() {
let s = tdg_summary(10, 1, 2);
let out = format_tdg_summary(&s);
assert!(out.contains("# Technical Debt Gradient Analysis"));
assert!(out.contains("## Summary"));
assert!(out.contains("Total files:** 10"));
assert!(out.contains("Critical files:** 1 (10.0%)"));
assert!(out.contains("Warning files:** 2 (20.0%)"));
assert!(out.contains("## Severity Distribution"));
assert!(out.contains("🔴 Critical"));
assert!(out.contains("🟡 Warning"));
assert!(out.contains("🟢 Normal"));
assert!(!out.contains("## Top Hotspots"));
}
#[test]
fn test_append_tdg_hotspots_section_empty_no_op() {
let mut out = String::new();
let s = tdg_summary(5, 0, 0);
append_tdg_hotspots_section(&mut out, &s);
assert!(out.is_empty());
}
#[test]
fn test_append_tdg_hotspots_section_populated_has_rows() {
let mut s = tdg_summary(10, 1, 0);
s.hotspots = vec![
TDGHotspot {
path: "src/a.rs".to_string(),
tdg_score: 3.15,
primary_factor: "Complexity".to_string(),
estimated_hours: 4.5,
},
TDGHotspot {
path: "src/b.rs".to_string(),
tdg_score: 2.5,
primary_factor: "Duplication".to_string(),
estimated_hours: 2.0,
},
];
let mut out = String::new();
append_tdg_hotspots_section(&mut out, &s);
assert!(out.contains("## Top Hotspots"));
assert!(out.contains("src/a.rs"));
assert!(out.contains("3.15"));
assert!(out.contains("Complexity"));
assert!(out.contains("src/b.rs"));
assert!(out.contains("Duplication"));
}
#[test]
fn test_append_tdg_severity_section_computes_normal_via_saturating_sub() {
let mut out = String::new();
let s = tdg_summary(3, 5, 10);
append_tdg_severity_section(&mut out, &s);
assert!(out.contains("🟢 Normal (<1.5): 0 files"));
}
#[test]
fn test_format_and_respond_tdg_json_format_emits_json_content() {
let s = tdg_summary(5, 1, 1);
let resp = format_and_respond_tdg(json!(42), s, Some("json".to_string()));
assert!(resp.error.is_none());
let result = resp.result.as_ref().unwrap();
let content = result["content"][0]["text"].as_str().unwrap();
let _: serde_json::Value = serde_json::from_str(content).unwrap();
assert_eq!(result["format"], "json");
}
#[test]
fn test_format_and_respond_tdg_default_format_is_summary() {
let s = tdg_summary(5, 0, 1);
let resp = format_and_respond_tdg(json!(99), s, None);
let result = resp.result.as_ref().unwrap();
assert_eq!(result["format"], "summary");
let content = result["content"][0]["text"].as_str().unwrap();
assert!(content.contains("# Technical Debt Gradient Analysis"));
}
}
#[cfg(test)]
mod part1_pure_helpers_tests {
use super::*;
#[test]
fn test_require_project_path_advanced_none_is_rejected() {
let err = require_project_path_advanced(None).unwrap_err();
assert!(err.contains("'project_path' is required"), "got: {err}");
assert!(err.contains("R22-1"), "must tag R22-1/D101: {err}");
}
#[test]
fn test_require_project_path_advanced_empty_string_is_rejected() {
let err = require_project_path_advanced(Some(String::new())).unwrap_err();
assert!(err.contains("non-empty string"), "got: {err}");
}
#[test]
fn test_require_project_path_advanced_whitespace_is_rejected() {
let err = require_project_path_advanced(Some(" \t\n ".to_string())).unwrap_err();
assert!(err.contains("non-empty string"), "got: {err}");
}
#[test]
fn test_require_project_path_advanced_valid_returns_pathbuf() {
let path = require_project_path_advanced(Some("/tmp/proj".to_string())).unwrap();
assert_eq!(path, std::path::PathBuf::from("/tmp/proj"));
}
#[test]
fn test_require_non_empty_path_empty_rejected() {
let err = require_non_empty_path("", "output").unwrap_err();
assert!(err.contains("'output'"), "field name must appear: {err}");
assert!(err.contains("non-empty string"), "got: {err}");
}
#[test]
fn test_require_non_empty_path_whitespace_rejected() {
let err = require_non_empty_path(" ", "project_path").unwrap_err();
assert!(err.contains("'project_path'"), "got: {err}");
}
#[test]
fn test_require_non_empty_path_valid_returns_pathbuf() {
let path = require_non_empty_path("/tmp/x", "project_path").unwrap();
assert_eq!(path, std::path::PathBuf::from("/tmp/x"));
}
#[test]
fn test_get_relative_path_strips_prefix() {
let base = std::path::Path::new("/home/noah/project");
let file = std::path::Path::new("/home/noah/project/src/main.rs");
assert_eq!(get_relative_path(file, base), "src/main.rs");
}
#[test]
fn test_get_relative_path_no_prefix_match_returns_original() {
let base = std::path::Path::new("/home/noah/project");
let file = std::path::Path::new("/tmp/other.rs");
assert_eq!(get_relative_path(file, base), "/tmp/other.rs");
}
#[test]
fn test_calculate_cyclomatic_complexity_trivial() {
assert_eq!(calculate_cyclomatic_complexity("fn f() {}"), 1);
}
#[test]
fn test_calculate_cyclomatic_complexity_counts_control_flow() {
let src = "if cond { for x in xs { while y { match z {} } } }";
let c = calculate_cyclomatic_complexity(src);
assert!(c >= 5, "if+for+while+match should count: got {c}");
}
#[test]
fn test_calculate_cognitive_complexity_is_1_5x_cyclomatic() {
assert_eq!(calculate_cognitive_complexity(10), 15);
assert_eq!(calculate_cognitive_complexity(1), 1); assert_eq!(calculate_cognitive_complexity(0), 0);
}
#[test]
fn test_calculate_duplicate_ratio_empty_returns_zero() {
let lines: Vec<&str> = vec![];
let r = calculate_duplicate_ratio(&lines);
assert_eq!(r, 0.0);
}
#[test]
fn test_calculate_duplicate_ratio_all_unique_is_zero() {
let lines = vec!["let a = 1;", "let b = 2;", "let c = 3;"];
assert_eq!(calculate_duplicate_ratio(&lines), 0.0);
}
#[test]
fn test_calculate_duplicate_ratio_duplicates_counted() {
let lines = vec!["let x = 1;", "let x = 1;", "let y = 2;"];
let r = calculate_duplicate_ratio(&lines);
assert!((r - (1.0f32 / 3.0f32)).abs() < 1e-5, "got {r}");
}
#[test]
fn test_calculate_duplicate_ratio_skips_comments_and_blanks() {
let lines = vec!["// comment", "", "let x = 1;", "let x = 1;", "// another"];
let r = calculate_duplicate_ratio(&lines);
assert!((r - 0.2).abs() < 1e-5, "got {r}");
}
#[test]
fn test_calculate_efferent_coupling_counts_use_lines() {
let src = "use foo::bar;\nuse x::y::z;\nfn main() {}\n// use commented";
assert_eq!(calculate_efferent_coupling(src), 2.0);
}
#[test]
fn test_calculate_efferent_coupling_zero_when_no_imports() {
assert_eq!(calculate_efferent_coupling("fn main() {}"), 0.0);
}
#[test]
fn test_is_public_declaration_recognizes_pub_items() {
assert!(is_public_declaration("pub fn foo() {}"));
assert!(is_public_declaration(" pub struct Bar;"));
assert!(is_public_declaration("pub enum E { A }"));
}
#[test]
fn test_is_public_declaration_rejects_non_pub() {
assert!(!is_public_declaration("fn foo() {}"));
assert!(!is_public_declaration("struct S;"));
assert!(!is_public_declaration("// pub fn comment"));
}
#[test]
fn test_get_churn_score_found() {
let mut m = std::collections::HashMap::new();
m.insert("src/foo.rs".to_string(), 0.75_f32);
assert!((get_churn_score("src/foo.rs", &m) - 0.75).abs() < 1e-6);
}
#[test]
fn test_get_churn_score_not_found_returns_default_floor() {
let m: std::collections::HashMap<String, f32> = std::collections::HashMap::new();
assert!((get_churn_score("src/missing.rs", &m) - 0.1).abs() < 1e-6);
}
#[test]
fn test_calculate_afferent_coupling_is_deterministic() {
let a = calculate_afferent_coupling("fn x() {}");
let b = calculate_afferent_coupling("fn x() {}");
assert!((a - b).abs() < 1e-6);
}
}
#[cfg(test)]
mod part3_pure_helpers_tests {
use super::*;
use crate::services::deep_context::{AnalysisType, CacheStrategy, DagType};
use crate::services::makefile_linter::Severity;
#[test]
fn test_default_project_path_is_empty() {
assert_eq!(default_project_path(), "");
}
#[test]
fn test_default_top_files_is_ten() {
assert_eq!(default_top_files(), 10);
}
#[test]
fn test_get_default_analysis_types_includes_ast_complexity_churn() {
let types = get_default_analysis_types();
assert_eq!(types.len(), 3);
assert!(matches!(types[0], AnalysisType::Ast));
assert!(matches!(types[1], AnalysisType::Complexity));
assert!(matches!(types[2], AnalysisType::Churn));
}
#[test]
fn test_parse_analysis_type_string_ast() {
assert!(matches!(
parse_analysis_type_string("ast"),
Some(AnalysisType::Ast)
));
}
#[test]
fn test_parse_analysis_type_string_complexity() {
assert!(matches!(
parse_analysis_type_string("complexity"),
Some(AnalysisType::Complexity)
));
}
#[test]
fn test_parse_analysis_type_string_churn() {
assert!(matches!(
parse_analysis_type_string("churn"),
Some(AnalysisType::Churn)
));
}
#[test]
fn test_parse_analysis_type_string_dag() {
assert!(matches!(
parse_analysis_type_string("dag"),
Some(AnalysisType::Dag)
));
}
#[test]
fn test_parse_analysis_type_string_dead_code() {
assert!(matches!(
parse_analysis_type_string("dead_code"),
Some(AnalysisType::DeadCode)
));
}
#[test]
fn test_parse_analysis_type_string_satd() {
assert!(matches!(
parse_analysis_type_string("satd"),
Some(AnalysisType::Satd)
));
}
#[test]
fn test_parse_analysis_type_string_tdg() {
assert!(matches!(
parse_analysis_type_string("tdg"),
Some(AnalysisType::TechnicalDebtGradient)
));
}
#[test]
fn test_parse_analysis_type_string_unknown_returns_none() {
assert!(parse_analysis_type_string("nonsense").is_none());
assert!(parse_analysis_type_string("").is_none());
}
#[test]
fn test_parse_analysis_types_none_returns_defaults() {
let result = parse_analysis_types(None);
assert_eq!(result.len(), 3);
}
#[test]
fn test_parse_analysis_types_some_filters_unknown() {
let result = parse_analysis_types(Some(vec![
"ast".to_string(),
"bogus".to_string(),
"satd".to_string(),
]));
assert_eq!(result.len(), 2);
}
#[test]
fn test_parse_analysis_types_empty_vec_returns_empty() {
let result = parse_analysis_types(Some(vec![]));
assert!(result.is_empty());
}
#[test]
fn test_parse_dag_type_import_graph() {
assert!(matches!(
parse_deep_context_dag_type(Some("import-graph".to_string())),
DagType::ImportGraph
));
}
#[test]
fn test_parse_dag_type_inheritance() {
assert!(matches!(
parse_deep_context_dag_type(Some("inheritance".to_string())),
DagType::Inheritance
));
}
#[test]
fn test_parse_dag_type_full_dependency() {
assert!(matches!(
parse_deep_context_dag_type(Some("full-dependency".to_string())),
DagType::FullDependency
));
}
#[test]
fn test_parse_dag_type_call_graph_explicit() {
assert!(matches!(
parse_deep_context_dag_type(Some("call-graph".to_string())),
DagType::CallGraph
));
}
#[test]
fn test_parse_dag_type_none_defaults_to_call_graph() {
assert!(matches!(
parse_deep_context_dag_type(None),
DagType::CallGraph
));
}
#[test]
fn test_parse_dag_type_unknown_defaults_to_call_graph() {
assert!(matches!(
parse_deep_context_dag_type(Some("garbage".to_string())),
DagType::CallGraph
));
}
#[test]
fn test_parse_cache_strategy_force_refresh() {
assert!(matches!(
parse_cache_strategy(Some("force-refresh".to_string())),
CacheStrategy::ForceRefresh
));
}
#[test]
fn test_parse_cache_strategy_offline() {
assert!(matches!(
parse_cache_strategy(Some("offline".to_string())),
CacheStrategy::Offline
));
}
#[test]
fn test_parse_cache_strategy_normal_explicit() {
assert!(matches!(
parse_cache_strategy(Some("normal".to_string())),
CacheStrategy::Normal
));
}
#[test]
fn test_parse_cache_strategy_none_defaults_to_normal() {
assert!(matches!(parse_cache_strategy(None), CacheStrategy::Normal));
}
#[test]
fn test_parse_cache_strategy_unknown_defaults_to_normal() {
assert!(matches!(
parse_cache_strategy(Some("xyzzy".to_string())),
CacheStrategy::Normal
));
}
#[test]
fn test_parse_deep_context_args_minimal() {
let v = serde_json::json!({});
let args = parse_deep_context_args(v).unwrap();
assert!(args.project_path.is_none());
}
#[test]
fn test_parse_deep_context_args_with_path() {
let v = serde_json::json!({"project_path": "/tmp/x"});
let args = parse_deep_context_args(v).unwrap();
assert_eq!(args.project_path.as_deref(), Some("/tmp/x"));
}
#[test]
fn test_parse_deep_context_args_invalid_returns_err() {
let v = serde_json::json!({"project_path": 42});
let err = parse_deep_context_args(v).unwrap_err();
assert!(err.contains("Invalid analyze_deep_context"));
}
#[test]
fn test_build_deep_context_config_uses_defaults_when_unset() {
let args = AnalyzeDeepContextArgs {
project_path: None,
format: None,
include_analyses: None,
exclude_analyses: None,
period_days: None,
dag_type: None,
max_depth: None,
include_pattern: None,
exclude_pattern: None,
cache_strategy: None,
parallel: None,
};
let cfg = build_deep_context_config(&args);
assert_eq!(cfg.period_days, 30); assert_eq!(cfg.parallel, 4); assert!(matches!(cfg.dag_type, DagType::CallGraph));
assert!(matches!(cfg.cache_strategy, CacheStrategy::Normal));
assert_eq!(cfg.include_analyses.len(), 3); }
#[test]
fn test_build_deep_context_config_threshold_constants_pinned() {
let args = AnalyzeDeepContextArgs {
project_path: None,
format: None,
include_analyses: None,
exclude_analyses: None,
period_days: None,
dag_type: None,
max_depth: None,
include_pattern: None,
exclude_pattern: None,
cache_strategy: None,
parallel: None,
};
let cfg = build_deep_context_config(&args);
let t = cfg.complexity_thresholds.unwrap();
assert_eq!(t.max_cyclomatic, 10);
assert_eq!(t.max_cognitive, 15);
}
#[test]
fn test_format_deep_context_as_sarif_emits_v2_1_0_skeleton() {
let dc = stub_deep_context();
let sarif = format_deep_context_as_sarif(&dc);
let parsed: serde_json::Value = serde_json::from_str(&sarif).unwrap();
assert_eq!(parsed["version"], "2.1.0");
assert_eq!(parsed["runs"][0]["tool"]["driver"]["name"], "pmat");
assert!(parsed["runs"][0]["results"].as_array().unwrap().is_empty());
}
fn stub_deep_context() -> crate::services::deep_context::DeepContext {
crate::services::deep_context::DeepContext::default()
}
#[test]
fn test_map_severity_error() {
assert_eq!(map_severity(&Severity::Error), "error");
}
#[test]
fn test_map_severity_warning() {
assert_eq!(map_severity(&Severity::Warning), "warning");
}
#[test]
fn test_map_severity_performance() {
assert_eq!(map_severity(&Severity::Performance), "performance");
}
#[test]
fn test_map_severity_info() {
assert_eq!(map_severity(&Severity::Info), "info");
}
#[test]
fn test_format_violation_emits_all_fields() {
use crate::services::makefile_linter::ast::SourceSpan;
use crate::services::makefile_linter::Violation;
let v = Violation {
rule: "MAKE001".to_string(),
severity: Severity::Warning,
span: SourceSpan {
start: 0,
end: 5,
line: 7,
column: 3,
},
message: "uses :=".to_string(),
fix_hint: Some("change to =".to_string()),
};
let json = format_violation(&v);
assert_eq!(json["rule"], "MAKE001");
assert_eq!(json["severity"], "warning");
assert_eq!(json["line"], 7);
assert_eq!(json["column"], 3);
assert_eq!(json["message"], "uses :=");
assert_eq!(json["fix_hint"], "change to =");
}
#[test]
fn test_format_violation_with_no_fix_hint() {
use crate::services::makefile_linter::ast::SourceSpan;
use crate::services::makefile_linter::Violation;
let v = Violation {
rule: "MAKE002".to_string(),
severity: Severity::Error,
span: SourceSpan {
start: 0,
end: 0,
line: 1,
column: 1,
},
message: "broken".to_string(),
fix_hint: None,
};
let json = format_violation(&v);
assert!(json["fix_hint"].is_null());
}
#[test]
fn test_count_violations_by_severity_filters_correctly() {
use crate::services::makefile_linter::ast::SourceSpan;
use crate::services::makefile_linter::Violation;
let span = SourceSpan {
start: 0,
end: 0,
line: 1,
column: 1,
};
let vs = vec![
Violation {
rule: "a".into(),
severity: Severity::Error,
span,
message: "".into(),
fix_hint: None,
},
Violation {
rule: "b".into(),
severity: Severity::Warning,
span,
message: "".into(),
fix_hint: None,
},
Violation {
rule: "c".into(),
severity: Severity::Info,
span,
message: "".into(),
fix_hint: None,
},
Violation {
rule: "d".into(),
severity: Severity::Warning,
span,
message: "".into(),
fix_hint: None,
},
];
assert_eq!(count_violations_by_severity(&vs, Severity::Error), 1);
assert_eq!(count_violations_by_severity(&vs, Severity::Warning), 2);
assert_eq!(count_violations_by_severity(&vs, Severity::Info), 1);
assert_eq!(count_violations_by_severity(&vs, Severity::Performance), 0);
}
#[test]
fn test_count_violations_by_severity_empty_returns_zero() {
assert_eq!(count_violations_by_severity(&[], Severity::Error), 0);
}
#[test]
fn test_parse_makefile_lint_args_none_rejected() {
let err = match parse_makefile_lint_args(None) {
Err(e) => e,
Ok(_) => panic!("expected Err"),
};
assert!(err.contains("Missing required arguments"));
}
#[test]
fn test_parse_makefile_lint_args_minimal() {
let v = serde_json::json!({"path": "/tmp/Makefile"});
let args = parse_makefile_lint_args(Some(v)).unwrap();
assert_eq!(args.path, "/tmp/Makefile");
assert!(args.rules.is_empty());
assert!(!args.fix);
}
#[test]
fn test_parse_makefile_lint_args_full() {
let v = serde_json::json!({
"path": "/x",
"rules": ["MAKE001", "MAKE002"],
"fix": true,
"gnu_version": "4.3",
});
let args = parse_makefile_lint_args(Some(v)).unwrap();
assert_eq!(args.rules, vec!["MAKE001", "MAKE002"]);
assert!(args.fix);
assert_eq!(args.gnu_version, "4.3");
}
#[test]
fn test_parse_makefile_lint_args_invalid_returns_err() {
let v = serde_json::json!({"path": 42}); let err = match parse_makefile_lint_args(Some(v)) {
Err(e) => e,
Ok(_) => panic!("expected Err"),
};
assert!(err.contains("Invalid analyze_makefile_lint"));
}
}