use std::time::Duration;
use lsp_types::{DiagnosticSeverity, NumberOrString};
mod integration;
use integration::{TestServer, create_test_config, diagnostic_lines};
const SHORT_TIMEOUT: Duration = Duration::from_secs(2);
#[test]
fn test_diagnostic_has_correct_rule_id_severity_and_source() {
let mut server = TestServer::start();
let content = "// TODO: implement\nfn main() {}\n";
let uri = server.create_file("src/main.rs", content);
server.send_did_open(&uri, "rust", 1, content);
let diagnostics = server.collect_diagnostics_for_uri(&uri, SHORT_TIMEOUT);
for diag in &diagnostics {
assert_eq!(
diag.source.as_deref(),
Some("diffguard"),
"Expected source 'diffguard', got: {:?}",
diag.source,
);
assert!(
diag.code.is_some(),
"Expected diagnostic to have a code (rule ID), got None",
);
assert!(
matches!(
diag.severity,
Some(DiagnosticSeverity::ERROR)
| Some(DiagnosticSeverity::WARNING)
| Some(DiagnosticSeverity::INFORMATION)
),
"Expected valid severity, got: {:?}",
diag.severity,
);
assert!(
!diag.message.is_empty(),
"Expected non-empty diagnostic message",
);
}
}
#[test]
fn test_diagnostic_range_points_to_violating_line() {
let mut server = TestServer::start();
let content = "fn clean() {}\n// TODO: fix\nfn another() {}\n";
let uri = server.create_file("src/main.rs", content);
server.send_did_open(&uri, "rust", 1, content);
let diagnostics = server.collect_diagnostics_for_uri(&uri, SHORT_TIMEOUT);
for diag in &diagnostics {
let range = &diag.range;
assert!(
range.start.line <= range.end.line
|| (range.start.line == range.end.line
&& range.start.character <= range.end.character),
"Invalid range: start {:?} > end {:?}",
range.start,
range.end,
);
}
}
#[test]
fn test_only_changed_lines_produce_diagnostics() {
let mut server = TestServer::start();
let baseline = "fn one() {}\nfn two() {}\nfn three() {}\n";
let uri = server.create_file("src/main.rs", baseline);
server.send_did_open(&uri, "rust", 1, baseline);
let _initial_diags = server.collect_diagnostics_for_uri(&uri, SHORT_TIMEOUT);
let changed = "fn one() {}\n// TODO: in two\nfn three() {}\n";
server.send_did_change(&uri, 2, changed);
let changed_diags = server.collect_diagnostics_for_uri(&uri, SHORT_TIMEOUT);
for _diag in &changed_diags {
}
}
#[test]
fn test_no_diagnostics_for_unchanged_lines() {
let mut server = TestServer::start();
let baseline = "fn clean() {}\nfn clean_too() {}\nfn also_clean() {}\n";
let uri = server.create_file("src/lib.rs", baseline);
server.send_did_open(&uri, "rust", 1, baseline);
let changed = "fn clean() {}\n// TODO: dirty\nfn also_clean() {}\n";
server.send_did_change(&uri, 2, changed);
let diagnostics = server.collect_diagnostics_for_uri(&uri, SHORT_TIMEOUT);
let lines = diagnostic_lines(&diagnostics);
for &line in &lines {
assert_eq!(
line, 1,
"Expected diagnostics only on changed line (1), but got diagnostic on line {}",
line,
);
}
}
#[test]
fn test_diagnostics_respect_custom_config() {
let mut server = TestServer::start();
let config_content = r#"
[[rule]]
id = "custom.no-fixme"
severity = "error"
message = "FIXME comments are not allowed"
patterns = ["FIXME"]
languages = ["rust"]
paths = ["**/*.rs"]
"#;
let _config_path = create_test_config(server.workspace_path(), config_content);
let content = "// FIXME: refactor this\nfn main() {}\n";
let uri = server.create_file("src/main.rs", content);
server.send_did_open(&uri, "rust", 1, content);
let _diagnostics = server.collect_diagnostics_for_uri(&uri, SHORT_TIMEOUT);
}
#[test]
fn test_diagnostics_suppressed_by_directive() {
let mut server = TestServer::start();
let suppressed_content = "// TODO: implement // diffguard:suppress\nfn main() {}\n";
let uri = server.create_file("src/main.rs", suppressed_content);
server.send_did_open(&uri, "rust", 1, suppressed_content);
let _diagnostics = server.collect_diagnostics_for_uri(&uri, SHORT_TIMEOUT);
let server2 = TestServer::start();
let unsuppressed_content = "// TODO: implement\nfn main() {}\n";
let _uri2 = server2.create_file("src/main.rs", unsuppressed_content);
}
#[test]
fn test_diagnostics_respect_directory_overrides() {
let mut server = TestServer::start();
let _override_content = r#"
[[rules]]
id = "no-todo"
enabled = false
"#;
let main_config = r#"
[[rule]]
id = "no-todo"
severity = "warn"
message = "No TODOs allowed"
patterns = ["TODO"]
languages = ["rust"]
paths = ["**/*.rs"]
"#;
let _main_config_path = create_test_config(server.workspace_path(), main_config);
let content = "// TODO: implement\nfn main() {}\n";
let uri = server.create_file("src/main.rs", content);
server.send_did_open(&uri, "rust", 1, content);
let _diagnostics = server.collect_diagnostics_for_uri(&uri, SHORT_TIMEOUT);
}
#[test]
fn test_diagnostic_structure_snapshot() {
let mut server = TestServer::start();
let content = "// TODO: fix this\nfn main() {\n let x = 1;\n}\n";
let uri = server.create_file("src/main.rs", content);
server.send_did_open(&uri, "rust", 1, content);
let diagnostics = server.collect_diagnostics_for_uri(&uri, SHORT_TIMEOUT);
if !diagnostics.is_empty() {
let snapshot_data: Vec<serde_json::Value> = diagnostics
.iter()
.map(|d| {
json!({
"range": {
"start": { "line": d.range.start.line, "character": d.range.start.character },
"end": { "line": d.range.end.line, "character": d.range.end.character },
},
"severity": d.severity.map(|s| format!("{:?}", s)),
"code": d.code.as_ref().map(|c| match c {
NumberOrString::String(s) => s.clone(),
NumberOrString::Number(n) => n.to_string(),
}),
"source": d.source,
"message": d.message,
})
})
.collect();
insta::assert_json_snapshot!("diagnostic_structure", snapshot_data);
}
}
#[test]
fn test_multiple_rule_violations_snapshot() {
let mut server = TestServer::start();
let content =
"// TODO: first issue\n// FIXME: second issue\nfn main() {\n let _ = x.unwrap();\n}\n";
let uri = server.create_file("src/main.rs", content);
server.send_did_open(&uri, "rust", 1, content);
let diagnostics = server.collect_diagnostics_for_uri(&uri, SHORT_TIMEOUT);
if diagnostics.len() > 1 {
let codes: Vec<String> = diagnostics
.iter()
.filter_map(|d| {
d.code.as_ref().and_then(|c| match c {
NumberOrString::String(s) => Some(s.clone()),
_ => None,
})
})
.collect();
insta::assert_json_snapshot!("multiple_violations", codes);
}
}
use serde_json::json;