sync from monorepo @ 2452e92e
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
//! 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);
|
||||
}
|
||||
Reference in New Issue
Block a user