Files
crewAI/lib/crewai-tools/tests/tools/grep_tool_test.py
2026-02-04 11:50:43 -08:00

244 lines
8.6 KiB
Python

"""Unit tests for GrepTool."""
from pathlib import Path
import pytest
from crewai_tools import GrepTool
@pytest.fixture
def sample_dir(tmp_path: Path) -> Path:
"""Create a temp directory with sample files for testing."""
# src/main.py
src = tmp_path / "src"
src.mkdir()
(src / "main.py").write_text(
"def hello():\n"
" print('Hello, world!')\n"
"\n"
"def goodbye():\n"
" print('Goodbye, world!')\n"
"\n"
"class MyClass:\n"
" pass\n"
)
# src/utils.py
(src / "utils.py").write_text(
"import os\n"
"\n"
"def helper():\n"
" return os.getcwd()\n"
"\n"
"CONSTANT = 42\n"
)
# docs/readme.md
docs = tmp_path / "docs"
docs.mkdir()
(docs / "readme.md").write_text(
"# Project\n"
"\n"
"This is a sample project.\n"
"It has multiple files.\n"
)
# data/binary.bin
data = tmp_path / "data"
data.mkdir()
(data / "binary.bin").write_bytes(b"\x00\x01\x02\x03\x04binary content")
# empty.txt
(tmp_path / "empty.txt").write_text("")
# .git/config (should be skipped)
git_dir = tmp_path / ".git"
git_dir.mkdir()
(git_dir / "config").write_text("[core]\n repositoryformatversion = 0\n")
return tmp_path
class TestGrepTool:
"""Tests for GrepTool."""
def setup_method(self) -> None:
"""Set up test fixtures."""
self.tool = GrepTool()
def test_tool_metadata(self) -> None:
"""Test tool has correct name and description."""
assert self.tool.name == "Search file contents"
assert "search" in self.tool.description.lower() or "Search" in self.tool.description
def test_args_schema(self) -> None:
"""Test that args_schema has correct fields and defaults."""
schema = self.tool.args_schema
fields = schema.model_fields
assert "pattern" in fields
assert fields["pattern"].is_required()
assert "path" in fields
assert not fields["path"].is_required()
assert "glob_pattern" in fields
assert not fields["glob_pattern"].is_required()
assert "output_mode" in fields
assert not fields["output_mode"].is_required()
assert "case_insensitive" in fields
assert not fields["case_insensitive"].is_required()
assert "context_lines" in fields
assert not fields["context_lines"].is_required()
assert "include_line_numbers" in fields
assert not fields["include_line_numbers"].is_required()
def test_basic_pattern_match(self, sample_dir: Path) -> None:
"""Test simple string pattern found in output."""
result = self.tool._run(pattern="Hello", path=str(sample_dir))
assert "Hello" in result
def test_regex_pattern(self, sample_dir: Path) -> None:
"""Test regex pattern matches function definitions."""
result = self.tool._run(pattern=r"def\s+\w+", path=str(sample_dir))
assert "def hello" in result
assert "def goodbye" in result
assert "def helper" in result
def test_case_sensitive_default(self, sample_dir: Path) -> None:
"""Test that search is case-sensitive by default."""
result = self.tool._run(pattern="hello", path=str(sample_dir))
# "hello" (lowercase) appears in "def hello():" but not in "Hello, world!"
assert "hello" in result
# Verify it found the function definition line
assert "def hello" in result
def test_case_insensitive(self, sample_dir: Path) -> None:
"""Test case-insensitive matching."""
result = self.tool._run(
pattern="hello", path=str(sample_dir), case_insensitive=True
)
# Should match both "def hello():" and "Hello, world!"
assert "hello" in result.lower()
assert "Hello" in result
def test_output_mode_content(self, sample_dir: Path) -> None:
"""Test content output mode shows file paths, line numbers, and text."""
result = self.tool._run(
pattern="CONSTANT", path=str(sample_dir), output_mode="content"
)
assert "utils.py" in result
assert "CONSTANT" in result
# Should have line numbers by default
assert ": " in result
def test_output_mode_files_with_matches(self, sample_dir: Path) -> None:
"""Test files_with_matches output mode shows only file paths."""
result = self.tool._run(
pattern="def", path=str(sample_dir), output_mode="files_with_matches"
)
assert "main.py" in result
assert "utils.py" in result
# Should not contain line content
assert "print" not in result
def test_output_mode_count(self, sample_dir: Path) -> None:
"""Test count output mode shows filepath: N format."""
result = self.tool._run(
pattern="def", path=str(sample_dir), output_mode="count"
)
# main.py has 2 def lines, utils.py has 1
assert "main.py: 2" in result
assert "utils.py: 1" in result
def test_context_lines(self, sample_dir: Path) -> None:
"""Test surrounding context lines are included."""
result = self.tool._run(
pattern="CONSTANT", path=str(sample_dir), context_lines=2
)
# Two lines before CONSTANT = 42 is " return os.getcwd()"
assert "return os.getcwd()" in result
assert "CONSTANT" in result
def test_line_numbers_disabled(self, sample_dir: Path) -> None:
"""Test output without line number prefixes."""
result = self.tool._run(
pattern="CONSTANT",
path=str(sample_dir),
include_line_numbers=False,
)
assert "CONSTANT = 42" in result
# Verify no line number prefix (e.g., "6: ")
for line in result.strip().split("\n"):
if "CONSTANT" in line:
assert not line[0].isdigit() or ": " not in line
def test_glob_pattern_filtering(self, sample_dir: Path) -> None:
"""Test glob pattern filters to specific file types."""
result = self.tool._run(
pattern="project",
path=str(sample_dir),
glob_pattern="*.py",
case_insensitive=True,
)
# "project" appears in readme.md but not in .py files
assert "No matches found" in result
def test_search_single_file(self, sample_dir: Path) -> None:
"""Test searching a single file by path."""
file_path = str(sample_dir / "src" / "main.py")
result = self.tool._run(pattern="def", path=file_path)
assert "def hello" in result
assert "def goodbye" in result
# Should not include results from other files
assert "helper" not in result
def test_path_not_found(self) -> None:
"""Test error message when path doesn't exist."""
result = self.tool._run(pattern="test", path="/nonexistent/path")
assert "Error" in result
assert "does not exist" in result
def test_invalid_regex(self, sample_dir: Path) -> None:
"""Test error message for invalid regex patterns."""
result = self.tool._run(pattern="[invalid", path=str(sample_dir))
assert "Error" in result
assert "Invalid regex" in result
def test_binary_files_skipped(self, sample_dir: Path) -> None:
"""Test binary files are not included in results."""
result = self.tool._run(pattern="binary", path=str(sample_dir))
# binary.bin has null bytes so it should be skipped
assert "binary.bin" not in result
def test_no_matches_found(self, sample_dir: Path) -> None:
"""Test message when no matches are found."""
result = self.tool._run(
pattern="zzz_nonexistent_pattern_zzz", path=str(sample_dir)
)
assert "No matches found" in result
def test_hidden_dirs_skipped(self, sample_dir: Path) -> None:
"""Test that .git/ directory contents are not searched."""
result = self.tool._run(pattern="repositoryformatversion", path=str(sample_dir))
assert "No matches found" in result
def test_empty_file(self, sample_dir: Path) -> None:
"""Test searching an empty file doesn't crash."""
result = self.tool._run(
pattern="anything", path=str(sample_dir / "empty.txt")
)
assert "No matches found" in result
def test_run_with_kwargs(self, sample_dir: Path) -> None:
"""Test _run ignores extra kwargs."""
result = self.tool._run(
pattern="Hello", path=str(sample_dir), extra_arg="ignored"
)
assert "Hello" in result