131 lines
5.6 KiB
Python
131 lines
5.6 KiB
Python
"""Test server booting and shut down."""
|
|
|
|
import os
|
|
from argparse import Namespace
|
|
|
|
import pytest
|
|
|
|
from labthings_fastapi.server.config_model import ThingServerConfig
|
|
|
|
# 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 openflexure_microscope_server.server import OFMApplicationData
|
|
|
|
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
REPO_ROOT = os.path.dirname(os.path.dirname(THIS_DIR))
|
|
FULL_CONFIG = os.path.join(REPO_ROOT, "ofm_config_full.json")
|
|
SIM_CONFIG = os.path.join(REPO_ROOT, "ofm_config_simulation.json")
|
|
MANUAL_CONFIG = os.path.join(REPO_ROOT, "ofm_config_manual.json")
|
|
|
|
|
|
@pytest.mark.parametrize("side_effect", [None, RuntimeError("Mock")])
|
|
def test_monkey_patched_handle_exit(side_effect, mocker):
|
|
"""Test that handle exit is monkey_patched."""
|
|
mock_app = mocker.Mock()
|
|
# 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(ofm_server, "Server")
|
|
original_mock_handle_exit = mock_fastapi_server.handle_exit
|
|
handle_exit_args = (mock_app, "mock_signal", "mock_frame")
|
|
|
|
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
|
|
ofm_server.Server.handle_exit(*handle_exit_args)
|
|
|
|
assert mock_shutdown_func.call_count == 1
|
|
assert original_mock_handle_exit.call_count == 1
|
|
# Check arguments are propagated correctly.
|
|
assert original_mock_handle_exit.call_args.args == handle_exit_args
|
|
else:
|
|
# 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"):
|
|
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.
|
|
# Note: that the non-mocked shutdown function is entirely wrapped in a
|
|
# try/except BaseException, so that handle_exit will always run.
|
|
assert original_mock_handle_exit.call_count == 0
|
|
|
|
|
|
def test_customise_server(mocker):
|
|
"""Check that all server customisation stages are called."""
|
|
mock_server = mocker.Mock()
|
|
# 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(ofm_server, "add_static_files")
|
|
mocked_v2_endpoints = mocker.patch.object(ofm_server, "add_v2_endpoints")
|
|
mocked_retrieve_log = mocker.patch.object(ofm_server, "retrieve_log")
|
|
mocked_retrieve_log_file = mocker.patch.object(ofm_server, "retrieve_log_from_file")
|
|
|
|
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
|
|
|
|
application_data = OFMApplicationData(
|
|
log_folder="mock_log_folder", data_folder="mock_data_folder"
|
|
)
|
|
# Finally we can run it!
|
|
ofm_server.customise_server(mock_server, application_data)
|
|
|
|
# Check each internal customisation function is called
|
|
assert mocked_add_static.call_count == 1
|
|
assert mocked_v2_endpoints.call_count == 1
|
|
# Check app.get adds two routes
|
|
assert mock_app.get.call_count == 2
|
|
assert wrapper.call_count == 2
|
|
|
|
# Check the routes and functions are as expected.
|
|
added_routes = [call.args[0] for call in mock_app.get.call_args_list]
|
|
assert "/log/" in added_routes
|
|
assert "/logfile/" in added_routes
|
|
wrapped_functions = [call.args[0] for call in wrapper.call_args_list]
|
|
assert mocked_retrieve_log in wrapped_functions
|
|
assert mocked_retrieve_log_file
|
|
|
|
|
|
def test_full_config_from_args_json():
|
|
"""Check that _full_config_from_args errors if json config is supplied."""
|
|
msg = "OpenFlexure Microscope Server must have a configuration file specified."
|
|
args = Namespace(
|
|
config=None,
|
|
json='{"things":{"example": "labthings_fastapi.example_things:MyThing"}}',
|
|
)
|
|
with pytest.raises(RuntimeError, match=msg):
|
|
ofm_server._full_config_from_args(args)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("config", "log_dir", "data_dir"),
|
|
[
|
|
(FULL_CONFIG, "/var/openflexure/logs/", "/var/openflexure/data/"),
|
|
(SIM_CONFIG, "./openflexure/logs/", "./openflexure/data/"),
|
|
(MANUAL_CONFIG, "./openflexure/logs/", "./openflexure/data/"),
|
|
],
|
|
)
|
|
def test_full_config_from_args(config, log_dir, data_dir, mocker):
|
|
"""Check that _full_config_from_args returns as expected if json is supplied."""
|
|
# Mock picamera library so LabThings can create its model.
|
|
mocker.patch.dict(
|
|
"sys.modules",
|
|
{
|
|
"picamera2": mocker.MagicMock(),
|
|
"picamera2.encoders": mocker.Mock(),
|
|
"picamera2.outputs": mocker.Mock(),
|
|
},
|
|
)
|
|
args = Namespace(config=config, json=None)
|
|
lt_conf = ofm_server._full_config_from_args(args)
|
|
# If json is supplied OFM config is default. As the json is passed dircectly to
|
|
# The Pydantic Mocel in LabThings. No custom data allowed.
|
|
assert isinstance(lt_conf, ThingServerConfig)
|
|
# Check the configuration validates
|
|
application_config = OFMApplicationData(**lt_conf.application_config)
|
|
# And has the correct values
|
|
assert application_config.log_folder == log_dir
|
|
assert application_config.data_folder == data_dir
|