view NewTests/helpers.py @ 580:f4579c9ec73b

fix timing
author Alain Mazy <am@osimis.io>
date Mon, 11 Sep 2023 09:09:26 +0200
parents 0649a19df194
children 6753d96dd71f
line wrap: on
line source

import unittest
from orthanc_api_client import OrthancApiClient
import subprocess
import json
import os
import typing
import shutil
from threading import Thread


import pathlib
here = pathlib.Path(__file__).parent.resolve()


default_base_config = {
    "AuthenticationEnabled": False,
    "RemoteAccessAllowed": True
}


class Helpers:

    orthanc_under_tests_hostname: str = 'localhost'
    orthanc_under_tests_http_port: int = 8052
    orthanc_under_tests_dicom_port: int = 4252
    orthanc_under_tests_exe: str = None
    orthanc_previous_version_exe: str = None
    orthanc_under_tests_docker_image: str = None
    skip_preparation: bool = False
    break_after_preparation: bool = False
    break_before_preparation: bool = False
    plugins: typing.List[str] = []

    @classmethod
    def get_orthanc_url(cls):
        return f"http://{cls.orthanc_under_tests_hostname}:{cls.orthanc_under_tests_http_port}"

    @classmethod
    def get_orthanc_ip(cls):
        return cls.orthanc_under_tests_hostname

    @classmethod
    def get_orthanc_dicom_port(cls):
        return cls.orthanc_under_tests_dicom_port

    @classmethod
    def is_docker(cls):
        return cls.orthanc_under_tests_exe is None and cls.orthanc_under_tests_docker_image is not None

    @classmethod
    def is_exe(cls):
        return cls.orthanc_under_tests_exe is not None and cls.orthanc_under_tests_docker_image is None

    @classmethod
    def find_executable(cls, name):
        p = os.path.join('/usr/local/bin', name)
        if os.path.isfile(p):
            return p

        p = os.path.join('/usr/local/sbin', name)
        if os.path.isfile(p):
            return p

        return name


class OrthancTestCase(unittest.TestCase):

    o: OrthancApiClient = None  # the orthanc under tests api client
    _orthanc_process = None
    _orthanc_container_name = None
    _orthanc_is_running = False
    _orthanc_logger_thread = None

    @classmethod
    def setUpClass(cls):

        cls.o = OrthancApiClient(Helpers.get_orthanc_url())
        cls._prepare()

    @classmethod
    def tearDownClass(cls):
        if not Helpers.break_after_preparation:
            cls.kill_orthanc()
        cls.terminate()

    @classmethod
    def prepare(cls):
        pass # to override

    @classmethod
    def terminate(cls):
        pass # to override

    @classmethod
    def _prepare(cls):
        if not Helpers.skip_preparation:
            cls.prepare()

    @classmethod
    def get_storage_path(cls, storage_name: str):
        return str(here / "storages" / f"{storage_name}")

    @classmethod
    def generate_configuration(cls, config_name: str, config: object, storage_name: str, plugins = []):
        
        # add plugins and default storge directory
        if plugins and len(plugins) > 0:
            config["Plugins"] = plugins

        if Helpers.is_exe() and not "StorageDirectory" in config:
            config["StorageDirectory"] = cls.get_storage_path(storage_name=storage_name)

        if not "Name" in config:
            config["Name"] = config_name

        if not "HttpPort" in config:
            config["HttpPort"] = Helpers.orthanc_under_tests_http_port

        if not "DicomPort" in config:
            config["DicomPort"] = Helpers.orthanc_under_tests_dicom_port

        # copy the values from the base config
        for k, v in default_base_config.items():
            if not k in config:
                config[k] = v

        # save to disk
        path = str(here / "configurations" / f"{config_name}.json")
        with open(path, "w") as f:
            json.dump(config, f, indent=4)

        return path

    @classmethod
    def clear_storage(cls, storage_name: str):
        if Helpers.is_exe():
            storage_path = cls.get_storage_path(storage_name=storage_name)
            shutil.rmtree(storage_path, ignore_errors=True)
        elif Helpers.is_docker():
            subprocess.run(["docker", "volume", "rm", "-f", storage_name])

    @classmethod
    def create_docker_network(cls, network: str):
        if Helpers.is_docker():
            subprocess.run(["docker", "network", "rm", network])      # ignore error
            subprocess.run(["docker", "network", "create", network])

    @classmethod
    def launch_orthanc_to_prepare_db(cls, config_name: str = None, config: object = None, config_path: str = None, storage_name: str = None, plugins = []):
        if config_name and storage_name and config:
            # generate the configuration file
            config_path = cls.generate_configuration(
                config_name=config_name,
                storage_name=storage_name,
                config=config,
                plugins=plugins
                )
        elif not config_path or not storage_name or not config_name:
            raise RuntimeError("Invalid configuration")

        # run orthanc
        if Helpers.orthanc_previous_version_exe:
            cls.launch_orthanc_exe(
                exe_path=Helpers.orthanc_previous_version_exe,
                config_path=config_path
            )
        elif Helpers.orthanc_previous_version_docker_image:
            cls.launch_orthanc_docker(
                docker_image=Helpers.orthanc_previous_version_docker_image,
                storage_name=storage_name,
                config_name=config_name,
                config_path=config_path
            )
        else:
            raise RuntimeError("Invalid configuration, can not launch Orthanc")

    @classmethod
    def launch_orthanc_under_tests(cls, config_name: str = None, config: object = None, config_path: str = None, storage_name: str = None, plugins = [], docker_network: str = None):
        if config_name and storage_name and config:
            # generate the configuration file
            config_path = cls.generate_configuration(
                config_name=config_name,
                storage_name=storage_name,
                config=config,
                plugins=plugins
                )
        elif not config_path or not storage_name or not config_name:
            raise RuntimeError("Invalid configuration")

        # run orthanc
        if Helpers.orthanc_under_tests_exe:
            cls.launch_orthanc_exe(
                exe_path=Helpers.orthanc_under_tests_exe,
                config_path=config_path
            )
        elif Helpers.orthanc_under_tests_docker_image:
            cls.launch_orthanc_docker(
                docker_image=Helpers.orthanc_under_tests_docker_image,
                storage_name=storage_name,
                config_name=config_name,
                config_path=config_path,
                network=docker_network
            )
        else:
            raise RuntimeError("Invalid configuration, can not launch Orthanc")

    @classmethod
    def launch_orthanc_exe(cls, exe_path: str, config_path: str):
            cls._orthanc_process = subprocess.Popen(
                [exe_path, "--verbose", config_path],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE
            )

            cls.o.wait_started(10)
            if not cls.o.is_alive():
                output = cls.get_orthanc_process_output()
                print("Orthanc output\n" + output)

                raise RuntimeError(f"Orthanc failed to start '{exe_path}', conf = '{config_path}'.  Check output above")

    @classmethod
    def launch_orthanc_docker(cls, docker_image: str, storage_name: str, config_path: str, config_name: str, network: str = None):
            storage_path = cls.get_storage_path(storage_name=storage_name)

            cmd = [
                    "docker", "run", "--rm", 
                    "-e", "VERBOSE_ENABLED=true",
                    "-e", "VERBOSE_STARTUP=true", 
                    "-v", f"{config_path}:/etc/orthanc/orthanc.json",
                    "-v", f"{storage_path}:/var/lib/orthanc/db/",
                    "--name", config_name,
                    "-p", f"{Helpers.orthanc_under_tests_http_port}:{Helpers.orthanc_under_tests_http_port}",
                    "-p", f"{Helpers.orthanc_under_tests_dicom_port}:{Helpers.orthanc_under_tests_dicom_port}"
                ]
            if network:
                cmd.extend(["--network", network])
            cmd.append(docker_image)

            cls._orthanc_container_name = config_name
            print("docker cmd line: " + " ".join(cmd))

            cls._orthanc_process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE
            )

            cls.o.wait_started(10)
            if not cls.o.is_alive():
                output = cls.get_orthanc_process_output()
                print("Orthanc output\n" + output)

                raise RuntimeError(f"Orthanc failed to start Orthanc through Docker '{docker_image}', conf = '{config_path}'.  Check output above")


    @classmethod
    def kill_orthanc(cls):
        if Helpers.is_exe():
            if cls._orthanc_process:
                cls._orthanc_process.kill()
            else:
                return
        else:
            subprocess.run(["docker", "stop", cls._orthanc_container_name])
        output = cls.get_orthanc_process_output()
        print("Orthanc output\n" + output)
        cls._orthanc_process = None

    @classmethod
    def get_orthanc_process_output(cls):
        outputs = cls._orthanc_process.communicate()
        output = ""
        for o in outputs:
            output += o.decode('utf-8')
        return output