#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use crate::services::satd_detector::*;
use proptest::prelude::*;
use std::io::Write;
use std::path::Path;
use tempfile::NamedTempFile;
prop_compose! {
fn arb_satd_marker()
(choice in 0usize..10)
-> &'static str
{
match choice {
0 => "TODO",
1 => "FIXME",
2 => "HACK",
3 => "BUG",
4 => "XXX",
5 => "OPTIMIZE",
6 => "SECURITY",
7 => "KLUDGE",
8 => "REFACTOR",
_ => "TECHNICAL DEBT",
}
}
}
prop_compose! {
fn arb_comment_style()
(style in 0usize..5,
padding in prop::collection::vec(prop::sample::select(vec![" ", "\t"]), 0..5))
-> String
{
let padding_str = padding.join("");
match style {
0 => format!("{}//", padding_str),
1 => format!("{}#", padding_str),
2 => format!("{}/*", padding_str),
3 => format!("{} *", padding_str),
_ => format!("{}--", padding_str),
}
}
}
prop_compose! {
fn arb_satd_comment()
(marker in arb_satd_marker(),
style in arb_comment_style(),
message in "[a-zA-Z0-9 .,!?-]{0,100}",
extra_spaces in 0usize..5)
-> String
{
format!("{}{}{}: {}",
style,
" ".repeat(extra_spaces),
marker,
message)
}
}
prop_compose! {
fn arb_source_with_satd()
(num_lines in 1usize..50,
satd_lines in prop::collection::vec((0usize..50, arb_satd_comment()), 0..10),
code_lines in prop::collection::vec("[a-zA-Z0-9_(){}; ]{0,80}", 0..50))
-> String
{
let mut lines = vec![];
let satd_map: std::collections::HashMap<_, _> = satd_lines.into_iter().collect();
for i in 0..num_lines {
if let Some(satd) = satd_map.get(&i) {
lines.push(satd.clone());
} else if let Some(code) = code_lines.get(i % code_lines.len().max(1)) {
lines.push(code.clone());
} else {
lines.push(String::new());
}
}
lines.join("\n")
}
}
prop_compose! {
fn arb_file_extension()
(choice in 0usize..10)
-> &'static str
{
match choice {
0 => "rs",
1 => "ts",
2 => "js",
3 => "py",
4 => "go",
5 => "java",
6 => "cpp",
7 => "c",
8 => "rb",
_ => "txt",
}
}
}
#[test]
#[ignore = "Property test hangs - parser stress test too slow"]
fn test_satd_parser_total_function() {
proptest!(|(source in arb_source_with_satd())| {
let detector = SATDDetector::new();
let result = std::panic::catch_unwind(|| {
detector.extract_from_content(&source, Path::new("test.rs"))
});
prop_assert!(result.is_ok(), "Parser panicked on input");
});
}
#[test]
fn test_satd_extraction_deterministic() {
proptest!(|(
source in arb_source_with_satd(),
ext in arb_file_extension()
)| {
let detector = SATDDetector::new();
let filename = format!("test.{}", ext);
let path = Path::new(&filename);
let result1 = detector.extract_from_content(&source, path);
let result2 = detector.extract_from_content(&source, path);
match (result1, result2) {
(Ok(debts1), Ok(debts2)) => {
prop_assert_eq!(debts1.len(), debts2.len(),
"Different number of SATD items on repeated parse");
for (d1, d2) in debts1.iter().zip(debts2.iter()) {
prop_assert_eq!(d1.category, d2.category);
prop_assert_eq!(d1.severity, d2.severity);
prop_assert_eq!(&d1.text, &d2.text);
prop_assert_eq!(d1.line, d2.line);
}
},
(Err(_), Err(_)) => {
},
_ => prop_assert!(false, "Inconsistent parsing results"),
}
});
}
#[test]
fn test_satd_line_numbers_valid() {
proptest!(|(source in arb_source_with_satd())| {
let detector = SATDDetector::new();
let result = detector.extract_from_content(&source, Path::new("test.rs"));
if let Ok(debts) = result {
let total_lines = source.lines().count() as u32;
for debt in debts {
prop_assert!(debt.line > 0, "Line number must be positive");
prop_assert!(debt.line <= total_lines,
"Line number {} exceeds total lines {}", debt.line, total_lines);
}
}
});
}
#[test]
fn test_satd_categories_consistent() {
proptest!(|(marker in arb_satd_marker())| {
let detector = SATDDetector::new();
let comment1 = format!("// {}: test issue", marker);
let comment2 = format!("# {}: different message", marker);
let result1 = detector.extract_from_content(&comment1, Path::new("test.rs"));
let result2 = detector.extract_from_content(&comment2, Path::new("test.py"));
if let (Ok(debts1), Ok(debts2)) = (result1, result2) {
if !debts1.is_empty() && !debts2.is_empty() {
prop_assert_eq!(debts1[0].category, debts2[0].category,
"Same marker should map to same category");
}
}
});
}
#[test]
fn test_satd_severity_ordering() {
use Severity::*;
assert!(Low < Medium);
assert!(Medium < High);
assert!(High < Critical);
assert_eq!(Low.escalate(), Medium);
assert_eq!(Medium.escalate(), High);
assert_eq!(High.escalate(), Critical);
assert_eq!(Critical.escalate(), Critical);
assert_eq!(Critical.reduce(), High);
assert_eq!(High.reduce(), Medium);
assert_eq!(Medium.reduce(), Low);
assert_eq!(Low.reduce(), Low); }
#[test]
fn test_satd_empty_input_handled() {
proptest!(|(whitespace in prop::collection::vec(
prop::sample::select(vec![" ", "\t", "\n", "\r"]), 0..100
))| {
let content = whitespace.join("");
let detector = SATDDetector::new();
let result = detector.extract_from_content(&content, Path::new("test.rs"));
prop_assert!(result.is_ok(), "Should handle empty input");
if let Ok(debts) = result {
prop_assert_eq!(debts.len(), 0, "Empty input should produce no SATD items");
}
});
}
#[test]
fn test_satd_malformed_comments_handled() {
proptest!(|(
prefix in "[^/\\*#-]{0,50}",
marker in arb_satd_marker(),
suffix in "[^\\n]{0,50}"
)| {
let malformed = format!("{}{}{}", prefix, marker, suffix);
let detector = SATDDetector::new();
let result = std::panic::catch_unwind(|| {
detector.extract_from_content(&malformed, Path::new("test.rs"))
});
prop_assert!(result.is_ok(), "Parser should not panic on malformed input");
});
}
#[test]
fn test_satd_content_extraction_preserves_structure() {
proptest!(|(
lines in prop::collection::vec("[a-zA-Z0-9 ]{0,100}", 1..20),
satd_indices in prop::collection::vec(0usize..20, 0..5)
)| {
let detector = SATDDetector::new();
let mut source_lines = lines.clone();
for &idx in &satd_indices {
if idx < source_lines.len() {
source_lines[idx] = format!("// TODO: issue at line {}", idx);
}
}
let source = source_lines.join("\n");
let result = detector.extract_from_content(&source, Path::new("test.rs"));
if let Ok(debts) = result {
for debt in debts {
let line_idx = (debt.line - 1) as usize;
prop_assert!(
line_idx < source_lines.len(),
"Line number {} out of bounds", debt.line
);
}
}
});
}
#[test]
#[ignore = "Property test hangs - file I/O too slow"]
fn test_satd_file_integration() {
proptest!(|(source in arb_source_with_satd())| {
let detector = SATDDetector::new();
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(source.as_bytes()).unwrap();
temp_file.flush().unwrap();
let path = temp_file.path();
let file_content = std::fs::read_to_string(path).unwrap();
prop_assert_eq!(&file_content, &source, "File content should match written content");
let result = detector.extract_from_content(&file_content, path);
prop_assert!(result.is_ok() || file_content.len() > 10000,
"Should successfully analyze file content");
});
}
#[test]
fn test_satd_test_block_exclusion() {
proptest!(|(
pre_test in prop::collection::vec("[a-zA-Z0-9_; ]{0,80}", 0..10),
test_content in prop::collection::vec(arb_satd_comment(), 1..5),
post_test in prop::collection::vec("[a-zA-Z0-9_; ]{0,80}", 0..10)
)| {
let mut lines = vec![];
lines.extend(pre_test);
lines.push("#[cfg(test)]".to_string());
lines.push("mod tests {".to_string());
let test_content_len = test_content.len();
lines.extend(test_content);
lines.push("}".to_string());
lines.extend(post_test);
let source = lines.join("\n");
let detector = SATDDetector::new();
let result = detector.extract_from_content(&source, Path::new("test.rs"));
if let Ok(debts) = result {
let test_start_line = lines.iter().position(|l| l.contains("#[cfg(test)]")).unwrap() + 1;
let test_end_line = test_start_line + 3 + test_content_len;
for debt in debts {
prop_assert!(
debt.line <= test_start_line as u32 || debt.line > test_end_line as u32,
"SATD found inside test block at line {}", debt.line
);
}
}
});
}
#[test]
fn test_satd_unicode_handling() {
proptest!(|(
prefix in "\\PC*",
marker in arb_satd_marker(),
suffix in "\\PC*"
)| {
let content = format!("// {} {}: {}", prefix, marker, suffix);
let detector = SATDDetector::new();
let result = std::panic::catch_unwind(|| {
detector.extract_from_content(&content, Path::new("test.rs"))
});
prop_assert!(result.is_ok(), "Should handle Unicode without panicking");
});
}
#[test]
#[ignore = "Property test hangs - large file generation too slow"]
fn test_satd_large_file_performance() {
proptest!(|(
line_template in "[a-zA-Z0-9 ]{0,100}",
num_repetitions in 100usize..1000
)| {
let detector = SATDDetector::new();
let mut lines = vec![];
for i in 0..num_repetitions {
if i % 10 == 0 {
lines.push(format!("// TODO: Fix issue {}", i));
} else {
lines.push(line_template.clone());
}
}
let content = lines.join("\n");
let result = detector.extract_from_content(&content, Path::new("test.rs"));
if let Ok(debts) = result {
let expected = num_repetitions / 10;
prop_assert!(
debts.len() >= expected - 1 && debts.len() <= expected + 1,
"Expected around {} SATD items, found {}", expected, debts.len()
);
}
});
}
#[test]
fn test_satd_nested_comments() {
proptest!(|(
depth in 1usize..5,
marker in arb_satd_marker()
)| {
let mut content = String::new();
for _ in 0..depth {
content.push_str("/* ");
}
content.push_str(&format!("{}: nested issue", marker));
for _ in 0..depth {
content.push_str(" */");
}
let detector = SATDDetector::new();
let result = detector.extract_from_content(&content, Path::new("test.c"));
prop_assert!(result.is_ok(), "Should handle nested comments");
});
}
#[test]
fn test_satd_multiline_handling() {
proptest!(|(
lines in prop::collection::vec("[a-zA-Z0-9 ]{0,50}", 2..10),
marker in arb_satd_marker()
)| {
let mut content = vec!["/*".to_string()];
content.push(format!(" * {}: multi-line issue", marker));
for line in lines {
content.push(format!(" * {}", line));
}
content.push(" */".to_string());
let source = content.join("\n");
let detector = SATDDetector::new();
let result = detector.extract_from_content(&source, Path::new("test.c"));
if let Ok(debts) = result {
if !debts.is_empty() {
prop_assert_eq!(debts[0].line, 2, "SATD should be on line 2");
}
}
});
}
}