Skip to content

Commit 6dafe76

Browse files
Merge branch 'main' into dev
2 parents 732486c + ecaee49 commit 6dafe76

File tree

4 files changed

+230
-4
lines changed

4 files changed

+230
-4
lines changed

src/gitingest/cli.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,14 @@ async def async_main(
109109
exclude_patterns.update(ignore_patterns)
110110

111111
# Perform the ingest operation with branch support
112-
summary, *_ = await ingest(source, max_size, include_patterns, exclude_patterns, branch=branch, output=output)
112+
summary, *_ = await ingest(
113+
source,
114+
max_size,
115+
include_patterns,
116+
exclude_patterns,
117+
branch=branch,
118+
output=output
119+
)
113120

114121
# Display results
115122
click.echo(f"Analysis complete! Output written to: {output}")
@@ -131,4 +138,4 @@ async def async_main(
131138

132139

133140
if __name__ == "__main__":
134-
main()
141+
main()

src/server/main.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
""" Main module for the FastAPI application. """
22

33
import os
4+
from pathlib import Path
45

56
from api_analytics.fastapi import Analytics
67
from dotenv import load_dotenv
@@ -24,8 +25,11 @@
2425
# Register the custom exception handler for rate limits
2526
app.add_exception_handler(RateLimitExceeded, rate_limit_exception_handler)
2627

27-
# Mount static files to serve CSS, JS, and other static assets
28-
app.mount("/static", StaticFiles(directory="static"), name="static")
28+
29+
# Mount static files dynamically to serve CSS, JS, and other static assets
30+
static_dir = Path(__file__).parent.parent / "static"
31+
app.mount("/static", StaticFiles(directory=static_dir), name="static")
32+
2933

3034
# Set up API analytics middleware if an API key is provided
3135
if app_analytics_key := os.getenv("API_ANALYTICS_KEY"):

tests/test_flow_integration.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
"""
2+
Integration tests for GitIngest.
3+
These tests cover core functionalities, edge cases, and concurrency handling.
4+
"""
5+
6+
import shutil
7+
from concurrent.futures import ThreadPoolExecutor
8+
from pathlib import Path
9+
from unittest.mock import patch
10+
11+
import pytest
12+
from fastapi.testclient import TestClient
13+
14+
from src.server.main import app
15+
16+
BASE_DIR = Path(__file__).resolve().parent.parent
17+
TEMPLATE_DIR = BASE_DIR / "src" / "templates"
18+
19+
20+
@pytest.fixture(scope="module")
21+
def test_client():
22+
"""Create a test client fixture."""
23+
with TestClient(app) as client_instance:
24+
client_instance.headers.update({"Host": "localhost"})
25+
yield client_instance
26+
27+
28+
@pytest.fixture(scope="module", autouse=True)
29+
def mock_static_files():
30+
"""Mock the static file mount to avoid directory errors."""
31+
with patch("src.server.main.StaticFiles") as mock_static:
32+
mock_static.return_value = None # Mocks the StaticFiles response
33+
yield mock_static
34+
35+
36+
@pytest.fixture(scope="module", autouse=True)
37+
def mock_templates():
38+
"""Mock Jinja2 template rendering to bypass actual file loading."""
39+
with patch("starlette.templating.Jinja2Templates.TemplateResponse") as mock_template:
40+
mock_template.return_value = "Mocked Template Response"
41+
yield mock_template
42+
43+
44+
def cleanup_temp_directories():
45+
temp_dir = Path("/tmp/gitingest")
46+
if temp_dir.exists():
47+
try:
48+
shutil.rmtree(temp_dir)
49+
except PermissionError as e:
50+
print(f"Error cleaning up {temp_dir}: {e}")
51+
52+
53+
@pytest.fixture(scope="module", autouse=True)
54+
def cleanup():
55+
"""Cleanup temporary directories after tests."""
56+
yield
57+
cleanup_temp_directories()
58+
59+
60+
@pytest.mark.asyncio
61+
async def test_remote_repository_analysis(request):
62+
"""Test the complete flow of analyzing a remote repository."""
63+
client = request.getfixturevalue("test_client")
64+
form_data = {
65+
"input_text": "https://github.com/octocat/Hello-World",
66+
"max_file_size": "243",
67+
"pattern_type": "exclude",
68+
"pattern": "",
69+
}
70+
71+
response = client.post("/", data=form_data)
72+
assert response.status_code == 200, f"Form submission failed: {response.text}"
73+
assert "Mocked Template Response" in response.text
74+
75+
76+
@pytest.mark.asyncio
77+
async def test_invalid_repository_url(request):
78+
"""Test handling of an invalid repository URL."""
79+
client = request.getfixturevalue("test_client")
80+
form_data = {
81+
"input_text": "https://github.com/nonexistent/repo",
82+
"max_file_size": "243",
83+
"pattern_type": "exclude",
84+
"pattern": "",
85+
}
86+
87+
response = client.post("/", data=form_data)
88+
assert response.status_code == 200, f"Request failed: {response.text}"
89+
assert "Mocked Template Response" in response.text
90+
91+
92+
@pytest.mark.asyncio
93+
async def test_large_repository(request):
94+
"""Simulate analysis of a large repository with nested folders."""
95+
client = request.getfixturevalue("test_client")
96+
form_data = {
97+
"input_text": "https://github.com/large/repo-with-many-files",
98+
"max_file_size": "243",
99+
"pattern_type": "exclude",
100+
"pattern": "",
101+
}
102+
103+
response = client.post("/", data=form_data)
104+
assert response.status_code == 200, f"Request failed: {response.text}"
105+
assert "Mocked Template Response" in response.text
106+
107+
108+
@pytest.mark.asyncio
109+
async def test_concurrent_requests(request):
110+
"""Test handling of multiple concurrent requests."""
111+
client = request.getfixturevalue("test_client")
112+
113+
def make_request():
114+
form_data = {
115+
"input_text": "https://github.com/octocat/Hello-World",
116+
"max_file_size": "243",
117+
"pattern_type": "exclude",
118+
"pattern": "",
119+
}
120+
response = client.post("/", data=form_data)
121+
assert response.status_code == 200, f"Request failed: {response.text}"
122+
assert "Mocked Template Response" in response.text
123+
124+
with ThreadPoolExecutor(max_workers=5) as executor:
125+
futures = [executor.submit(make_request) for _ in range(5)]
126+
for future in futures:
127+
future.result()
128+
129+
130+
@pytest.mark.asyncio
131+
async def test_large_file_handling(request):
132+
"""Test handling of repositories with large files."""
133+
client = request.getfixturevalue("test_client")
134+
form_data = {
135+
"input_text": "https://github.com/octocat/Hello-World",
136+
"max_file_size": "1",
137+
"pattern_type": "exclude",
138+
"pattern": "",
139+
}
140+
141+
response = client.post("/", data=form_data)
142+
assert response.status_code == 200, f"Request failed: {response.text}"
143+
assert "Mocked Template Response" in response.text
144+
145+
146+
@pytest.mark.asyncio
147+
async def test_repository_with_patterns(request):
148+
"""Test repository analysis with include/exclude patterns."""
149+
client = request.getfixturevalue("test_client")
150+
form_data = {
151+
"input_text": "https://github.com/octocat/Hello-World",
152+
"max_file_size": "243",
153+
"pattern_type": "include",
154+
"pattern": "*.md",
155+
}
156+
157+
response = client.post("/", data=form_data)
158+
assert response.status_code == 200, f"Request failed: {response.text}"
159+
assert "Mocked Template Response" in response.text

tests/test_repository_clone.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""
77

88
import asyncio
9+
import os
910
from unittest.mock import AsyncMock, patch
1011

1112
import pytest
@@ -306,3 +307,58 @@ async def test_clone_repo_with_timeout() -> None:
306307
mock_exec.side_effect = asyncio.TimeoutError
307308
with pytest.raises(AsyncTimeoutError, match="Operation timed out after"):
308309
await clone_repo(clone_config)
310+
311+
312+
@pytest.mark.asyncio
313+
async def test_clone_specific_branch(tmp_path):
314+
"""
315+
Test cloning a specific branch of a repository.
316+
317+
Given a valid repository URL and a branch name:
318+
When `clone_repo` is called,
319+
Then the repository should be cloned and checked out at that branch.
320+
"""
321+
repo_url = "https://github.com/cyclotruc/gitingest.git"
322+
branch_name = "main"
323+
local_path = tmp_path / "gitingest"
324+
325+
config = CloneConfig(url=repo_url, local_path=str(local_path), branch=branch_name)
326+
await clone_repo(config)
327+
328+
# Assertions
329+
assert local_path.exists(), "The repository was not cloned successfully."
330+
assert local_path.is_dir(), "The cloned repository path is not a directory."
331+
332+
# Check the current branch
333+
current_branch = os.popen(f"git -C {local_path} branch --show-current").read().strip()
334+
assert current_branch == branch_name, f"Expected branch '{branch_name}', got '{current_branch}'."
335+
336+
337+
@pytest.mark.asyncio
338+
async def test_clone_branch_with_slashes(tmp_path):
339+
"""
340+
Test cloning a branch with slashes in the name.
341+
342+
Given a valid repository URL and a branch name with slashes:
343+
When `clone_repo` is called,
344+
Then the repository should be cloned and checked out at that branch.
345+
"""
346+
repo_url = "https://github.com/user/repo"
347+
branch_name = "fix/in-operator"
348+
local_path = tmp_path / "gitingest"
349+
350+
clone_config = CloneConfig(url=repo_url, local_path=str(local_path), branch=branch_name)
351+
with patch("gitingest.repository_clone._check_repo_exists", return_value=True):
352+
with patch("gitingest.repository_clone._run_git_command", new_callable=AsyncMock) as mock_exec:
353+
await clone_repo(clone_config)
354+
355+
mock_exec.assert_called_once_with(
356+
"git",
357+
"clone",
358+
"--depth=1",
359+
"--single-branch",
360+
"--branch",
361+
"fix/in-operator",
362+
clone_config.url,
363+
clone_config.local_path,
364+
)

0 commit comments

Comments
 (0)