use anyhow::Result;
use lumin::search::{SearchOptions, search_files};
use serial_test::serial;
use std::path::Path;
mod test_helpers;
use test_helpers::{TEST_DIR, TestEnvironment, setup_multiple_file_types};
#[cfg(test)]
mod search_include_glob_tests {
use super::*;
#[test]
#[serial]
fn test_include_single_glob() -> Result<()> {
let _env = TestEnvironment::setup()?;
let additional_files = setup_multiple_file_types()?;
let pattern = "content";
let mut options = SearchOptions::default();
let all_results = search_files(pattern, Path::new(TEST_DIR), &options)?;
assert!(
all_results
.lines
.iter()
.any(|r| r.file_path.to_string_lossy().ends_with(".json")),
"Expected to find the pattern in JSON files"
);
assert!(
all_results
.lines
.iter()
.any(|r| r.file_path.to_string_lossy().ends_with(".txt")),
"Expected to find the pattern in TXT files"
);
options.include_glob = Some(vec!["*.json".to_string()]);
let results = search_files(pattern, Path::new(TEST_DIR), &options)?;
assert!(
!results.lines.is_empty(),
"Expected to find matches in JSON files"
);
assert!(
results
.lines
.iter()
.all(|r| r.file_path.to_string_lossy().ends_with(".json")),
"Found non-JSON files despite only including JSON files"
);
for file in &additional_files {
std::fs::remove_file(file)?;
}
Ok(())
}
#[test]
#[serial]
fn test_include_multiple_globs() -> Result<()> {
let _env = TestEnvironment::setup()?;
let additional_files = setup_multiple_file_types()?;
let pattern = "content";
let mut options = SearchOptions::default();
options.include_glob = Some(vec!["*.json".to_string(), "*.yaml".to_string()]);
let results = search_files(pattern, Path::new(TEST_DIR), &options)?;
assert!(
!results.lines.is_empty(),
"Expected to find matches in JSON or YAML files"
);
assert!(
results.lines.iter().all(|r| {
let path = r.file_path.to_string_lossy();
path.ends_with(".json") || path.ends_with(".yaml")
}),
"Found files other than JSON or YAML despite only including those types"
);
for file in &additional_files {
std::fs::remove_file(file)?;
}
Ok(())
}
#[test]
#[serial]
fn test_include_recursive_glob() -> Result<()> {
let _env = TestEnvironment::setup()?;
let additional_files = setup_multiple_file_types()?;
let pattern = "content";
let all_results = search_files(pattern, Path::new(TEST_DIR), &SearchOptions::default())?;
assert!(
all_results
.lines
.iter()
.any(|r| r.file_path.to_string_lossy().contains("/docs/")),
"Expected to find the pattern in the docs directory"
);
let mut options = SearchOptions::default();
options.include_glob = Some(vec!["**/docs/**".to_string()]);
let results = search_files(pattern, Path::new(TEST_DIR), &options)?;
assert!(
!results.lines.is_empty(),
"Expected to find matches in docs directory"
);
assert!(
results
.lines
.iter()
.all(|r| r.file_path.to_string_lossy().contains("/docs/")),
"Found files outside the docs directory despite only including it"
);
for file in &additional_files {
std::fs::remove_file(file)?;
}
Ok(())
}
#[test]
#[serial]
fn test_include_glob_case_sensitivity() -> Result<()> {
let _env = TestEnvironment::setup()?;
let additional_files = setup_multiple_file_types()?;
let mixed_case_file1 = Path::new(TEST_DIR).join("test.JsonML");
let mixed_case_file2 = Path::new(TEST_DIR).join("test.JSON"); std::fs::write(
&mixed_case_file1,
"This file has content with a mixed case extension",
)?;
std::fs::write(
&mixed_case_file2,
"This file has content with an all caps extension",
)?;
let pattern = "content";
let mut options = SearchOptions::default();
options.case_sensitive = true;
options.include_glob = Some(vec!["*.json".to_string()]);
let results = search_files(pattern, Path::new(TEST_DIR), &options)?;
assert!(
results
.lines
.iter()
.all(|r| r.file_path.to_string_lossy().ends_with(".json")),
"Found non-lowercase .json files despite case sensitivity"
);
let mut options = SearchOptions::default();
options.case_sensitive = false;
options.include_glob = Some(vec!["*.json".to_string()]);
let results = search_files(pattern, Path::new(TEST_DIR), &options)?;
assert!(
results.lines.iter().all(|r| {
let path = r.file_path.to_string_lossy();
path.ends_with(".json") || path.ends_with(".JSON") || path.ends_with(".JsonML")
}),
"Expected to find all JSON files case-insensitively"
);
std::fs::remove_file(&mixed_case_file1)?;
std::fs::remove_file(&mixed_case_file2)?;
for file in &additional_files {
std::fs::remove_file(file)?;
}
Ok(())
}
#[test]
#[serial]
fn test_empty_include_glob() -> Result<()> {
let _env = TestEnvironment::setup()?;
let additional_files = setup_multiple_file_types()?;
let pattern = "content";
let default_results =
search_files(pattern, Path::new(TEST_DIR), &SearchOptions::default())?;
assert!(
!default_results.lines.is_empty(),
"Expected to find matches with default options"
);
let mut options = SearchOptions::default();
options.include_glob = Some(vec![]);
let results = search_files(pattern, Path::new(TEST_DIR), &options)?;
assert!(
results.lines.is_empty(),
"Expected to find no matches with empty include_glob"
);
for file in &additional_files {
std::fs::remove_file(file)?;
}
Ok(())
}
#[test]
#[serial]
fn test_include_glob_none_vs_empty() -> Result<()> {
let _env = TestEnvironment::setup()?;
let additional_files = setup_multiple_file_types()?;
let pattern = "content";
let default_options = SearchOptions::default();
let default_results = search_files(pattern, Path::new(TEST_DIR), &default_options)?;
assert!(
!default_results.lines.is_empty(),
"Expected to find matches with include_glob = None"
);
let mut empty_options = SearchOptions::default();
empty_options.include_glob = Some(vec![]);
let empty_results = search_files(pattern, Path::new(TEST_DIR), &empty_options)?;
assert!(
empty_results.lines.is_empty(),
"Expected to find no matches with include_glob = Some(empty vec)"
);
for file in &additional_files {
std::fs::remove_file(file)?;
}
Ok(())
}
#[test]
#[serial]
fn test_include_glob_with_gitignore() -> Result<()> {
let _env = TestEnvironment::setup()?;
let additional_files = setup_multiple_file_types()?;
let pattern = "content";
let mut options = SearchOptions::default();
options.include_glob = Some(vec!["*.md".to_string(), "*.log".to_string()]);
let results = search_files(pattern, Path::new(TEST_DIR), &options)?;
assert!(
results
.lines
.iter()
.any(|r| r.file_path.to_string_lossy().ends_with(".md")),
"Expected to find markdown files"
);
assert!(
!results
.lines
.iter()
.any(|r| r.file_path.to_string_lossy().ends_with(".log")),
"Found log files despite them being in gitignore"
);
for file in &additional_files {
std::fs::remove_file(file)?;
}
Ok(())
}
#[test]
#[serial]
fn test_include_and_exclude_glob_combination() -> Result<()> {
let _env = TestEnvironment::setup()?;
let additional_files = setup_multiple_file_types()?;
let pattern = "content";
let mut options = SearchOptions::default();
options.include_glob = Some(vec!["**/*.txt".to_string()]);
options.exclude_glob = Some(vec!["docs/**".to_string()]);
let results = search_files(pattern, Path::new(TEST_DIR), &options)?;
assert!(
!results.lines.is_empty(),
"Expected to find matches in text files outside docs"
);
assert!(
results
.lines
.iter()
.all(|r| r.file_path.to_string_lossy().ends_with(".txt")),
"Found non-txt files despite only including txt files"
);
assert!(
!results
.lines
.iter()
.any(|r| r.file_path.to_string_lossy().contains("/docs/")),
"Found files in docs directory despite excluding it"
);
for file in &additional_files {
std::fs::remove_file(file)?;
}
Ok(())
}
#[test]
#[serial]
fn test_include_glob_syntax() -> Result<()> {
let _env = TestEnvironment::setup()?;
let additional_files = setup_multiple_file_types()?;
let test_files = [
("test1.rs", "fn main() { println!(\"content\"); }"),
("test2.rs", "fn test() { println!(\"content\"); }"),
("test.py", "print(\"content\")"),
("script.py", "print(\"python content\")"),
("nested/deep/file.txt", "deep nested content"),
];
let mut created_files = Vec::new();
for (filename, content) in &test_files {
let file_path = Path::new(TEST_DIR).join(filename);
if let Some(parent) = file_path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)?;
}
}
std::fs::write(&file_path, content)?;
created_files.push(file_path);
}
let pattern = "content";
let mut options = SearchOptions::default();
options.include_glob = Some(vec!["**/*.{rs,py}".to_string()]);
let results = search_files(pattern, Path::new(TEST_DIR), &options)?;
assert!(
!results.lines.is_empty(),
"Expected to find matches with brace expansion"
);
assert!(
results.lines.iter().all(|r| {
let path = r.file_path.to_string_lossy();
path.ends_with(".rs") || path.ends_with(".py")
}),
"Found files other than .rs or .py with brace expansion"
);
let mut options = SearchOptions::default();
options.include_glob = Some(vec!["**/test[0-9].rs".to_string()]);
let results = search_files(pattern, Path::new(TEST_DIR), &options)?;
assert!(
!results.lines.is_empty(),
"Expected to find matches with character class"
);
assert!(
results.lines.iter().all(|r| {
let path = r.file_path.to_string_lossy();
path.ends_with("test1.rs") || path.ends_with("test2.rs")
}),
"Found files other than test[digit].rs with character class"
);
let mut options = SearchOptions::default();
options.include_glob = Some(vec!["**/file.txt".to_string()]);
let results = search_files(pattern, Path::new(TEST_DIR), &options)?;
assert!(
!results.lines.is_empty(),
"Expected to find matches with double asterisk"
);
assert!(
results.lines.iter().any(|r| r
.file_path
.to_string_lossy()
.contains("nested/deep/file.txt")),
"Failed to find deeply nested file with double asterisk"
);
for file in &created_files {
std::fs::remove_file(file)?;
if let Some(parent) = file.parent() {
if parent != Path::new(TEST_DIR) && parent.exists() {
let _ = std::fs::remove_dir_all(parent);
}
}
}
for file in &additional_files {
std::fs::remove_file(file)?;
}
Ok(())
}
}