#![expect(clippy::unwrap_used)]
#![expect(clippy::create_dir)]
#![expect(clippy::case_sensitive_file_extension_comparisons)]
#![expect(clippy::fn_params_excessive_bools)]
use coding_agent_tools::types::OutputMode;
use std::fs;
use tempfile::TempDir;
fn run_grep(
root: &str,
pattern: &str,
mode: OutputMode,
include_globs: Vec<String>,
ignore_globs: Vec<String>,
include_hidden: bool,
case_insensitive: bool,
multiline: bool,
line_numbers: bool,
context: Option<u32>,
context_before: Option<u32>,
context_after: Option<u32>,
include_binary: bool,
head_limit: usize,
offset: usize,
) -> Result<coding_agent_tools::types::GrepOutput, agentic_tools_core::ToolError> {
let cfg = coding_agent_tools::grep::GrepConfig {
root: root.to_string(),
pattern: pattern.to_string(),
mode,
include_globs,
ignore_globs,
include_hidden,
case_insensitive,
multiline,
line_numbers,
context,
context_before,
context_after,
include_binary,
head_limit,
offset,
};
coding_agent_tools::grep::run(cfg)
}
fn setup_test_dir() -> TempDir {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join("hello.txt"), "Hello World\nfoo bar\nbaz").unwrap();
fs::write(
tmp.path().join("code.rs"),
"fn main() {\n println!(\"Hello\");\n}\n",
)
.unwrap();
fs::write(
tmp.path().join("test.py"),
"def hello():\n print('world')\n",
)
.unwrap();
fs::create_dir(tmp.path().join("subdir")).unwrap();
fs::write(
tmp.path().join("subdir/nested.txt"),
"nested content\nwith hello inside",
)
.unwrap();
fs::write(tmp.path().join(".hidden"), "hidden hello content").unwrap();
tmp
}
#[test]
fn test_grep_files_mode_basic() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result = run_grep(
&root,
"hello",
OutputMode::Files,
vec![],
vec![],
false,
true, false,
true,
None,
None,
None,
false,
200,
0,
)
.unwrap();
assert!(!result.lines.is_empty(), "Should find matches");
assert!(
result.lines.iter().any(|p| p.contains("hello.txt")),
"Should find hello.txt"
);
assert!(
result.lines.iter().any(|p| p.contains("code.rs")),
"Should find code.rs (contains Hello)"
);
}
#[test]
fn test_grep_content_mode_with_line_numbers() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result = run_grep(
&root,
"hello",
OutputMode::Content,
vec![],
vec![],
false,
true,
false,
true, None,
None,
None,
false,
200,
0,
)
.unwrap();
assert!(!result.lines.is_empty());
for line in &result.lines {
assert!(
line.contains(':'),
"Line should be in path:line: format: {line}"
);
}
}
#[test]
fn test_grep_count_mode() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result = run_grep(
&root,
"hello",
OutputMode::Count,
vec![],
vec![],
false,
true,
false,
true,
None,
None,
None,
false,
200,
0,
)
.unwrap();
assert!(result.summary.is_some());
assert!(result.summary.unwrap().contains("Total matches:"));
assert!(result.lines.is_empty()); }
#[test]
fn test_grep_include_globs() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result = run_grep(
&root,
"hello",
OutputMode::Files,
vec!["*.txt".to_string()], vec![],
false,
true,
false,
true,
None,
None,
None,
false,
200,
0,
)
.unwrap();
for path in &result.lines {
assert!(
path.ends_with(".txt"),
"Should only match .txt files: {path}"
);
}
}
#[test]
fn test_grep_ignore_globs() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result = run_grep(
&root,
"hello",
OutputMode::Files,
vec![],
vec!["*.rs".to_string()], false,
true,
false,
true,
None,
None,
None,
false,
200,
0,
)
.unwrap();
for path in &result.lines {
assert!(!path.ends_with(".rs"), "Should not match .rs files: {path}");
}
}
#[test]
fn test_grep_include_hidden() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result_no_hidden = run_grep(
&root,
"hello",
OutputMode::Files,
vec![],
vec![],
false, true,
false,
true,
None,
None,
None,
false,
200,
0,
)
.unwrap();
let result_with_hidden = run_grep(
&root,
"hello",
OutputMode::Files,
vec![],
vec![],
true, true,
false,
true,
None,
None,
None,
false,
200,
0,
)
.unwrap();
let has_hidden_no = result_no_hidden.lines.iter().any(|p| p.contains(".hidden"));
let has_hidden_yes = result_with_hidden
.lines
.iter()
.any(|p| p.contains(".hidden"));
assert!(!has_hidden_no, "Hidden file should not appear without flag");
assert!(has_hidden_yes, "Hidden file should appear with flag");
}
#[test]
fn test_grep_case_sensitive() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result = run_grep(
&root,
"hello",
OutputMode::Files,
vec![],
vec![],
false,
false, false,
true,
None,
None,
None,
false,
200,
0,
)
.unwrap();
assert!(
result.lines.iter().any(|p| p.contains("nested.txt")),
"Should find nested.txt with lowercase hello"
);
}
#[test]
fn test_grep_multiline() {
let tmp = TempDir::new().unwrap();
fs::write(tmp.path().join("multiline.txt"), "start\nfoo\nbar\nend").unwrap();
let root = tmp.path().to_string_lossy().to_string();
let result = run_grep(
&root,
"foo.*bar",
OutputMode::Content,
vec![],
vec![],
false,
false,
true, true,
None,
None,
None,
false,
200,
0,
)
.unwrap();
assert!(!result.lines.is_empty(), "Should find multiline match");
}
#[test]
fn test_grep_context_lines() {
let tmp = TempDir::new().unwrap();
fs::write(
tmp.path().join("context.txt"),
"line1\nline2\nTARGET\nline4\nline5\n",
)
.unwrap();
let root = tmp.path().to_string_lossy().to_string();
let result = run_grep(
&root,
"TARGET",
OutputMode::Content,
vec![],
vec![],
false,
false,
false,
true,
Some(1), None,
None,
false,
200,
0,
)
.unwrap();
assert!(result.lines.len() >= 3, "Should include context lines");
let content: String = result.lines.join("\n");
assert!(content.contains("line2"), "Should have context before");
assert!(content.contains("TARGET"), "Should have match");
assert!(content.contains("line4"), "Should have context after");
}
#[test]
fn test_grep_pagination() {
let tmp = TempDir::new().unwrap();
for i in 0..10 {
fs::write(
tmp.path().join(format!("file{i}.txt")),
format!("content {i}\nmatch here"),
)
.unwrap();
}
let root = tmp.path().to_string_lossy().to_string();
let result1 = run_grep(
&root,
"match",
OutputMode::Files,
vec![],
vec![],
false,
false,
false,
true,
None,
None,
None,
false,
3, 0, )
.unwrap();
assert_eq!(result1.lines.len(), 3);
assert!(result1.has_more, "Should have more results");
let result2 = run_grep(
&root,
"match",
OutputMode::Files,
vec![],
vec![],
false,
false,
false,
true,
None,
None,
None,
false,
3,
3, )
.unwrap();
assert_eq!(result2.lines.len(), 3);
assert!(result2.has_more);
}
#[test]
fn test_grep_binary_file_skip() {
let tmp = TempDir::new().unwrap();
let binary_content = b"binary\x00content";
fs::write(tmp.path().join("binary.bin"), binary_content).unwrap();
fs::write(tmp.path().join("text.txt"), "binary text").unwrap();
let root = tmp.path().to_string_lossy().to_string();
let result = run_grep(
&root,
"binary",
OutputMode::Files,
vec![],
vec![],
false,
false,
false,
true,
None,
None,
None,
false, 200,
0,
)
.unwrap();
assert!(
result.lines.iter().any(|p| p.contains("text.txt")),
"Should find text file"
);
assert!(
!result.lines.iter().any(|p| p.contains("binary.bin")),
"Should not find binary file"
);
assert!(
result.warnings.iter().any(|w| w.contains("binary")),
"Should warn about skipped binary"
);
}
#[test]
fn test_grep_invalid_regex() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result = run_grep(
&root,
"[invalid", OutputMode::Files,
vec![],
vec![],
false,
false,
false,
true,
None,
None,
None,
false,
200,
0,
);
assert!(result.is_err(), "Should fail with invalid regex");
let err = result.unwrap_err();
assert!(
err.to_string().contains("Invalid regex"),
"Error should mention invalid regex"
);
}
#[test]
fn test_grep_no_matches() {
let tmp = setup_test_dir();
let root = tmp.path().to_string_lossy().to_string();
let result = run_grep(
&root,
"xyznonexistent123",
OutputMode::Files,
vec![],
vec![],
false,
false,
false,
true,
None,
None,
None,
false,
200,
0,
)
.unwrap();
assert!(result.lines.is_empty(), "Should have no matches");
assert!(!result.has_more);
}
#[test]
fn test_grep_single_file() {
let tmp = setup_test_dir();
let file_path = tmp.path().join("hello.txt").to_string_lossy().to_string();
let result = run_grep(
&file_path,
"Hello",
OutputMode::Content,
vec![],
vec![],
false,
false,
false,
true,
None,
None,
None,
false,
200,
0,
)
.unwrap();
assert!(!result.lines.is_empty(), "Should find match in single file");
}