422 lines
12 KiB
Rust
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);
|
|
}
|