From: Simon Glass <simon.glass@canonical.com> Add a pipeline_fix table (schema v4) for tracking pipeline-fix attempts per MR. Each row records the MR IID, pipeline ID, attempt number, status and timestamp. A UNIQUE constraint on (mr_iid, pipeline_id) ensures each pipeline is only processed once. Add pfix_count(), pfix_add() and pfix_has() accessors. Co-developed-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- tools/pickman/database.py | 65 +++++++++++++++++++++++++++++- tools/pickman/ftest.py | 84 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/tools/pickman/database.py b/tools/pickman/database.py index 92bff7a5702..317668a979d 100644 --- a/tools/pickman/database.py +++ b/tools/pickman/database.py @@ -19,7 +19,7 @@ from u_boot_pylib import tools from u_boot_pylib import tout # Schema version (version 0 means there is no database yet) -LATEST = 3 +LATEST = 4 # Default database filename DB_FNAME = '.pickman.db' @@ -141,6 +141,19 @@ class Database: # pylint: disable=too-many-public-methods 'processed_at TEXT, ' 'UNIQUE(mr_iid, comment_id))') + def _create_v4(self): + """Migrate database to v4 schema - add pipeline_fix table""" + # Table for tracking pipeline fix attempts per MR + self.cur.execute( + 'CREATE TABLE pipeline_fix (' + 'id INTEGER PRIMARY KEY AUTOINCREMENT, ' + 'mr_iid INTEGER, ' + 'pipeline_id INTEGER, ' + 'attempt INTEGER, ' + 'status TEXT, ' + 'created_at TEXT, ' + 'UNIQUE(mr_iid, pipeline_id))') + def migrate_to(self, dest_version): """Migrate the database to the selected version @@ -165,6 +178,8 @@ class Database: # pylint: disable=too-many-public-methods self._create_v2() elif version == 3: self._create_v3() + elif version == 4: + self._create_v4() self.cur.execute('DELETE FROM schema_version') self.cur.execute( @@ -481,3 +496,51 @@ class Database: # pylint: disable=too-many-public-methods 'SELECT comment_id FROM comment WHERE mr_iid = ?', (mr_iid,)) return [row[0] for row in res.fetchall()] + + # pipeline_fix functions + + def pfix_count(self, mr_iid): + """Count fix attempts for an MR + + Args: + mr_iid (int): Merge request IID + + Return: + int: Number of fix attempts + """ + res = self.execute( + 'SELECT COUNT(*) FROM pipeline_fix WHERE mr_iid = ?', + (mr_iid,)) + return res.fetchone()[0] + + def pfix_add(self, mr_iid, pipeline_id, attempt, status): + """Record a pipeline fix attempt + + Args: + mr_iid (int): Merge request IID + pipeline_id (int): Pipeline ID + attempt (int): Attempt number + status (str): Status ('success' or 'failure') + """ + self.execute( + 'INSERT OR IGNORE INTO pipeline_fix ' + '(mr_iid, pipeline_id, attempt, status, created_at) ' + 'VALUES (?, ?, ?, ?, ?)', + (mr_iid, pipeline_id, attempt, status, + datetime.now().isoformat())) + + def pfix_has(self, mr_iid, pipeline_id): + """Check if a pipeline has already been handled + + Args: + mr_iid (int): Merge request IID + pipeline_id (int): Pipeline ID + + Return: + bool: True if already handled + """ + res = self.execute( + 'SELECT id FROM pipeline_fix ' + 'WHERE mr_iid = ? AND pipeline_id = ?', + (mr_iid, pipeline_id)) + return res.fetchone() is not None diff --git a/tools/pickman/ftest.py b/tools/pickman/ftest.py index 4a58a9371ce..67a7d004ca6 100644 --- a/tools/pickman/ftest.py +++ b/tools/pickman/ftest.py @@ -818,6 +818,90 @@ class TestDatabaseComment(unittest.TestCase): dbs.close() +class TestDatabasePipelineFix(unittest.TestCase): + """Tests for Database pipeline_fix functions.""" + + def setUp(self): + """Set up test fixtures.""" + fd, self.db_path = tempfile.mkstemp(suffix='.db') + os.close(fd) + os.unlink(self.db_path) + + def tearDown(self): + """Clean up test fixtures.""" + if os.path.exists(self.db_path): + os.unlink(self.db_path) + database.Database.instances.clear() + + def test_pfix_add(self): + """Test adding a pipeline fix record""" + with terminal.capture(): + dbs = database.Database(self.db_path) + dbs.start() + + dbs.pfix_add(123, 456, 1, 'success') + dbs.commit() + + self.assertTrue(dbs.pfix_has(123, 456)) + + dbs.close() + + def test_pfix_count(self): + """Test counting pipeline fix attempts""" + with terminal.capture(): + dbs = database.Database(self.db_path) + dbs.start() + + self.assertEqual(dbs.pfix_count(123), 0) + + dbs.pfix_add(123, 100, 1, 'failure') + dbs.pfix_add(123, 200, 2, 'success') + dbs.commit() + + self.assertEqual(dbs.pfix_count(123), 2) + # Different MR should have 0 + self.assertEqual(dbs.pfix_count(999), 0) + + dbs.close() + + def test_pfix_has(self): + """Test checking if a pipeline was already handled""" + with terminal.capture(): + dbs = database.Database(self.db_path) + dbs.start() + + self.assertFalse(dbs.pfix_has(123, 456)) + + dbs.pfix_add(123, 456, 1, 'success') + dbs.commit() + + self.assertTrue(dbs.pfix_has(123, 456)) + # Different pipeline should not be handled + self.assertFalse(dbs.pfix_has(123, 789)) + # Different MR should not be handled + self.assertFalse(dbs.pfix_has(999, 456)) + + dbs.close() + + def test_pfix_unique(self): + """Test that duplicate mr_iid/pipeline_id pairs are ignored""" + with terminal.capture(): + dbs = database.Database(self.db_path) + dbs.start() + + dbs.pfix_add(123, 456, 1, 'failure') + dbs.commit() + + # Adding same pair again should not raise (OR IGNORE) + dbs.pfix_add(123, 456, 2, 'success') + dbs.commit() + + # Count should still be 1 (second insert ignored) + self.assertEqual(dbs.pfix_count(123), 1) + + dbs.close() + + class TestListSources(unittest.TestCase): """Tests for list-sources command.""" -- 2.43.0