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