[Concept,12/16] pickman: Add a database table to track pipeline fixes

Message ID 20260222154303.2851319-13-sjg@u-boot.org
State New
Headers
Series pickman: Support monitoring and fixing pipeline failures |

Commit Message

Simon Glass Feb. 22, 2026, 3:42 p.m. UTC
  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(-)
  

Patch

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."""