use super::{line_starts, scan_file, scan_one_set};
use std::panic::AssertUnwindSafe;
fn temp_dir(label: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("frx-scan-{}-{}", label, std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create temp dir");
return dir
}
fn load_runtime(rules: &str) -> crate::frx_load::LoadedRules {
let dir = temp_dir("rules");
let path = dir.join("rules.txt");
std::fs::write(&path, rules).expect("write rules");
let loaded = crate::frx_load::load(path.to_str().expect("utf8 path"), false, true, b"")
.expect("load runtime rules");
let _ = std::fs::remove_dir_all(&dir);
return loaded
}
#[test]
fn line_starts_plain_lines_index_each_start() {
assert_eq!(line_starts(b"abc\ndef"), vec![0, 4]);
}
#[test]
fn line_starts_unterminated_final_line_keeps_its_start() {
assert_eq!(line_starts(b"a\nbc"), vec![0, 2]);
}
#[test]
fn line_starts_trailing_newline_drops_empty_final_line() {
assert_eq!(line_starts(b"abc\n"), vec![0]);
assert_eq!(line_starts(b"a\nb\n"), vec![0, 2]);
}
#[test]
fn line_starts_crlf_offsets_point_past_the_newline() {
assert_eq!(line_starts(b"abc\r\ndef"), vec![0, 5]);
assert_eq!(line_starts(b"abc\r\n"), vec![0]);
}
#[test]
fn line_starts_interior_empty_lines_keep_their_offset() {
assert_eq!(line_starts(b"a\n\nb"), vec![0, 2, 3]);
}
#[test]
fn line_starts_empty_buffer_is_just_the_zero_start() {
assert_eq!(line_starts(b""), vec![0]);
}
#[test]
fn scan_one_set_normal_return_formats_and_offsets() {
let matcher = AssertUnwindSafe(|| vec![(0usize, 0usize), (2usize, 1usize)]);
let hits = scan_one_set("a.txt", 5, matcher);
assert_eq!(hits, vec!["a.txt:1 rule=5".to_string(), "a.txt:3 rule=6".to_string()]);
}
#[test]
fn scan_one_set_engine_panic_fails_closed() {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let matcher = AssertUnwindSafe(|| -> Vec<(usize, usize)> { panic!("synthetic engine fault") });
let hits = scan_one_set("a.txt", 0, matcher);
std::panic::set_hook(previous);
assert_eq!(hits, vec!["a.txt: engine error".to_string()]);
}
#[test]
fn scan_file_reports_matched_line_numbers() {
let loaded = load_runtime("NEEDLE_LITERAL_LONG_ENOUGH\n");
let hits = scan_file("t.txt", b"clean\nalso clean\nNEEDLE_LITERAL_LONG_ENOUGH\n", &loaded);
assert_eq!(hits, vec!["t.txt:3 rule=0".to_string()]);
}
#[test]
fn scan_file_crlf_line_still_matches() {
let loaded = load_runtime("NEEDLE_LITERAL_LONG_ENOUGH\n");
let hits = scan_file("t.txt", b"NEEDLE_LITERAL_LONG_ENOUGH\r\ntail\r\n", &loaded);
assert_eq!(hits, vec!["t.txt:1 rule=0".to_string()]);
}
#[test]
fn scan_file_interior_empty_line_preserves_numbering() {
let loaded = load_runtime("NEEDLE_LITERAL_LONG_ENOUGH\n");
let hits = scan_file("t.txt", b"first\n\nNEEDLE_LITERAL_LONG_ENOUGH\n", &loaded);
assert_eq!(hits, vec!["t.txt:3 rule=0".to_string()]);
}
#[test]
fn scan_file_empty_buffer_is_clean() {
let loaded = load_runtime("NEEDLE_LITERAL_LONG_ENOUGH\n");
assert!(scan_file("t.txt", b"", &loaded).is_empty());
}