Files
2026-05-08 01:59:04 +02:00

422 lines
12 KiB
Rust

//! Integration tests for search operations (ls, glob, grep).
//!
//! These tests verify:
//! - Directory listing with filtering
//! - Glob pattern matching
//! - Regex content search with context
//! - Result limits enforcement
//! - Binary file detection
//! - Windows path support
use dirigent_tools::config::SearchConfig;
use dirigent_tools::search::{
glob_search, grep_search, ls, FileKind, GlobRequest, GrepRequest, LsRequest,
};
use std::fs;
use std::io::Write;
use tempfile::TempDir;
/// Helper to create test directory structure
fn create_test_structure() -> TempDir {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
// Create directory structure
fs::create_dir(base.join("src")).unwrap();
fs::create_dir(base.join("tests")).unwrap();
fs::create_dir(base.join("target")).unwrap(); // Should be excluded by default
fs::create_dir(base.join(".git")).unwrap(); // Should be excluded by default
// Create some files
fs::write(base.join("README.md"), "# Test Project\n").unwrap();
fs::write(base.join("Cargo.toml"), "[package]\nname = \"test\"\n").unwrap();
fs::write(
base.join("src/main.rs"),
"fn main() {\n println!(\"Hello, world!\");\n}\n",
)
.unwrap();
fs::write(
base.join("src/lib.rs"),
"pub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n",
)
.unwrap();
fs::write(
base.join("tests/integration.rs"),
"#[test]\nfn test_add() {\n assert_eq!(add(2, 2), 4);\n}\n",
)
.unwrap();
// Create a file in excluded directory (should not appear in searches)
fs::write(base.join("target/debug.log"), "debug info\n").unwrap();
temp_dir
}
#[tokio::test]
async fn test_ls_basic() {
let temp_dir = create_test_structure();
let base = temp_dir.path();
let config = SearchConfig::default();
let request = LsRequest {
path: base.to_string_lossy().to_string(),
};
let response = ls(request, &config).await.unwrap();
// Should have files and directories, but not .git or target (excluded by default)
assert!(response.entries.len() >= 4); // README, Cargo.toml, src, tests
// Check we have both files and directories
let has_file = response.entries.iter().any(|e| e.kind == FileKind::File);
let has_dir = response.entries.iter().any(|e| e.kind == FileKind::Dir);
assert!(has_file);
assert!(has_dir);
// Check that excluded directories are not present
let has_target = response
.entries
.iter()
.any(|e| e.path.file_name().unwrap() == "target");
let has_git = response
.entries
.iter()
.any(|e| e.path.file_name().unwrap() == ".git");
assert!(!has_target, "target/ should be excluded by default");
assert!(!has_git, ".git/ should be excluded by default");
}
#[tokio::test]
async fn test_ls_file_sizes() {
let temp_dir = create_test_structure();
let base = temp_dir.path();
let config = SearchConfig::default();
let request = LsRequest {
path: base.to_string_lossy().to_string(),
};
let response = ls(request, &config).await.unwrap();
// Files should have sizes, directories should not
for entry in &response.entries {
match entry.kind {
FileKind::File => assert!(entry.size.is_some(), "Files should have sizes"),
FileKind::Dir => assert!(entry.size.is_none(), "Directories should not have sizes"),
FileKind::Symlink => {} // Symlinks may or may not have sizes
}
}
}
#[tokio::test]
async fn test_glob_basic() {
let temp_dir = create_test_structure();
let base = temp_dir.path();
let config = SearchConfig::default();
// Search for all Rust files
let request = GlobRequest {
path: base.to_string_lossy().to_string(),
pattern: "**/*.rs".to_string(),
exclude: None,
max_results: None,
};
let response = glob_search(request, &config).await.unwrap();
// Should find main.rs, lib.rs, and integration.rs (3 files)
assert_eq!(response.matches.len(), 3);
assert!(!response.truncated);
// Verify all matches end with .rs
for path in &response.matches {
assert!(path.extension().unwrap() == "rs");
}
}
#[tokio::test]
async fn test_glob_pattern_variations() {
let temp_dir = create_test_structure();
let base = temp_dir.path();
let config = SearchConfig::default();
// Test single-level wildcard
let request = GlobRequest {
path: base.to_string_lossy().to_string(),
pattern: "*.md".to_string(),
exclude: None,
max_results: None,
};
let response = glob_search(request, &config).await.unwrap();
assert_eq!(response.matches.len(), 1); // README.md
assert!(!response.truncated);
// Test specific directory
let request = GlobRequest {
path: base.to_string_lossy().to_string(),
pattern: "src/*.rs".to_string(),
exclude: None,
max_results: None,
};
let response = glob_search(request, &config).await.unwrap();
assert_eq!(response.matches.len(), 2); // main.rs, lib.rs
assert!(!response.truncated);
}
#[tokio::test]
async fn test_glob_max_results() {
let temp_dir = create_test_structure();
let base = temp_dir.path();
let config = SearchConfig::default();
// Limit to 2 results
let request = GlobRequest {
path: base.to_string_lossy().to_string(),
pattern: "**/*.rs".to_string(),
exclude: None,
max_results: Some(2),
};
let response = glob_search(request, &config).await.unwrap();
assert_eq!(response.matches.len(), 2);
assert!(response.truncated); // Should be truncated since there are 3 .rs files
}
#[tokio::test]
async fn test_glob_exclude_patterns() {
let temp_dir = create_test_structure();
let base = temp_dir.path();
let config = SearchConfig::default();
// Search for all .rs files but exclude tests
let request = GlobRequest {
path: base.to_string_lossy().to_string(),
pattern: "**/*.rs".to_string(),
exclude: Some(vec!["**/tests/**".to_string()]),
max_results: None,
};
let response = glob_search(request, &config).await.unwrap();
assert_eq!(response.matches.len(), 2); // Only main.rs and lib.rs, not integration.rs
assert!(!response.truncated);
}
#[tokio::test]
async fn test_grep_basic() {
let temp_dir = create_test_structure();
let base = temp_dir.path();
let config = SearchConfig::default();
// Search for "main" in all files
let request = GrepRequest {
path: base.to_string_lossy().to_string(),
pattern: "main".to_string(),
file_pattern: None,
ignore_case: false,
context_before: 0,
context_after: 0,
max_results: None,
};
let response = grep_search(request, &config).await.unwrap();
// Should find "main" in src/main.rs
assert!(response.matches.len() >= 1);
assert!(!response.truncated);
// Verify line numbers are 1-indexed
for m in &response.matches {
assert!(m.line_number > 0);
}
}
#[tokio::test]
async fn test_grep_case_insensitive() {
let temp_dir = create_test_structure();
let base = temp_dir.path();
let config = SearchConfig::default();
// Search for "HELLO" (case insensitive)
let request = GrepRequest {
path: base.to_string_lossy().to_string(),
pattern: "HELLO".to_string(),
file_pattern: Some("**/*.rs".to_string()),
ignore_case: true,
context_before: 0,
context_after: 0,
max_results: None,
};
let response = grep_search(request, &config).await.unwrap();
// Should find "Hello" in main.rs
assert!(response.matches.len() >= 1);
}
#[tokio::test]
async fn test_grep_context_lines() {
let temp_dir = create_test_structure();
let base = temp_dir.path();
// Create a file with more content
fs::write(
base.join("src/context_test.rs"),
"line 1\nline 2\nMATCH HERE\nline 4\nline 5\n",
)
.unwrap();
let config = SearchConfig::default();
// Search with context
let request = GrepRequest {
path: base.to_string_lossy().to_string(),
pattern: "MATCH".to_string(),
file_pattern: Some("**/context_test.rs".to_string()),
ignore_case: false,
context_before: 2,
context_after: 2,
max_results: None,
};
let response = grep_search(request, &config).await.unwrap();
assert_eq!(response.matches.len(), 1);
let m = &response.matches[0];
// Should have 2 lines before and 2 lines after
assert_eq!(m.context_before.len(), 2);
assert_eq!(m.context_after.len(), 2);
assert_eq!(m.context_before[0], "line 1");
assert_eq!(m.context_before[1], "line 2");
assert_eq!(m.context_after[0], "line 4");
assert_eq!(m.context_after[1], "line 5");
}
#[tokio::test]
async fn test_grep_max_results() {
let temp_dir = create_test_structure();
let base = temp_dir.path();
// Create a file with multiple matches
fs::write(
base.join("many_matches.txt"),
"match\nmatch\nmatch\nmatch\nmatch\n",
)
.unwrap();
let config = SearchConfig::default();
// Limit to 3 results
let request = GrepRequest {
path: base.to_string_lossy().to_string(),
pattern: "match".to_string(),
file_pattern: None,
ignore_case: false,
context_before: 0,
context_after: 0,
max_results: Some(3),
};
let response = grep_search(request, &config).await.unwrap();
assert_eq!(response.matches.len(), 3);
assert!(response.truncated);
}
#[tokio::test]
async fn test_grep_binary_file_skip() {
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
// Create a binary file with null bytes
let binary_path = base.join("binary.bin");
let mut file = fs::File::create(&binary_path).unwrap();
file.write_all(&[0x00, 0x01, 0x02, 0x03]).unwrap();
// Create a text file
fs::write(base.join("text.txt"), "some text\n").unwrap();
let config = SearchConfig::default();
// Search for any character
let request = GrepRequest {
path: base.to_string_lossy().to_string(),
pattern: ".".to_string(),
file_pattern: None,
ignore_case: false,
context_before: 0,
context_after: 0,
max_results: None,
};
let response = grep_search(request, &config).await.unwrap();
// Should only find matches in text.txt, not binary.bin
assert!(response.matches.len() > 0);
for m in &response.matches {
assert!(
m.path.to_string_lossy().contains("text.txt"),
"Should not match binary files"
);
}
}
#[tokio::test]
async fn test_grep_regex_patterns() {
let temp_dir = create_test_structure();
let base = temp_dir.path();
let config = SearchConfig::default();
// Test regex with character classes
let request = GrepRequest {
path: base.to_string_lossy().to_string(),
pattern: r"\d+".to_string(), // Match numbers
file_pattern: None,
ignore_case: false,
context_before: 0,
context_after: 0,
max_results: None,
};
let response = grep_search(request, &config).await.unwrap();
// Should find numbers in test files (e.g., "2, 2" in assert)
assert!(response.matches.len() > 0);
}
#[cfg(windows)]
#[tokio::test]
async fn test_windows_paths() {
let temp_dir = create_test_structure();
let base = temp_dir.path();
let config = SearchConfig::default();
// Test with Windows path separators
let path_str = base.to_string_lossy().to_string();
// Test ls
let request = LsRequest { path: path_str.clone() };
let response = ls(request, &config).await.unwrap();
assert!(response.entries.len() > 0);
// Test glob
let request = GlobRequest {
path: path_str.clone(),
pattern: "**\\*.rs".to_string(), // Windows-style pattern
exclude: None,
max_results: None,
};
let response = glob_search(request, &config).await.unwrap();
assert!(response.matches.len() > 0);
}