Skip to content

Commit f59744b

Browse files
committed
tests: Add content server fixture
This runs an HTTP server for the test content so that we can import channels and content during tests just like they were being imported from studio.
1 parent 1d4984b commit f59744b

File tree

2 files changed

+117
-0
lines changed

2 files changed

+117
-0
lines changed

kolibri_explore_plugin/test/conftest.py

+31
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
# SPDX-License-Identifier: GPL-2.0-or-later
55
import pytest
66
from django.core.management import call_command
7+
from kolibri.utils.conf import OPTIONS
78

9+
from .utils import ContentServer
810
from .utils import create_contentdir
911

1012

@@ -26,3 +28,32 @@ def contentdir(serverdir):
2628
contentdir = serverdir / "content"
2729
create_contentdir(contentdir)
2830
return contentdir
31+
32+
33+
@pytest.fixture
34+
def content_server(serverdir, contentdir, monkeypatch):
35+
"""HTTP content server using test data"""
36+
from kolibri.core.discovery.utils.network.client import NetworkClient
37+
from kolibri.core.content.utils import resource_import
38+
39+
with ContentServer(serverdir) as server:
40+
# Override the Kolibri content server URL.
41+
monkeypatch.setitem(
42+
OPTIONS["Urls"],
43+
"CENTRAL_CONTENT_BASE_URL",
44+
server.url,
45+
)
46+
47+
# Don't introspect the server for info.
48+
monkeypatch.setattr(
49+
NetworkClient,
50+
"build_for_address",
51+
lambda addr: NetworkClient(addr),
52+
)
53+
monkeypatch.setattr(
54+
resource_import,
55+
"lookup_channel_listing_status",
56+
lambda channel_id, baseurl: None,
57+
)
58+
59+
yield server

kolibri_explore_plugin/test/utils.py

+86
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@
22
#
33
# Copyright 2023 Endless OS Foundation LLC
44
# SPDX-License-Identifier: GPL-2.0-or-later
5+
import functools
56
import json
67
import logging
8+
import multiprocessing
79
import os
10+
import queue
11+
import threading
812
from base64 import b64decode
913
from glob import iglob
1014
from hashlib import md5
15+
from http.server import SimpleHTTPRequestHandler
16+
from http.server import ThreadingHTTPServer
1117
from pathlib import Path
1218

1319
logger = logging.getLogger(__name__)
@@ -16,6 +22,12 @@
1622
CHANNELSDIR = TESTDIR / "channels"
1723

1824

25+
class ExploreTestError(Exception):
26+
"""Exceptions from kolibri-explore-plugin tests"""
27+
28+
pass
29+
30+
1931
def create_contentdir(content_path, channels_path=CHANNELSDIR):
2032
"""Create content directory from channel JSON files"""
2133
from kolibri.core.content.constants.schema_versions import (
@@ -101,3 +113,77 @@ def create_contentdir(content_path, channels_path=CHANNELSDIR):
101113
bridge.connection.execute(table.insert(), data[table.name])
102114

103115
bridge.end()
116+
117+
118+
class LoggingHTTPRequestHandler(SimpleHTTPRequestHandler):
119+
"""SimpleHTTPRequestHandler with logging"""
120+
121+
def log_message(self, format, *args):
122+
logger.debug(
123+
"%s: %s - - [%s] %s",
124+
threading.current_thread().name,
125+
self.address_string(),
126+
self.log_date_time_string(),
127+
format % args,
128+
)
129+
130+
131+
class ContentServer:
132+
"""Content HTTP server"""
133+
134+
def __init__(self, path):
135+
self.path = Path(path)
136+
self.proc = None
137+
self.address = None
138+
self.url = None
139+
140+
if not self.path.is_dir():
141+
raise ValueError(f"{path} is not a directory")
142+
143+
def __enter__(self):
144+
self.start()
145+
return self
146+
147+
def __exit__(self, exc_type, exc_value, traceback):
148+
self.stop()
149+
150+
def _run_server(self, path, queue):
151+
handler_class = functools.partial(
152+
LoggingHTTPRequestHandler,
153+
directory=path,
154+
)
155+
server = ThreadingHTTPServer(("127.0.0.1", 0), handler_class)
156+
queue.put(server.server_address)
157+
server.serve_forever()
158+
159+
def start(self):
160+
"""Start the HTTP server
161+
162+
A separate process is used so that the HTTP server can block.
163+
"""
164+
addr_queue = multiprocessing.Queue()
165+
self.proc = multiprocessing.Process(
166+
target=self._run_server, args=(self.path, addr_queue)
167+
)
168+
self.proc.start()
169+
if not self.proc.is_alive():
170+
raise ExploreTestError(f"HTTP process {self.proc.pid} exited")
171+
try:
172+
self.address = addr_queue.get(True, 5)
173+
except queue.Empty:
174+
raise ExploreTestError(
175+
"HTTP process did not write address to queue"
176+
) from None
177+
178+
self.url = f"http://{self.address[0]}:{self.address[1]}"
179+
logger.debug(
180+
f"Serving {self.path} on {self.url} from process {self.proc.pid}"
181+
)
182+
183+
def stop(self):
184+
"""Stop the HTTP server"""
185+
if self.proc is not None:
186+
if self.proc.is_alive():
187+
logger.debug(f"Stopping HTTP server process {self.proc.pid}")
188+
self.proc.terminate()
189+
self.proc = None

0 commit comments

Comments
 (0)