From 234f7a7621ac869d37c77867f8458829f6dff4ec Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 9 Jul 2025 11:58:02 +0100 Subject: [PATCH] Start writing python-integration test that starts simulation server --- integration-tests/testfile.py | 134 ++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100755 integration-tests/testfile.py diff --git a/integration-tests/testfile.py b/integration-tests/testfile.py new file mode 100755 index 00000000..d767945d --- /dev/null +++ b/integration-tests/testfile.py @@ -0,0 +1,134 @@ +#! /usr/bin/env python3 +"""This module fires up a server in a subprocess to allow for integration tests."" + +These are seperated from the unit tests to stop them artificially inflating the test +coverage. For now this file should be run directly not with a test framework +""" + +import subprocess +import os +import shutil +from time import sleep + + +THIS_DIR = os.path.dirname(os.path.realpath(__file__)) +WORKING_DIR = os.path.join(THIS_DIR, "working_dir") +REPO_DIR = os.path.dirname(THIS_DIR) +CONFIG_FILE = os.path.join(REPO_DIR, "ofm_config_simulation.json") +CMD = ["openflexure-microscope-server", "--fallback", "-c", CONFIG_FILE] + + +def main(): + """Set up the server, run basic checks, shutdown, check for graceful exit""" + set_up_working_dir() + os.chdir(WORKING_DIR) + + try: + print("Starting server") + server_process = start_server() + print("Waiting 15s to ensure started up") + sleep(15) + error_if_server_not_started(server_process) + + finally: + # Ensure the server process is really dead + if server_process.poll() is None: + print("Server still running, shutting down.") + server_process.terminate() + sleep(2) + if server_process.poll() is None: + server_process.kill() + raise RuntimeError("Server needed killing at end of test.") + + +def set_up_working_dir() -> None: + """If working dir exists, delete it and make a new one.""" + if os.path.exists(WORKING_DIR): + shutil.rmtree(WORKING_DIR) + os.makedirs(WORKING_DIR) + + +def start_server() -> subprocess.Popen: + """ + Start the server in a subprocess. + + The server is started in a background thread and all outputs are + buffered. + + :return: Popen object for the ongoing process + """ + process = subprocess.Popen( + CMD, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=1, + universal_newlines=True, + ) + os.set_blocking(process.stdout.fileno(), False) + os.set_blocking(process.stderr.fileno(), False) + return process + + +def error_if_server_not_started(server_process: subprocess.Popen) -> None: + """Check the server started up as expected. + + Check the subprocess is running + Check the logs have no errors + Read the Logs to check uvicorn is running on http://127.0.0.1:5000 + """ + + stdout, stderr = read_process_buffers(server_process) + output = format_stdout_stderr(stdout, stderr) + if server_process.poll() is not None: + raise RuntimeError(f"Server process is not running!\n{output}") + + confirmed_uvicorn_is_running = False + # Note that logs go to stderr once OFM logging process stars, but if there is an + # error very early (such as on import) on logs may go to stdout + for line in stdout + stderr: + if line.startswith("ERROR:"): + raise RuntimeError(f"Server started up with error\n{line}\n\n{output}") + if "Uvicorn running on http://127.0.0.1:5000" in line: + confirmed_uvicorn_is_running = True + + if not confirmed_uvicorn_is_running: + raise RuntimeError( + f"Cannot confirm Uvicorn is running on http://127.0.0.1:5000\n\n{output}" + ) + + # If we reached here Everything is fine! print the logs! + print("Server is running with following outputs:\n\n") + print(output) + + +def read_process_buffers(process: subprocess.Popen) -> tuple[list[str], list[str]]: + """Return STDOUT and STDERR from a process""" + stdout = [] + stderr = [] + + while line := process.stdout.readline(): + stdout.append(line) + + while line := process.stderr.readline(): + stderr.append(line) + + return stdout, stderr + + +def format_stdout_stderr(stdout: list[str], stderr: list[str]) -> str: + """Return a ready to print format for stdout and stderr""" + text = "" + if stdout: + text += "**STDOUT**\n" + "".join(stdout) + "\n" + else: + text += "**STDOUT IS EMPTY**\n\n" + + if stderr: + text += "**STDERR**\n" + "".join(stderr) + "\n" + else: + text += "**STDERR IS EMPTY**\n\n" + return text + + +if __name__ == "__main__": + main()