Files
crewAI/lib/crewai/tests/tools/agent_tools/test_glob_tool.py
lorenzejay 1078dbd886 feat: introduce GlobTool for file pattern matching
- Added GlobTool to facilitate finding files that match specified glob patterns.
- Enhanced agent_tools module to include GlobTool and GrepTool.
- Implemented comprehensive functionality for recursive file searching, output formatting, and handling of hidden files.
- Created unit tests for GlobTool to ensure reliability and correctness in various scenarios.

This addition complements existing tools and enhances the file management capabilities within the CrewAI framework.
2026-02-04 11:39:44 -08:00

231 lines
9.2 KiB
Python

"""Unit tests for GlobTool."""
from pathlib import Path
import pytest
from crewai.tools.agent_tools.glob_tool import GlobTool
@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 main(): pass\n")
(src / "utils.py").write_text("def helper(): pass\n")
(src / "config.yaml").write_text("key: value\n")
# src/components/
components = src / "components"
components.mkdir()
(components / "button.tsx").write_text("export const Button = () => {};\n")
(components / "input.tsx").write_text("export const Input = () => {};\n")
(components / "index.ts").write_text("export * from './button';\n")
# tests/
tests = tmp_path / "tests"
tests.mkdir()
(tests / "test_main.py").write_text("def test_main(): pass\n")
(tests / "test_utils.py").write_text("def test_utils(): pass\n")
# docs/
docs = tmp_path / "docs"
docs.mkdir()
(docs / "readme.md").write_text("# Project\n")
(docs / "api.md").write_text("# API\n")
# data/binary.bin
data = tmp_path / "data"
data.mkdir()
(data / "binary.bin").write_bytes(b"\x00\x01\x02\x03binary content")
# empty.txt
(tmp_path / "empty.txt").write_text("")
# Hidden files (should be skipped by default)
(tmp_path / ".hidden").write_text("hidden content\n")
hidden_dir = tmp_path / ".hidden_dir"
hidden_dir.mkdir()
(hidden_dir / "secret.txt").write_text("secret\n")
# .git/config (should be skipped)
git_dir = tmp_path / ".git"
git_dir.mkdir()
(git_dir / "config").write_text("[core]\n repositoryformatversion = 0\n")
# node_modules (should be skipped)
node_modules = tmp_path / "node_modules"
node_modules.mkdir()
(node_modules / "package.json").write_text('{"name": "test"}\n')
return tmp_path
class TestGlobTool:
"""Tests for GlobTool."""
def setup_method(self) -> None:
"""Set up test fixtures."""
self.tool = GlobTool()
def test_tool_metadata(self) -> None:
"""Test tool has correct name and description."""
assert self.tool.name == "glob"
assert "find" in self.tool.description.lower() or "pattern" in self.tool.description.lower()
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 "output_mode" in fields
assert not fields["output_mode"].is_required()
assert "include_hidden" in fields
assert not fields["include_hidden"].is_required()
def test_basic_pattern_match(self, sample_dir: Path) -> None:
"""Test simple glob pattern finds files."""
result = self.tool._run(pattern="*.py", path=str(sample_dir))
assert "main.py" in result
assert "utils.py" in result
assert "test_main.py" in result
assert "test_utils.py" in result
def test_recursive_pattern(self, sample_dir: Path) -> None:
"""Test recursive glob pattern with **."""
result = self.tool._run(pattern="**/*.tsx", path=str(sample_dir))
assert "button.tsx" in result
assert "input.tsx" in result
def test_auto_recursive_prefix(self, sample_dir: Path) -> None:
"""Test that patterns without ** are auto-prefixed for recursive search."""
result = self.tool._run(pattern="*.yaml", path=str(sample_dir))
assert "config.yaml" in result
def test_specific_directory_pattern(self, sample_dir: Path) -> None:
"""Test pattern targeting specific directory."""
result = self.tool._run(pattern="src/**/*.py", path=str(sample_dir))
assert "main.py" in result
assert "utils.py" in result
# Should not include test files
assert "test_main.py" not in result
def test_output_mode_paths(self, sample_dir: Path) -> None:
"""Test paths output mode shows full file paths."""
result = self.tool._run(pattern="*.md", path=str(sample_dir), output_mode="paths")
assert "readme.md" in result
assert "api.md" in result
def test_output_mode_detailed(self, sample_dir: Path) -> None:
"""Test detailed output mode includes file sizes."""
result = self.tool._run(pattern="*.md", path=str(sample_dir), output_mode="detailed")
assert "readme.md" in result
# Should have size information
assert "B" in result # Bytes unit
def test_output_mode_tree(self, sample_dir: Path) -> None:
"""Test tree output mode shows directory structure."""
result = self.tool._run(pattern="*.py", path=str(sample_dir), output_mode="tree")
assert "src/" in result or "src" in result
assert "tests/" in result or "tests" in result
def test_hidden_files_excluded_by_default(self, sample_dir: Path) -> None:
"""Test hidden files are not included by default."""
result = self.tool._run(pattern="*", path=str(sample_dir))
assert ".hidden" not in result
assert "secret.txt" not in result
def test_hidden_files_included_when_requested(self, sample_dir: Path) -> None:
"""Test hidden files are included when include_hidden=True."""
result = self.tool._run(pattern="*", path=str(sample_dir), include_hidden=True)
assert ".hidden" in result
def test_git_directory_skipped(self, sample_dir: Path) -> None:
"""Test .git directory contents are not included."""
result = self.tool._run(pattern="*", path=str(sample_dir), include_hidden=True)
# Even with include_hidden, .git should be skipped
# The .git directory itself might show but not its contents
assert "config" not in result or ".git" not in result.split("config")[0].split("\n")[-1]
def test_node_modules_skipped(self, sample_dir: Path) -> None:
"""Test node_modules directory contents are not included."""
result = self.tool._run(pattern="*.json", path=str(sample_dir))
assert "package.json" not in result
def test_path_not_found(self) -> None:
"""Test error message when path doesn't exist."""
result = self.tool._run(pattern="*.py", path="/nonexistent/path")
assert "Error" in result
assert "does not exist" in result
def test_path_is_not_directory(self, sample_dir: Path) -> None:
"""Test error message when path is a file, not directory."""
file_path = str(sample_dir / "empty.txt")
result = self.tool._run(pattern="*.py", path=file_path)
assert "Error" in result
assert "not a directory" in result
def test_no_matches_found(self, sample_dir: Path) -> None:
"""Test message when no files match pattern."""
result = self.tool._run(pattern="*.nonexistent", path=str(sample_dir))
assert "No files found" in result
def test_files_only_default(self, sample_dir: Path) -> None:
"""Test that only files are matched by default (not directories)."""
result = self.tool._run(pattern="*", path=str(sample_dir))
# Should have files
assert ".txt" in result or ".py" in result
# Directories shouldn't have trailing slash in paths mode
lines = [l for l in result.split("\n") if "src/" in l and l.strip().endswith("/")]
# Should not list src/ as a match (it's a directory)
assert len(lines) == 0 or "tree" in result.lower()
def test_dirs_only(self, sample_dir: Path) -> None:
"""Test dirs_only flag matches only directories."""
result = self.tool._run(
pattern="*", path=str(sample_dir), dirs_only=True, files_only=False
)
assert "src" in result
assert "tests" in result
assert "docs" in result
# Should not include files
assert ".py" not in result
assert ".txt" not in result
def test_match_count_summary(self, sample_dir: Path) -> None:
"""Test that result includes count of matched files."""
result = self.tool._run(pattern="*.py", path=str(sample_dir))
assert "Found" in result
assert "file" in result.lower()
def test_run_with_kwargs(self, sample_dir: Path) -> None:
"""Test _run ignores extra kwargs."""
result = self.tool._run(
pattern="*.py", path=str(sample_dir), extra_arg="ignored"
)
assert "main.py" in result
def test_test_file_pattern(self, sample_dir: Path) -> None:
"""Test finding test files with test_*.py pattern."""
result = self.tool._run(pattern="test_*.py", path=str(sample_dir))
assert "test_main.py" in result
assert "test_utils.py" in result
# Should not include non-test files
assert "main.py" not in result or "test_main.py" in result
def test_typescript_files(self, sample_dir: Path) -> None:
"""Test finding TypeScript files with combined pattern."""
result = self.tool._run(pattern="*.ts", path=str(sample_dir))
assert "index.ts" in result
# .tsx files should not match *.ts
assert "button.tsx" not in result