Add further server setup tests splitting up configuration testing from CLI
This commit is contained in:
parent
405299b221
commit
867cd0cad1
3 changed files with 132 additions and 20 deletions
|
|
@ -17,3 +17,11 @@ def mock_app(mocker):
|
|||
app = mocker.Mock()
|
||||
app.get.return_value = wrapper
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_server(mock_app, mocker):
|
||||
"""Return a LabThings FastAPI server where .app is the mock_app fixture."""
|
||||
server = mocker.Mock()
|
||||
server.app = mock_app
|
||||
return server
|
||||
|
|
|
|||
102
tests/test_server_cli.py
Normal file
102
tests/test_server_cli.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
"""Test server booting and shut down."""
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
|
||||
# Import as ofm server to attempt to minimise confusion with server as a var in other
|
||||
# functions and also FastAPI `Server`.
|
||||
from openflexure_microscope_server import server as ofm_server
|
||||
|
||||
from .test_server_config import FULL_CONFIG
|
||||
|
||||
|
||||
def test_no_config():
|
||||
"""Check that an error is thrown if no configuration is set for the microspe via CLI."""
|
||||
with pytest.raises(RuntimeError, match="No configuration"):
|
||||
ofm_server.serve_from_cli([])
|
||||
|
||||
|
||||
def test_successful_start(mock_server, mocker, caplog):
|
||||
"""Check some basic things from the microscope setup.
|
||||
|
||||
This test checks that logging is set up and that the actual function used for
|
||||
handling shutdown events shuts down the mjpeg streams.
|
||||
"""
|
||||
# Create a mock for the camera so we can check the MJPEG streams are closed on
|
||||
# shutdown.
|
||||
mock_camera = mocker.Mock()
|
||||
mock_server.things = {"/camera/": mock_camera}
|
||||
# Mock the LabThings function that returns the server so we have a mock server
|
||||
mocker.patch(
|
||||
"openflexure_microscope_server.server.lt.cli.server_from_config",
|
||||
return_value=mock_server,
|
||||
)
|
||||
# Also mock customisation or it will try to access the hard drive and make files.
|
||||
mock_customise = mocker.patch.object(ofm_server, "customise_server")
|
||||
# And mock uvicorn.run as we don't want to start a server process
|
||||
mock_uvicorn_run = mocker.patch("openflexure_microscope_server.server.uvicorn.run")
|
||||
|
||||
# Run the mock CLI
|
||||
ofm_server.serve_from_cli(["-c", FULL_CONFIG])
|
||||
|
||||
# Check that the server was customised and the run
|
||||
assert mock_customise.call_count == 1
|
||||
assert mock_uvicorn_run.call_count == 1
|
||||
# Read the log config that was set
|
||||
log_config = mock_uvicorn_run.call_args.kwargs["log_config"]
|
||||
# Check both uvicorn loggers were set to propagate
|
||||
assert log_config["loggers"]["uvicorn"]["propagate"]
|
||||
assert log_config["loggers"]["uvicorn.access"]["propagate"]
|
||||
|
||||
# Now Mock a shutdown signal being sent
|
||||
ofm_server.Server.handle_exit(mock_server.app, "mock_signal", "mock_frame")
|
||||
|
||||
# Check
|
||||
assert mock_camera.kill_mjpeg_streams.call_count == 1
|
||||
|
||||
# Mock shutdown again but with the MJPEG stream throwing a BaseException
|
||||
mock_camera.kill_mjpeg_streams.side_effect = BaseException("mock-54321")
|
||||
|
||||
# This should log at error level but even a BaseException will not cause an error
|
||||
# that stops the shutdown signal propagating
|
||||
with caplog.at_level(logging.ERROR):
|
||||
ofm_server.Server.handle_exit(mock_server.app, "mock_signal", "mock_frame")
|
||||
# Assert there is one error log
|
||||
assert len(caplog.records) == 1
|
||||
# And that it is the message raised by the kill_mjpeg_streams mock
|
||||
assert str(caplog.records[0].msg) == "mock-54321"
|
||||
|
||||
|
||||
def test_failed_customise(mock_server, mocker):
|
||||
"""Check what happens if there is an error before the server is started by uvicorn.
|
||||
|
||||
This differs based on whether the --fallback flag is set.
|
||||
"""
|
||||
# Mock the LabThings function that returns the server so we have a mock server
|
||||
mocker.patch(
|
||||
"openflexure_microscope_server.server.lt.cli.server_from_config",
|
||||
return_value=mock_server,
|
||||
)
|
||||
# Also mock customisation or it will try to access the hard drive and make files.
|
||||
mocker.patch.object(
|
||||
ofm_server, "customise_server", side_effect=RuntimeError("Can't touch this")
|
||||
)
|
||||
# And mock uvicorn.run as we don't want to start a server process
|
||||
mock_uvicorn_run = mocker.patch("openflexure_microscope_server.server.uvicorn.run")
|
||||
|
||||
# Running the mock CLI will error
|
||||
with pytest.raises(RuntimeError, match="Can't touch this"):
|
||||
ofm_server.serve_from_cli(["-c", FULL_CONFIG])
|
||||
|
||||
# But with the fallback flag uvicorn run will be run
|
||||
ofm_server.serve_from_cli(["-c", FULL_CONFIG, "--fallback"])
|
||||
|
||||
assert mock_uvicorn_run.call_count == 1
|
||||
# Get the fallback app passed to uvicorn tun
|
||||
fallback_app = mock_uvicorn_run.call_args.args[0]
|
||||
# Check it really is a fastapi
|
||||
assert isinstance(fallback_app, FastAPI)
|
||||
# An that it has the error to display
|
||||
assert str(fallback_app.labthings_error) == "Can't touch this"
|
||||
|
|
@ -6,7 +6,9 @@ from copy import deepcopy
|
|||
|
||||
import pytest
|
||||
|
||||
from openflexure_microscope_server import server
|
||||
# Import as ofm server to attempt to minimise confusion with server as a var in other
|
||||
# functions and also FastAPI `Server`.
|
||||
from openflexure_microscope_server import server as ofm_server
|
||||
|
||||
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
REPO_ROOT = os.path.dirname(THIS_DIR)
|
||||
|
|
@ -20,16 +22,16 @@ def test_monkey_patched_handle_exit(side_effect, mock_app, mocker):
|
|||
# Setup the same for any side_effect
|
||||
# Create mocks for FastAPI.Server, and for our custom shutdown function
|
||||
mock_shutdown_func = mocker.Mock(side_effect=side_effect)
|
||||
mock_fastapi_server = mocker.patch.object(server, "Server")
|
||||
mock_fastapi_server = mocker.patch.object(ofm_server, "Server")
|
||||
original_mock_handle_exit = mock_fastapi_server.handle_exit
|
||||
handle_exit_args = (mock_app, "mock_signal", "mock_frame")
|
||||
|
||||
server.set_shutdown_function(mock_shutdown_func)
|
||||
ofm_server.set_shutdown_function(mock_shutdown_func)
|
||||
|
||||
if side_effect is None:
|
||||
# In normal operation, both the custom shutdown function and the original
|
||||
# handle_exit should be called
|
||||
server.Server.handle_exit(*handle_exit_args)
|
||||
ofm_server.Server.handle_exit(*handle_exit_args)
|
||||
|
||||
assert mock_shutdown_func.call_count == 1
|
||||
assert original_mock_handle_exit.call_count == 1
|
||||
|
|
@ -39,30 +41,30 @@ def test_monkey_patched_handle_exit(side_effect, mock_app, mocker):
|
|||
# Use an error side effect to check that our custom shutdown function is called
|
||||
# before the standard `handle_exit`
|
||||
with pytest.raises(RuntimeError, match="Mock"):
|
||||
server.Server.handle_exit(*handle_exit_args)
|
||||
ofm_server.Server.handle_exit(*handle_exit_args)
|
||||
# The error was raised so we know our custom function was run, check that
|
||||
# handle_exit wasn't
|
||||
# handle_exit wasn't.
|
||||
# Note: that the non-mocked shutdown function is entirely wrapped in a
|
||||
# try/except BaseExcpetion, so that handle_exit will always run.
|
||||
assert original_mock_handle_exit.call_count == 0
|
||||
|
||||
|
||||
def test_customise_server(mock_app, mocker):
|
||||
def test_customise_server(mock_server, mocker):
|
||||
"""Check that all server customisation stages are called."""
|
||||
# Mock everything to be called in customisation, partly setup may not be complete,
|
||||
# but also to limit excess coverage reporting.
|
||||
mocked_add_static = mocker.patch.object(server, "add_static_files")
|
||||
mocked_v2_endpoints = mocker.patch.object(server, "add_v2_endpoints")
|
||||
mocked_configure_logging = mocker.patch.object(server, "configure_logging")
|
||||
mocked_retrieve_log = mocker.patch.object(server, "retrieve_log")
|
||||
mocked_retrieve_log_file = mocker.patch.object(server, "retrieve_log_from_file")
|
||||
mocked_add_static = mocker.patch.object(ofm_server, "add_static_files")
|
||||
mocked_v2_endpoints = mocker.patch.object(ofm_server, "add_v2_endpoints")
|
||||
mocked_configure_logging = mocker.patch.object(ofm_server, "configure_logging")
|
||||
mocked_retrieve_log = mocker.patch.object(ofm_server, "retrieve_log")
|
||||
mocked_retrieve_log_file = mocker.patch.object(ofm_server, "retrieve_log_from_file")
|
||||
|
||||
# Set up a mock server
|
||||
mock_server = mocker.Mock()
|
||||
mock_server.app = mock_app
|
||||
mock_app = mock_server.app
|
||||
# The wrapper returned for app.get so we can see what functions are decorated.
|
||||
wrapper = mock_app.get.return_value
|
||||
|
||||
# Finally we can run it!
|
||||
server.customise_server(mock_server, "mock_log_folder", "mock_scan_folder")
|
||||
ofm_server.customise_server(mock_server, "mock_log_folder", "mock_scan_folder")
|
||||
|
||||
# Check each internal customisation function is called
|
||||
assert mocked_add_static.call_count == 1
|
||||
|
|
@ -89,7 +91,7 @@ def test_get_scans_dir_ok(config_file, expected_scan_dir):
|
|||
"""Test the _get_scans_dir function and also check the standard config files."""
|
||||
with open(config_file, "r", encoding="utf-8") as f_obj:
|
||||
config_dict = json.load(f_obj)
|
||||
assert server._get_scans_dir(config_dict) == expected_scan_dir
|
||||
assert ofm_server._get_scans_dir(config_dict) == expected_scan_dir
|
||||
|
||||
|
||||
def test_get_scans_dir_no_smart_scan():
|
||||
|
|
@ -100,7 +102,7 @@ def test_get_scans_dir_no_smart_scan():
|
|||
# Delete smart scan
|
||||
del config_dict["things"]["/smart_scan/"]
|
||||
# No SmartScanThing, should return None
|
||||
assert server._get_scans_dir(config_dict) is None
|
||||
assert ofm_server._get_scans_dir(config_dict) is None
|
||||
|
||||
|
||||
def test_get_scans_dir_bad_smart_scan_config():
|
||||
|
|
@ -115,11 +117,11 @@ def test_get_scans_dir_bad_smart_scan_config():
|
|||
del broken_config["things"]["/smart_scan/"]["kwargs"]
|
||||
# Creates an Error
|
||||
with pytest.raises(RuntimeError):
|
||||
server._get_scans_dir(broken_config)
|
||||
ofm_server._get_scans_dir(broken_config)
|
||||
|
||||
# Same thing should happen if just the scans_folder key is deleted
|
||||
broken_config = deepcopy(config_dict)
|
||||
del broken_config["things"]["/smart_scan/"]["kwargs"]
|
||||
# Creates an Error
|
||||
with pytest.raises(RuntimeError):
|
||||
server._get_scans_dir(broken_config)
|
||||
ofm_server._get_scans_dir(broken_config)
|
||||
Loading…
Add table
Add a link
Reference in a new issue