From 45d8c1ee2762e78d9c514504b6c7035ba8f83573 Mon Sep 17 00:00:00 2001 From: Hannes Engelhardt Date: Fri, 15 Dec 2023 11:07:37 +0100 Subject: [PATCH 01/29] working example --- pytest_monitor/handler.py | 114 ++++++++++++++++++++++++++++++- pytest_monitor/pytest_monitor.py | 10 ++- pytest_monitor/session.py | 14 ++-- 3 files changed, 129 insertions(+), 9 deletions(-) diff --git a/pytest_monitor/handler.py b/pytest_monitor/handler.py index 6aaa208..b23c103 100644 --- a/pytest_monitor/handler.py +++ b/pytest_monitor/handler.py @@ -1,7 +1,9 @@ import sqlite3 +import os +import psycopg2 -class DBHandler: +class SqliteDBHandler: def __init__(self, db_path): self.__db = db_path self.__cnx = sqlite3.connect(self.__db) if db_path else None @@ -131,3 +133,113 @@ def prepare(self): """ ) self.__cnx.commit() + + def get_env_id(self, env_hash): + return self.query("SELECT ENV_H FROM EXECUTION_CONTEXTS WHERE ENV_H= ?", (env_hash,)) + + +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 connect(self): + connection_string = f"dbname='{self.__db}' user='{self.__user}' password='{self.__password}' host='{self.__host}' port='{self.__port}'" + return psycopg2.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): + with self.__cnx: + 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)) + + 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): + with self.__cnx: + 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)) + + def insert_execution_context(self, exc_context): + with self.__cnx: + 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())) + + 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): + return self.query('select ENV_H from EXECUTION_CONTEXTS where ENV_H = %s', (env_hash,)) + \ No newline at end of file diff --git a/pytest_monitor/pytest_monitor.py b/pytest_monitor/pytest_monitor.py index 3e9c12c..81166c3 100644 --- a/pytest_monitor/pytest_monitor.py +++ b/pytest_monitor/pytest_monitor.py @@ -64,6 +64,12 @@ 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", @@ -245,12 +251,12 @@ def pytest_sessionstart(session): 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 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 diff --git a/pytest_monitor/session.py b/pytest_monitor/session.py index 677362c..d42591f 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 SqliteDBHandler, PostgresDBHandler from pytest_monitor.sys_utils import ( ExecutionContext, collect_ci_info, @@ -18,10 +18,12 @@ 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 @@ -50,7 +52,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()}") @@ -109,7 +111,7 @@ 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()) From 2df836ca5a0713b17e6acaf8f8787a2d8a47b4ee Mon Sep 17 00:00:00 2001 From: Hannes Engelhardt Date: Wed, 20 Dec 2023 10:54:49 +0100 Subject: [PATCH 02/29] Fix bug returning returning a tuple instead of string --- pytest_monitor/handler.py | 136 +++++++++++++++++++++++++++----------- 1 file changed, 97 insertions(+), 39 deletions(-) diff --git a/pytest_monitor/handler.py b/pytest_monitor/handler.py index b23c103..612af75 100644 --- a/pytest_monitor/handler.py +++ b/pytest_monitor/handler.py @@ -1,8 +1,9 @@ +import os import sqlite3 -import os import psycopg2 + class SqliteDBHandler: def __init__(self, db_path): self.__db = db_path @@ -135,31 +136,41 @@ def prepare(self): self.__cnx.commit() def get_env_id(self, env_hash): - return self.query("SELECT ENV_H FROM EXECUTION_CONTEXTS WHERE ENV_H= ?", (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') + 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') + 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') + 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') + 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') + 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 connect(self): - connection_string = f"dbname='{self.__db}' user='{self.__user}' password='{self.__password}' host='{self.__host}' port='{self.__port}'" + connection_string = f"dbname='{self.__db}' user='{self.__user}' password='{self.__password}'" + f"host='{self.__host}' port='{self.__port}'" return psycopg2.connect(connection_string) def query(self, what, bind_to, many=False): @@ -169,41 +180,86 @@ def query(self, what, bind_to, many=False): def insert_session(self, h, run_date, scm_id, description): with self.__cnx: - 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.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), + ) - 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): + 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, + ): with self.__cnx: - 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.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, + ), + ) def insert_execution_context(self, exc_context): with self.__cnx: - 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.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(), + ), + ) def prepare(self): cursor = self.__cnx.cursor() - cursor.execute(''' + 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(''' +);""" + ) + cursor.execute( + """ CREATE TABLE IF NOT EXISTS EXECUTION_CONTEXTS ( ENV_H varchar(64) primary key not null unique, CPU_COUNT integer, @@ -217,8 +273,10 @@ def prepare(self): SYSTEM_INFO varchar(256), PYTHON_INFO varchar(512) ); -''') - cursor.execute(''' +""" + ) + cursor.execute( + """ CREATE TABLE IF NOT EXISTS TEST_METRICS ( SESSION_H varchar(64), -- Session identifier ENV_H varchar(64), -- Environment description identifier @@ -236,10 +294,10 @@ def prepare(self): 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): - return self.query('select ENV_H from EXECUTION_CONTEXTS where ENV_H = %s', (env_hash,)) - \ No newline at end of file + return self.query("select ENV_H from EXECUTION_CONTEXTS where ENV_H = %s", (env_hash,)) From 0b3685f45b01a892a0d436738597f2fc23218e2b Mon Sep 17 00:00:00 2001 From: Hannes Engelhardt Date: Wed, 20 Dec 2023 11:13:44 +0100 Subject: [PATCH 03/29] fix connection string --- pytest_monitor/handler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pytest_monitor/handler.py b/pytest_monitor/handler.py index 612af75..5409cbc 100644 --- a/pytest_monitor/handler.py +++ b/pytest_monitor/handler.py @@ -169,8 +169,10 @@ def __init__(self): self.prepare() def connect(self): - connection_string = f"dbname='{self.__db}' user='{self.__user}' password='{self.__password}'" - f"host='{self.__host}' port='{self.__port}'" + connection_string = ( + f"dbname='{self.__db}' user='{self.__user}' password='{self.__password}' " + + f"host='{self.__host}' port='{self.__port}'" + ) return psycopg2.connect(connection_string) def query(self, what, bind_to, many=False): From da6631468418c47ad35d25fd9611c687637793ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3=20=C3=81gila=20Bitsch?= Date: Tue, 7 May 2024 11:58:53 +0200 Subject: [PATCH 04/29] feat: allow and default to psycopg instead of psycopg2 --- pyproject.toml | 6 ++++++ pytest_monitor/handler.py | 7 +++++-- 2 files changed, 11 insertions(+), 2 deletions(-) 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 5409cbc..bc25485 100644 --- a/pytest_monitor/handler.py +++ b/pytest_monitor/handler.py @@ -1,7 +1,10 @@ import os import sqlite3 -import psycopg2 +try: + import psycopg +except ImportError: + import psycopg2 as psycopg class SqliteDBHandler: @@ -173,7 +176,7 @@ def connect(self): f"dbname='{self.__db}' user='{self.__user}' password='{self.__password}' " + f"host='{self.__host}' port='{self.__port}'" ) - return psycopg2.connect(connection_string) + return psycopg.connect(connection_string) def query(self, what, bind_to, many=False): cursor = self.__cnx.cursor() From 45d29b664f2c19e3b3e449b35492fa0e95070e20 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 4 Jun 2024 10:25:06 +0200 Subject: [PATCH 05/29] Refactor handler.py: Add destructors for db handlers in order to properly close the connections on drop/garbage collection --- pytest_monitor/handler.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pytest_monitor/handler.py b/pytest_monitor/handler.py index bc25485..b77c435 100644 --- a/pytest_monitor/handler.py +++ b/pytest_monitor/handler.py @@ -13,6 +13,9 @@ def __init__(self, db_path): self.__cnx = sqlite3.connect(self.__db) if db_path else None self.prepare() + def __del__(self): + self.__cnx.close() + def query(self, what, bind_to, many=False): cursor = self.__cnx.cursor() cursor.execute(what, bind_to) @@ -170,6 +173,9 @@ def __init__(self): 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 connect(self): connection_string = ( From 05b5f7f51563a1c40ac831c958ac94183d769338 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 4 Jun 2024 10:25:06 +0200 Subject: [PATCH 06/29] Refactor handler.py: Add destructors for db handlers in order to properly close the connections on drop/garbage collection --- pytest_monitor/handler.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pytest_monitor/handler.py b/pytest_monitor/handler.py index bc25485..e29306b 100644 --- a/pytest_monitor/handler.py +++ b/pytest_monitor/handler.py @@ -13,6 +13,9 @@ def __init__(self, db_path): self.__cnx = sqlite3.connect(self.__db) if db_path else None self.prepare() + def __del__(self): + self.__cnx.close() + def query(self, what, bind_to, many=False): cursor = self.__cnx.cursor() cursor.execute(what, bind_to) @@ -171,6 +174,11 @@ def __init__(self): self.__cnx = self.connect() self.prepare() + + def __del__(self): + self.__cnx.close() + + def connect(self): connection_string = ( f"dbname='{self.__db}' user='{self.__user}' password='{self.__password}' " From b54b24555fb296c0e0391c21ced5abfc291c9d50 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 4 Jun 2024 10:45:12 +0200 Subject: [PATCH 07/29] Refactor handler.py: Remove duplicate destructor in PostgresDBHandler. --- pytest_monitor/handler.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pytest_monitor/handler.py b/pytest_monitor/handler.py index ba67ab4..3e6879c 100644 --- a/pytest_monitor/handler.py +++ b/pytest_monitor/handler.py @@ -178,10 +178,6 @@ def __del__(self): self.__cnx.close() - def __del__(self): - self.__cnx.close() - - def connect(self): connection_string = ( f"dbname='{self.__db}' user='{self.__user}' password='{self.__password}' " From 02e5b4a9e3f9dec56b62f091f4593ba04d40f75d Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Mon, 10 Jun 2024 15:28:10 +0200 Subject: [PATCH 08/29] Fix: Fix 3 issues: with-context, db-close and env-id problem with-context: Remove the with-contexts inside the PostgresDBHandler and do manual commit after cursor execution instead to prevent closing of the database connection. db-close: Add close() function to both db-handlers and the PytestMonitorSession class in order to properly close all database connections at the end of the tests. This happens in pytest-monitor.py with the pytest_sessionfinish hook implementation that calls the close() function on the PytestMonitorSession object. env-id: The query for the env-id PostgresDBHandler.get_env_id() queries for the env id in the database and gets a tuple from the query but does return it without unwrapping the value inside. This leads to problems as the value is meant to be unwrapped, therefore return the unwrapped value. --- pytest_monitor/handler.py | 117 +++++++++++++++++-------------- pytest_monitor/pytest_monitor.py | 103 +++++++++++++++++++++------ pytest_monitor/session.py | 24 +++++-- 3 files changed, 164 insertions(+), 80 deletions(-) diff --git a/pytest_monitor/handler.py b/pytest_monitor/handler.py index 3e6879c..dc5b00b 100644 --- a/pytest_monitor/handler.py +++ b/pytest_monitor/handler.py @@ -13,6 +13,9 @@ def __init__(self, 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() @@ -24,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), ) @@ -142,7 +146,9 @@ 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,)) + query_result = self.query( + "SELECT ENV_H FROM EXECUTION_CONTEXTS WHERE ENV_H= ?", (env_hash,) + ) return query_result[0] if query_result else None @@ -170,14 +176,15 @@ def __init__(self): ) 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.") + 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 connect(self): connection_string = ( f"dbname='{self.__db}' user='{self.__user}' password='{self.__password}' " @@ -191,11 +198,12 @@ def query(self, what, bind_to, many=False): return cursor.fetchall() if many else cursor.fetchone() def insert_session(self, h, run_date, scm_id, description): - with self.__cnx: - 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.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, @@ -214,50 +222,50 @@ def insert_metric( cpu_usage, mem_usage, ): - with self.__cnx: - 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.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): - with self.__cnx: - 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.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() @@ -312,4 +320,7 @@ def prepare(self): self.__cnx.commit() def get_env_id(self, env_hash): - return self.query("select ENV_H from EXECUTION_CONTEXTS where ENV_H = %s", (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 81166c3..740858c 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,23 +68,26 @@ def pytest_addoption(parser): dest="mtr_no_db", help="Do not store results in local db.", ) - group.addoption("--use-postgres", + group.addoption( + "--use-postgres", action="store_true", dest="mtr_use_postgres", default=False, - help="Use postgres as the database for storing results." - ) + 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", @@ -105,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", @@ -132,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] @@ -211,7 +231,9 @@ def wrapped_function(): 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] @@ -239,28 +261,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 or session.config.option.mtr_use_postgres) + 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, use_postgres=session.config.option.mtr_use_postgres, 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 @@ -301,7 +356,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 d42591f..e97d57c 100644 --- a/pytest_monitor/session.py +++ b/pytest_monitor/session.py @@ -9,7 +9,7 @@ import psutil import requests -from pytest_monitor.handler import SqliteDBHandler, PostgresDBHandler +from pytest_monitor.handler import PostgresDBHandler, SqliteDBHandler from pytest_monitor.sys_utils import ( ExecutionContext, collect_ci_info, @@ -18,7 +18,15 @@ class PyTestMonitorSession: - def __init__(self, db=None, use_postgres=False, 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 use_postgres: self.__db = PostgresDBHandler() @@ -33,6 +41,10 @@ def __init__(self, db=None, use_postgres=False, remote=None, component="", scope 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 @@ -116,7 +128,9 @@ def set_environment_info(self, env): # 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"] @@ -126,7 +140,9 @@ def prepare(self): def dummy(): return True - memuse = memory_profiler.memory_usage((dummy,), max_iterations=1, max_usage=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 def add_test_info( From b08232aed1be069821053c21616699eb141d0c4b Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Mon, 10 Jun 2024 15:34:43 +0200 Subject: [PATCH 09/29] Feature: Add Bitbucket integration in collect_ci_info() This is a helper function for generating a session description in session.py. --- pytest_monitor/sys_utils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pytest_monitor/sys_utils.py b/pytest_monitor/sys_utils.py index b07db51..2ed66dd 100644 --- a/pytest_monitor/sys_utils.py +++ b/pytest_monitor/sys_utils.py @@ -47,6 +47,13 @@ def collect_ci_info(): "pipeline_build_no": os.environ["CI_PIPELINE_ID"], "__ci__": "gitlabci", } + # Test for Bitbucket CI + if "BITBUCKET_BRANCH" in os.environ and "BITBUCKET_BUILD_NUMBER" in os.environ: + return { + "pipeline_branch": os.environ["BITBUCKET_BRANCH"], + "pipeline_build_no": os.environ["BITBUCKET_BUILD_NUMBER"], + "__ci__": "bitbucketci", + } return {} From 6cd3a908e593147e8a320dfafd9eb93be73dcdc2 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 11 Jun 2024 16:23:56 +0200 Subject: [PATCH 10/29] Update documentation to explain usage of PostgreSQL Handler --- docs/sources/introduction.rst | 2 +- docs/sources/operating.rst | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) 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): From a481510d4a3f621b9493281f82c22f7225b91852 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 11 Jun 2024 16:53:57 +0200 Subject: [PATCH 11/29] Update changelog.rst and add this feature. --- docs/sources/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sources/changelog.rst b/docs/sources/changelog.rst index 0b77e9e..72f761f 100644 --- a/docs/sources/changelog.rst +++ b/docs/sources/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= +* :feature: `#77` Add a PostgreSQL backend implementation to optionally use a PostgreSQL Database for test metric logging. * :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. From 425698686abdaa3c6e883b3287cd73bfc468f9b7 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 11 Jun 2024 19:05:24 +0200 Subject: [PATCH 12/29] Fix: Add close() function for proper closing of PostgreSQL connection after tests are done. --- pytest_monitor/handler.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytest_monitor/handler.py b/pytest_monitor/handler.py index dc5b00b..05be172 100644 --- a/pytest_monitor/handler.py +++ b/pytest_monitor/handler.py @@ -185,6 +185,9 @@ def __init__(self): 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}' " From 1520ac1aae841db130a598ed50402e770ecf6ba0 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 11 Jun 2024 19:06:13 +0200 Subject: [PATCH 13/29] Add tests for session and database implementations (dbhandlers) The session is tested for proper closing of the database connection. The close() function is called from the hookimplementation in pytest_sessionfinish(). The dbhandlers are tested for proper setup of the tables and in consequence a working connection. The PostgresDBHandler requires a running postgres server, that's what the docker-compose.yml file is for --- docker-compose.yml | 29 +++++++++++++ tests/test_monitor_handler.py | 82 +++++++++++++++++++++++++++++++++++ tests/test_monitor_session.py | 38 ++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 docker-compose.yml create mode 100644 tests/test_monitor_handler.py create mode 100644 tests/test_monitor_session.py 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/tests/test_monitor_handler.py b/tests/test_monitor_handler.py new file mode 100644 index 0000000..1ce9655 --- /dev/null +++ b/tests/test_monitor_handler.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +import datetime +import os +import sqlite3 +import sys + +import pytest + +try: + import psycopg + from psycopg.cursor import BaseCursor as PostgresCursor +except ImportError: + import psycopg2 as psycopg + from psycopg2.extensions import cursor as PostgresCursor + +from pytest_monitor.handler import PostgresDBHandler, SqliteDBHandler +from pytest_monitor.sys_utils import determine_scm_revision + +DB_Context = psycopg.Connection | sqlite3.Connection + + +# helper function +def reset_db(db_context: DB_Context): + # 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(): + 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(pytester): + # 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): + 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..c1717c7 --- /dev/null +++ b/tests/test_monitor_session.py @@ -0,0 +1,38 @@ +import os + +import pytest + +from pytest_monitor.session import PyTestMonitorSession + + +@pytest.fixture +def setup_environment_postgres(): + 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" + + +def test_pytestmonitorsession_close_connection(setup_environment_postgres): + session = PyTestMonitorSession(":memory:") + db = session._PyTestMonitorSession__db + + try: + db.query("SELECT * FROM sqlite_master LIMIT 1", ()) + except Exception: + assert False + + session.close() + + try: + db.query("SELECT * FROM sqlite_master LIMIT 1", ()) + assert False + 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 From b675b7744aff4032c07f1d70084ef81fa04492d3 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 18 Jun 2024 15:27:06 +0200 Subject: [PATCH 14/29] Add Flake8 and Ruff linting hints. --- pytest_monitor/pytest_monitor.py | 2 +- pytest_monitor/session.py | 4 ++-- tests/test_monitor_handler.py | 6 +----- tests/test_monitor_session.py | 10 +++++----- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/pytest_monitor/pytest_monitor.py b/pytest_monitor/pytest_monitor.py index 740858c..10c89b7 100644 --- a/pytest_monitor/pytest_monitor.py +++ b/pytest_monitor/pytest_monitor.py @@ -236,7 +236,7 @@ def prof(): ) 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) diff --git a/pytest_monitor/session.py b/pytest_monitor/session.py index e97d57c..c988074 100644 --- a/pytest_monitor/session.py +++ b/pytest_monitor/session.py @@ -90,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: @@ -143,7 +143,7 @@ def dummy(): 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 + self.__mem_usage_base = memuse[0] if isinstance(memuse, list) else memuse def add_test_info( self, diff --git a/tests/test_monitor_handler.py b/tests/test_monitor_handler.py index 1ce9655..62de21b 100644 --- a/tests/test_monitor_handler.py +++ b/tests/test_monitor_handler.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import datetime import os import sqlite3 import sys @@ -8,13 +7,10 @@ try: import psycopg - from psycopg.cursor import BaseCursor as PostgresCursor except ImportError: import psycopg2 as psycopg - from psycopg2.extensions import cursor as PostgresCursor from pytest_monitor.handler import PostgresDBHandler, SqliteDBHandler -from pytest_monitor.sys_utils import determine_scm_revision DB_Context = psycopg.Connection | sqlite3.Connection @@ -38,7 +34,7 @@ def reset_db(db_context: DB_Context): # cleanup_cursor.execute("GRANT ALL ON SCHEMA public TO public;") -@pytest.fixture +@pytest.fixture() def connected_PostgresDBHandler(): os.environ["PYTEST_MONITOR_DB_NAME"] = "postgres" os.environ["PYTEST_MONITOR_DB_USER"] = "postgres" diff --git a/tests/test_monitor_session.py b/tests/test_monitor_session.py index c1717c7..6d766da 100644 --- a/tests/test_monitor_session.py +++ b/tests/test_monitor_session.py @@ -5,8 +5,8 @@ from pytest_monitor.session import PyTestMonitorSession -@pytest.fixture -def setup_environment_postgres(): +@pytest.fixture() +def _setup_environment_postgres(): os.environ["PYTEST_MONITOR_DB_NAME"] = "postgres" os.environ["PYTEST_MONITOR_DB_USER"] = "postgres" os.environ["PYTEST_MONITOR_DB_PASSWORD"] = "testing_db" @@ -14,14 +14,14 @@ def setup_environment_postgres(): os.environ["PYTEST_MONITOR_DB_PORT"] = "5432" -def test_pytestmonitorsession_close_connection(setup_environment_postgres): +def test_pytestmonitorsession_close_connection(_setup_environment_postgres): session = PyTestMonitorSession(":memory:") db = session._PyTestMonitorSession__db try: db.query("SELECT * FROM sqlite_master LIMIT 1", ()) except Exception: - assert False + pytest.fail("Database should be available") session.close() @@ -29,7 +29,7 @@ def test_pytestmonitorsession_close_connection(setup_environment_postgres): db.query("SELECT * FROM sqlite_master LIMIT 1", ()) assert False except Exception: - assert True + pytest.fail("Database should be available") session = PyTestMonitorSession(use_postgres=True) db = session._PyTestMonitorSession__db From 0638d39467f9a56dc0fa7ee546f3fe6e05ed2ec2 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 18 Jun 2024 15:34:45 +0200 Subject: [PATCH 15/29] Fix bug in test_monitor_session.py introduced in last commit. --- tests/test_monitor_session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_monitor_session.py b/tests/test_monitor_session.py index 6d766da..d9aa1ac 100644 --- a/tests/test_monitor_session.py +++ b/tests/test_monitor_session.py @@ -29,7 +29,7 @@ def test_pytestmonitorsession_close_connection(_setup_environment_postgres): db.query("SELECT * FROM sqlite_master LIMIT 1", ()) assert False except Exception: - pytest.fail("Database should be available") + assert True session = PyTestMonitorSession(use_postgres=True) db = session._PyTestMonitorSession__db From 0c137a5b3d6bf6cf052a56f044677a061c8af6f0 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 18 Jun 2024 15:37:47 +0200 Subject: [PATCH 16/29] Fix linting hints round 2. --- tests/test_monitor_session.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_monitor_session.py b/tests/test_monitor_session.py index d9aa1ac..30cafa0 100644 --- a/tests/test_monitor_session.py +++ b/tests/test_monitor_session.py @@ -14,7 +14,8 @@ def _setup_environment_postgres(): os.environ["PYTEST_MONITOR_DB_PORT"] = "5432" -def test_pytestmonitorsession_close_connection(_setup_environment_postgres): +@pytest.mark.usefixtures("_setup_environment_postgres") +def test_pytestmonitorsession_close_connection(): session = PyTestMonitorSession(":memory:") db = session._PyTestMonitorSession__db @@ -27,7 +28,7 @@ def test_pytestmonitorsession_close_connection(_setup_environment_postgres): try: db.query("SELECT * FROM sqlite_master LIMIT 1", ()) - assert False + pytest.fail("Database should not be available anymore") except Exception: assert True From c48a4eb0bae393c01ba6d2777252aa028fac178b Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 18 Jun 2024 15:40:59 +0200 Subject: [PATCH 17/29] Update requirements.txt to include psycopg module --- requirements.txt | 1 + 1 file changed, 1 insertion(+) 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 From ec046b9a497540e94d6d010cc2707de9eb6d94a5 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 18 Jun 2024 15:46:23 +0200 Subject: [PATCH 18/29] Fix python interpreter compatibility issues (import annotations future) --- tests/test_monitor_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_monitor_handler.py b/tests/test_monitor_handler.py index 62de21b..a748abc 100644 --- a/tests/test_monitor_handler.py +++ b/tests/test_monitor_handler.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- +from __future__ import annotations import os import sqlite3 import sys - import pytest try: From af2e9e3f6c13c228ce0bd3d0b6eb5d796c671754 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 18 Jun 2024 15:46:23 +0200 Subject: [PATCH 19/29] Fix python interpreter compatibility issues (import annotations future) --- tests/test_monitor_handler.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_monitor_handler.py b/tests/test_monitor_handler.py index 62de21b..ad8063c 100644 --- a/tests/test_monitor_handler.py +++ b/tests/test_monitor_handler.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- +from __future__ import annotations import os import sqlite3 import sys - import pytest try: @@ -12,11 +12,9 @@ from pytest_monitor.handler import PostgresDBHandler, SqliteDBHandler -DB_Context = psycopg.Connection | sqlite3.Connection - # helper function -def reset_db(db_context: DB_Context): +def reset_db(db_context: psycopg.Connection | sqlite3.Connection): # cleanup_cursor.execute("DROP DATABASE postgres") # cleanup_cursor.execute("CREATE DATABASE postgres") cleanup_cursor = db_context.cursor() From a31d7fabdb753f5ebe4e02482e46a6407adacbea Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 18 Jun 2024 16:24:56 +0200 Subject: [PATCH 20/29] Fix missing equal signs in requirements.dev.txt --- requirements.dev.txt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements.dev.txt b/requirements.dev.txt index f0d0810..a9d39bc 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -4,11 +4,11 @@ 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 \ No newline at end of file From f5788dfee6711b52e2673a999ec2c0644ce869b8 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 18 Jun 2024 16:29:50 +0200 Subject: [PATCH 21/29] Add mock module to requirements.dev.txt --- requirements.dev.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.dev.txt b/requirements.dev.txt index a9d39bc..467ea58 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -11,4 +11,5 @@ 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 +pre-commit==3.3.3 +mock>=5.1.0 From 482d8d8b470cf15b95b3e6881fffdc6659957e93 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 18 Jun 2024 16:49:09 +0200 Subject: [PATCH 22/29] Add updated gitlab-ci.yml to include PostgreSQL container for testing --- .gitlab-ci.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3106b06..d8c4764 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 From 3aca81b7f394acab03d8f3a926ccfd505b7b2797 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 18 Jun 2024 17:09:41 +0200 Subject: [PATCH 23/29] Fix wrong formatting in gitlab-ci.yml --- .gitlab-ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d8c4764..b1f60c8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,11 +6,11 @@ variables: - 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 + - 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 From ed4c67e729dc00bfd8b30e7e4fd0990a1def8194 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 18 Jun 2024 17:10:04 +0200 Subject: [PATCH 24/29] Update .circleci/config.yml in order to provide postgresdb container during testing. --- .circleci/config.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ec70861..87db88d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,6 +3,7 @@ version: 2.1 aliases: docker-image: &image - image: mambaorg/micromamba + filter-pr-only: &PR-only branches: ignore: @@ -222,7 +223,21 @@ jobs: 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 From 31ed8abd8420fd7d5db1d2c8219584943b389236 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 18 Jun 2024 17:19:19 +0200 Subject: [PATCH 25/29] Fix circleci config errors --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 87db88d..235772b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2.1 aliases: docker-image: &image - - image: mambaorg/micromamba + image: mambaorg/micromamba filter-pr-only: &PR-only branches: From 50a627e852a95ec184ec426441f12a980938fbea Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 18 Jun 2024 17:26:03 +0200 Subject: [PATCH 26/29] Fix circleci config file part 2 --- .circleci/config.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 87db88d..caa9f0d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2.1 aliases: docker-image: &image - - image: mambaorg/micromamba + image: mambaorg/micromamba filter-pr-only: &PR-only branches: @@ -217,7 +217,8 @@ workflows: jobs: lint: - docker: *image + docker: + - *image steps: - make-env: use_specific_requirements_file: requirements.dev.txt @@ -250,7 +251,8 @@ jobs: pytest: << parameters.pytest >> - test-project publish: - docker: *image + docker: + - *image steps: - make-env: extra_deps: twine setuptools build From d6bb03d5ccfae0978df1c024c3eb0d5cf31e445a Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 18 Jun 2024 17:31:36 +0200 Subject: [PATCH 27/29] Remove unneeded pytester fixture from test_monitor_handler.py --- tests/test_monitor_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_monitor_handler.py b/tests/test_monitor_handler.py index ad8063c..f2e47fe 100644 --- a/tests/test_monitor_handler.py +++ b/tests/test_monitor_handler.py @@ -45,7 +45,7 @@ def connected_PostgresDBHandler(): db._PostgresDBHandler__cnx.close() -def test_sqlite_handler(pytester): +def test_sqlite_handler(): # db handler db = SqliteDBHandler(":memory:") session, metrics, exc_context = db.query( From 653d746aad08de0782c9c9336a91351c01ef925a Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Tue, 18 Jun 2024 17:48:33 +0200 Subject: [PATCH 28/29] Add docstrings to new tests. --- tests/test_monitor_handler.py | 4 ++++ tests/test_monitor_session.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/tests/test_monitor_handler.py b/tests/test_monitor_handler.py index f2e47fe..21b1532 100644 --- a/tests/test_monitor_handler.py +++ b/tests/test_monitor_handler.py @@ -15,6 +15,7 @@ # 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() @@ -34,6 +35,7 @@ def reset_db(db_context: psycopg.Connection | sqlite3.Connection): @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" @@ -46,6 +48,7 @@ def connected_PostgresDBHandler(): def test_sqlite_handler(): + """Test for working sqlite database""" # db handler db = SqliteDBHandler(":memory:") session, metrics, exc_context = db.query( @@ -57,6 +60,7 @@ def test_sqlite_handler(): 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'", diff --git a/tests/test_monitor_session.py b/tests/test_monitor_session.py index 30cafa0..998d6e8 100644 --- a/tests/test_monitor_session.py +++ b/tests/test_monitor_session.py @@ -7,6 +7,7 @@ @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" @@ -16,6 +17,7 @@ def _setup_environment_postgres(): @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 From 4edc0baae7e45bf7e20d801441611909faf1e413 Mon Sep 17 00:00:00 2001 From: Lucas Haupt Date: Mon, 15 Jul 2024 15:57:28 +0200 Subject: [PATCH 29/29] Add workaround fix for faulty memory_profiler module The memory profiler only reports Exceptions when it should report all Exceptions that inherit from BaseExceptions. This is ultimately reworked in a PR incorporating a simplified memory profiler module into the codebase that fixes not only this issue but also gives the possibility of getting measurements for failing tests. This workaround uses return values to work around the issue. This way pytest-monitor can check for BaseExceptions and act accordingly. Like described earlier the ultimate goal should probably be to replace the whole memory profiler as proposed in this PR: https://github.com/CFMTech/pytest-monitor/pull/82 --- pytest_monitor/pytest_monitor.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pytest_monitor/pytest_monitor.py b/pytest_monitor/pytest_monitor.py index 10c89b7..dab24f4 100644 --- a/pytest_monitor/pytest_monitor.py +++ b/pytest_monitor/pytest_monitor.py @@ -228,6 +228,13 @@ 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(): @@ -241,7 +248,14 @@ def prof(): 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()