Files
crewAI/lib/crewai-tools/tests/tools/test_file_read_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

196 lines
7.8 KiB
Python

"""Unit tests for FileReadTool."""
from pathlib import Path
import pytest
from crewai_tools.tools.file_read_tool.file_read_tool import FileReadTool
@pytest.fixture
def sample_file(tmp_path: Path) -> Path:
"""Create a sample text file with numbered lines."""
file_path = tmp_path / "sample.txt"
lines = [f"Line {i}: This is line number {i}." for i in range(1, 101)]
file_path.write_text("\n".join(lines) + "\n")
return file_path
@pytest.fixture
def binary_file(tmp_path: Path) -> Path:
"""Create a binary file with null bytes."""
file_path = tmp_path / "binary.bin"
file_path.write_bytes(b"\x00\x01\x02\x03binary content\x00\x04\x05")
return file_path
@pytest.fixture
def empty_file(tmp_path: Path) -> Path:
"""Create an empty file."""
file_path = tmp_path / "empty.txt"
file_path.write_text("")
return file_path
class TestFileReadTool:
"""Tests for FileReadTool."""
def setup_method(self) -> None:
"""Set up test fixtures."""
self.tool = FileReadTool()
def test_tool_metadata(self) -> None:
"""Test tool has correct name and description."""
assert self.tool.name == "read_file"
assert "read" in self.tool.description.lower()
def test_args_schema(self) -> None:
"""Test that args_schema has correct fields."""
schema = self.tool.args_schema
fields = schema.model_fields
assert "file_path" in fields
assert fields["file_path"].is_required()
assert "offset" in fields
assert not fields["offset"].is_required()
assert "limit" in fields
assert not fields["limit"].is_required()
assert "include_line_numbers" in fields
assert not fields["include_line_numbers"].is_required()
def test_read_entire_file(self, sample_file: Path) -> None:
"""Test reading entire file with line numbers."""
result = self.tool._run(file_path=str(sample_file))
assert "File:" in result
assert "Total lines: 100" in result
assert "Line 1:" in result
assert "|" in result # Line number separator
def test_read_with_offset(self, sample_file: Path) -> None:
"""Test reading from a specific line offset."""
result = self.tool._run(file_path=str(sample_file), offset=50, limit=10)
assert "Showing lines: 50-59" in result
assert "Line 50:" in result
assert "Line 59:" in result
# Should not include lines before offset
assert "Line 49:" not in result
def test_negative_offset_reads_from_end(self, sample_file: Path) -> None:
"""Test negative offset reads from end of file."""
result = self.tool._run(file_path=str(sample_file), offset=-10)
assert "Showing lines: 91-100" in result
assert "Line 91:" in result
assert "Line 100:" in result
def test_limit_controls_line_count(self, sample_file: Path) -> None:
"""Test limit parameter controls how many lines are read."""
result = self.tool._run(file_path=str(sample_file), offset=1, limit=5)
assert "Showing lines: 1-5" in result
# Count output lines (excluding header)
content_lines = [l for l in result.split("\n") if "|" in l and l.strip()]
assert len(content_lines) == 5
def test_line_numbers_included_by_default(self, sample_file: Path) -> None:
"""Test line numbers are included by default."""
result = self.tool._run(file_path=str(sample_file), limit=5)
# Lines should have format " 1|content"
assert "|" in result
for line in result.split("\n"):
if "Line 1:" in line:
assert "|" in line
def test_line_numbers_can_be_disabled(self, sample_file: Path) -> None:
"""Test line numbers can be disabled."""
result = self.tool._run(
file_path=str(sample_file), limit=5, include_line_numbers=False
)
# Content lines shouldn't have the line number prefix
content_section = result.split("\n\n", 1)[-1] # Skip header
for line in content_section.split("\n"):
if line.strip() and "Line" in line:
# Should not start with number|
assert not line.strip()[0].isdigit() or "|" not in line[:10]
def test_binary_file_detection(self, binary_file: Path) -> None:
"""Test binary files are detected and not read as text."""
result = self.tool._run(file_path=str(binary_file))
assert "Error" in result
assert "binary" in result.lower()
def test_empty_file(self, empty_file: Path) -> None:
"""Test reading empty file returns appropriate message."""
result = self.tool._run(file_path=str(empty_file))
assert "Total lines: 0" in result
assert "Empty file" in result
def test_file_not_found(self) -> None:
"""Test error message when file doesn't exist."""
result = self.tool._run(file_path="/nonexistent/file.txt")
assert "Error" in result
assert "not found" in result.lower()
def test_directory_path_error(self, tmp_path: Path) -> None:
"""Test error when path is a directory."""
result = self.tool._run(file_path=str(tmp_path))
assert "Error" in result
assert "directory" in result.lower()
def test_file_metadata_in_header(self, sample_file: Path) -> None:
"""Test file metadata is included in response header."""
result = self.tool._run(file_path=str(sample_file), limit=10)
# Should have file path
assert str(sample_file) in result
# Should have total lines
assert "Total lines:" in result
def test_large_file_auto_truncation(self, tmp_path: Path) -> None:
"""Test large files are automatically truncated."""
# Create a file with 1000 lines
large_file = tmp_path / "large.txt"
lines = [f"Line {i}" for i in range(1, 1001)]
large_file.write_text("\n".join(lines))
result = self.tool._run(file_path=str(large_file))
# Should be truncated and include message about it
assert "truncated" in result.lower() or "Showing lines" in result
# Should not read all 1000 lines without explicit limit
assert "Line 1000" not in result or "limit" in result.lower()
def test_legacy_start_line_parameter(self, sample_file: Path) -> None:
"""Test backward compatibility with start_line parameter."""
result = self.tool._run(file_path=str(sample_file), start_line=10, line_count=5)
assert "Showing lines: 10-14" in result
assert "Line 10:" in result
def test_constructor_with_file_path(self, sample_file: Path) -> None:
"""Test constructing tool with default file path."""
tool = FileReadTool(file_path=str(sample_file))
result = tool._run()
assert "Line 1:" in result
def test_constructor_file_path_override(self, sample_file: Path, tmp_path: Path) -> None:
"""Test runtime file_path overrides constructor file_path."""
other_file = tmp_path / "other.txt"
other_file.write_text("Different content\n")
tool = FileReadTool(file_path=str(sample_file))
result = tool._run(file_path=str(other_file))
assert "Different content" in result
assert "Line 1:" not in result
def test_no_file_path_error(self) -> None:
"""Test error when no file path is provided."""
result = self.tool._run()
assert "Error" in result
assert "No file path" in result
def test_offset_beyond_file_length(self, sample_file: Path) -> None:
"""Test offset beyond file length returns empty content."""
result = self.tool._run(file_path=str(sample_file), offset=200)
# File has 100 lines, offset 200 should show nothing
# But header should still show file info
assert "Total lines: 100" in result