Skip to content

Commit 550cce1

Browse files
committed
feat: add comprehensive test suite for cache, filters, validation, integration, retry, integrity, and rate limiting, along with related configuration updates
1 parent 5f77bc9 commit 550cce1

File tree

11 files changed

+1732
-4
lines changed

11 files changed

+1732
-4
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,6 @@ dmypy.json
132132

133133
.vscode/**
134134
requirements_temp.txt
135+
136+
# Integration test downloads
137+
tests/downloads/

pyproject.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,21 @@ dependencies = [
3030
]
3131

3232
[dependency-groups]
33-
dev = ["pytest>=9.0.2", "pytest-asyncio>=1.3.0", "ruff>=0.14.10", "ty>=0.0.7"]
33+
dev = [
34+
"pytest>=9.0.2",
35+
"pytest-asyncio>=1.3.0",
36+
"pytest-cov>=7.0.0",
37+
"ruff>=0.14.10",
38+
"ty>=0.0.7",
39+
]
3440

3541
[tool.pytest.ini_options]
3642
minversion = "6.0"
3743
addopts = "-ra -q --asyncio-mode=auto"
3844
testpaths = ["tests"]
45+
markers = [
46+
"integration: marks tests as integration tests (require internet, may be slow)",
47+
]
3948

4049
[project.urls]
4150
homepage = "https://github.com/leynier/gh-folder-download"

ruff.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ exclude = [
55
"__pycache__",
66
"build",
77
"dist",
8+
"tests/downloads",
89
]
910
line-length = 120
1011
target-version = "py312"

tests/test_cache.py

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
"""Tests for caching system."""
2+
3+
import time
4+
5+
import pytest
6+
7+
from gh_folder_download.cache import CacheEntry, DownloadCache
8+
9+
10+
@pytest.fixture
11+
def cache_dir(tmp_path):
12+
"""Temporary cache directory."""
13+
return tmp_path / ".gh-folder-download-cache"
14+
15+
16+
@pytest.fixture
17+
def download_cache(cache_dir):
18+
"""DownloadCache instance with temp directory."""
19+
return DownloadCache(cache_dir=cache_dir)
20+
21+
22+
@pytest.fixture
23+
def sample_cache_entry():
24+
"""Sample cache entry for testing."""
25+
return CacheEntry(
26+
file_path="src/main.py",
27+
sha="abc123def456",
28+
size=1024,
29+
last_modified="2024-01-01T00:00:00Z",
30+
download_time=time.time(),
31+
checksums={"sha256": "deadbeef" * 8},
32+
)
33+
34+
35+
class TestCacheEntry:
36+
"""Tests for CacheEntry class."""
37+
38+
def test_to_dict(self, sample_cache_entry):
39+
"""Test serialization to dictionary."""
40+
data = sample_cache_entry.to_dict()
41+
42+
assert data["file_path"] == "src/main.py"
43+
assert data["sha"] == "abc123def456"
44+
assert data["size"] == 1024
45+
assert "checksums" in data
46+
47+
def test_from_dict(self):
48+
"""Test deserialization from dictionary."""
49+
data = {
50+
"file_path": "src/main.py",
51+
"sha": "abc123def456",
52+
"size": 1024,
53+
"last_modified": "2024-01-01T00:00:00Z",
54+
"download_time": 1234567890.0,
55+
"checksums": {"sha256": "deadbeef" * 8},
56+
}
57+
58+
entry = CacheEntry.from_dict(data)
59+
60+
assert entry.file_path == "src/main.py"
61+
assert entry.sha == "abc123def456"
62+
assert entry.size == 1024
63+
64+
def test_is_current_match(self, sample_cache_entry):
65+
"""Test is_current returns True when SHA and size match."""
66+
result = sample_cache_entry.is_current(
67+
github_sha="abc123def456",
68+
github_size=1024,
69+
)
70+
71+
assert result is True
72+
73+
def test_is_current_sha_mismatch(self, sample_cache_entry):
74+
"""Test is_current returns False when SHA differs."""
75+
result = sample_cache_entry.is_current(
76+
github_sha="different_sha",
77+
github_size=1024,
78+
)
79+
80+
assert result is False
81+
82+
def test_is_current_size_mismatch(self, sample_cache_entry):
83+
"""Test is_current returns False when size differs."""
84+
result = sample_cache_entry.is_current(
85+
github_sha="abc123def456",
86+
github_size=2048,
87+
)
88+
89+
assert result is False
90+
91+
92+
class TestDownloadCache:
93+
"""Tests for DownloadCache class."""
94+
95+
def test_cache_dir_created(self, cache_dir):
96+
"""Test cache directory is created."""
97+
cache = DownloadCache(cache_dir=cache_dir)
98+
99+
assert cache_dir.exists()
100+
101+
def test_is_file_cached_new_file(self, download_cache, tmp_path):
102+
"""Test is_file_cached returns False for new file."""
103+
local_file = tmp_path / "test.txt"
104+
local_file.write_text("content")
105+
106+
result = download_cache.is_file_cached(
107+
repo_full_name="user/repo",
108+
file_path="src/main.py",
109+
ref="main",
110+
github_sha="abc123",
111+
github_size=1024,
112+
local_file_path=local_file,
113+
)
114+
115+
assert result is False
116+
117+
def test_add_and_retrieve_cached_file(self, download_cache, tmp_path):
118+
"""Test adding and retrieving a cached file."""
119+
local_file = tmp_path / "test.txt"
120+
local_file.write_text("content")
121+
122+
# Add to cache
123+
download_cache.add_file_to_cache(
124+
repo_full_name="user/repo",
125+
file_path="src/main.py",
126+
ref="main",
127+
github_sha="abc123",
128+
github_size=len("content"),
129+
local_file_path=local_file,
130+
)
131+
132+
# Check if cached
133+
result = download_cache.is_file_cached(
134+
repo_full_name="user/repo",
135+
file_path="src/main.py",
136+
ref="main",
137+
github_sha="abc123",
138+
github_size=len("content"),
139+
local_file_path=local_file,
140+
)
141+
142+
assert result is True
143+
144+
def test_cached_file_stale_after_sha_change(self, download_cache, tmp_path):
145+
"""Test cached file becomes stale when SHA changes."""
146+
local_file = tmp_path / "test.txt"
147+
local_file.write_text("content")
148+
149+
# Add to cache
150+
download_cache.add_file_to_cache(
151+
repo_full_name="user/repo",
152+
file_path="src/main.py",
153+
ref="main",
154+
github_sha="abc123",
155+
github_size=len("content"),
156+
local_file_path=local_file,
157+
)
158+
159+
# Check with different SHA
160+
result = download_cache.is_file_cached(
161+
repo_full_name="user/repo",
162+
file_path="src/main.py",
163+
ref="main",
164+
github_sha="different_sha",
165+
github_size=len("content"),
166+
local_file_path=local_file,
167+
)
168+
169+
assert result is False
170+
171+
def test_get_cached_checksums(self, download_cache, tmp_path):
172+
"""Test retrieving cached checksums."""
173+
local_file = tmp_path / "test.txt"
174+
local_file.write_text("content")
175+
176+
checksums = {"sha256": "abc123"}
177+
178+
download_cache.add_file_to_cache(
179+
repo_full_name="user/repo",
180+
file_path="src/main.py",
181+
ref="main",
182+
github_sha="abc123",
183+
github_size=len("content"),
184+
local_file_path=local_file,
185+
checksums=checksums,
186+
)
187+
188+
result = download_cache.get_cached_checksums(
189+
repo_full_name="user/repo",
190+
file_path="src/main.py",
191+
ref="main",
192+
)
193+
194+
assert result == checksums
195+
196+
def test_get_cached_checksums_not_found(self, download_cache):
197+
"""Test get_cached_checksums returns None for missing entry."""
198+
result = download_cache.get_cached_checksums(
199+
repo_full_name="user/repo",
200+
file_path="nonexistent.py",
201+
ref="main",
202+
)
203+
204+
assert result is None
205+
206+
207+
class TestCacheCleanup:
208+
"""Tests for cache cleanup functionality."""
209+
210+
def test_clean_cache_removes_old_entries(self, download_cache, tmp_path):
211+
"""Test clean_cache removes old entries."""
212+
local_file = tmp_path / "test.txt"
213+
local_file.write_text("content")
214+
215+
# Add to cache with old download time
216+
download_cache.add_file_to_cache(
217+
repo_full_name="user/repo",
218+
file_path="old_file.py",
219+
ref="main",
220+
github_sha="abc123",
221+
github_size=len("content"),
222+
local_file_path=local_file,
223+
)
224+
225+
# Manually set old download time
226+
cache_key = download_cache._get_cache_key("user/repo", "old_file.py", "main")
227+
if cache_key in download_cache.cache_data:
228+
download_cache.cache_data[cache_key].download_time = time.time() - (40 * 24 * 3600)
229+
230+
# Clean with 30 day max age
231+
removed = download_cache.clean_cache(max_age_days=30)
232+
233+
assert removed >= 1
234+
235+
def test_clear_cache(self, download_cache, tmp_path):
236+
"""Test clear_cache removes all entries."""
237+
local_file = tmp_path / "test.txt"
238+
local_file.write_text("content")
239+
240+
# Add some entries
241+
for i in range(3):
242+
download_cache.add_file_to_cache(
243+
repo_full_name="user/repo",
244+
file_path=f"file{i}.py",
245+
ref="main",
246+
github_sha=f"sha{i}",
247+
github_size=len("content"),
248+
local_file_path=local_file,
249+
)
250+
251+
download_cache.clear_cache()
252+
253+
stats = download_cache.get_cache_stats()
254+
assert stats["total_entries"] == 0
255+
256+
257+
class TestCacheStats:
258+
"""Tests for cache statistics."""
259+
260+
def test_get_cache_stats_empty(self, download_cache):
261+
"""Test stats for empty cache."""
262+
stats = download_cache.get_cache_stats()
263+
264+
assert stats["total_entries"] == 0
265+
assert stats["total_size_mb"] == 0
266+
267+
def test_get_cache_stats_with_entries(self, download_cache, tmp_path):
268+
"""Test stats with cached entries."""
269+
local_file = tmp_path / "test.txt"
270+
local_file.write_text("content")
271+
272+
download_cache.add_file_to_cache(
273+
repo_full_name="user/repo",
274+
file_path="file.py",
275+
ref="main",
276+
github_sha="abc123",
277+
github_size=100,
278+
local_file_path=local_file,
279+
)
280+
281+
stats = download_cache.get_cache_stats()
282+
283+
assert stats["total_entries"] == 1
284+
assert stats["total_size_mb"] >= 0 # Size in MB

0 commit comments

Comments
 (0)