Refactor test web server to allow multiple instances and log capture

This commit is contained in:
Roman Yurchak 2018-09-18 19:04:53 +02:00
parent 2e23079aed
commit 387fb18ceb
3 changed files with 85 additions and 33 deletions

View File

@ -2,13 +2,16 @@
Various common utilities for testing. Various common utilities for testing.
""" """
import atexit import contextlib
import multiprocessing import multiprocessing
import textwrap import textwrap
import tempfile
import time
import os import os
import pathlib import pathlib
import queue import queue
import sys import sys
import shutil
try: try:
import pytest import pytest
@ -46,7 +49,8 @@ def _display_driver_logs(browser, driver):
class SeleniumWrapper: class SeleniumWrapper:
def __init__(self): def __init__(self, server_port, server_hostname='127.0.0.1',
server_log=None):
from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support.wait import WebDriverWait
from selenium.common.exceptions import TimeoutException from selenium.common.exceptions import TimeoutException
@ -56,7 +60,7 @@ class SeleniumWrapper:
# selenium does not expose HTTP response codes # selenium does not expose HTTP response codes
raise ValueError(f"{(BUILD_PATH / 'test.html').resolve()} " raise ValueError(f"{(BUILD_PATH / 'test.html').resolve()} "
f"does not exist!") f"does not exist!")
driver.get(f'http://127.0.0.1:{PORT}/test.html') driver.get(f'http://{server_hostname}:{server_port}/test.html')
try: try:
wait.until(PyodideInited()) wait.until(PyodideInited())
except TimeoutException as exc: except TimeoutException as exc:
@ -64,6 +68,9 @@ class SeleniumWrapper:
raise TimeoutException() raise TimeoutException()
self.wait = wait self.wait = wait
self.driver = driver self.driver = driver
self.server_port = server_port
self.server_hostname = server_hostname
self.server_log = server_log
@property @property
def logs(self): def logs(self):
@ -151,12 +158,15 @@ class ChromeWrapper(SeleniumWrapper):
if pytest is not None: if pytest is not None:
@pytest.fixture(params=['firefox', 'chrome']) @pytest.fixture(params=['firefox', 'chrome'])
def selenium_standalone(request): def selenium_standalone(request, web_server_main):
server_hostname, server_port, server_log = web_server_main
if request.param == 'firefox': if request.param == 'firefox':
cls = FirefoxWrapper cls = FirefoxWrapper
elif request.param == 'chrome': elif request.param == 'chrome':
cls = ChromeWrapper cls = ChromeWrapper
selenium = cls() selenium = cls(server_port=server_port,
server_hostname=server_hostname,
server_log=server_log)
try: try:
yield selenium yield selenium
finally: finally:
@ -164,14 +174,17 @@ if pytest is not None:
selenium.driver.quit() selenium.driver.quit()
@pytest.fixture(params=['firefox', 'chrome'], scope='module') @pytest.fixture(params=['firefox', 'chrome'], scope='module')
def _selenium_cached(request): def _selenium_cached(request, web_server_main):
# Cached selenium instance. This is a copy-paste of # Cached selenium instance. This is a copy-paste of
# selenium_standalone to avoid fixture scope issues # selenium_standalone to avoid fixture scope issues
server_hostname, server_port, server_log = web_server_main
if request.param == 'firefox': if request.param == 'firefox':
cls = FirefoxWrapper cls = FirefoxWrapper
elif request.param == 'chrome': elif request.param == 'chrome':
cls = ChromeWrapper cls = ChromeWrapper
selenium = cls() selenium = cls(server_port=server_port,
server_hostname=server_hostname,
server_log=server_log)
try: try:
yield selenium yield selenium
finally: finally:
@ -187,35 +200,61 @@ if pytest is not None:
print(_selenium_cached.logs) print(_selenium_cached.logs)
PORT = 0 @pytest.fixture(scope='session')
def web_server_main():
with spawn_web_server() as output:
yield output
@pytest.fixture(scope='session')
def web_server_secondary():
with spawn_web_server() as output:
yield output
@contextlib.contextmanager
def spawn_web_server(): def spawn_web_server():
global PORT
print("Spawning webserver...")
tmp_dir = tempfile.mkdtemp()
log_path = pathlib.Path(tmp_dir) / 'http-server.log'
q = multiprocessing.Queue() q = multiprocessing.Queue()
p = multiprocessing.Process(target=run_web_server, args=(q,)) p = multiprocessing.Process(target=run_web_server, args=(q, log_path))
def shutdown_webserver(): try:
p.start()
port = q.get()
hostname = '127.0.0.1'
print(f"Spawning webserver at http://{hostname}:{port} "
f"(see logs in {log_path})")
yield hostname, port, log_path
finally:
q.put("TERMINATE") q.put("TERMINATE")
p.join() p.join()
atexit.register(shutdown_webserver) shutil.rmtree(tmp_dir)
p.start()
PORT = q.get()
def run_web_server(q): def run_web_server(q, log_filepath):
"""Start the HTTP web server
Parameters
----------
q : Queue
communication queue
log_path : pathlib.Path
path to the file where to store the logs
"""
import http.server import http.server
import socketserver import socketserver
print("Running webserver...")
os.chdir(BUILD_PATH) os.chdir(BUILD_PATH)
log_fh = log_filepath.open('w', buffering=1)
sys.stdout = log_fh
sys.stderr = log_fh
class Handler(http.server.CGIHTTPRequestHandler): class Handler(http.server.CGIHTTPRequestHandler):
def translate_path(self, path): def translate_path(self, path):
if path.startswith('/test/'): if path.startswith('/test/'):
return TEST_PATH / path[6:] return TEST_PATH / path[6:]
@ -227,14 +266,17 @@ def run_web_server(q):
return True return True
return False return False
def log_message(self, *args, **kwargs): def log_message(self, format_, *args):
pass print("[%s] source: %s:%s - %s"
% (self.log_date_time_string(),
*self.client_address,
format_ % args))
Handler.extensions_map['.wasm'] = 'application/wasm' Handler.extensions_map['.wasm'] = 'application/wasm'
with socketserver.TCPServer(("", 0), Handler) as httpd: with socketserver.TCPServer(("", 0), Handler) as httpd:
host, port = httpd.server_address host, port = httpd.server_address
print("serving at port", port) print(f"Starting webserver at http://{host}:{port}")
httpd.server_name = 'test-server' httpd.server_name = 'test-server'
httpd.server_port = port httpd.server_port = port
q.put(port) q.put(port)
@ -242,8 +284,8 @@ def run_web_server(q):
def service_actions(): def service_actions():
try: try:
if q.get(False) == "TERMINATE": if q.get(False) == "TERMINATE":
print('Stopping server...')
sys.exit(0) sys.exit(0)
httpd.shutdown()
except queue.Empty: except queue.Empty:
pass pass
@ -251,10 +293,10 @@ def run_web_server(q):
httpd.serve_forever() httpd.serve_forever()
@pytest.fixture if (__name__ == '__main__'
def web_server(): and multiprocessing.current_process().name == 'MainProcess'
return '127.0.0.1', PORT and not hasattr(sys, "_pytest_session")):
with spawn_web_server():
# run forever
if multiprocessing.current_process().name == 'MainProcess': while True:
spawn_web_server() time.sleep(1)

View File

@ -1,9 +1,10 @@
import pytest import pytest
def test_load_from_url(selenium_standalone, web_server): def test_load_from_url(selenium_standalone):
url, port = web_server url = selenium_standalone.server_hostname
port = selenium_standalone.server_port
selenium_standalone.load_package(f"http://{url}:{port}/pyparsing.js") selenium_standalone.load_package(f"http://{url}:{port}/pyparsing.js")
assert "Invalid package name or URI" not in selenium_standalone.logs assert "Invalid package name or URI" not in selenium_standalone.logs

View File

@ -1,3 +1,6 @@
import pathlib
def test_pytest(selenium): def test_pytest(selenium):
selenium.load_package(['pytest', 'numpy', 'nose']) selenium.load_package(['pytest', 'numpy', 'nose'])
@ -15,3 +18,9 @@ def test_pytest(selenium):
logs = '\n'.join(selenium.logs) logs = '\n'.join(selenium.logs)
assert 'INTERNALERROR' not in logs assert 'INTERNALERROR' not in logs
def test_web_server_secondary(selenium, web_server_secondary):
host, port, logs = web_server_secondary
assert pathlib.Path(logs).exists()
assert selenium.server_port != port