From 7d78c3c1723f514081a0d679d3a2c7c5cd9cb233 Mon Sep 17 00:00:00 2001 From: Laurens Miers Date: Sun, 30 Mar 2025 19:47:03 +0200 Subject: [PATCH 01/10] fix(robot_results_parser): datetime deprecation notice --- dbbot/reader/robot_results_parser.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dbbot/reader/robot_results_parser.py b/dbbot/reader/robot_results_parser.py index b2fbdec..bd963bd 100644 --- a/dbbot/reader/robot_results_parser.py +++ b/dbbot/reader/robot_results_parser.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. from __future__ import with_statement -from datetime import datetime +import datetime + from hashlib import sha1 from robot.api import ExecutionResult from sqlalchemy.exc import IntegrityError @@ -35,8 +36,8 @@ def xml_to_db(self, xml_file): try: test_run_id = self._db.insert('test_runs', { 'hash': hash_string, - 'imported_at': datetime.utcnow(), 'source_file': test_run.source, + 'imported_at': datetime.datetime.now(datetime.UTC), 'started_at': self._format_robot_timestamp(test_run.suite.starttime), 'finished_at': self._format_robot_timestamp(test_run.suite.endtime) }) @@ -220,7 +221,7 @@ def _parse_arguments(self, args, keyword_id): @staticmethod def _format_robot_timestamp(timestamp): - return datetime.strptime(timestamp, '%Y%m%d %H:%M:%S.%f') if timestamp else None + return datetime.datetime.strptime(timestamp, '%Y%m%d %H:%M:%S.%f') if timestamp else None @staticmethod def _string_hash(string): From d2a5f6d03cd55737adf44092e0839f5e4727e14c Mon Sep 17 00:00:00 2001 From: Laurens Miers Date: Sun, 30 Mar 2025 19:48:56 +0200 Subject: [PATCH 02/10] fix(database_write): Store elapsed time as datetime.timedelta --- dbbot/reader/database_writer.py | 12 ++++++------ dbbot/reader/robot_results_parser.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/dbbot/reader/database_writer.py b/dbbot/reader/database_writer.py index 545b5ba..a634ef8 100644 --- a/dbbot/reader/database_writer.py +++ b/dbbot/reader/database_writer.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from sqlalchemy import create_engine, Column, DateTime, ForeignKey, Integer, MetaData, Sequence, String, Table, Text, \ +from sqlalchemy import create_engine, Column, DateTime, ForeignKey, Interval, Integer, MetaData, Sequence, String, Table, Text, \ UniqueConstraint from sqlalchemy.sql import and_, select from sqlalchemy.exc import IntegrityError @@ -57,7 +57,7 @@ def _create_table_test_run_status(self): return self._create_table('test_run_status', ( Column('test_run_id', Integer, ForeignKey('test_runs.id'), nullable=False), Column('name', String(256), nullable=False), - Column('elapsed', Integer), + Column('elapsed', Interval), Column('failed', Integer, nullable=False), Column('passed', Integer, nullable=False) ), ('test_run_id', 'name')) @@ -76,7 +76,7 @@ def _create_table_tag_status(self): Column('test_run_id', Integer, ForeignKey('test_runs.id'), nullable=False), Column('name', String(256), nullable=False), Column('critical', Integer, nullable=False), - Column('elapsed', Integer), + Column('elapsed', Interval), Column('failed', Integer, nullable=False), Column('passed', Integer, nullable=False) ), ('test_run_id', 'name')) @@ -94,7 +94,7 @@ def _create_table_suite_status(self): return self._create_table('suite_status', ( Column('test_run_id', Integer, ForeignKey('test_runs.id'), nullable=False), Column('suite_id', Integer, ForeignKey('suites.id'), nullable=False), - Column('elapsed', Integer, nullable=False), + Column('elapsed', Interval, nullable=False), Column('failed', Integer, nullable=False), Column('passed', Integer, nullable=False), Column('status', String(4), nullable=False) @@ -114,7 +114,7 @@ def _create_table_test_status(self): Column('test_run_id', Integer, ForeignKey('test_runs.id'), nullable=False), Column('test_id', Integer, ForeignKey('tests.id'), nullable=False), Column('status', String(4), nullable=False), - Column('elapsed', Integer, nullable=False) + Column('elapsed', Interval, nullable=False), ), ('test_run_id', 'test_id')) def _create_table_keywords(self): @@ -133,7 +133,7 @@ def _create_table_keyword_status(self): Column('test_run_id', Integer, ForeignKey('test_runs.id'), nullable=False), Column('keyword_id', Integer, ForeignKey('keywords.id'), nullable=False), Column('status', String(4), nullable=False), - Column('elapsed', Integer, nullable=False) + Column('elapsed', Interval, nullable=False), )) def _create_table_messages(self): diff --git a/dbbot/reader/robot_results_parser.py b/dbbot/reader/robot_results_parser.py index bd963bd..b6aaecc 100644 --- a/dbbot/reader/robot_results_parser.py +++ b/dbbot/reader/robot_results_parser.py @@ -128,7 +128,7 @@ def _parse_suite_status(self, test_run_id, suite_id, suite): 'suite_id': suite_id, 'passed': suite.statistics.all.passed, 'failed': suite.statistics.all.failed, - 'elapsed': suite.elapsedtime, + 'elapsed': suite.elapsed_time, 'status': suite.status }) @@ -162,7 +162,7 @@ def _parse_test_status(self, test_run_id, test_id, test): 'test_run_id': test_run_id, 'test_id': test_id, 'status': test.status, - 'elapsed': test.elapsedtime + 'elapsed': test.elapsed_time }) def _parse_tags(self, tags, test_id): @@ -199,7 +199,7 @@ def _parse_keyword_status(self, test_run_id, keyword_id, keyword): 'test_run_id': test_run_id, 'keyword_id': keyword_id, 'status': keyword.status, - 'elapsed': keyword.elapsedtime + 'elapsed': keyword.elapsed_time }) def _parse_messages(self, messages, keyword_id): From 3d3bfea30511fb95e19681e6331b940875bb502f Mon Sep 17 00:00:00 2001 From: Laurens Miers Date: Sun, 30 Mar 2025 19:51:58 +0200 Subject: [PATCH 03/10] fix(robot_results_parser): Convert Path-object to string --- dbbot/reader/robot_results_parser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dbbot/reader/robot_results_parser.py b/dbbot/reader/robot_results_parser.py index b6aaecc..af1fe59 100644 --- a/dbbot/reader/robot_results_parser.py +++ b/dbbot/reader/robot_results_parser.py @@ -36,14 +36,14 @@ def xml_to_db(self, xml_file): try: test_run_id = self._db.insert('test_runs', { 'hash': hash_string, - 'source_file': test_run.source, + 'source_file': str(test_run.source), 'imported_at': datetime.datetime.now(datetime.UTC), 'started_at': self._format_robot_timestamp(test_run.suite.starttime), 'finished_at': self._format_robot_timestamp(test_run.suite.endtime) }) except IntegrityError: test_run_id = self._db.fetch_id('test_runs', { - 'source_file': test_run.source, + 'source_file': str(test_run.source), 'started_at': self._format_robot_timestamp(test_run.suite.starttime), 'finished_at': self._format_robot_timestamp(test_run.suite.endtime) }) @@ -109,13 +109,13 @@ def _parse_suite(self, suite, test_run_id, parent_suite_id=None): 'suite_id': parent_suite_id, 'xml_id': suite.id, 'name': suite.name, - 'source': suite.source, + 'source': str(suite.source), 'doc': suite.doc }) except IntegrityError: suite_id = self._db.fetch_id('suites', { 'name': suite.name, - 'source': suite.source + 'source': str(suite.source) }) self._parse_suite_status(test_run_id, suite_id, suite) self._parse_suites(suite, test_run_id, suite_id) From 907adaf7c7532f8f38bf5470bef45693dd2941a8 Mon Sep 17 00:00:00 2001 From: Laurens Miers Date: Sun, 30 Mar 2025 19:52:32 +0200 Subject: [PATCH 04/10] fix(robot_results_parser): store timestamp directly --- dbbot/reader/robot_results_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dbbot/reader/robot_results_parser.py b/dbbot/reader/robot_results_parser.py index af1fe59..1f0d45e 100644 --- a/dbbot/reader/robot_results_parser.py +++ b/dbbot/reader/robot_results_parser.py @@ -66,7 +66,7 @@ def _parse_errors(self, errors, test_run_id): for error in errors: self._db.insert_or_ignore('test_run_errors', { 'test_run_id': test_run_id, 'level': error.level, - 'timestamp': self._format_robot_timestamp(error.timestamp), + 'timestamp': error.timestamp, 'content': error.message, 'content_hash': self._string_hash(error.message) }) @@ -206,7 +206,7 @@ def _parse_messages(self, messages, keyword_id): for message in messages: self._db.insert_or_ignore('messages', { 'keyword_id': keyword_id, 'level': message.level, - 'timestamp': self._format_robot_timestamp(message.timestamp), + 'timestamp': message.timestamp, 'content': message.message, 'content_hash': self._string_hash(message.message) }) From 464a25f84c88819128d1a3bcd44b5477b9ed558d Mon Sep 17 00:00:00 2001 From: Laurens Miers Date: Sun, 30 Mar 2025 19:53:13 +0200 Subject: [PATCH 05/10] refactor(robot_results_parser): Replace critical with skipped tests RobotFramework(RF) v4 removed critical in favour of skipped tests. Remove the critical count in the DB and replace it with skipped. --- dbbot/reader/robot_results_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbbot/reader/robot_results_parser.py b/dbbot/reader/robot_results_parser.py index 1f0d45e..82eda3e 100644 --- a/dbbot/reader/robot_results_parser.py +++ b/dbbot/reader/robot_results_parser.py @@ -87,7 +87,7 @@ def _parse_tag_stats(self, stat, test_run_id): self._db.insert_or_ignore('tag_status', { 'test_run_id': test_run_id, 'name': stat.name, - 'critical': int(stat.critical), + 'skipped': int(stat.skipped), 'elapsed': getattr(stat, 'elapsed', None), 'failed': stat.failed, 'passed': stat.passed From 924a841e188bae9696949e0aee08d87d6491d55b Mon Sep 17 00:00:00 2001 From: Laurens Miers Date: Sun, 30 Mar 2025 19:54:30 +0200 Subject: [PATCH 06/10] refactor(robot_results_parser): Update suite passed/failed statistics API RobotFramework(RF) v4 changed the suite.statistics API --- dbbot/reader/robot_results_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dbbot/reader/robot_results_parser.py b/dbbot/reader/robot_results_parser.py index 82eda3e..120adb9 100644 --- a/dbbot/reader/robot_results_parser.py +++ b/dbbot/reader/robot_results_parser.py @@ -126,8 +126,8 @@ def _parse_suite_status(self, test_run_id, suite_id, suite): self._db.insert_or_ignore('suite_status', { 'test_run_id': test_run_id, 'suite_id': suite_id, - 'passed': suite.statistics.all.passed, - 'failed': suite.statistics.all.failed, + 'passed': suite.statistics.passed, + 'failed': suite.statistics.failed, 'elapsed': suite.elapsed_time, 'status': suite.status }) From 46d25a6477d5f8150ccd79df6ebaeb26d3047547 Mon Sep 17 00:00:00 2001 From: Laurens Miers Date: Sun, 30 Mar 2025 19:55:05 +0200 Subject: [PATCH 07/10] fix(robot_results_parser): Don't parse non-keywords like Message, ... --- dbbot/reader/robot_results_parser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dbbot/reader/robot_results_parser.py b/dbbot/reader/robot_results_parser.py index 120adb9..986ef8e 100644 --- a/dbbot/reader/robot_results_parser.py +++ b/dbbot/reader/robot_results_parser.py @@ -174,6 +174,8 @@ def _parse_keywords(self, keywords, test_run_id, suite_id, test_id, keyword_id=N [self._parse_keyword(keyword, test_run_id, suite_id, test_id, keyword_id) for keyword in keywords] def _parse_keyword(self, keyword, test_run_id, suite_id, test_id, keyword_id): + if keyword.type != "KEYWORD": + return try: keyword_id = self._db.insert('keywords', { 'suite_id': suite_id, From 6d0dc2e1c31209b20670a24ce52d9e9a20456234 Mon Sep 17 00:00:00 2001 From: Laurens Miers Date: Sun, 30 Mar 2025 19:55:31 +0200 Subject: [PATCH 08/10] fix(robot_results_parser): Update to sqlalchemy v2 API - select() uses id directly iso list - connection.execute uses dictionary iso keywords arguments - Do explicit connection.commit() iso relying on implicit commit, which was removed in v2. --- dbbot/reader/database_writer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dbbot/reader/database_writer.py b/dbbot/reader/database_writer.py index a634ef8..27479ab 100644 --- a/dbbot/reader/database_writer.py +++ b/dbbot/reader/database_writer.py @@ -167,7 +167,7 @@ def _create_table(self, table_name, columns, unique_columns=()): def fetch_id(self, table_name, criteria): table = getattr(self, table_name) - sql_statement = select([table.c.id]).where( + sql_statement = select(table.c.id).where( and_(*(getattr(table.c, key) == value for key, value in criteria.items())) ) result = self._connection.execute(sql_statement).first() @@ -178,7 +178,7 @@ def fetch_id(self, table_name, criteria): def insert(self, table_name, criteria): sql_statement = getattr(self, table_name).insert() - result = self._connection.execute(sql_statement, **criteria) + result = self._connection.execute(sql_statement, criteria) return result.inserted_primary_key[0] def insert_or_ignore(self, table_name, criteria): @@ -190,4 +190,5 @@ def insert_or_ignore(self, table_name, criteria): def close(self): self._verbose('- Closing database connection') + self._connection.commit() self._connection.close() From ba0ffe3724f004b5e0edbcc726a64323b55f016f Mon Sep 17 00:00:00 2001 From: Laurens Miers Date: Sun, 30 Mar 2025 19:57:55 +0200 Subject: [PATCH 09/10] fix(robot_results_parser): Update to 'keywords' RF v4 API - Test and keyword 'keywords' replaced with 'body' - Removed suite.keywords --- dbbot/reader/robot_results_parser.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dbbot/reader/robot_results_parser.py b/dbbot/reader/robot_results_parser.py index 986ef8e..5645023 100644 --- a/dbbot/reader/robot_results_parser.py +++ b/dbbot/reader/robot_results_parser.py @@ -120,7 +120,6 @@ def _parse_suite(self, suite, test_run_id, parent_suite_id=None): self._parse_suite_status(test_run_id, suite_id, suite) self._parse_suites(suite, test_run_id, suite_id) self._parse_tests(suite.tests, test_run_id, suite_id) - self._parse_keywords(suite.keywords, test_run_id, suite_id, None) def _parse_suite_status(self, test_run_id, suite_id, suite): self._db.insert_or_ignore('suite_status', { @@ -155,7 +154,7 @@ def _parse_test(self, test, test_run_id, suite_id): }) self._parse_test_status(test_run_id, test_id, test) self._parse_tags(test.tags, test_id) - self._parse_keywords(test.keywords, test_run_id, None, test_id) + self._parse_keywords(test.body, test_run_id, None, test_id) def _parse_test_status(self, test_run_id, test_id, test): self._db.insert_or_ignore('test_status', { @@ -194,7 +193,7 @@ def _parse_keyword(self, keyword, test_run_id, suite_id, test_id, keyword_id): self._parse_keyword_status(test_run_id, keyword_id, keyword) self._parse_messages(keyword.messages, keyword_id) self._parse_arguments(keyword.args, keyword_id) - self._parse_keywords(keyword.keywords, test_run_id, None, None, keyword_id) + self._parse_keywords(keyword.body, test_run_id, None, None, keyword_id) def _parse_keyword_status(self, test_run_id, keyword_id, keyword): self._db.insert_or_ignore('keyword_status', { From ff1301272f326e6ecad11daa7e0cb253cbf00eb8 Mon Sep 17 00:00:00 2001 From: Laurens Miers Date: Sun, 30 Mar 2025 20:02:42 +0200 Subject: [PATCH 10/10] fix(database_writer): Remove unique columns for keywords table If a test is calling the same keyword in a test, we would get an error informing us we violated the unique constraint. A keyword should not be limited by any unique constraint since each test can call it multiple times, with multiple args, .. and all these scenarios have the same name and type which will result in a unique constraint violation. --- dbbot/reader/database_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbbot/reader/database_writer.py b/dbbot/reader/database_writer.py index 27479ab..c139377 100644 --- a/dbbot/reader/database_writer.py +++ b/dbbot/reader/database_writer.py @@ -126,7 +126,7 @@ def _create_table_keywords(self): Column('type', String(64), nullable=False), Column('timeout', String(4)), Column('doc', Text) - ), ('name', 'type')) + )) def _create_table_keyword_status(self): return self._create_table('keyword_status', (