Skip to content

Commit 0bfdfc0

Browse files
LalatenduMohantyclaude
authored andcommitted
test(server): add coverage for wheel server
Closes: #951 Co-Authored-By: Claude <claude@anthropic.com> Signed-off-by: Lalatendu Mohanty <lmohanty@redhat.com>
1 parent 8d850ff commit 0bfdfc0

1 file changed

Lines changed: 197 additions & 0 deletions

File tree

tests/test_server.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
import pathlib
5+
import zipfile
6+
from unittest.mock import Mock, patch
7+
8+
import pytest
9+
from starlette.exceptions import HTTPException
10+
from starlette.responses import FileResponse, HTMLResponse
11+
12+
from fromager import context, server
13+
14+
15+
class _FakeRequest:
16+
"""Minimal stand-in for starlette.requests.Request."""
17+
18+
def __init__(self, path_params: dict[str, str] | None = None) -> None:
19+
self.path_params = path_params or {}
20+
21+
22+
def _create_fake_wheel(directory: pathlib.Path, name: str) -> pathlib.Path:
23+
"""Create a minimal valid wheel file for testing."""
24+
wheel_path = directory.joinpath(name)
25+
with zipfile.ZipFile(wheel_path, "w") as zf:
26+
zf.writestr("dummy.txt", "fake wheel content")
27+
return wheel_path
28+
29+
30+
@pytest.fixture
31+
def simple_dir(tmp_path: pathlib.Path) -> pathlib.Path:
32+
"""Create a simple index directory with test data."""
33+
basedir = tmp_path.joinpath("simple")
34+
basedir.mkdir()
35+
project_dir = basedir.joinpath("testpkg")
36+
project_dir.mkdir()
37+
_create_fake_wheel(project_dir, "testpkg-1.0-py3-none-any.whl")
38+
project_dir.joinpath("testpkg-1.0-py3-none-any.whl.metadata").write_bytes(
39+
b"Metadata-Version: 2.1\nName: testpkg\nVersion: 1.0\n"
40+
)
41+
project_dir.joinpath("testpkg-1.0.tar.gz").write_bytes(b"fake tarball")
42+
return basedir
43+
44+
45+
@pytest.fixture
46+
def handler(simple_dir: pathlib.Path) -> server.SimpleHTMLIndex:
47+
"""Create a SimpleHTMLIndex instance for testing."""
48+
return server.SimpleHTMLIndex(simple_dir)
49+
50+
51+
def test_update_wheel_mirror_moves_to_downloads(
52+
tmp_context: context.WorkContext,
53+
) -> None:
54+
"""Verify wheels are moved from build dir to downloads dir."""
55+
_create_fake_wheel(tmp_context.wheels_build, "foo-1.0-py3-none-any.whl")
56+
57+
server.update_wheel_mirror(tmp_context)
58+
59+
assert not tmp_context.wheels_build.joinpath("foo-1.0-py3-none-any.whl").exists()
60+
assert tmp_context.wheels_downloads.joinpath("foo-1.0-py3-none-any.whl").exists()
61+
62+
63+
def test_update_wheel_mirror_creates_symlink(
64+
tmp_context: context.WorkContext,
65+
) -> None:
66+
"""Verify symlinks are created in the simple index structure."""
67+
_create_fake_wheel(tmp_context.wheels_build, "foo-1.0-py3-none-any.whl")
68+
69+
server.update_wheel_mirror(tmp_context)
70+
71+
symlink = tmp_context.wheel_server_dir.joinpath("foo", "foo-1.0-py3-none-any.whl")
72+
assert symlink.is_symlink()
73+
assert symlink.is_file()
74+
75+
76+
def test_update_wheel_mirror_prebuilt_wheels(
77+
tmp_context: context.WorkContext,
78+
) -> None:
79+
"""Verify wheels in prebuilt directory get symlinked."""
80+
_create_fake_wheel(tmp_context.wheels_prebuilt, "bar-2.0-py3-none-any.whl")
81+
82+
server.update_wheel_mirror(tmp_context)
83+
84+
symlink = tmp_context.wheel_server_dir.joinpath("bar", "bar-2.0-py3-none-any.whl")
85+
assert symlink.is_symlink()
86+
assert symlink.is_file()
87+
88+
89+
def test_update_wheel_mirror_cleans_dangling_symlinks(
90+
tmp_context: context.WorkContext,
91+
) -> None:
92+
"""Verify dangling symlinks are removed and recreated."""
93+
wheel_path = _create_fake_wheel(
94+
tmp_context.wheels_downloads, "foo-1.0-py3-none-any.whl"
95+
)
96+
server.update_wheel_mirror(tmp_context)
97+
98+
symlink = tmp_context.wheel_server_dir.joinpath("foo", "foo-1.0-py3-none-any.whl")
99+
assert symlink.is_symlink()
100+
101+
# Remove the target to create a dangling symlink
102+
wheel_path.unlink()
103+
assert symlink.is_symlink()
104+
assert not symlink.is_file() # dangling
105+
106+
# Re-create the wheel and update again
107+
_create_fake_wheel(tmp_context.wheels_downloads, "foo-1.0-py3-none-any.whl")
108+
server.update_wheel_mirror(tmp_context)
109+
110+
assert symlink.is_symlink()
111+
assert symlink.is_file() # no longer dangling
112+
113+
114+
def test_project_page_lists_files(handler: server.SimpleHTMLIndex) -> None:
115+
"""Verify /simple/{project} lists wheel files."""
116+
request = _FakeRequest({"project": "testpkg"})
117+
response = asyncio.run(handler.project_page(request)) # type: ignore[arg-type]
118+
assert isinstance(response, HTMLResponse)
119+
assert response.status_code == 200
120+
assert isinstance(response.body, bytes)
121+
body = response.body.decode()
122+
assert "testpkg-1.0-py3-none-any.whl" in body
123+
assert "testpkg-1.0.tar.gz" in body
124+
assert "testpkg-1.0-py3-none-any.whl.metadata" in body
125+
126+
127+
def test_project_page_missing_project(handler: server.SimpleHTMLIndex) -> None:
128+
"""Verify /simple/{project} raises 404 for unknown project."""
129+
request = _FakeRequest({"project": "nonexistent"})
130+
with pytest.raises(HTTPException) as exc_info:
131+
asyncio.run(handler.project_page(request)) # type: ignore[arg-type]
132+
assert exc_info.value.status_code == 404
133+
134+
135+
def test_serve_wheel_file(handler: server.SimpleHTMLIndex) -> None:
136+
"""Verify serving a .whl file with correct media type."""
137+
request = _FakeRequest(
138+
{"project": "testpkg", "filename": "testpkg-1.0-py3-none-any.whl"}
139+
)
140+
response = asyncio.run(handler.server_file(request)) # type: ignore[arg-type]
141+
assert isinstance(response, FileResponse)
142+
assert response.media_type == "application/zip"
143+
144+
145+
def test_serve_file_not_found(handler: server.SimpleHTMLIndex) -> None:
146+
"""Verify 404 for missing file."""
147+
request = _FakeRequest(
148+
{"project": "testpkg", "filename": "nonexistent-1.0-py3-none-any.whl"}
149+
)
150+
with pytest.raises(HTTPException) as exc_info:
151+
asyncio.run(handler.server_file(request)) # type: ignore[arg-type]
152+
assert exc_info.value.status_code == 404
153+
154+
155+
def test_serve_file_bad_extension(
156+
simple_dir: pathlib.Path,
157+
handler: server.SimpleHTMLIndex,
158+
) -> None:
159+
"""Verify 400 for unsupported file extension."""
160+
simple_dir.joinpath("testpkg", "bad.txt").write_text("bad")
161+
request = _FakeRequest({"project": "testpkg", "filename": "bad.txt"})
162+
with pytest.raises(HTTPException) as exc_info:
163+
asyncio.run(handler.server_file(request)) # type: ignore[arg-type]
164+
assert exc_info.value.status_code == 400
165+
166+
167+
@patch("fromager.server.run_wheel_server")
168+
@patch("fromager.server.update_wheel_mirror")
169+
def test_start_wheel_server_uses_external_url(
170+
mock_mirror: Mock,
171+
mock_run: Mock,
172+
tmp_context: context.WorkContext,
173+
) -> None:
174+
"""Verify no local server starts when external URL is configured."""
175+
tmp_context.wheel_server_url = "http://external:8080/simple/"
176+
177+
server.start_wheel_server(tmp_context)
178+
179+
mock_mirror.assert_called_once_with(tmp_context)
180+
mock_run.assert_not_called()
181+
assert tmp_context.wheel_server_url == "http://external:8080/simple/"
182+
183+
184+
@patch("fromager.server.run_wheel_server")
185+
@patch("fromager.server.update_wheel_mirror")
186+
def test_start_wheel_server_starts_local(
187+
mock_mirror: Mock,
188+
mock_run: Mock,
189+
tmp_context: context.WorkContext,
190+
) -> None:
191+
"""Verify local server starts when no external URL is set."""
192+
assert tmp_context.wheel_server_url == ""
193+
194+
server.start_wheel_server(tmp_context)
195+
196+
mock_mirror.assert_called_once_with(tmp_context)
197+
mock_run.assert_called_once_with(tmp_context)

0 commit comments

Comments
 (0)