#![cfg(unix)]
use std::io;
use proptest::prelude::*;
use rstest::rstest;
use tempfile::TempDir;
#[path = "support/script_stubs.rs"]
mod script_stubs;
use script_stubs::{stub_command, write_stub};
#[path = "support/static_regex_guard.rs"]
mod static_regex_guard;
use static_regex_guard::{open_dir, run_guard, scan_dir_with, utf8};
const PROHIBITED_DIAGNOSTIC: &str = "static regular expressions must use lazy_regex!";
const PROHIBITED_FORMS: &[&str] = &[
"lazylock_direct",
"lazylock_qualified",
"lazylock_move",
"once_cell_lazy_direct",
"once_cell_lazy_qualified",
"once_cell_lazy_move",
];
#[rstest]
fn rejects_prohibited_lazy_wrapper_form(#[values(0, 1, 2, 3, 4, 5)] index: usize) {
let label = PROHIBITED_FORMS[index];
let dir = scan_dir_with(label).expect("stage the labelled fixture");
let output = run_guard(utf8(dir.path()).expect("temporary path is UTF-8"), None)
.expect("execute the guard");
assert_eq!(
output.status.code(),
Some(1),
"form `{label}` should be rejected with status 1"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains(PROHIBITED_DIAGNOSTIC),
"form `{label}` should emit the prohibited diagnostic, got: {stdout}"
);
}
#[test]
fn accepts_clean_sources() {
let dir = scan_dir_with("clean").expect("stage clean fixture");
let output = run_guard(utf8(dir.path()).expect("temporary path is UTF-8"), None)
.expect("execute the guard");
assert_eq!(
output.status.code(),
Some(0),
"clean sources should pass; stdout: {}",
String::from_utf8_lossy(&output.stdout)
);
}
#[test]
fn propagates_ripgrep_scan_failure() {
let dir = TempDir::new().expect("failed to create temp dir");
let scan_dir = utf8(dir.path()).expect("temporary path is UTF-8");
let stub = write_stub(scan_dir, "rg-stub.sh", "#!/bin/sh\nexit 3\n")
.expect("write the failing ripgrep stub");
let output = run_guard(scan_dir, Some(&stub_command(&stub))).expect("execute the guard");
assert_eq!(
output.status.code(),
Some(3),
"a ripgrep scan failure should propagate its exit status"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("failed to scan Rust sources (rg exit 3)"),
"scan failure should emit the scan-failure diagnostic, got: {stderr}"
);
}
#[rstest]
#[case::plain("scan")]
#[case::single_quote("scan's")]
fn preserves_arguments_supplied_through_rg(#[case] dir_name: &str) {
const STUB_NAME: &str = "rg-stub.sh";
let root = TempDir::new().expect("failed to create temp dir");
let root_dir = utf8(root.path()).expect("temporary path is UTF-8");
open_dir(root_dir)
.expect("open the temporary root")
.create_dir(dir_name)
.expect("failed to create scan dir");
let dir = root_dir.join(dir_name);
let stub = write_stub(
&dir,
STUB_NAME,
"#!/bin/sh\nfor a in \"$@\"; do printf '%s\\n' \"$a\"; done > \"$0.argv\"\nexit 1\n",
)
.expect("write the argument-recording ripgrep stub");
let output = run_guard(&dir, Some(&format!("{} --pcre2", stub_command(&stub))))
.expect("execute the guard");
assert_eq!(
output.status.code(),
Some(0),
"an argument-bearing RG override must still run; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let argv: Vec<String> = open_dir(&dir)
.expect("open the scan directory")
.read_to_string(format!("{STUB_NAME}.argv"))
.expect("stub should have recorded its argv")
.lines()
.map(str::to_owned)
.collect();
assert_eq!(
argv.first().map(String::as_str),
Some("--pcre2"),
"the RG override's own arguments must be forwarded first, got: {argv:?}"
);
assert!(
argv.contains(&"-U".to_owned()) && argv.contains(&"--glob".to_owned()),
"the guard's own ripgrep arguments must follow, got: {argv:?}"
);
assert_eq!(
argv.last().map(String::as_str),
Some(dir.as_str()),
"the scan directory must remain the final argument, got: {argv:?}"
);
}
fn optional_ws() -> impl Strategy<Value = String> {
prop::sample::select(vec!["", " ", " ", "\t", "\n", "\n "]).prop_map(str::to_owned)
}
fn required_ws() -> impl Strategy<Value = String> {
prop::sample::select(vec![" ", " ", "\t", "\n", "\n "]).prop_map(str::to_owned)
}
fn qualifier() -> impl Strategy<Value = String> {
prop::collection::vec("[a-z][a-z0-9_]{0,6}", 0..3).prop_map(|segments| {
segments.iter().fold(String::new(), |mut acc, segment| {
acc.push_str(segment);
acc.push_str("::");
acc
})
})
}
prop_compose! {
fn prohibited_declaration()(
name in "[A-Z][A-Z0-9_]{0,7}",
wrapper in prop::sample::select(vec!["LazyLock", "Lazy"]),
wrapper_qualifier in qualifier(),
regex_qualifier in qualifier(),
pattern in "[a-z0-9]{1,8}",
after_eq in optional_ws(),
after_new in optional_ws(),
after_paren in optional_ws(),
after_closure in optional_ws(),
move_gap in prop::option::of(required_ws()),
braced in any::<bool>(),
) -> String {
let move_kw = move_gap.map_or_else(String::new, |gap| format!("move{gap}"));
let (open, close) = if braced { ("{ ", " }") } else { ("", "") };
format!(
"static {name}: {wrapper}<Regex> ={after_eq}{wrapper_qualifier}{wrapper}::new\
{after_new}({after_paren}{move_kw}||{after_closure}{open}\
{regex_qualifier}Regex::new(\"{pattern}\").unwrap(){close});\n"
)
}
}
prop_compose! {
fn clean_declaration()(
name in "[A-Z][A-Z0-9_]{0,7}",
pattern in "[a-z0-9]{1,8}",
form in 0_usize..5,
) -> String {
match form {
0 => format!("static {name}: LazyLock<Regex> = lazy_regex!(\"{pattern}\");\n"),
1 => format!("fn build_{name}() {{ let re = Regex::new(\"{pattern}\").unwrap(); }}\n"),
2 => format!("static {name}: LazyLock<String> = LazyLock::new(|| String::new());\n"),
3 => format!("static {name}: OnceLock<Regex> = OnceLock::new();\n"),
_ => format!("static {name}: LazyLock<Regex> = LazyLock::new(make_{name});\n"),
}
}
}
fn scan_generated(declarations: &[String]) -> io::Result<(TempDir, std::process::Output)> {
let dir = TempDir::new()?;
let handle = open_dir(utf8(dir.path())?)?;
for (index, declaration) in declarations.iter().enumerate() {
handle.write(format!("case{index}.rs"), declaration)?;
}
let output = run_guard(utf8(dir.path())?, None)?;
Ok((dir, output))
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
fn rejects_generated_prohibited_declarations(
declarations in prop::collection::vec(prohibited_declaration(), 1..5),
) {
let (_dir, output) = scan_generated(&declarations)
.map_err(|error| TestCaseError::fail(error.to_string()))?;
prop_assert_eq!(
output.status.code(),
Some(1),
"generated declarations should be rejected: {:?}",
declarations
);
let stdout = String::from_utf8_lossy(&output.stdout);
prop_assert!(stdout.contains(PROHIBITED_DIAGNOSTIC));
for (index, declaration) in declarations.iter().enumerate() {
prop_assert!(
stdout.contains(&format!("case{index}.rs")),
"declaration {} went undetected: {:?}\nguard output: {stdout}",
index,
declaration
);
}
}
#[test]
fn accepts_generated_clean_declarations(
declarations in prop::collection::vec(clean_declaration(), 1..5),
) {
let (_dir, output) = scan_generated(&declarations)
.map_err(|error| TestCaseError::fail(error.to_string()))?;
prop_assert_eq!(
output.status.code(),
Some(0),
"clean declarations should pass: {:?}\nguard output: {}",
declarations,
String::from_utf8_lossy(&output.stdout)
);
}
}
#[test]
fn static_regex_issue_586_stub_survives_an_open_write_descriptor() {
let dir = scan_dir_with("clean").expect("stage clean fixture");
let scan_dir = utf8(dir.path()).expect("temporary path is UTF-8");
let stub = write_stub(scan_dir, "rg-stub.sh", "#!/bin/sh\nexit 3\n").expect("write the stub");
let _held_open = open_dir(scan_dir)
.expect("open the scan directory")
.open_with("rg-stub.sh", cap_std::fs::OpenOptions::new().append(true))
.expect("hold the stub open for writing");
let output = run_guard(scan_dir, Some(&stub_command(&stub))).expect("execute the guard");
assert_eq!(
output.status.code(),
Some(3),
"the stub must run despite the open descriptor; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}