Skip to content

Commit 343434a

Browse files
committed
Add tests for CodeTide file detection and organization with git support
1 parent 78afdb1 commit 343434a

File tree

2 files changed

+179
-1
lines changed

2 files changed

+179
-1
lines changed

codetide/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
import time
2020
import json
2121
import os
22+
2223
class CodeTide(BaseModel):
2324
"""Root model representing a complete codebase"""
24-
rootpath : Union[str, Path]
25+
rootpath :Union[str, Path]
2526
codebase :CodeBase = Field(default_factory=CodeBase)
2627
files :Dict[Path, datetime]= Field(default_factory=dict)
2728
_instantiated_parsers :Dict[str, BaseParser] = {}

tests/test_codetide.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
from codetide.core.models import CodeBase, CodeFileModel
2+
from codetide import CodeTide
3+
4+
from unittest.mock import patch, MagicMock, AsyncMock
5+
from datetime import datetime, timezone
6+
import pytest
7+
import time
8+
import os
9+
10+
# Fixture to create a temporary directory structure for testing
11+
@pytest.fixture
12+
def temp_code_root(tmp_path):
13+
"""Creates a temporary directory with a mock project structure."""
14+
root = tmp_path / "project"
15+
root.mkdir()
16+
(root / "src").mkdir()
17+
(root / "src" / "main.py").write_text("print('hello')")
18+
(root / "src" / "utils.js").write_text("console.log('utils');")
19+
(root / "README.md").write_text("# Project")
20+
(root / ".gitignore").write_text("*.log\n__pycache__/\n.env")
21+
(root / "ignored_file.log").write_text("this is a log")
22+
(root / ".env").write_text("SECRET=123")
23+
(root / "__pycache__").mkdir()
24+
(root / "__pycache__" / "cache_file.pyc").write_text("cached")
25+
return root
26+
27+
@pytest.mark.asyncio
28+
async def test_find_code_files_with_git(temp_code_root):
29+
"""
30+
Tests that _find_code_files correctly identifies files using git,
31+
respecting .gitignore.
32+
"""
33+
# Initialize a git repository in the temp directory
34+
os.chdir(temp_code_root)
35+
os.system("git init")
36+
os.system("git add .")
37+
os.system("git commit -m 'initial commit'")
38+
39+
# Create an untracked file that should be ignored
40+
(temp_code_root / "untracked.log").write_text("untracked log")
41+
42+
# Create an untracked file that should be included
43+
(temp_code_root / "new_feature.py").write_text("def new_func(): pass")
44+
45+
tide = CodeTide(rootpath=temp_code_root)
46+
47+
# We use a patch to avoid depending on a real git installation in the test runner
48+
with patch('pygit2.Repository') as mock_repo:
49+
# Mock the repository and its status to simulate git behavior
50+
mock_repo.return_value.workdir = str(temp_code_root)
51+
mock_repo.return_value.index = [MagicMock(path="src/main.py"), MagicMock(path="src/utils.js"), MagicMock(path="README.md")]
52+
mock_repo.return_value.status.return_value = {"new_feature.py": 1, "untracked.log": 128} # 1 = WT_NEW, 128 = IGNORED
53+
mock_repo.return_value.path_is_ignored = lambda x: x == 'untracked.log'
54+
55+
found_files = tide._find_code_files()
56+
57+
# Assert that the correct files were found and ignored files were excluded
58+
assert temp_code_root / "src" / "main.py" in found_files
59+
assert temp_code_root / "src" / "utils.js" in found_files
60+
assert temp_code_root / "README.md" in found_files
61+
assert temp_code_root / "new_feature.py" not in found_files
62+
assert temp_code_root / ".gitignore" not in found_files # .gitignore is not a code file by default
63+
assert temp_code_root / "untracked.log" not in found_files
64+
65+
def test_find_code_files_no_git(temp_code_root):
66+
"""
67+
Tests _find_code_files fallback to a simple directory walk when not a git repo.
68+
"""
69+
tide = CodeTide(rootpath=temp_code_root)
70+
found_files = tide._find_code_files(languages=['python', 'javascript'])
71+
72+
# Assert that only files with the specified extensions are found
73+
assert temp_code_root / "src/main.py" in found_files
74+
assert temp_code_root / "src/utils.js" in found_files
75+
assert temp_code_root / "README.md" not in found_files
76+
assert len(found_files) == 2
77+
78+
def test_organize_files_by_language(temp_code_root):
79+
"""
80+
Tests that _organize_files_by_language correctly groups files by their language.
81+
"""
82+
files = [
83+
temp_code_root / "src/main.py",
84+
temp_code_root / "src/utils.js",
85+
temp_code_root / "README.md"
86+
]
87+
organized = CodeTide._organize_files_by_language(files)
88+
89+
# Assert that files are grouped under the correct language key
90+
assert "python" in organized
91+
assert "javascript" in organized
92+
assert "markdown" in organized
93+
assert organized["python"] == [temp_code_root / "src/main.py"]
94+
assert organized["javascript"] == [temp_code_root / "src/utils.js"]
95+
assert organized["markdown"] == [temp_code_root / "README.md"]
96+
97+
def test_serialize_deserialize(temp_code_root):
98+
"""
99+
Tests the serialization and deserialization of a CodeTide instance.
100+
"""
101+
tide = CodeTide(rootpath=temp_code_root, codebase=CodeBase(root=[CodeFileModel(file_path="test.py")]))
102+
tide.files = {temp_code_root / "test.py": datetime.now(timezone.utc)}
103+
104+
serialization_path = temp_code_root / "storage" / "tide.json"
105+
tide.serialize(filepath=serialization_path, store_in_project_root=False)
106+
107+
# Assert that the serialization file was created
108+
assert serialization_path.exists()
109+
110+
deserialized_tide = CodeTide.deserialize(filepath=serialization_path)
111+
112+
# Assert that the deserialized instance is of the correct type and has the correct data
113+
assert isinstance(deserialized_tide, CodeTide)
114+
assert deserialized_tide.rootpath == temp_code_root
115+
assert len(deserialized_tide.codebase.root) == 1
116+
assert deserialized_tide.codebase.root[0].file_path == "test.py"
117+
# Note: Pydantic converts Path objects to strings on serialization, so we compare strings
118+
assert str(temp_code_root / "test.py") in [str(p) for p in deserialized_tide.files.keys()]
119+
120+
121+
@pytest.mark.asyncio
122+
async def test_check_for_updates(temp_code_root):
123+
"""
124+
Tests the check_for_updates method to detect new, modified, and deleted files.
125+
"""
126+
# Mock the parser and its processing methods to isolate the test to file detection logic
127+
with patch('codetide.parsers.PythonParser.parse_file') as mock_parse, \
128+
patch('codetide.parsers.PythonParser.resolve_inter_files_dependencies'), \
129+
patch('codetide.parsers.PythonParser.resolve_intra_file_dependencies'):
130+
131+
mock_parse.return_value = CodeFileModel(file_path=str(temp_code_root / "src/main.py"))
132+
133+
tide = await CodeTide.from_path(temp_code_root)
134+
initial_file_count = len(tide.files)
135+
assert temp_code_root / "src/main.py" in tide.files
136+
137+
# 1. Test file modification
138+
time.sleep(0.1) # Ensure modification time is different
139+
(temp_code_root / "src/main.py").write_text("print('updated')")
140+
141+
changed_files, deletion_detected = tide._get_changed_files()
142+
assert not deletion_detected
143+
assert temp_code_root / "src/main.py" in changed_files
144+
145+
# 2. Test new file creation
146+
(temp_code_root / "src/new_file.py").write_text("pass")
147+
mock_parse.return_value = CodeFileModel(file_path=str(temp_code_root / "src/new_file.py"))
148+
149+
await tide.check_for_updates(serialize=False)
150+
assert len(tide.files) == initial_file_count + 1
151+
assert temp_code_root / "src/new_file.py" in tide.files
152+
153+
# 3. Test file deletion
154+
(temp_code_root / "src/main.py").unlink()
155+
156+
# Mock the reset method to verify it gets called on deletion
157+
with patch.object(tide, '_reset', new_callable=AsyncMock) as mock_reset:
158+
await tide.check_for_updates(serialize=False)
159+
mock_reset.assert_called_once()
160+
161+
@pytest.mark.asyncio
162+
async def test_from_path_initialization(temp_code_root):
163+
"""
164+
Tests the basic initialization of CodeTide using the from_path classmethod.
165+
This is a high-level test to ensure the factory method runs without crashing.
166+
"""
167+
# Mock the parser to avoid dependency on tree-sitter binaries
168+
with patch('codetide.parsers.PythonParser.parse_file') as mock_parse:
169+
mock_parse.return_value = CodeFileModel(file_path="mock.py")
170+
171+
tide = await CodeTide.from_path(temp_code_root)
172+
173+
# Assert that the CodeTide instance was created with the correct rootpath
174+
assert tide.rootpath == temp_code_root
175+
# Assert that some files were found (the exact number depends on the mock setup)
176+
assert len(tide.files) > 0
177+
assert len(tide.codebase.root) > 0

0 commit comments

Comments
 (0)