use std::collections::BTreeMap;
use std::path::PathBuf;
use wiremock::MockServer;
use crate::analysis::result::{AnalysisResult, FailureReason};
use crate::diff::hunks::{Hunk, HunkLine};
use super::support::{analyzer_for, hunks_for_python_at};
use crate::test_support::{request_count, server_returning};
#[tokio::test]
async fn unknown_extension_returns_empty_and_makes_no_request() {
let server = MockServer::start().await;
let (analyzer, _dir) = analyzer_for(&server);
let hunks = vec![Hunk {
file_path: PathBuf::from("notes.xyz"),
old_start: 1,
old_count: 0,
new_start: 1,
new_count: 1,
lines: vec![HunkLine::Added("n/a".to_owned())],
}];
let result = analyzer.analyze_file(&hunks).await;
assert!(
result.findings.is_empty(),
"no language means no analysis, got {:?}",
result.findings
);
assert!(
result.failed_files.is_empty(),
"no language means no failure, got {:?}",
result.failed_files
);
assert_eq!(
request_count(&server).await,
0,
"an unknown extension must not produce an HTTP request"
);
}
#[tokio::test]
async fn empty_hunks_return_empty_and_make_no_request() {
let server = MockServer::start().await;
let (analyzer, _dir) = analyzer_for(&server);
let result = analyzer.analyze_file(&[]).await;
assert!(result.findings.is_empty());
assert!(result.failed_files.is_empty());
assert_eq!(
request_count(&server).await,
0,
"empty hunks must not produce an HTTP request"
);
}
#[tokio::test]
async fn mixed_file_hunks_are_analyzed_and_attributed_separately() {
let finding = r#"{"issues":[{"line":1,"severity":"high","message":"found"}]}"#;
let server = server_returning(&[finding]).await;
let (analyzer, _dir) = analyzer_for(&server);
let mut hunks = hunks_for_python_at(1);
hunks.push(Hunk {
file_path: PathBuf::from("other.py"),
old_start: 1,
old_count: 0,
new_start: 1,
new_count: 1,
lines: vec![HunkLine::Added("different = True".to_owned())],
});
let result = analyzer.analyze_file(&hunks).await;
assert_eq!(request_count(&server).await, 2);
assert_eq!(result.findings.len(), 2, "findings: {:?}", result.findings);
let paths: std::collections::BTreeSet<_> = result
.findings
.iter()
.map(|finding| finding.file_path.as_str())
.collect();
assert_eq!(paths, ["other.py", "src/lib.py"].into_iter().collect());
}
#[tokio::test]
async fn clean_response_yields_no_findings_and_no_failures() {
let server = server_returning(&["{\"issues\": [], \"summary\": \"ok\"}"]).await;
let (analyzer, _dir) = analyzer_for(&server);
let result = analyzer.analyze_file(&hunks_for_python_at(100)).await;
assert_eq!(
request_count(&server).await,
1,
"a clean result must come from a response, not from never asking"
);
assert!(
result.findings.is_empty(),
"clean response is empty findings, got {:?}",
result.findings
);
assert!(
result.failed_files.is_empty(),
"clean response is not a failure, got {:?}",
result.failed_files
);
assert_eq!(
result.dropped_out_of_range, 0,
"clean response has no out-of-range drops"
);
}
#[test]
fn failed_files_is_a_btreemap_in_the_returned_result() {
let result = AnalysisResult::default();
let _: BTreeMap<PathBuf, FailureReason> = result.failed_files;
}
#[tokio::test]
async fn an_empty_suggestion_is_none_rather_than_an_empty_string() {
let server = server_returning(&[
r#"{"issues": [{"line": 100, "severity": "medium", "category": "style",
"message": "m", "suggestion": ""}], "summary": "s"}"#,
])
.await;
let (analyzer, _dir) = analyzer_for(&server);
let result = analyzer.analyze_file(&hunks_for_python_at(100)).await;
assert!(
result.failed_files.is_empty(),
"an empty suggestion is legal"
);
assert_eq!(result.findings.len(), 1);
assert_eq!(
result.findings[0].suggestion, None,
"an empty suggestion must not survive as Some(\"\")"
);
}
#[tokio::test]
async fn a_non_string_suggestion_makes_the_file_unanalyzed() {
let server = server_returning(&[
r#"{"issues": [{"line": 100, "severity": "medium", "category": "style",
"message": "m", "suggestion": 7}], "summary": "s"}"#,
])
.await;
let (analyzer, _dir) = analyzer_for(&server);
let result = analyzer.analyze_file(&hunks_for_python_at(100)).await;
let reason = result
.failed_files
.values()
.next()
.expect("a non-string suggestion is malformed");
assert!(
matches!(reason, FailureReason::MalformedFinding(detail) if detail.contains("suggestion")),
"got {reason:?}"
);
}
#[tokio::test]
async fn a_non_string_category_makes_the_file_unanalyzed() {
let server = server_returning(&[
r#"{"issues": [{"line": 100, "severity": "medium", "category": 7,
"message": "m"}], "summary": "s"}"#,
])
.await;
let (analyzer, _dir) = analyzer_for(&server);
let result = analyzer.analyze_file(&hunks_for_python_at(100)).await;
let reason = result
.failed_files
.values()
.next()
.expect("a non-string category is malformed");
assert!(
matches!(reason, FailureReason::MalformedFinding(detail) if detail.contains("category")),
"got {reason:?}"
);
}
#[tokio::test]
async fn an_absent_category_defaults_to_unknown() {
let server = server_returning(&[
r#"{"issues": [{"line": 100, "severity": "medium", "message": "m"}], "summary": "s"}"#,
])
.await;
let (analyzer, _dir) = analyzer_for(&server);
let result = analyzer.analyze_file(&hunks_for_python_at(100)).await;
assert!(
result.failed_files.is_empty(),
"an absent category is legal"
);
assert_eq!(result.findings[0].kind, "unknown");
}