use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use wiremock::MockServer;
use super::support::run_drep;
use crate::analysis::findings::{Finding, Severity};
use crate::cli::OutputFormat;
use crate::cli::check::{self, CheckArgs};
use crate::llm::cache::Cache;
use crate::test_support::mount_sse;
use crate::test_support::sse;
use crate::test_support::write_executable;
async fn setup_mock(body: &str) -> (tempfile::TempDir, MockServer) {
let dir = tempfile::tempdir().expect("tempdir");
let server = MockServer::start().await;
mount_sse(
&server,
wiremock::ResponseTemplate::new(200).set_body_raw(sse(&[body]), "text/event-stream"),
)
.await;
(dir, server)
}
fn args(paths: Vec<std::path::PathBuf>, fail_on: Option<Severity>) -> CheckArgs {
CheckArgs {
paths,
staged: false,
diff: None,
tip: None,
format: OutputFormat::Text,
fail_on,
cache_only: false,
push_gate: false,
}
}
async fn run_paths_with(args: &CheckArgs, dir: &Path) -> check::Exit {
let cache = Cache::new(dir.join("test-cache"), 30, 8 * 1024 * 1024);
check::run_with(args, dir, cache)
.await
.expect("check::run succeeds")
}
async fn run_paths(path: std::path::PathBuf, dir: &Path) -> check::Exit {
run_paths_with(&args(vec![path], None), dir).await
}
fn install_fake_ruff(dir: &Path) {
let bin = dir.join("venv/bin/ruff");
std::fs::create_dir_all(bin.parent().expect("ruff parent")).expect("bin dir");
write_executable(
&bin,
"#!/bin/sh\nprintf '%s' '[{\"code\":\"F401\",\"filename\":\"src/lib.py\",\"location\":{\"row\":1,\"column\":1},\"message\":\"unused import\"}]'\n",
);
std::fs::write(dir.join("pyproject.toml"), "").expect("pyproject");
std::fs::create_dir_all(dir.join("src")).expect("src dir");
std::fs::write(dir.join("src/lib.py"), "import os\n").expect("lib.py");
}
#[tokio::test]
async fn clean_run_returns_clean() {
let (dir, server) = setup_mock(r#"{"issues": []}"#).await;
crate::test_support::write_drep_toml(dir.path(), &format!("{}/v1", server.uri()));
std::fs::write(dir.path().join("lib.py"), "x = 1\n").expect("lib.py");
let exit = run_paths(dir.path().join("lib.py"), dir.path()).await;
assert_eq!(exit, check::Exit::Clean, "clean run must return Clean");
assert_eq!(exit.code(), 0, "Clean must map to exit 0");
}
#[tokio::test]
async fn a_completed_run_enforces_the_cache_size_ceiling() {
let (dir, server) = setup_mock(r#"{"issues": []}"#).await;
crate::test_support::write_drep_toml(dir.path(), &format!("{}/v1", server.uri()));
let source = dir.path().join("lib.py");
std::fs::write(&source, "x = 1\n").expect("lib.py");
let cache_root = dir.path().join("tiny-cache");
let cache = Cache::new(cache_root.clone(), 30, 1);
let exit = check::run_with(&args(vec![source], None), dir.path(), cache)
.await
.expect("check succeeds");
assert_eq!(exit, check::Exit::Clean);
let bytes = crate::test_support::two_level_tree_size(&cache_root);
assert!(bytes <= 1, "cache retained {bytes} bytes past its ceiling");
}
#[tokio::test]
async fn cache_only_miss_exits_3_without_contacting_the_provider() {
let (dir, server) = setup_mock(r#"{"issues": []}"#).await;
crate::test_support::write_drep_toml(dir.path(), &format!("{}/v1", server.uri()));
let source = dir.path().join("lib.py");
std::fs::write(&source, "x = 1\n").expect("lib.py");
let mut args = args(vec![source], None);
args.cache_only = true;
let exit = run_paths_with(&args, dir.path()).await;
assert_eq!(exit, check::Exit::CacheMiss);
assert_eq!(exit.code(), 3);
assert_eq!(crate::test_support::request_count(&server).await, 0);
}
#[tokio::test]
async fn push_gate_warms_once_then_the_cached_retry_is_clean() {
let (dir, server) = setup_mock(r#"{"issues": []}"#).await;
crate::test_support::write_drep_toml(dir.path(), &format!("{}/v1", server.uri()));
let source = dir.path().join("lib.py");
std::fs::write(&source, "x = 1\n").expect("lib.py");
let cache = Cache::new(dir.path().join("push-cache"), 30, 8 * 1024 * 1024);
let mut args = args(vec![source], None);
args.push_gate = true;
let first = check::run_with(&args, dir.path(), cache.clone())
.await
.expect("cold push gate");
let second = check::run_with(&args, dir.path(), cache)
.await
.expect("warm push gate");
assert_eq!(first, check::Exit::CacheMiss);
assert_eq!(second, check::Exit::Clean);
assert_eq!(crate::test_support::request_count(&server).await, 1);
}
#[tokio::test(flavor = "multi_thread")]
async fn a_successful_push_warm_renders_only_the_reconnect_instruction() {
let (dir, server) = setup_mock(r#"{"issues": []}"#).await;
crate::test_support::write_drep_toml(dir.path(), &format!("{}/v1", server.uri()));
std::fs::write(dir.path().join("lib.py"), "x = 1\n").expect("lib.py");
let output = run_drep(dir.path(), &["check", "--push-gate", "lib.py"]);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(output.status.code(), Some(3), "stdout: {stdout}");
assert!(stdout.contains("Run git push again"), "stdout: {stdout}");
assert!(
!stdout.contains("could not be analyzed"),
"stdout: {stdout}"
);
assert!(!stdout.contains("not cached"), "stdout: {stdout}");
}
#[tokio::test]
async fn tool_finding_blocks_with_no_fail_on() {
let (dir, server) = setup_mock(r#"{"issues": []}"#).await;
crate::test_support::write_drep_toml(dir.path(), &format!("{}/v1", server.uri()));
install_fake_ruff(dir.path());
let exit = run_paths(dir.path().join("src/lib.py"), dir.path()).await;
assert_eq!(
exit,
check::Exit::FoundIssues,
"a tool finding with no --fail-on must return FoundIssues"
);
assert_eq!(exit.code(), 1, "FoundIssues must map to exit 1");
}
#[tokio::test]
async fn tool_findings_outrank_cache_only_misses() {
let (dir, server) = setup_mock(r#"{"issues": []}"#).await;
crate::test_support::write_drep_toml(dir.path(), &format!("{}/v1", server.uri()));
install_fake_ruff(dir.path());
let mut check_args = args(vec![dir.path().join("src/lib.py")], None);
check_args.cache_only = true;
let exit = run_paths_with(&check_args, dir.path()).await;
assert_eq!(exit, check::Exit::FoundIssues);
assert_eq!(crate::test_support::request_count(&server).await, 0);
}
#[test]
fn llm_finding_does_not_block_without_fail_on_but_renders() {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("runtime");
let (dir, _server) = runtime.block_on(async {
let dir = tempfile::tempdir().expect("tempdir");
let server = MockServer::start().await;
let body = r#"{"issues":[{"line":1,"severity":"critical","category":"bug","message":"something smells"}]}"#;
mount_sse(
&server,
wiremock::ResponseTemplate::new(200)
.set_body_raw(sse(&[body]), "text/event-stream"),
)
.await;
crate::test_support::write_drep_toml(dir.path(), &format!("{}/v1", server.uri()));
std::fs::write(dir.path().join("lib.py"), "x = 1\n").expect("lib.py");
(dir, server)
});
let output = run_drep(dir.path(), &["check", "lib.py"]);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
output.status.code(),
Some(check::Exit::Clean.code() as i32),
"no --fail-on means LLM findings inform, not block; stderr was {:?}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
stdout.contains("something smells"),
"the LLM finding must appear in rendered output, got {stdout:?}"
);
}
#[tokio::test]
async fn llm_finding_at_error_blocks_under_fail_on_error() {
let (dir, server) = setup_mock(
r#"{"issues":[{"line":1,"severity":"critical","category":"bug","message":"bad"}]}"#,
)
.await;
crate::test_support::write_drep_toml(dir.path(), &format!("{}/v1", server.uri()));
std::fs::write(dir.path().join("lib.py"), "x = 1\n").expect("lib.py");
let args = args(vec![dir.path().join("lib.py")], Some(Severity::Error));
let exit = run_paths_with(&args, dir.path()).await;
assert_eq!(
exit,
check::Exit::FoundIssues,
"an error-level LLM finding under --fail-on error must block"
);
}
#[tokio::test]
async fn llm_finding_at_warning_does_not_block_under_fail_on_error() {
let (dir, server) = setup_mock(
r#"{"issues":[{"line":1,"severity":"medium","category":"style","message":"meh"}]}"#,
)
.await;
crate::test_support::write_drep_toml(dir.path(), &format!("{}/v1", server.uri()));
std::fs::write(dir.path().join("lib.py"), "x = 1\n").expect("lib.py");
let args = args(vec![dir.path().join("lib.py")], Some(Severity::Error));
let exit = run_paths_with(&args, dir.path()).await;
assert_eq!(
exit,
check::Exit::Clean,
"warning under --fail-on error must not block"
);
}
#[tokio::test]
async fn failure_outranks_finding() {
let (dir, server) = setup_mock(r#"{"issues": []}"#).await;
crate::test_support::write_drep_toml(dir.path(), &format!("{}/v1", server.uri()));
install_fake_ruff(dir.path());
std::fs::write(dir.path().join("src/bad.py"), [0xFFu8, 0xFE]).expect("bad.py");
let args = args(
vec![dir.path().join("src/lib.py"), dir.path().join("src/bad.py")],
None,
);
let exit = run_paths_with(&args, dir.path()).await;
assert_eq!(
exit,
check::Exit::Unanalyzed,
"a run with a blocking tool finding AND an unanalyzed file must \
exit 2, not 1"
);
assert_eq!(exit.code(), 2, "Unanalyzed must map to exit 2");
}
#[test]
fn unreachable_endpoint_exits_2_and_does_not_print_clean() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind loopback");
let port = listener.local_addr().expect("local address").port();
drop(listener);
let dir = tempfile::tempdir().expect("tempdir");
crate::test_support::write_drep_toml(dir.path(), &format!("http://127.0.0.1:{port}/v1"));
std::fs::write(dir.path().join("lib.py"), "x = 1\n").expect("lib.py");
let output = run_drep(dir.path(), &["check", "lib.py"]);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
output.status.code(),
Some(check::Exit::Unanalyzed.code() as i32),
"an unreachable endpoint must exit 2, stderr was {:?}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
!stdout.contains("No issues found."),
"an unanalyzed run must not print 'No issues found.', got {stdout:?}"
);
}
#[test]
fn successful_compilation_suppresses_only_explicit_compile_failure_claims() {
let finding = |path: &str, compile_failure: bool| Finding {
kind: "bug".to_owned(),
severity: Severity::Error,
file_path: path.to_owned(),
line: 1,
column: None,
message: "review".to_owned(),
suggestion: None,
asserts_compile_failure: compile_failure,
fingerprint: None,
};
let mut findings = vec![
finding("src/compiled.rs", true),
finding("src/compiled.rs", false),
finding("src/unchecked.rs", true),
];
let compiled = BTreeSet::from([PathBuf::from("src/compiled.rs")]);
check::suppress_disproved_compile_claims(&mut findings, &compiled);
assert_eq!(findings.len(), 2);
assert!(
findings
.iter()
.any(|finding| !finding.asserts_compile_failure)
);
assert!(
findings
.iter()
.any(|finding| finding.file_path == "src/unchecked.rs")
);
}