Skip to content

Commit c057c11

Browse files
authored
Fix Glancer to return correct FileWindow reflecting actual content read (#20)
The Glancer.glance() method was returning the requested FileWindow instead of the actual window that was read from the file. For example, requesting 100 lines from an empty file would return window with line_count=100 instead of line_count=0. Changes: - Add actual_window field to FileReadResult to track what was actually read - Update StreamingFileReader to return actual window information - Modify Glancer.glance() to use actual_window instead of requested window - Allow FileWindow.line_count=0 for reporting empty reads - Add comprehensive tests covering various edge cases
1 parent 421c2e8 commit c057c11

File tree

6 files changed

+152
-13
lines changed

6 files changed

+152
-13
lines changed

src/glob_grep_glance/_defaults.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def read_window(
7373

7474
contents = ""
7575
truncated = False
76+
lines_read = 0
7677

7778
try:
7879
# Open file with UTF-8 encoding and ignore errors for binary files
@@ -87,7 +88,6 @@ def read_window(
8788
current_line += 1
8889

8990
# Read the requested number of lines
90-
lines_read = 0
9191
while lines_read < window.line_count:
9292
line = f.readline()
9393
if not line: # EOF reached
@@ -103,7 +103,14 @@ def read_window(
103103
except BudgetExceeded:
104104
truncated = True
105105

106-
return FileReadResult(contents=contents, truncated=truncated)
106+
# Create actual window reflecting what was actually read
107+
actual_window = FileWindow(
108+
line_offset=window.line_offset, line_count=lines_read
109+
)
110+
111+
return FileReadResult(
112+
contents=contents, truncated=truncated, actual_window=actual_window
113+
)
107114

108115

109116
class StreamingRegexSearcher(BaseModel):

src/glob_grep_glance/common.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class FileWindow(BaseModel):
2424
"""Defines bounds for viewing a portion of a file."""
2525

2626
line_offset: int = Field(ge=0, description="Starting line number (0-based)")
27-
line_count: int = Field(ge=1, description="Number of lines to read")
27+
line_count: int = Field(ge=0, description="Number of lines to read")
2828

2929

3030
class FileContent(BaseModel):
@@ -40,3 +40,4 @@ class FileReadResult(BaseModel):
4040

4141
contents: str
4242
truncated: bool
43+
actual_window: FileWindow

src/glob_grep_glance/glance.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ def glance(
3636
# Read the file window using the file reader
3737
result = self.file_reader.read_window(file_path, window, budget)
3838

39-
# Create FileContent from the result
40-
view = FileContent(path=file_path, contents=result.contents, window=window)
39+
# Create FileContent from the result using the actual window that was read
40+
view = FileContent(
41+
path=file_path, contents=result.contents, window=result.actual_window
42+
)
4143

4244
return GlanceOutput(view=view, truncated=result.truncated)

tests/test_file_reader.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,14 +306,14 @@ def test_nonexistent_file(
306306
def test_zero_line_count_window(
307307
self, temp_sandbox: tuple[Path, Sandbox], reader: StreamingFileReader
308308
) -> None:
309-
"""Test window with zero line count (should be invalid)."""
309+
"""Test window with zero line count (now valid for actual windows)."""
310310
sandbox_dir, _ = temp_sandbox
311311
test_file = sandbox_dir / "test.txt"
312312
test_file.write_text("line 1\n", encoding="utf-8")
313313

314-
# FileWindow should reject line_count=0 during validation
315-
with pytest.raises(ValueError):
316-
FileWindow(line_offset=0, line_count=0)
314+
# FileWindow should now accept line_count=0 (for actual window reporting)
315+
window = FileWindow(line_offset=0, line_count=0)
316+
assert window.line_count == 0
317317

318318
def test_negative_line_offset_window(
319319
self, temp_sandbox: tuple[Path, Sandbox], reader: StreamingFileReader
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""Test cases for Glancer FileWindow behavior to ensure returned window reflects actual content."""
2+
3+
import tempfile
4+
from pathlib import Path
5+
from typing import Generator
6+
7+
import pytest
8+
9+
from glob_grep_glance import FileWindow, Glancer, OutputBudget, Sandbox
10+
11+
12+
class TestGlancerWindowBehavior:
13+
"""Test that Glancer returns accurate FileWindow information."""
14+
15+
@pytest.fixture
16+
def temp_sandbox(self) -> Generator[Path, None, None]:
17+
"""Create a temporary directory for sandboxed operations."""
18+
with tempfile.TemporaryDirectory() as temp_dir:
19+
sandbox_path = Path(temp_dir)
20+
21+
# Create test files
22+
(sandbox_path / "empty.txt").write_text("")
23+
(sandbox_path / "one_line.txt").write_text("single line\n")
24+
(sandbox_path / "three_lines.txt").write_text("line 1\nline 2\nline 3\n")
25+
26+
yield sandbox_path
27+
28+
@pytest.fixture
29+
def sandbox(self, temp_sandbox: Path) -> Sandbox:
30+
"""Create a sandbox configuration."""
31+
return Sandbox(sandbox_dir=temp_sandbox, blocked_files=[], allow_hidden=False)
32+
33+
@pytest.fixture
34+
def budget(self) -> OutputBudget:
35+
"""Create an output budget for testing."""
36+
return OutputBudget(limit=1000)
37+
38+
def test_empty_file_returns_correct_window(
39+
self, sandbox: Sandbox, budget: OutputBudget
40+
) -> None:
41+
"""Test that empty file returns window with line_count=0."""
42+
glancer = Glancer.from_sandbox(sandbox)
43+
empty_file = sandbox.sandbox_dir / "empty.txt"
44+
window = FileWindow(line_offset=0, line_count=100)
45+
46+
result = glancer.glance(empty_file, window, budget)
47+
48+
# Should return window reflecting what was actually read (0 lines)
49+
assert result.view.window.line_offset == 0
50+
assert result.view.window.line_count == 0
51+
assert result.view.contents == ""
52+
53+
def test_request_more_lines_than_available(
54+
self, sandbox: Sandbox, budget: OutputBudget
55+
) -> None:
56+
"""Test requesting more lines than file contains."""
57+
glancer = Glancer.from_sandbox(sandbox)
58+
one_line_file = sandbox.sandbox_dir / "one_line.txt"
59+
window = FileWindow(line_offset=0, line_count=5)
60+
61+
result = glancer.glance(one_line_file, window, budget)
62+
63+
# Should return window reflecting what was actually read (1 line)
64+
assert result.view.window.line_offset == 0
65+
assert result.view.window.line_count == 1
66+
assert "single line" in result.view.contents
67+
68+
def test_offset_beyond_file_end(
69+
self, sandbox: Sandbox, budget: OutputBudget
70+
) -> None:
71+
"""Test offset beyond end of file."""
72+
glancer = Glancer.from_sandbox(sandbox)
73+
three_line_file = sandbox.sandbox_dir / "three_lines.txt"
74+
window = FileWindow(line_offset=10, line_count=5)
75+
76+
result = glancer.glance(three_line_file, window, budget)
77+
78+
# Should return window with line_count=0 since no lines were read
79+
assert result.view.window.line_offset == 10
80+
assert result.view.window.line_count == 0
81+
assert result.view.contents == ""
82+
83+
def test_partial_read_from_offset(
84+
self, sandbox: Sandbox, budget: OutputBudget
85+
) -> None:
86+
"""Test reading from offset with more lines requested than available."""
87+
glancer = Glancer.from_sandbox(sandbox)
88+
three_line_file = sandbox.sandbox_dir / "three_lines.txt"
89+
window = FileWindow(line_offset=2, line_count=5) # Start at line 2, request 5
90+
91+
result = glancer.glance(three_line_file, window, budget)
92+
93+
# Should return window reflecting what was actually read (1 line from offset 2)
94+
assert result.view.window.line_offset == 2
95+
assert result.view.window.line_count == 1
96+
assert "line 3" in result.view.contents
97+
98+
def test_exact_line_count_match(
99+
self, sandbox: Sandbox, budget: OutputBudget
100+
) -> None:
101+
"""Test when requested lines exactly matches available lines."""
102+
glancer = Glancer.from_sandbox(sandbox)
103+
three_line_file = sandbox.sandbox_dir / "three_lines.txt"
104+
window = FileWindow(line_offset=0, line_count=3)
105+
106+
result = glancer.glance(three_line_file, window, budget)
107+
108+
# Should return window reflecting what was actually read (3 lines)
109+
assert result.view.window.line_offset == 0
110+
assert result.view.window.line_count == 3
111+
assert "line 1" in result.view.contents
112+
assert "line 2" in result.view.contents
113+
assert "line 3" in result.view.contents

tests/test_protocols.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ class TestFileReader:
6161
def test_protocol_compliance(self) -> None:
6262
"""Test that a mock implementation satisfies the FileReader protocol."""
6363
mock_reader = Mock(spec=FileReader)
64-
expected_result = FileReadResult(contents="test content", truncated=False)
64+
expected_result = FileReadResult(
65+
contents="test content",
66+
truncated=False,
67+
actual_window=FileWindow(line_offset=0, line_count=1),
68+
)
6569
mock_reader.read_window = Mock(return_value=expected_result)
6670

6771
file_path = Path("test.txt")
@@ -76,7 +80,11 @@ def test_protocol_compliance(self) -> None:
7680
def test_budget_constraint_handling(self) -> None:
7781
"""Test FileReader handles budget constraints properly."""
7882
mock_reader = Mock(spec=FileReader)
79-
expected_result = FileReadResult(contents="truncated", truncated=True)
83+
expected_result = FileReadResult(
84+
contents="truncated",
85+
truncated=True,
86+
actual_window=FileWindow(line_offset=0, line_count=1),
87+
)
8088
mock_reader.read_window = Mock(return_value=expected_result)
8189

8290
file_path = Path("large_file.txt")
@@ -91,7 +99,11 @@ def test_budget_constraint_handling(self) -> None:
9199
def test_window_parameters(self) -> None:
92100
"""Test FileReader respects window parameters."""
93101
mock_reader = Mock(spec=FileReader)
94-
expected_result = FileReadResult(contents="lines 5-15", truncated=False)
102+
expected_result = FileReadResult(
103+
contents="lines 5-15",
104+
truncated=False,
105+
actual_window=FileWindow(line_offset=5, line_count=10),
106+
)
95107
mock_reader.read_window = Mock(return_value=expected_result)
96108

97109
file_path = Path("data.txt")
@@ -179,7 +191,11 @@ def test_budget_consistency_across_protocols(self) -> None:
179191
# FileReader uses OutputBudget
180192
mock_reader = Mock(spec=FileReader)
181193
mock_reader.read_window = Mock(
182-
return_value=FileReadResult(contents="", truncated=False)
194+
return_value=FileReadResult(
195+
contents="",
196+
truncated=False,
197+
actual_window=FileWindow(line_offset=0, line_count=0),
198+
)
183199
)
184200
mock_reader.read_window(
185201
Path("test.txt"), FileWindow(line_offset=0, line_count=1), budget

0 commit comments

Comments
 (0)