#![cfg_attr(coverage_nightly, coverage(off))]
use crate::cli::commands::AnnotateOutputFormat;
use crate::cli::commands::WorkPriority;
use crate::models::roadmap::{ItemStatus, Priority, Roadmap, RoadmapItem};
use crate::services::roadmap_service::RoadmapService;
use std::path::PathBuf;
use tempfile::TempDir;
fn setup_temp_roadmap() -> (TempDir, PathBuf) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let roadmap_dir = temp_dir.path().join("docs/roadmaps");
std::fs::create_dir_all(&roadmap_dir).expect("Failed to create roadmap dir");
let roadmap_path = roadmap_dir.join("roadmap.yaml");
(temp_dir, roadmap_path)
}
fn write_valid_roadmap(path: &std::path::Path) {
let content = r#"roadmap_version: '1.0'
github_enabled: true
github_repo: test/repo
roadmap:
- id: PMAT-001
title: "First ticket"
status: planned
priority: medium
created: "2024-01-01T00:00:00Z"
updated: "2024-01-01T00:00:00Z"
acceptance_criteria:
- Test criterion 1
- id: PMAT-002
title: "Second ticket"
status: inprogress
priority: high
created: "2024-01-01T00:00:00Z"
updated: "2024-01-01T00:00:00Z"
acceptance_criteria: []
"#;
std::fs::write(path, content).expect("Failed to write roadmap");
}
#[test]
fn test_extract_line_from_yaml_error_with_line_info() {
let error = "expected a mapping value at line 5 column 3";
let result = extract_line_from_yaml_error_test(error);
assert_eq!(result, Some(5));
}
#[test]
fn test_extract_line_from_yaml_error_without_line_info() {
let error = "invalid type: expected string";
let result = extract_line_from_yaml_error_test(error);
assert_eq!(result, None);
}
#[test]
fn test_extract_line_from_yaml_error_edge_case() {
let error = "at line 42column 1";
let result = extract_line_from_yaml_error_test(error);
assert!(result.is_none() || result == Some(42));
}
#[test]
fn test_extract_line_from_yaml_error_at_line_0() {
let error = "at line 0 column 1";
let result = extract_line_from_yaml_error_test(error);
assert_eq!(result, Some(0));
}
#[test]
fn test_extract_line_from_yaml_error_large_line_number() {
let error = "at line 9999 column 1";
let result = extract_line_from_yaml_error_test(error);
assert_eq!(result, Some(9999));
}
fn extract_line_from_yaml_error_test(error: &str) -> Option<usize> {
if let Some(pos) = error.find("at line ") {
let rest = &error[pos + 8..];
if let Some(end) = rest.find(' ') {
return rest[..end].parse().ok();
}
}
None
}
#[test]
fn test_generate_next_id_empty_roadmap() {
let roadmap = Roadmap::new(None);
let next_id = generate_next_id_test(&roadmap);
assert_eq!(next_id, "PMAT-001");
}
#[test]
fn test_generate_next_id_with_existing_items() {
let mut roadmap = Roadmap::new(None);
roadmap.upsert_item(RoadmapItem::new("PMAT-001".to_string(), "Test".to_string()));
roadmap.upsert_item(RoadmapItem::new("PMAT-005".to_string(), "Test".to_string()));
let next_id = generate_next_id_test(&roadmap);
assert_eq!(next_id, "PMAT-006");
}
#[test]
fn test_generate_next_id_with_mixed_prefixes() {
let mut roadmap = Roadmap::new(None);
roadmap.upsert_item(RoadmapItem::new("GH-100".to_string(), "Test".to_string()));
roadmap.upsert_item(RoadmapItem::new("PMAT-003".to_string(), "Test".to_string()));
roadmap.upsert_item(RoadmapItem::new("TASK-050".to_string(), "Test".to_string()));
let next_id = generate_next_id_test(&roadmap);
assert_eq!(next_id, "PMAT-101");
}
#[test]
fn test_generate_next_id_with_non_numeric_ids() {
let mut roadmap = Roadmap::new(None);
roadmap.upsert_item(RoadmapItem::new(
"no-number".to_string(),
"Test".to_string(),
));
roadmap.upsert_item(RoadmapItem::new(
"also-no-number".to_string(),
"Test".to_string(),
));
let next_id = generate_next_id_test(&roadmap);
assert_eq!(next_id, "PMAT-001");
}
fn generate_next_id_test(roadmap: &Roadmap) -> String {
let mut max_num = 0u32;
for item in &roadmap.roadmap {
if let Some(num_str) = item.id.split('-').next_back() {
if let Ok(num) = num_str.parse::<u32>() {
max_num = max_num.max(num);
}
}
}
format!("PMAT-{:03}", max_num + 1)
}
#[test]
fn test_tdg_severity_label_excellent() {
assert_eq!(tdg_severity_label_test(0.5), "Excellent");
assert_eq!(tdg_severity_label_test(1.0), "Excellent");
}
#[test]
fn test_tdg_severity_label_good() {
assert_eq!(tdg_severity_label_test(1.5), "Good");
assert_eq!(tdg_severity_label_test(2.0), "Good");
}
#[test]
fn test_tdg_severity_label_moderate() {
assert_eq!(tdg_severity_label_test(2.5), "Moderate");
assert_eq!(tdg_severity_label_test(3.0), "Moderate");
}
#[test]
fn test_tdg_severity_label_critical() {
assert_eq!(tdg_severity_label_test(3.5), "Critical");
assert_eq!(tdg_severity_label_test(5.0), "Critical");
}
fn tdg_severity_label_test(score: f64) -> &'static str {
if score <= 1.0 {
"Excellent"
} else if score <= 2.0 {
"Good"
} else if score <= 3.0 {
"Moderate"
} else {
"Critical"
}
}
#[test]
fn test_calculate_spec_score_empty_spec() {
let spec = TestParsedSpec {
issue_refs: vec![],
code_examples: vec![],
acceptance_criteria: vec![],
claims: vec![],
title: String::new(),
test_requirements: vec![],
};
assert_eq!(calculate_spec_score_test(&spec), 0.0);
}
#[test]
fn test_calculate_spec_score_with_issue_refs() {
let spec = TestParsedSpec {
issue_refs: vec!["GH-123".to_string()],
code_examples: vec![],
acceptance_criteria: vec![],
claims: vec![],
title: String::new(),
test_requirements: vec![],
};
assert_eq!(calculate_spec_score_test(&spec), 10.0);
}
#[test]
fn test_calculate_spec_score_with_title() {
let spec = TestParsedSpec {
issue_refs: vec![],
code_examples: vec![],
acceptance_criteria: vec![],
claims: vec![],
title: "Test Title".to_string(),
test_requirements: vec![],
};
assert_eq!(calculate_spec_score_test(&spec), 5.0);
}
#[test]
fn test_calculate_spec_score_max_code_examples() {
let spec = TestParsedSpec {
issue_refs: vec![],
code_examples: vec!["ex".to_string(); 10], acceptance_criteria: vec![],
claims: vec![],
title: String::new(),
test_requirements: vec![],
};
assert_eq!(calculate_spec_score_test(&spec), 20.0);
}
#[test]
fn test_calculate_spec_score_max_score() {
let spec = TestParsedSpec {
issue_refs: vec!["GH-1".to_string()], code_examples: vec!["ex".to_string(); 5], acceptance_criteria: vec!["ac".to_string(); 10], claims: vec!["cl".to_string(); 20], title: "Title".to_string(), test_requirements: vec!["tr".to_string(); 5], };
assert_eq!(calculate_spec_score_test(&spec), 100.0);
}
#[test]
fn test_calculate_spec_score_capped_at_100() {
let spec = TestParsedSpec {
issue_refs: vec!["GH-1".to_string()],
code_examples: vec!["ex".to_string(); 20], acceptance_criteria: vec!["ac".to_string(); 50],
claims: vec!["cl".to_string(); 100],
title: "Title".to_string(),
test_requirements: vec!["tr".to_string(); 50],
};
assert_eq!(calculate_spec_score_test(&spec), 100.0);
}
struct TestParsedSpec {
issue_refs: Vec<String>,
code_examples: Vec<String>,
acceptance_criteria: Vec<String>,
claims: Vec<String>,
title: String,
test_requirements: Vec<String>,
}
fn calculate_spec_score_test(spec: &TestParsedSpec) -> f64 {
let mut score = 0.0;
if !spec.issue_refs.is_empty() {
score += 10.0;
}
score += (spec.code_examples.len().min(5) * 4) as f64;
score += (spec.acceptance_criteria.len().min(10) * 3) as f64;
score += (spec.claims.len().min(20)) as f64;
if !spec.title.is_empty() {
score += 5.0;
}
score += (spec.test_requirements.len().min(5) * 3) as f64;
score.min(100.0)
}
#[test]
fn test_work_priority_to_roadmap_priority_low() {
let wp = WorkPriority::Low;
assert_eq!(wp.to_roadmap_priority(), Priority::Low);
}
#[test]
fn test_work_priority_to_roadmap_priority_medium() {
let wp = WorkPriority::Medium;
assert_eq!(wp.to_roadmap_priority(), Priority::Medium);
}
#[test]
fn test_work_priority_to_roadmap_priority_high() {
let wp = WorkPriority::High;
assert_eq!(wp.to_roadmap_priority(), Priority::High);
}
#[test]
fn test_work_priority_to_roadmap_priority_critical() {
let wp = WorkPriority::Critical;
assert_eq!(wp.to_roadmap_priority(), Priority::Critical);
}
#[test]
fn test_work_priority_default() {
let wp = WorkPriority::default();
assert_eq!(wp, WorkPriority::Medium);
}
#[test]
fn test_annotate_output_format_default() {
let fmt = AnnotateOutputFormat::default();
assert_eq!(format!("{:?}", fmt), "Text");
}
#[test]
fn test_annotate_output_format_variants() {
let text = AnnotateOutputFormat::Text;
let json = AnnotateOutputFormat::Json;
let md = AnnotateOutputFormat::Markdown;
assert_eq!(format!("{:?}", text), "Text");
assert_eq!(format!("{:?}", json), "Json");
assert_eq!(format!("{:?}", md), "Markdown");
}
#[test]
fn test_roadmap_service_exists() {
let (temp_dir, roadmap_path) = setup_temp_roadmap();
let service = RoadmapService::new(&roadmap_path);
assert!(!service.exists());
write_valid_roadmap(&roadmap_path);
assert!(service.exists());
drop(temp_dir);
}
#[test]
fn test_roadmap_service_load_valid() {
let (temp_dir, roadmap_path) = setup_temp_roadmap();
write_valid_roadmap(&roadmap_path);
let service = RoadmapService::new(&roadmap_path);
let roadmap = service.load().expect("Failed to load roadmap");
assert_eq!(roadmap.roadmap_version, "1.0");
assert!(roadmap.github_enabled);
assert_eq!(roadmap.github_repo, Some("test/repo".to_string()));
assert_eq!(roadmap.roadmap.len(), 2);
drop(temp_dir);
}
#[test]
fn test_roadmap_service_upsert_and_find() {
let (temp_dir, roadmap_path) = setup_temp_roadmap();
let service = RoadmapService::new(&roadmap_path);
let item = RoadmapItem::new("TEST-001".to_string(), "Test ticket".to_string());
service.upsert_item(item.clone()).expect("Failed to upsert");
let found = service
.find_item("TEST-001")
.expect("Failed to find")
.expect("Item not found");
assert_eq!(found.id, "TEST-001");
assert_eq!(found.title, "Test ticket");
drop(temp_dir);
}
#[test]
fn test_roadmap_service_remove() {
let (temp_dir, roadmap_path) = setup_temp_roadmap();
let service = RoadmapService::new(&roadmap_path);
let item = RoadmapItem::new("TEST-001".to_string(), "Test ticket".to_string());
service.upsert_item(item).expect("Failed to upsert");
let removed = service.remove_item("TEST-001").expect("Failed to remove");
assert!(removed.is_some());
assert_eq!(removed.unwrap().id, "TEST-001");
let not_found = service.find_item("TEST-001").expect("Failed to find");
assert!(not_found.is_none());
drop(temp_dir);
}
#[test]
fn test_fuzzy_matching_exact() {
let mut roadmap = Roadmap::new(None);
roadmap.upsert_item(RoadmapItem::new("PMAT-001".to_string(), "Test".to_string()));
let found = roadmap.find_item("PMAT-001");
assert!(found.is_some());
assert_eq!(found.unwrap().id, "PMAT-001");
}
#[test]
fn test_fuzzy_matching_case_insensitive() {
let mut roadmap = Roadmap::new(None);
roadmap.upsert_item(RoadmapItem::new("PMAT-001".to_string(), "Test".to_string()));
let found = roadmap.find_item("pmat-001");
assert!(found.is_some());
assert_eq!(found.unwrap().id, "PMAT-001");
}
#[test]
fn test_fuzzy_matching_prefix() {
let mut roadmap = Roadmap::new(None);
roadmap.upsert_item(RoadmapItem::new("PMAT-001".to_string(), "Test".to_string()));
let found = roadmap.find_item("PMAT-0");
assert!(found.is_some());
assert_eq!(found.unwrap().id, "PMAT-001");
}
#[test]
fn test_fuzzy_matching_contains() {
let mut roadmap = Roadmap::new(None);
roadmap.upsert_item(RoadmapItem::new("PMAT-001".to_string(), "Test".to_string()));
let found = roadmap.find_item("001");
assert!(found.is_some());
assert_eq!(found.unwrap().id, "PMAT-001");
}
#[test]
fn test_fuzzy_matching_not_found() {
let mut roadmap = Roadmap::new(None);
roadmap.upsert_item(RoadmapItem::new("PMAT-001".to_string(), "Test".to_string()));
let found = roadmap.find_item("NONEXISTENT");
assert!(found.is_none());
}
#[test]
fn test_item_status_from_string_completed() {
assert_eq!(
ItemStatus::from_string("completed").unwrap(),
ItemStatus::Completed
);
assert_eq!(
ItemStatus::from_string("done").unwrap(),
ItemStatus::Completed
);
assert_eq!(
ItemStatus::from_string("DONE").unwrap(),
ItemStatus::Completed
);
}
#[test]
fn test_item_status_from_string_inprogress() {
assert_eq!(
ItemStatus::from_string("inprogress").unwrap(),
ItemStatus::InProgress
);
assert_eq!(
ItemStatus::from_string("wip").unwrap(),
ItemStatus::InProgress
);
assert_eq!(
ItemStatus::from_string("in-progress").unwrap(),
ItemStatus::InProgress
);
}
#[test]
fn test_item_status_from_string_planned() {
assert_eq!(
ItemStatus::from_string("planned").unwrap(),
ItemStatus::Planned
);
assert_eq!(
ItemStatus::from_string("todo").unwrap(),
ItemStatus::Planned
);
assert_eq!(
ItemStatus::from_string("open").unwrap(),
ItemStatus::Planned
);
}
#[test]
fn test_item_status_from_string_blocked() {
assert_eq!(
ItemStatus::from_string("blocked").unwrap(),
ItemStatus::Blocked
);
assert_eq!(
ItemStatus::from_string("stuck").unwrap(),
ItemStatus::Blocked
);
assert_eq!(
ItemStatus::from_string("on-hold").unwrap(),
ItemStatus::Blocked
);
}
#[test]
fn test_item_status_from_string_review() {
assert_eq!(
ItemStatus::from_string("review").unwrap(),
ItemStatus::Review
);
assert_eq!(ItemStatus::from_string("pr").unwrap(), ItemStatus::Review);
assert_eq!(
ItemStatus::from_string("pending-review").unwrap(),
ItemStatus::Review
);
}
#[test]
fn test_item_status_from_string_cancelled() {
assert_eq!(
ItemStatus::from_string("cancelled").unwrap(),
ItemStatus::Cancelled
);
assert_eq!(
ItemStatus::from_string("canceled").unwrap(),
ItemStatus::Cancelled
);
assert_eq!(
ItemStatus::from_string("wontfix").unwrap(),
ItemStatus::Cancelled
);
}
#[test]
fn test_item_status_from_string_invalid() {
let result = ItemStatus::from_string("invalid_status");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("unknown status"));
assert!(err.contains("did you mean"));
}
#[test]
fn test_item_status_valid_values() {
let values = ItemStatus::valid_values();
assert!(values.contains(&"completed"));
assert!(values.contains(&"inprogress"));
assert!(values.contains(&"planned"));
assert!(values.contains(&"blocked"));
assert!(values.contains(&"review"));
assert!(values.contains(&"cancelled"));
}
#[test]
fn test_roadmap_item_new() {
let item = RoadmapItem::new("TEST-001".to_string(), "Test title".to_string());
assert_eq!(item.id, "TEST-001");
assert_eq!(item.title, "Test title");
assert_eq!(item.status, ItemStatus::Planned);
assert_eq!(item.priority, Priority::Medium);
assert!(item.github_issue.is_none());
}
#[test]
fn test_roadmap_item_from_github_issue() {
let item = RoadmapItem::from_github_issue(42, "GitHub issue title".to_string());
assert_eq!(item.id, "GH-42");
assert_eq!(item.github_issue, Some(42));
assert_eq!(item.title, "GitHub issue title");
}
#[test]
fn test_roadmap_item_completion_percentage_planned() {
let item = RoadmapItem::new("TEST".to_string(), "Test".to_string());
assert_eq!(item.completion_percentage(), 0);
}
#[test]
fn test_roadmap_item_completion_percentage_inprogress() {
let mut item = RoadmapItem::new("TEST".to_string(), "Test".to_string());
item.status = ItemStatus::InProgress;
assert_eq!(item.completion_percentage(), 50);
}
#[test]
fn test_roadmap_item_completion_percentage_review() {
let mut item = RoadmapItem::new("TEST".to_string(), "Test".to_string());
item.status = ItemStatus::Review;
assert_eq!(item.completion_percentage(), 90);
}
#[test]
fn test_roadmap_item_completion_percentage_completed() {
let mut item = RoadmapItem::new("TEST".to_string(), "Test".to_string());
item.status = ItemStatus::Completed;
assert_eq!(item.completion_percentage(), 100);
}
#[test]
fn test_roadmap_item_is_github_synced() {
let mut item = RoadmapItem::new("TEST".to_string(), "Test".to_string());
assert!(!item.is_github_synced());
item.github_issue = Some(123);
assert!(item.is_github_synced());
}
#[test]
fn test_roadmap_new() {
let roadmap = Roadmap::new(Some("owner/repo".to_string()));
assert_eq!(roadmap.roadmap_version, "1.0");
assert!(roadmap.github_enabled);
assert_eq!(roadmap.github_repo, Some("owner/repo".to_string()));
assert!(roadmap.roadmap.is_empty());
}
#[test]
fn test_roadmap_default() {
let roadmap = Roadmap::default();
assert_eq!(roadmap.roadmap_version, "1.0");
assert!(roadmap.github_enabled);
assert!(roadmap.github_repo.is_none());
assert!(roadmap.roadmap.is_empty());
}
#[test]
fn test_roadmap_upsert_item() {
let mut roadmap = Roadmap::new(None);
let item = RoadmapItem::new("TEST-001".to_string(), "Test".to_string());
roadmap.upsert_item(item.clone());
assert_eq!(roadmap.roadmap.len(), 1);
let mut updated = item.clone();
updated.title = "Updated title".to_string();
roadmap.upsert_item(updated);
assert_eq!(roadmap.roadmap.len(), 1);
assert_eq!(roadmap.roadmap[0].title, "Updated title");
}
#[test]
fn test_roadmap_remove_item() {
let mut roadmap = Roadmap::new(None);
roadmap.upsert_item(RoadmapItem::new("TEST-001".to_string(), "Test".to_string()));
let removed = roadmap.remove_item("TEST-001");
assert!(removed.is_some());
assert!(roadmap.roadmap.is_empty());
let not_found = roadmap.remove_item("NONEXISTENT");
assert!(not_found.is_none());
}
#[test]
fn test_roadmap_find_item_by_github_issue() {
let mut roadmap = Roadmap::new(None);
roadmap.upsert_item(RoadmapItem::from_github_issue(42, "Test".to_string()));
let found = roadmap.find_item_by_github_issue(42);
assert!(found.is_some());
assert_eq!(found.unwrap().id, "GH-42");
let not_found = roadmap.find_item_by_github_issue(999);
assert!(not_found.is_none());
}
#[test]
fn test_roadmap_yaml_only_items() {
let mut roadmap = Roadmap::new(None);
roadmap.upsert_item(RoadmapItem::from_github_issue(42, "GH".to_string()));
roadmap.upsert_item(RoadmapItem::new("YAML-001".to_string(), "YAML".to_string()));
let yaml_only = roadmap.yaml_only_items();
assert_eq!(yaml_only.len(), 1);
assert_eq!(yaml_only[0].id, "YAML-001");
}
#[test]
fn test_roadmap_epic_items() {
use crate::models::roadmap::ItemType;
let mut roadmap = Roadmap::new(None);
let task = RoadmapItem::new("TASK-001".to_string(), "Task".to_string());
let mut epic = RoadmapItem::new("EPIC-001".to_string(), "Epic".to_string());
epic.item_type = ItemType::Epic;
roadmap.upsert_item(task);
roadmap.upsert_item(epic);
let epics = roadmap.epic_items();
assert_eq!(epics.len(), 1);
assert_eq!(epics[0].id, "EPIC-001");
}
#[test]
fn test_title_truncation_short() {
let title = "Short title";
let truncated = truncate_title_test(title);
assert_eq!(truncated, "Short title");
}
#[test]
fn test_title_truncation_exactly_40() {
let title = "This is exactly forty characters long!!!";
assert_eq!(title.len(), 40);
let truncated = truncate_title_test(title);
assert_eq!(truncated, title);
}
#[test]
fn test_title_truncation_over_40() {
let title = "This is a very long title that exceeds forty characters";
let truncated = truncate_title_test(title);
assert_eq!(truncated.len(), 40); assert!(truncated.ends_with("..."));
}
fn truncate_title_test(title: &str) -> String {
if title.len() > 40 {
format!("{}...", &title[..37])
} else {
title.to_string()
}
}
#[test]
fn test_title_with_special_chars() {
let (temp_dir, roadmap_path) = setup_temp_roadmap();
let service = RoadmapService::new(&roadmap_path);
let item = RoadmapItem::new("TEST-001".to_string(), "Title with: colons".to_string());
service.upsert_item(item.clone()).expect("Failed to upsert");
let loaded = service.find_item("TEST-001").unwrap().unwrap();
assert_eq!(loaded.title, "Title with: colons");
drop(temp_dir);
}
#[test]
fn test_title_with_unicode() {
let (temp_dir, roadmap_path) = setup_temp_roadmap();
let service = RoadmapService::new(&roadmap_path);
let item = RoadmapItem::new(
"TEST-001".to_string(),
"Unicode: \u{2713} \u{2717}".to_string(),
);
service.upsert_item(item).expect("Failed to upsert");
let loaded = service.find_item("TEST-001").unwrap().unwrap();
assert!(loaded.title.contains("\u{2713}"));
drop(temp_dir);
}
#[test]
fn test_long_id_warning_threshold() {
let short_id = "a".repeat(50);
let long_id = "b".repeat(51);
assert!(short_id.chars().count() <= 50);
assert!(long_id.chars().count() > 50);
}
#[test]
fn test_long_id_unicode_chars() {
let unicode_id = "\u{1F600}".repeat(51); assert!(unicode_id.chars().count() > 50);
assert!(unicode_id.len() > 51 * 3); }
#[test]
fn test_status_normalization_patterns() {
let patterns = [
("status: done", "status: completed"),
("status: Done", "status: completed"),
("status: DONE", "status: completed"),
("status: finished", "status: completed"),
("status: in progress", "status: inprogress"),
("status: WIP", "status: inprogress"),
("status: wip", "status: inprogress"),
("status: stuck", "status: blocked"),
("status: on-hold", "status: blocked"),
("status: todo", "status: planned"),
("status: TODO", "status: planned"),
("status: open", "status: planned"),
];
for (old, new) in patterns {
let mut content = format!("test content with {}", old);
content = content.replace(old, new);
assert!(
content.contains(new),
"Failed to replace {} with {}",
old,
new
);
}
}
#[test]
fn test_parse_lcov_lines() {
let lcov_content = r#"SF:src/main.rs
LF:100
LH:80
end_of_record
SF:src/lib.rs
LF:200
LH:150
end_of_record
"#;
let (lines_found, lines_hit) = parse_lcov_test(lcov_content);
assert_eq!(lines_found, 300);
assert_eq!(lines_hit, 230);
let coverage = (lines_hit as f64 / lines_found as f64) * 100.0;
assert!((coverage - 76.67).abs() < 0.01);
}
#[test]
fn test_parse_lcov_empty() {
let lcov_content = "";
let (lines_found, lines_hit) = parse_lcov_test(lcov_content);
assert_eq!(lines_found, 0);
assert_eq!(lines_hit, 0);
}
#[test]
fn test_parse_lcov_no_data() {
let lcov_content = r#"SF:src/main.rs
end_of_record
"#;
let (lines_found, lines_hit) = parse_lcov_test(lcov_content);
assert_eq!(lines_found, 0);
assert_eq!(lines_hit, 0);
}
fn parse_lcov_test(content: &str) -> (usize, usize) {
let mut lines_found: usize = 0;
let mut lines_hit: usize = 0;
for line in content.lines() {
if let Some(num) = line.strip_prefix("LF:") {
lines_found += num.parse::<usize>().unwrap_or(0);
} else if let Some(num) = line.strip_prefix("LH:") {
lines_hit += num.parse::<usize>().unwrap_or(0);
}
}
(lines_found, lines_hit)
}
#[test]
fn test_ticket_annotations_serialization() {
use serde_json;
#[derive(Debug, Clone, serde::Serialize)]
struct TicketAnnotations {
ticket_id: String,
title: String,
status: String,
priority: String,
spec_path: Option<PathBuf>,
spec_score: Option<f64>,
files: Vec<PathBuf>,
avg_tdg: Option<f64>,
total_churn: Option<usize>,
coverage_percent: Option<f64>,
}
let annotations = TicketAnnotations {
ticket_id: "PMAT-001".to_string(),
title: "Test ticket".to_string(),
status: "Planned".to_string(),
priority: "High".to_string(),
spec_path: Some(PathBuf::from("docs/spec.md")),
spec_score: Some(85.0),
files: vec![PathBuf::from("src/main.rs")],
avg_tdg: Some(2.5),
total_churn: Some(10),
coverage_percent: Some(95.5),
};
let json = serde_json::to_string(&annotations).expect("Failed to serialize");
assert!(json.contains("PMAT-001"));
assert!(json.contains("95.5"));
}
#[test]
fn test_churn_result_defaults() {
struct ChurnResult {
total_commits: usize,
hotspots: Vec<String>,
repeated_fixes: Vec<RepeatedFix>,
}
#[allow(dead_code)]
struct RepeatedFix {
file: String,
line_range: String,
fix_count: usize,
description: String,
}
let result = ChurnResult {
total_commits: 0,
hotspots: Vec::new(),
repeated_fixes: Vec::new(),
};
assert_eq!(result.total_commits, 0);
assert!(result.hotspots.is_empty());
assert!(result.repeated_fixes.is_empty());
}
#[test]
fn test_churn_hotspot_threshold() {
let commit_counts = [(3, false), (5, false), (6, true), (10, true)];
for (count, is_hotspot) in commit_counts {
assert_eq!(count > 5, is_hotspot);
}
}
#[test]
fn test_file_tdg_score_serialization() {
use serde_json;
#[derive(Debug, Clone, serde::Serialize)]
struct FileTdgScore {
file: String,
score: f64,
severity: String,
}
let tdg = FileTdgScore {
file: "src/main.rs".to_string(),
score: 2.5,
severity: "Moderate".to_string(),
};
let json = serde_json::to_string(&tdg).expect("Failed to serialize");
assert!(json.contains("src/main.rs"));
assert!(json.contains("2.5"));
assert!(json.contains("Moderate"));
}
#[test]
fn test_file_path_regex_pattern() {
let pattern = regex::Regex::new(r"`([\w/._-]+\.(?:rs|ts|py|go|js))`").unwrap();
let test_cases = [
("`src/main.rs`", Some("src/main.rs")),
("`path/to/file.ts`", Some("path/to/file.ts")),
("`test.py`", Some("test.py")),
("`module.go`", Some("module.go")),
("`script.js`", Some("script.js")),
("`file.txt`", None),
("`no-extension`", None),
("src/main.rs", None), ];
for (input, expected) in test_cases {
let result = pattern
.captures(input)
.and_then(|c| c.get(1))
.map(|m| m.as_str());
assert_eq!(result, expected, "Failed for input: {}", input);
}
}
#[test]
fn test_priority_ordering() {
assert!(Priority::Low < Priority::Medium);
assert!(Priority::Medium < Priority::High);
assert!(Priority::High < Priority::Critical);
}
#[test]
fn test_priority_equality() {
assert_eq!(Priority::Low, Priority::Low);
assert_ne!(Priority::Low, Priority::High);
}
#[test]
fn test_yaml_with_empty_acceptance_criteria() {
let yaml = r#"roadmap_version: '1.0'
github_enabled: true
roadmap:
- id: TEST-001
title: "No acceptance criteria"
status: planned
priority: medium
created: "2024-01-01T00:00:00Z"
updated: "2024-01-01T00:00:00Z"
acceptance_criteria: []
"#;
let roadmap: Roadmap = serde_yaml_ng::from_str(yaml).expect("Failed to parse");
assert!(roadmap.roadmap[0].acceptance_criteria.is_empty());
}
#[test]
fn test_yaml_with_missing_optional_fields() {
let yaml = r#"roadmap_version: '1.0'
github_enabled: true
roadmap:
- id: TEST-001
title: "Minimal item"
status: planned
"#;
let roadmap: Roadmap = serde_yaml_ng::from_str(yaml).expect("Failed to parse");
let item = &roadmap.roadmap[0];
assert!(item.spec.is_none());
assert!(item.estimated_effort.is_none());
assert!(item.notes.is_none());
assert!(item.assigned_to.is_none());
}
#[test]
fn test_yaml_with_all_status_values() {
let statuses = [
"planned",
"inprogress",
"blocked",
"review",
"completed",
"cancelled",
];
for status in statuses {
let yaml = format!(
r#"roadmap_version: '1.0'
github_enabled: true
roadmap:
- id: TEST-001
title: "Test"
status: {}
"#,
status
);
let result: Result<Roadmap, _> = serde_yaml_ng::from_str(&yaml);
assert!(result.is_ok(), "Failed to parse status: {}", status);
}
}
#[test]
fn test_handle_work_list_filtering_logic() {
let mut roadmap = Roadmap::new(None);
let mut item1 = RoadmapItem::new("PMAT-001".to_string(), "Task 1".to_string());
item1.status = ItemStatus::Planned;
item1.priority = Priority::High;
let mut item2 = RoadmapItem::new("PMAT-002".to_string(), "Task 2".to_string());
item2.status = ItemStatus::InProgress;
item2.priority = Priority::Medium;
let mut item3 = RoadmapItem::new("PMAT-003".to_string(), "Task 3".to_string());
item3.status = ItemStatus::Completed;
item3.priority = Priority::High;
roadmap.upsert_item(item1);
roadmap.upsert_item(item2);
roadmap.upsert_item(item3);
let planned: Vec<_> = roadmap
.roadmap
.iter()
.filter(|item| {
format!("{:?}", item.status)
.to_lowercase()
.contains("planned")
})
.collect();
assert_eq!(planned.len(), 1);
assert_eq!(planned[0].id, "PMAT-001");
let high_priority: Vec<_> = roadmap
.roadmap
.iter()
.filter(|item| item.priority == Priority::High)
.collect();
assert_eq!(high_priority.len(), 2);
}
#[test]
fn test_handle_work_edit_changes_tracking() {
let mut item = RoadmapItem::new("TEST-001".to_string(), "Original".to_string());
let mut changes: Vec<String> = Vec::new();
let new_title = "Updated Title".to_string();
item.title = new_title.clone();
changes.push(format!("title: {}", new_title));
item.priority = Priority::Critical;
changes.push(format!("priority: {:?}", Priority::Critical));
item.status = ItemStatus::InProgress;
changes.push("status: inprogress".to_string());
assert_eq!(changes.len(), 3);
assert!(changes[0].contains("Updated Title"));
assert!(changes[1].contains("Critical"));
assert!(changes[2].contains("inprogress"));
}
#[test]
fn test_roadmap_not_found_error_message() {
let path = PathBuf::from("/nonexistent/docs/roadmaps/roadmap.yaml");
let expected_msg = format!(
"No roadmap found at {}. Run 'pmat work init' first.",
path.display()
);
assert!(expected_msg.contains("No roadmap found"));
assert!(expected_msg.contains("pmat work init"));
}
#[test]
fn test_ticket_not_found_error_message() {
let id = "NONEXISTENT-001";
let expected_msg = format!(
"Ticket '{}' not found. Use 'pmat work list' to see available tickets.",
id
);
assert!(expected_msg.contains(id));
assert!(expected_msg.contains("pmat work list"));
}
#[test]
fn test_ambiguous_id_error_message() {
let id = "TEST";
let matches = vec!["TEST-001", "TEST-002", "TEST-003"];
let expected_msg = format!(
"Ambiguous ID '{}'. Multiple matches: {}. Please be more specific.",
id,
matches.join(", ")
);
assert!(expected_msg.contains("Ambiguous"));
assert!(expected_msg.contains("TEST-001, TEST-002, TEST-003"));
}