diff --git a/.circleci/config.yml b/.circleci/config.yml index ec70861..caa9f0d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,8 @@ version: 2.1 aliases: docker-image: &image - - image: mambaorg/micromamba + image: mambaorg/micromamba + filter-pr-only: &PR-only branches: ignore: @@ -216,13 +217,28 @@ workflows: jobs: lint: - docker: *image + docker: + - *image steps: - make-env: use_specific_requirements_file: requirements.dev.txt - lint-project build: - docker: *image + docker: + - *image + - image: circleci/postgres:12-alpine + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: testing_db + POSTGRES_HOST: localhost + POSTGRES_PORT: 5432 + PYTEST_MONITOR_DB_NAME: postgres + PYTEST_MONITOR_DB_USER: postgres + PYTEST_MONITOR_DB_PASSWORD: testing_db + PYTEST_MONITOR_DB_HOST: localhost + PYTEST_MONITOR_DB_PORT: 5432 + parameters: python: type: string @@ -235,7 +251,8 @@ jobs: pytest: << parameters.pytest >> - test-project publish: - docker: *image + docker: + - *image steps: - make-env: extra_deps: twine setuptools build diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3106b06..b1f60c8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,20 @@ image: continuumio/miniconda +variables: + - POSTGRES_DB: postgres + - POSTGRES_USER: postgres + - POSTGRES_PASSWORD: testing_db + - POSTGRES_HOST: localhost + - POSTGRES_PORT: 5432 + - PYTEST_MONITOR_DB_NAME: postgres + - PYTEST_MONITOR_DB_USER: postgres + - PYTEST_MONITOR_DB_PASSWORD: testing_db + - PYTEST_MONITOR_DB_HOST: localhost + - PYTEST_MONITOR_DB_PORT: 5432 + +services: + - name: postgres:16 + stages: - test - deploy diff --git a/AUTHORS b/AUTHORS index 3a4ff25..6414267 100644 --- a/AUTHORS +++ b/AUTHORS @@ -3,4 +3,5 @@ Project developed and lead by Jean-Sébastine Dieu. Contributors include: - Raymond Gauthier (jraygauthier) added Python 3.5 support. - Kyle Altendorf (altendky) fixed bugs on session teardown - - Hannes Engelhardt (veritogen) added Bitbucket CI support. + - Hannes Engelhardt (veritogen) added Bitbucket CI support and Postgres DB Handler. + - Lucas Haupt (lhpt2) added Postgres DB Handler. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1e5a9b0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +# Use postgres/example user/password credentials +services: + + db: + image: postgres + restart: always + # set shared memory limit when using docker-compose + shm_size: 128mb + # or set shared memory limit when deploy via swarm stack + #volumes: + # - type: tmpfs + # target: /dev/shm + # tmpfs: + # size: 134217728 # 128*2^20 bytes = 128Mb + environment: + POSTGRES_PASSWORD: testing_db + ports: + - 5432:5432 + command: [ "postgres", "-c", "log_statement=all" ] + logging: + driver: "json-file" + options: + max-size: "50m" + + adminer: + image: adminer + restart: always + ports: + - 8080:8080 diff --git a/docs/sources/changelog.rst b/docs/sources/changelog.rst index 769dd6a..fa20add 100644 --- a/docs/sources/changelog.rst +++ b/docs/sources/changelog.rst @@ -3,8 +3,8 @@ Changelog ========= * :release:`to be discussed` +* :feature: `#77` Add a PostgreSQL backend implementation to optionally use a PostgreSQL Database for test metric logging. * :feature:`#75` Automatically gather CI build information for Bitbucket CI. - * :release:`1.6.6 <2023-05-06>` * :bug:`#64` Prepare version 1.7.0 of pytest-monitor. Last version to support Python <= 3.7 and all pytest <= 5.* * :bug:`#0` Improve and fix some CI issues, notably one that may cause python to not be the requested one but a more recent one. diff --git a/docs/sources/introduction.rst b/docs/sources/introduction.rst index 72add78..cda1071 100644 --- a/docs/sources/introduction.rst +++ b/docs/sources/introduction.rst @@ -28,4 +28,4 @@ Extending your application with new features, or fixing its bugs, might have an Usage ----- -Simply run pytest as usual: pytest-monitor is active by default as soon as it is installed. After running your first session, a .pymon sqlite database will be accessible in the directory where pytest was run. +Simply run pytest as usual: pytest-monitor is active by default as soon as it is installed. After running your first session, a .pymon sqlite database (or optionally another database implementation like PostgreSQL) will be accessible in the directory where pytest was run. diff --git a/docs/sources/operating.rst b/docs/sources/operating.rst index 8de5b7d..da11b8c 100644 --- a/docs/sources/operating.rst +++ b/docs/sources/operating.rst @@ -19,6 +19,26 @@ You are free to override the name of this database by setting the `--db` option: pytest --db /path/to/your/monitor/database +There is also a PostgreSQL implementation that talks to a PostgreSQL database. +The `--use-postgres` option is set for using PostgreSQL as database. + +.. code-block:: shell + + pytest --use-postgres + +The connection parameters are set by the following environment variables: + +PYTEST_MONITOR_DB_HOST + The hostname of the instance running the PostgreSQL server +PYTEST_MONITOR_DB_PORT + The port the PostgreSQL server listens on. +PYTEST_MONITOR_DB_NAME + The name of the database to connect to. +PYTEST_MONITOR_DB_USER + The name of the user to log into the database as. +PYTEST_MONITOR_DB_PASSWORD + The password to log into the database. + You can also sends your tests result to a monitor server (under development at that time) in order to centralize your Metrics and Execution Context (see below): diff --git a/pyproject.toml b/pyproject.toml index ed2d9df..9f0b952 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,12 @@ dev = [ "flake8-pyproject==1.2.3", "pre-commit==3.3.3" ] +psycopg = [ + "psycopg" +] +psycopg2 = [ + "psycopg2" +] [tool.flake8] max-line-length = 120 diff --git a/pytest_monitor/handler.py b/pytest_monitor/handler.py index 6aaa208..05be172 100644 --- a/pytest_monitor/handler.py +++ b/pytest_monitor/handler.py @@ -1,12 +1,24 @@ +import os import sqlite3 +try: + import psycopg +except ImportError: + import psycopg2 as psycopg -class DBHandler: + +class SqliteDBHandler: def __init__(self, db_path): self.__db = db_path self.__cnx = sqlite3.connect(self.__db) if db_path else None self.prepare() + def close(self): + self.__cnx.close() + + def __del__(self): + self.__cnx.close() + def query(self, what, bind_to, many=False): cursor = self.__cnx.cursor() cursor.execute(what, bind_to) @@ -15,7 +27,8 @@ def query(self, what, bind_to, many=False): def insert_session(self, h, run_date, scm_id, description): with self.__cnx: self.__cnx.execute( - "insert into TEST_SESSIONS(SESSION_H, RUN_DATE, SCM_ID, RUN_DESCRIPTION)" " values (?,?,?,?)", + "insert into TEST_SESSIONS(SESSION_H, RUN_DATE, SCM_ID, RUN_DESCRIPTION)" + " values (?,?,?,?)", (h, run_date, scm_id, description), ) @@ -131,3 +144,186 @@ def prepare(self): """ ) self.__cnx.commit() + + def get_env_id(self, env_hash): + query_result = self.query( + "SELECT ENV_H FROM EXECUTION_CONTEXTS WHERE ENV_H= ?", (env_hash,) + ) + return query_result[0] if query_result else None + + +class PostgresDBHandler: + def __init__(self): + self.__db = os.getenv("PYTEST_MONITOR_DB_NAME") + if not self.__db: + raise Exception( + "Please provide the postgres db name using the PYTEST_MONITOR_DB_NAME environment variable." + ) + self.__user = os.getenv("PYTEST_MONITOR_DB_USER") + if not self.__user: + raise Exception( + "Please provide the postgres user name using the PYTEST_MONITOR_DB_USER environment variable." + ) + self.__password = os.getenv("PYTEST_MONITOR_DB_PASSWORD") + if not self.__password: + raise Exception( + "Please provide the postgres user password using the PYTEST_MONITOR_DB_PASSWORD environment variable." + ) + self.__host = os.getenv("PYTEST_MONITOR_DB_HOST") + if not self.__host: + raise Exception( + "Please provide the postgres hostname using the PYTEST_MONITOR_DB_HOST environment variable." + ) + self.__port = os.getenv("PYTEST_MONITOR_DB_PORT") + if not self.__port: + raise Exception( + "Please provide the postgres port using the PYTEST_MONITOR_DB_PORT environment variable." + ) + self.__cnx = self.connect() + self.prepare() + + def __del__(self): + self.__cnx.close() + + def close(self): + self.__cnx.close() + + def connect(self): + connection_string = ( + f"dbname='{self.__db}' user='{self.__user}' password='{self.__password}' " + + f"host='{self.__host}' port='{self.__port}'" + ) + return psycopg.connect(connection_string) + + def query(self, what, bind_to, many=False): + cursor = self.__cnx.cursor() + cursor.execute(what, bind_to) + return cursor.fetchall() if many else cursor.fetchone() + + def insert_session(self, h, run_date, scm_id, description): + self.__cnx.cursor().execute( + "insert into TEST_SESSIONS(SESSION_H, RUN_DATE, SCM_ID, RUN_DESCRIPTION)" + " values (%s,%s,%s,%s)", + (h, run_date, scm_id, description), + ) + self.__cnx.commit() + + def insert_metric( + self, + session_id, + env_id, + item_start_date, + item, + item_path, + item_variant, + item_loc, + kind, + component, + total_time, + user_time, + kernel_time, + cpu_usage, + mem_usage, + ): + self.__cnx.cursor().execute( + "insert into TEST_METRICS(SESSION_H,ENV_H,ITEM_START_TIME,ITEM," + "ITEM_PATH,ITEM_VARIANT,ITEM_FS_LOC,KIND,COMPONENT,TOTAL_TIME," + "USER_TIME,KERNEL_TIME,CPU_USAGE,MEM_USAGE) " + "values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", + ( + session_id, + env_id, + item_start_date, + item, + item_path, + item_variant, + item_loc, + kind, + component, + total_time, + user_time, + kernel_time, + cpu_usage, + mem_usage, + ), + ) + self.__cnx.commit() + + def insert_execution_context(self, exc_context): + self.__cnx.cursor().execute( + "insert into EXECUTION_CONTEXTS(CPU_COUNT,CPU_FREQUENCY_MHZ,CPU_TYPE,CPU_VENDOR," + "RAM_TOTAL_MB,MACHINE_NODE,MACHINE_TYPE,MACHINE_ARCH,SYSTEM_INFO," + "PYTHON_INFO,ENV_H) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", + ( + exc_context.cpu_count, + exc_context.cpu_frequency, + exc_context.cpu_type, + exc_context.cpu_vendor, + exc_context.ram_total, + exc_context.fqdn, + exc_context.machine, + exc_context.architecture, + exc_context.system_info, + exc_context.python_info, + exc_context.compute_hash(), + ), + ) + self.__cnx.commit() + + def prepare(self): + cursor = self.__cnx.cursor() + cursor.execute( + """ +CREATE TABLE IF NOT EXISTS TEST_SESSIONS( + SESSION_H varchar(64) primary key not null unique, -- Session identifier + RUN_DATE varchar(64), -- Date of test run + SCM_ID varchar(128), -- SCM change id + RUN_DESCRIPTION json +);""" + ) + cursor.execute( + """ +CREATE TABLE IF NOT EXISTS EXECUTION_CONTEXTS ( + ENV_H varchar(64) primary key not null unique, + CPU_COUNT integer, + CPU_FREQUENCY_MHZ integer, + CPU_TYPE varchar(64), + CPU_VENDOR varchar(256), + RAM_TOTAL_MB integer, + MACHINE_NODE varchar(512), + MACHINE_TYPE varchar(32), + MACHINE_ARCH varchar(16), + SYSTEM_INFO varchar(256), + PYTHON_INFO varchar(512) +); +""" + ) + cursor.execute( + """ +CREATE TABLE IF NOT EXISTS TEST_METRICS ( + SESSION_H varchar(64), -- Session identifier + ENV_H varchar(64), -- Environment description identifier + ITEM_START_TIME varchar(64), -- Effective start time of the test + ITEM_PATH varchar(4096), -- Path of the item, following Python import specification + ITEM varchar(2048), -- Name of the item + ITEM_VARIANT varchar(2048), -- Optional parametrization of an item. + ITEM_FS_LOC varchar(2048), -- Relative path from pytest invocation directory to the item's module. + KIND varchar(64), -- Package, Module or function + COMPONENT varchar(512) NULL, -- Tested component if any + TOTAL_TIME float, -- Total time spent running the item + USER_TIME float, -- time spent in user space + KERNEL_TIME float, -- time spent in kernel space + CPU_USAGE float, -- cpu usage + MEM_USAGE float, -- Max resident memory used. + FOREIGN KEY (ENV_H) REFERENCES EXECUTION_CONTEXTS(ENV_H), + FOREIGN KEY (SESSION_H) REFERENCES TEST_SESSIONS(SESSION_H) +);""" + ) + + self.__cnx.commit() + + def get_env_id(self, env_hash): + query_result = self.query( + "select ENV_H from EXECUTION_CONTEXTS where ENV_H = %s", (env_hash,) + ) + return query_result[0] if query_result else None diff --git a/pytest_monitor/pytest_monitor.py b/pytest_monitor/pytest_monitor.py index 3e9c12c..dab24f4 100644 --- a/pytest_monitor/pytest_monitor.py +++ b/pytest_monitor/pytest_monitor.py @@ -22,7 +22,9 @@ "monitor_test_if": (True, "monitor_force_test", lambda x: bool(x), False), } PYTEST_MONITOR_DEPRECATED_MARKERS = {} -PYTEST_MONITOR_ITEM_LOC_MEMBER = "_location" if tuple(pytest.__version__.split(".")) < ("5", "3") else "location" +PYTEST_MONITOR_ITEM_LOC_MEMBER = ( + "_location" if tuple(pytest.__version__.split(".")) < ("5", "3") else "location" +) PYTEST_MONITORING_ENABLED = True @@ -44,7 +46,9 @@ def pytest_addoption(parser): help="Set this option to distinguish parametrized tests given their values." " This requires the parameters to be stringifiable.", ) - group.addoption("--no-monitor", action="store_true", dest="mtr_none", help="Disable all traces") + group.addoption( + "--no-monitor", action="store_true", dest="mtr_none", help="Disable all traces" + ) group.addoption( "--remote-server", action="store", @@ -64,17 +68,26 @@ def pytest_addoption(parser): dest="mtr_no_db", help="Do not store results in local db.", ) + group.addoption( + "--use-postgres", + action="store_true", + dest="mtr_use_postgres", + default=False, + help="Use postgres as the database for storing results.", + ) group.addoption( "--force-component", action="store", dest="mtr_force_component", - help="Force the component to be set at the given value for the all tests run" " in this session.", + help="Force the component to be set at the given value for the all tests run" + " in this session.", ) group.addoption( "--component-prefix", action="store", dest="mtr_component_prefix", - help="Prefix each found components with the given value (applies to all tests" " run in this session).", + help="Prefix each found components with the given value (applies to all tests" + " run in this session).", ) group.addoption( "--no-gc", @@ -99,10 +112,13 @@ def pytest_addoption(parser): def pytest_configure(config): - config.addinivalue_line("markers", "monitor_skip_test: mark test to be executed but not monitored.") + config.addinivalue_line( + "markers", "monitor_skip_test: mark test to be executed but not monitored." + ) config.addinivalue_line( "markers", - "monitor_skip_test_if(cond): mark test to be executed but " "not monitored if cond is verified.", + "monitor_skip_test_if(cond): mark test to be executed but " + "not monitored if cond is verified.", ) config.addinivalue_line( "markers", @@ -126,14 +142,24 @@ def pytest_runtest_setup(item): """ if not PYTEST_MONITORING_ENABLED: return - item_markers = {mark.name: mark for mark in item.iter_markers() if mark and mark.name.startswith("monitor_")} + item_markers = { + mark.name: mark + for mark in item.iter_markers() + if mark and mark.name.startswith("monitor_") + } mark_to_del = [] for set_marker in item_markers.keys(): if set_marker not in PYTEST_MONITOR_VALID_MARKERS: - warnings.warn("Nothing known about marker {}. Marker will be dropped.".format(set_marker)) + warnings.warn( + "Nothing known about marker {}. Marker will be dropped.".format( + set_marker + ) + ) mark_to_del.append(set_marker) if set_marker in PYTEST_MONITOR_DEPRECATED_MARKERS: - warnings.warn(f"Marker {set_marker} is deprecated. Consider upgrading your tests") + warnings.warn( + f"Marker {set_marker} is deprecated. Consider upgrading your tests" + ) for marker in mark_to_del: del item_markers[marker] @@ -202,18 +228,34 @@ def wrapped_function(): except Exception: raise except BaseException as e: + # this is a workaround to fix the faulty behavior of the memory profiler + # that only catches Exceptions but should catch BaseExceptions instead + # actually BaseExceptions should be raised here, but without modifications + # of the memory profiler (like proposed in PR + # https://github.com/CFMTech/pytest-monitor/pull/82 ) this problem + # can just be worked around like so (BaseException can only come through + # this way) return e def prof(): - m = memory_profiler.memory_usage((wrapped_function, ()), max_iterations=1, max_usage=True, retval=True) + m = memory_profiler.memory_usage( + (wrapped_function, ()), max_iterations=1, max_usage=True, retval=True + ) if isinstance(m[1], BaseException): # Do we have any outcome? raise m[1] - memuse = m[0][0] if type(m[0]) is list else m[0] + memuse = m[0][0] if isinstance(m[0], list) else m[0] setattr(pyfuncitem, "mem_usage", memuse) setattr(pyfuncitem, "monitor_results", True) if not PYTEST_MONITORING_ENABLED: - wrapped_function() + try: + # this is a workaround to fix the faulty behavior of the memory profiler + # that only catches Exceptions but should catch BaseExceptions instead + e = wrapped_function() + if isinstance(e, BaseException): + raise e + except BaseException: + raise else: if not pyfuncitem.session.config.option.mtr_disable_gc: gc.collect() @@ -233,28 +275,61 @@ def pytest_sessionstart(session): Instantiate a monitor session to save collected metrics. We yield at the end to let pytest pursue the execution. """ - if session.config.option.mtr_force_component and session.config.option.mtr_component_prefix: - raise pytest.UsageError("Invalid usage: --force-component and --component-prefix are incompatible options!") - if session.config.option.mtr_no_db and not session.config.option.mtr_remote and not session.config.option.mtr_none: - warnings.warn("pytest-monitor: No storage specified but monitoring is requested. Disabling monitoring.") + if ( + session.config.option.mtr_force_component + and session.config.option.mtr_component_prefix + ): + raise pytest.UsageError( + "Invalid usage: --force-component and --component-prefix are incompatible options!" + ) + if ( + session.config.option.mtr_no_db + and not session.config.option.mtr_remote + and not session.config.option.mtr_none + ): + warnings.warn( + "pytest-monitor: No storage specified but monitoring is requested. Disabling monitoring." + ) session.config.option.mtr_none = True - component = session.config.option.mtr_force_component or session.config.option.mtr_component_prefix + component = ( + session.config.option.mtr_force_component + or session.config.option.mtr_component_prefix + ) if session.config.option.mtr_component_prefix: component += ".{user_component}" if not component: component = "{user_component}" db = ( None - if (session.config.option.mtr_none or session.config.option.mtr_no_db) + if ( + session.config.option.mtr_none + or session.config.option.mtr_no_db + or session.config.option.mtr_use_postgres + ) else session.config.option.mtr_db_out ) - remote = None if session.config.option.mtr_none else session.config.option.mtr_remote + remote = ( + None if session.config.option.mtr_none else session.config.option.mtr_remote + ) session.pytest_monitor = PyTestMonitorSession( - db=db, remote=remote, component=component, scope=session.config.option.mtr_scope + db=db, + use_postgres=session.config.option.mtr_use_postgres, + remote=remote, + component=component, + scope=session.config.option.mtr_scope, ) global PYTEST_MONITORING_ENABLED PYTEST_MONITORING_ENABLED = not session.config.option.mtr_none - session.pytest_monitor.compute_info(session.config.option.mtr_description, session.config.option.mtr_tags) + session.pytest_monitor.compute_info( + session.config.option.mtr_description, session.config.option.mtr_tags + ) + yield + + +@pytest.hookimpl(hookwrapper=True) +def pytest_sessionfinish(session): + if session.pytest_monitor is not None: + session.pytest_monitor.close() yield @@ -295,7 +370,9 @@ def _prf_tracer(request): ptimes_a = request.session.pytest_monitor.process.cpu_times() yield ptimes_b = request.session.pytest_monitor.process.cpu_times() - if not request.node.monitor_skip_test and getattr(request.node, "monitor_results", False): + if not request.node.monitor_skip_test and getattr( + request.node, "monitor_results", False + ): item_name = request.node.originalname or request.node.name item_loc = getattr(request.node, PYTEST_MONITOR_ITEM_LOC_MEMBER)[0] request.session.pytest_monitor.add_test_info( diff --git a/pytest_monitor/session.py b/pytest_monitor/session.py index 677362c..c988074 100644 --- a/pytest_monitor/session.py +++ b/pytest_monitor/session.py @@ -9,7 +9,7 @@ import psutil import requests -from pytest_monitor.handler import DBHandler +from pytest_monitor.handler import PostgresDBHandler, SqliteDBHandler from pytest_monitor.sys_utils import ( ExecutionContext, collect_ci_info, @@ -18,10 +18,20 @@ class PyTestMonitorSession: - def __init__(self, db=None, remote=None, component="", scope=None, tracing=True): + def __init__( + self, + db=None, + use_postgres=False, + remote=None, + component="", + scope=None, + tracing=True, + ): self.__db = None - if db: - self.__db = DBHandler(db) + if use_postgres: + self.__db = PostgresDBHandler() + elif db: + self.__db = SqliteDBHandler(db) self.__monitor_enabled = tracing self.__remote = remote self.__component = component @@ -31,6 +41,10 @@ def __init__(self, db=None, remote=None, component="", scope=None, tracing=True) self.__mem_usage_base = None self.__process = psutil.Process(os.getpid()) + def close(self): + if self.__db is not None: + self.__db.close() + @property def monitoring_enabled(self): return self.__monitor_enabled @@ -50,7 +64,7 @@ def process(self): def get_env_id(self, env): db, remote = None, None if self.__db: - row = self.__db.query("SELECT ENV_H FROM EXECUTION_CONTEXTS WHERE ENV_H= ?", (env.compute_hash(),)) + row = self.__db.get_env_id(env.compute_hash()) db = row[0] if row else None if self.__remote: r = requests.get(f"{self.__remote}/contexts/{env.compute_hash()}") @@ -76,7 +90,7 @@ def compute_info(self, description, tags): if description: d["description"] = description for tag in tags: - if type(tag) is str: + if isinstance(tag, str): _tag_info = tag.split("=", 1) d[_tag_info[0]] = _tag_info[1] else: @@ -109,12 +123,14 @@ def set_environment_info(self, env): db_id, remote_id = self.__eid if self.__db and db_id is None: self.__db.insert_execution_context(env) - db_id = self.__db.query("select ENV_H from EXECUTION_CONTEXTS where ENV_H = ?", (env.compute_hash(),))[0] + db_id = self.__db.get_env_id(env.compute_hash()) if self.__remote and remote_id is None: # We must postpone that to be run at the end of the pytest session. r = requests.post(f"{self.__remote}/contexts/", json=env.to_dict()) if r.status_code != HTTPStatus.CREATED: - warnings.warn(f"Cannot insert execution context in remote server (rc={r.status_code}! Deactivating...") + warnings.warn( + f"Cannot insert execution context in remote server (rc={r.status_code}! Deactivating..." + ) self.__remote = "" else: remote_id = json.loads(r.text)["h"] @@ -124,8 +140,10 @@ def prepare(self): def dummy(): return True - memuse = memory_profiler.memory_usage((dummy,), max_iterations=1, max_usage=True) - self.__mem_usage_base = memuse[0] if type(memuse) is list else memuse + memuse = memory_profiler.memory_usage( + (dummy,), max_iterations=1, max_usage=True + ) + self.__mem_usage_base = memuse[0] if isinstance(memuse, list) else memuse def add_test_info( self, diff --git a/requirements.dev.txt b/requirements.dev.txt index f0d0810..467ea58 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -4,11 +4,12 @@ pytest requests black isort -flake8=6.1.0 -flake8-builtins=2.1.0 -flake8-simplify=0.19.3 -flake8-comprehensions=3.10.1 -flake8-pytest-style=1.6.0 -flake8-return=1.2.0 -flake8-pyproject=1.2.3 -pre-commit=3.3.3 \ No newline at end of file +flake8==6.1.0 +flake8-builtins==2.1.0 +flake8-simplify==0.19.3 +flake8-comprehensions==3.10.1 +flake8-pytest-style==1.6.0 +flake8-return==1.2.0 +flake8-pyproject==1.2.3 +pre-commit==3.3.3 +mock>=5.1.0 diff --git a/requirements.txt b/requirements.txt index 5182b1c..141c648 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ psutil>=5.1.0 memory_profiler>=0.58 pytest requests +psycopg diff --git a/tests/test_monitor_handler.py b/tests/test_monitor_handler.py new file mode 100644 index 0000000..21b1532 --- /dev/null +++ b/tests/test_monitor_handler.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +import os +import sqlite3 +import sys +import pytest + +try: + import psycopg +except ImportError: + import psycopg2 as psycopg + +from pytest_monitor.handler import PostgresDBHandler, SqliteDBHandler + + +# helper function +def reset_db(db_context: psycopg.Connection | sqlite3.Connection): + """Empty all tables inside the database to provide a clean slate for the next test.""" + # cleanup_cursor.execute("DROP DATABASE postgres") + # cleanup_cursor.execute("CREATE DATABASE postgres") + cleanup_cursor = db_context.cursor() + cleanup_cursor.execute("DROP TABLE IF EXISTS TEST_METRICS") + cleanup_cursor.execute("DROP TABLE IF EXISTS TEST_SESSIONS") + cleanup_cursor.execute("DROP TABLE IF EXISTS EXECUTION_CONTEXTS") + db_context.commit() + cleanup_cursor.close() + + # cleanup_cursor.execute("CREATE SCHEMA public;") + # cleanup_cursor.execute("ALTER DATABASE postgres SET search_path TO public;") + # cleanup_cursor.execute("ALTER ROLE postgres SET search_path TO public;") + # cleanup_cursor.execute("ALTER SCHEMA public OWNER to postgres;") + # cleanup_cursor.execute("GRANT ALL ON SCHEMA public TO postgres;") + # cleanup_cursor.execute("GRANT ALL ON SCHEMA public TO public;") + + +@pytest.fixture() +def connected_PostgresDBHandler(): + """Provide a DBHandler connected to a Postgres database.""" + os.environ["PYTEST_MONITOR_DB_NAME"] = "postgres" + os.environ["PYTEST_MONITOR_DB_USER"] = "postgres" + os.environ["PYTEST_MONITOR_DB_PASSWORD"] = "testing_db" + os.environ["PYTEST_MONITOR_DB_HOST"] = "localhost" + os.environ["PYTEST_MONITOR_DB_PORT"] = "5432" + db = PostgresDBHandler() + yield db + reset_db(db._PostgresDBHandler__cnx) + db._PostgresDBHandler__cnx.close() + + +def test_sqlite_handler(): + """Test for working sqlite database""" + # db handler + db = SqliteDBHandler(":memory:") + session, metrics, exc_context = db.query( + "SELECT name FROM sqlite_master where type='table'", (), many=True + ) + assert session[0] == "TEST_SESSIONS" + assert metrics[0] == "TEST_METRICS" + assert exc_context[0] == "EXECUTION_CONTEXTS" + + +def test_postgres_handler(connected_PostgresDBHandler): + """Test for working postgres database""" + db = connected_PostgresDBHandler + tables = db.query( + "SELECT tablename FROM pg_tables where schemaname='public'", + (), + many=True, + ) + tables = [table for (table,) in tables] + try: + assert "test_sessions" in tables + assert "test_metrics" in tables + assert "execution_contexts" in tables + except Exception as e: + print( + "There might be no postgresql database available, consider using docker containers in project", + file=sys.stderr, + ) + raise e diff --git a/tests/test_monitor_session.py b/tests/test_monitor_session.py new file mode 100644 index 0000000..998d6e8 --- /dev/null +++ b/tests/test_monitor_session.py @@ -0,0 +1,41 @@ +import os + +import pytest + +from pytest_monitor.session import PyTestMonitorSession + + +@pytest.fixture() +def _setup_environment_postgres(): + """Fixture to set environment variables for postgres connection.""" + os.environ["PYTEST_MONITOR_DB_NAME"] = "postgres" + os.environ["PYTEST_MONITOR_DB_USER"] = "postgres" + os.environ["PYTEST_MONITOR_DB_PASSWORD"] = "testing_db" + os.environ["PYTEST_MONITOR_DB_HOST"] = "localhost" + os.environ["PYTEST_MONITOR_DB_PORT"] = "5432" + + +@pytest.mark.usefixtures("_setup_environment_postgres") +def test_pytestmonitorsession_close_connection(): + """Test to check properly closed database connection""" + session = PyTestMonitorSession(":memory:") + db = session._PyTestMonitorSession__db + + try: + db.query("SELECT * FROM sqlite_master LIMIT 1", ()) + except Exception: + pytest.fail("Database should be available") + + session.close() + + try: + db.query("SELECT * FROM sqlite_master LIMIT 1", ()) + pytest.fail("Database should not be available anymore") + except Exception: + assert True + + session = PyTestMonitorSession(use_postgres=True) + db = session._PyTestMonitorSession__db + assert db._PostgresDBHandler__cnx.closed == 0 + session.close() + assert db._PostgresDBHandler__cnx.closed > 0