From 43fa9224a9b785e4d65cdbdd77f17764d78be17e Mon Sep 17 00:00:00 2001 From: Carlos Date: Sun, 11 Jan 2026 09:19:32 +0100 Subject: [PATCH 1/3] fix: verify GCP VM creation before recording gcp_instance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Tests could get stuck forever in "Preparation" phase when GCP VM creation failed (e.g., due to quota limits) but a gcp_instance database record was still created. ### Root Cause Investigation (Test #7768) While investigating why tests weren't running for CCExtractor PR #2014, I discovered test #7768 had been stuck in "Preparation" for 12+ hours: 1. **Database state**: gcp_instance record existed for `linux-7768`, created at 2026-01-10 23:23:07, but `timestamp_prep_finished` was NULL and there were zero test_progress entries. 2. **GCP state**: The VM `linux-7768` did NOT exist in GCP. 3. **GCP operation history**: The VM creation operation returned HTTP 403: ``` error: errors: - code: QUOTA_EXCEEDED message: "Quota 'IN_USE_ADDRESSES' exceeded. Limit: 8.0 in region us-central1." httpErrorStatusCode: 403 ``` 4. **The bug**: The platform created the gcp_instance record BEFORE verifying the GCP operation completed successfully. When the operation failed asynchronously with QUOTA_EXCEEDED, the record remained in the database. The cron job saw this record and assumed the test was running, so it never retried. The test was stuck forever. ## Solution After calling `create_instance()`, wait for the GCP operation to complete (with a 60-second timeout) before creating the gcp_instance record: - If operation completes successfully → create record, test proceeds - If operation fails (QUOTA_EXCEEDED, etc.) → mark test failed, NO record - If operation times out → create record optimistically (slow VM creation) The 60-second verification timeout is sufficient to catch quota errors (which fail within seconds) while not blocking too long for legitimate slow VM creations. ## Changes - `mod_ci/controllers.py`: - Added `GCP_VM_CREATE_VERIFY_TIMEOUT = 60` constant - Modified `start_test()` to wait for operation verification - Only creates gcp_instance record after confirming success or timeout - `tests/test_ci/test_controllers.py`: - Updated existing `test_start_test` to mock `wait_for_operation` - Added `TestVMCreationVerification` class with 3 new tests: - `test_start_test_quota_exceeded_no_db_record`: Verifies QUOTA_EXCEEDED prevents record creation (the exact scenario from test #7768) - `test_start_test_vm_verified_creates_db_record`: Verifies successful VM creation creates record - `test_start_test_operation_timeout_creates_db_record`: Verifies timeout still creates record (for slow VMs) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- mod_ci/controllers.py | 30 ++++- tests/test_ci/test_controllers.py | 211 +++++++++++++++++++++++++++++- 2 files changed, 231 insertions(+), 10 deletions(-) diff --git a/mod_ci/controllers.py b/mod_ci/controllers.py index 81f6ddb0..5cd449fe 100755 --- a/mod_ci/controllers.py +++ b/mod_ci/controllers.py @@ -51,6 +51,7 @@ GCP_API_TIMEOUT = 60 # Timeout for GCP API calls ARTIFACT_DOWNLOAD_TIMEOUT = 300 # 5 minutes for artifact downloads GCP_OPERATION_MAX_WAIT = 1800 # 30 minutes max wait for GCP operations +GCP_VM_CREATE_VERIFY_TIMEOUT = 60 # 60 seconds to verify VM creation started # Retry constants MAX_RETRIES = 3 @@ -925,14 +926,31 @@ def start_test(compute, app, db, repository: Repository.Repository, test, bot_to mark_test_failed(db, test, repository, error_msg) return - # VM creation request was accepted - record the instance optimistically - # We don't wait for the operation to complete because: - # 1. Waiting can take 60+ seconds, blocking gunicorn workers - # 2. If VM creation ultimately fails, the test won't report progress - # and will be cleaned up by the expired instances cron job + # Wait for the VM creation operation to complete (or timeout) + # This catches quota errors and other failures that occur shortly after the + # insert request is accepted. Without this check, tests can get stuck forever + # when VM creation fails but a GcpInstance record is created. op_name = operation.get('name', 'unknown') - log.info(f"Test {test.id}: VM creation initiated (op: {op_name})") + log.info(f"Test {test.id}: VM creation initiated (op: {op_name}), waiting for verification...") + result = wait_for_operation(compute, project_id, zone, op_name, max_wait=GCP_VM_CREATE_VERIFY_TIMEOUT) + + # Check if operation completed with an error (e.g., QUOTA_EXCEEDED) + if 'error' in result: + error_msg = parse_gcp_error(result) + log.error(f"Test {test.id}: VM creation failed: {error_msg}") + log.error(f"Test {test.id}: Full GCP response: {result}") + mark_test_failed(db, test, repository, error_msg) + return + + # Check for timeout - operation still running, which is OK for slow VM creation + if result.get('status') == 'TIMEOUT': + log.warning(f"Test {test.id}: VM creation still in progress after {GCP_VM_CREATE_VERIFY_TIMEOUT}s, " + "recording instance optimistically") + else: + log.info(f"Test {test.id}: VM creation verified successfully") + + # VM creation succeeded (or is still in progress) - record the instance db.add(status) if not safe_db_commit(db, f"recording GCP instance for test {test.id}"): log.error(f"Failed to record GCP instance for test {test.id}, but VM creation was initiated") diff --git a/tests/test_ci/test_controllers.py b/tests/test_ci/test_controllers.py index 03ff6c72..40305839 100644 --- a/tests/test_ci/test_controllers.py +++ b/tests/test_ci/test_controllers.py @@ -222,13 +222,14 @@ def test_cron_job_empty_token(self, mock_log): cron() mock_log.error.assert_called_with('GITHUB_TOKEN not configured, cannot run CI cron') + @mock.patch('mod_ci.controllers.wait_for_operation') @mock.patch('mod_ci.controllers.create_instance') @mock.patch('builtins.open', new_callable=mock.mock_open()) @mock.patch('mod_ci.controllers.g') @mock.patch('mod_ci.controllers.TestProgress') @mock.patch('mod_ci.controllers.GcpInstance') def test_start_test(self, mock_gcp_instance, mock_test_progress, mock_g, mock_open_file, - mock_create_instance): + mock_create_instance, mock_wait_for_operation): """Test start_test function.""" import zipfile @@ -282,10 +283,9 @@ def extractall(*args, **kwargs): mock_g.db.commit.reset_mock() mock_create_instance.reset_mock() - # Test when gcp create instance is successful (no error in operation response) - # Note: We no longer wait for the operation to complete - we record the instance - # optimistically and let the expired instances cron handle failures + # Test when gcp create instance is successful and verified mock_create_instance.return_value = {'name': 'test-operation-123', 'status': 'RUNNING'} + mock_wait_for_operation.return_value = {'status': 'DONE'} # Success start_test(mock.ANY, self.app, mock_g.db, repository, test, mock.ANY) mock_g.db.commit.assert_called_once() mock_create_instance.assert_called_once() @@ -3807,3 +3807,206 @@ def test_retry_deletion_vm_not_found(self, mock_log, mock_commit, mock_delete): # Should remove the pending record (VM is gone) db.delete.assert_called_once_with(pending) + + +class TestVMCreationVerification(BaseTestCase): + """Tests for VM creation verification to prevent stuck tests. + + These tests verify the fix for the issue where tests would get stuck forever + when VM creation failed (e.g., QUOTA_EXCEEDED) but a GcpInstance record was + still created in the database. + + Root cause investigated: Test #7768 for CCExtractor PR #2014 was stuck in + "Preparation" phase for 12+ hours because: + 1. GCP VM creation failed with QUOTA_EXCEEDED (8 IP addresses limit) + 2. The platform created a gcp_instance DB record before verifying success + 3. The cron job saw the record and assumed the test was running + 4. The test was stuck forever with no way to recover + + The fix: Wait for the GCP operation to complete (or timeout) before creating + the gcp_instance record. If the operation fails, mark the test as failed. + """ + + @mock.patch('run.log') + @mock.patch('mod_ci.controllers.mark_test_failed') + @mock.patch('mod_ci.controllers.wait_for_operation') + @mock.patch('mod_ci.controllers.create_instance') + @mock.patch('mod_ci.controllers.find_artifact_for_commit') + @mock.patch('mod_ci.controllers.requests.get') + @mock.patch('mod_ci.controllers.zipfile.ZipFile') + @mock.patch('builtins.open', new_callable=mock.mock_open()) + @mock.patch('mod_ci.controllers.g') + @mock.patch('mod_ci.controllers.TestProgress') + @mock.patch('mod_ci.controllers.GcpInstance') + def test_start_test_quota_exceeded_no_db_record( + self, mock_gcp_instance, mock_test_progress, mock_g, mock_open_file, + mock_zipfile, mock_requests_get, mock_find_artifact, + mock_create_instance, mock_wait_for_operation, mock_mark_failed, mock_log): + """Test that QUOTA_EXCEEDED error prevents gcp_instance record creation. + + This is the exact scenario from test #7768: + - GCP operation returns successfully (just an operation ID) + - But the operation completes with QUOTA_EXCEEDED error + - The test should be marked as failed + - NO gcp_instance record should be created + """ + from mod_ci.controllers import start_test + + # Mock locking checks to return None (no existing instances/progress) + mock_gcp_instance.query.filter.return_value.first.return_value = None + mock_test_progress.query.filter.return_value.first.return_value = None + + # Set up mock db query chain + mock_query = create_mock_db_query(mock_g) + mock_query.c.got = MagicMock() + + test = Test.query.first() + repository = MagicMock() + + # Mock successful artifact download + mock_artifact = MagicMock() + mock_artifact.name = 'CCExtractor Linux build' + mock_artifact.id = 123 + mock_artifact.archive_download_url = 'https://example.com/artifact.zip' + mock_find_artifact.return_value = mock_artifact + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = b'fake zip content' + mock_requests_get.return_value = mock_response + + mock_zip = MagicMock() + mock_zipfile.return_value.__enter__ = MagicMock(return_value=mock_zip) + mock_zipfile.return_value.__exit__ = MagicMock(return_value=False) + + # VM creation returns operation ID (appears successful initially) + mock_create_instance.return_value = { + 'name': 'operation-1768087398971-64810ed58f840-79d64e2e-ea386c76', + 'status': 'RUNNING' + } + + # But wait_for_operation returns QUOTA_EXCEEDED error + # This is the exact error from test #7768 + mock_wait_for_operation.return_value = { + 'status': 'DONE', + 'error': { + 'errors': [{ + 'code': 'QUOTA_EXCEEDED', + 'message': "Quota 'IN_USE_ADDRESSES' exceeded. Limit: 8.0 in region us-central1." + }] + }, + 'httpErrorStatusCode': 403, + 'httpErrorMessage': 'FORBIDDEN' + } + + start_test(MagicMock(), self.app, mock_g.db, repository, test, "token") + + # Should call mark_test_failed + mock_mark_failed.assert_called_once() + + # Should NOT add a gcp_instance record to the database + mock_g.db.add.assert_not_called() + + @mock.patch('run.log') + @mock.patch('mod_ci.controllers.safe_db_commit') + @mock.patch('mod_ci.controllers.wait_for_operation') + @mock.patch('mod_ci.controllers.create_instance') + @mock.patch('mod_ci.controllers.find_artifact_for_commit') + @mock.patch('mod_ci.controllers.requests.get') + @mock.patch('mod_ci.controllers.zipfile.ZipFile') + @mock.patch('builtins.open', new_callable=mock.mock_open()) + @mock.patch('mod_ci.controllers.g') + @mock.patch('mod_ci.controllers.TestProgress') + @mock.patch('mod_ci.controllers.GcpInstance') + def test_start_test_vm_verified_creates_db_record( + self, mock_gcp_instance, mock_test_progress, mock_g, mock_open_file, + mock_zipfile, mock_requests_get, mock_find_artifact, + mock_create_instance, mock_wait_for_operation, mock_commit, mock_log): + """Test that successful VM creation creates gcp_instance record.""" + from mod_ci.controllers import start_test + + # Mock locking checks + mock_gcp_instance.query.filter.return_value.first.return_value = None + mock_test_progress.query.filter.return_value.first.return_value = None + mock_query = create_mock_db_query(mock_g) + mock_query.c.got = MagicMock() + + test = Test.query.first() + repository = MagicMock() + + # Mock successful artifact download + mock_artifact = MagicMock() + mock_artifact.name = 'CCExtractor Linux build' + mock_artifact.archive_download_url = 'https://example.com/artifact.zip' + mock_find_artifact.return_value = mock_artifact + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = b'fake zip content' + mock_requests_get.return_value = mock_response + + mock_zip = MagicMock() + mock_zipfile.return_value.__enter__ = MagicMock(return_value=mock_zip) + mock_zipfile.return_value.__exit__ = MagicMock(return_value=False) + + # VM creation succeeds + mock_create_instance.return_value = {'name': 'op-123', 'status': 'RUNNING'} + mock_wait_for_operation.return_value = {'status': 'DONE'} # Success, no error + mock_commit.return_value = True + + start_test(MagicMock(), self.app, mock_g.db, repository, test, "token") + + # Should add the gcp_instance record + mock_g.db.add.assert_called_once() + + @mock.patch('run.log') + @mock.patch('mod_ci.controllers.safe_db_commit') + @mock.patch('mod_ci.controllers.wait_for_operation') + @mock.patch('mod_ci.controllers.create_instance') + @mock.patch('mod_ci.controllers.find_artifact_for_commit') + @mock.patch('mod_ci.controllers.requests.get') + @mock.patch('mod_ci.controllers.zipfile.ZipFile') + @mock.patch('builtins.open', new_callable=mock.mock_open()) + @mock.patch('mod_ci.controllers.g') + @mock.patch('mod_ci.controllers.TestProgress') + @mock.patch('mod_ci.controllers.GcpInstance') + def test_start_test_operation_timeout_creates_db_record( + self, mock_gcp_instance, mock_test_progress, mock_g, mock_open_file, + mock_zipfile, mock_requests_get, mock_find_artifact, + mock_create_instance, mock_wait_for_operation, mock_commit, mock_log): + """Test that operation timeout still creates record (VM may be starting slowly).""" + from mod_ci.controllers import start_test + + # Mock locking checks + mock_gcp_instance.query.filter.return_value.first.return_value = None + mock_test_progress.query.filter.return_value.first.return_value = None + mock_query = create_mock_db_query(mock_g) + mock_query.c.got = MagicMock() + + test = Test.query.first() + repository = MagicMock() + + # Mock successful artifact download + mock_artifact = MagicMock() + mock_artifact.name = 'CCExtractor Linux build' + mock_artifact.archive_download_url = 'https://example.com/artifact.zip' + mock_find_artifact.return_value = mock_artifact + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = b'fake zip content' + mock_requests_get.return_value = mock_response + + mock_zip = MagicMock() + mock_zipfile.return_value.__enter__ = MagicMock(return_value=mock_zip) + mock_zipfile.return_value.__exit__ = MagicMock(return_value=False) + + # VM creation started but verification timed out (VM still being created) + mock_create_instance.return_value = {'name': 'op-123', 'status': 'RUNNING'} + mock_wait_for_operation.return_value = {'status': 'TIMEOUT'} # Timeout, no error + mock_commit.return_value = True + + start_test(MagicMock(), self.app, mock_g.db, repository, test, "token") + + # Should still add the gcp_instance record (optimistic for slow VMs) + mock_g.db.add.assert_called_once() From 6f3ea862cd652d69f0d73f3c2c4ba8b46e29bc0d Mon Sep 17 00:00:00 2001 From: Carlos Date: Sun, 11 Jan 2026 09:28:08 +0100 Subject: [PATCH 2/3] refactor: extract common test setup to reduce duplication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses SonarCloud quality gate failure for duplicated lines. Extracted common mock setup into _setup_start_test_mocks() helper method. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- tests/test_ci/test_controllers.py | 144 +++++++++--------------------- 1 file changed, 40 insertions(+), 104 deletions(-) diff --git a/tests/test_ci/test_controllers.py b/tests/test_ci/test_controllers.py index 40305839..2046bbb8 100644 --- a/tests/test_ci/test_controllers.py +++ b/tests/test_ci/test_controllers.py @@ -3827,46 +3827,18 @@ class TestVMCreationVerification(BaseTestCase): the gcp_instance record. If the operation fails, mark the test as failed. """ - @mock.patch('run.log') - @mock.patch('mod_ci.controllers.mark_test_failed') - @mock.patch('mod_ci.controllers.wait_for_operation') - @mock.patch('mod_ci.controllers.create_instance') - @mock.patch('mod_ci.controllers.find_artifact_for_commit') - @mock.patch('mod_ci.controllers.requests.get') - @mock.patch('mod_ci.controllers.zipfile.ZipFile') - @mock.patch('builtins.open', new_callable=mock.mock_open()) - @mock.patch('mod_ci.controllers.g') - @mock.patch('mod_ci.controllers.TestProgress') - @mock.patch('mod_ci.controllers.GcpInstance') - def test_start_test_quota_exceeded_no_db_record( - self, mock_gcp_instance, mock_test_progress, mock_g, mock_open_file, - mock_zipfile, mock_requests_get, mock_find_artifact, - mock_create_instance, mock_wait_for_operation, mock_mark_failed, mock_log): - """Test that QUOTA_EXCEEDED error prevents gcp_instance record creation. - - This is the exact scenario from test #7768: - - GCP operation returns successfully (just an operation ID) - - But the operation completes with QUOTA_EXCEEDED error - - The test should be marked as failed - - NO gcp_instance record should be created - """ - from mod_ci.controllers import start_test - + def _setup_start_test_mocks(self, mock_g, mock_gcp_instance, mock_test_progress, + mock_find_artifact, mock_requests_get, mock_zipfile): + """Set up common mocks for start_test VM creation verification tests.""" # Mock locking checks to return None (no existing instances/progress) mock_gcp_instance.query.filter.return_value.first.return_value = None mock_test_progress.query.filter.return_value.first.return_value = None - - # Set up mock db query chain mock_query = create_mock_db_query(mock_g) mock_query.c.got = MagicMock() - test = Test.query.first() - repository = MagicMock() - # Mock successful artifact download mock_artifact = MagicMock() mock_artifact.name = 'CCExtractor Linux build' - mock_artifact.id = 123 mock_artifact.archive_download_url = 'https://example.com/artifact.zip' mock_find_artifact.return_value = mock_artifact @@ -3879,32 +3851,42 @@ def test_start_test_quota_exceeded_no_db_record( mock_zipfile.return_value.__enter__ = MagicMock(return_value=mock_zip) mock_zipfile.return_value.__exit__ = MagicMock(return_value=False) - # VM creation returns operation ID (appears successful initially) - mock_create_instance.return_value = { - 'name': 'operation-1768087398971-64810ed58f840-79d64e2e-ea386c76', - 'status': 'RUNNING' - } + @mock.patch('run.log') + @mock.patch('mod_ci.controllers.mark_test_failed') + @mock.patch('mod_ci.controllers.wait_for_operation') + @mock.patch('mod_ci.controllers.create_instance') + @mock.patch('mod_ci.controllers.find_artifact_for_commit') + @mock.patch('mod_ci.controllers.requests.get') + @mock.patch('mod_ci.controllers.zipfile.ZipFile') + @mock.patch('builtins.open', new_callable=mock.mock_open()) + @mock.patch('mod_ci.controllers.g') + @mock.patch('mod_ci.controllers.TestProgress') + @mock.patch('mod_ci.controllers.GcpInstance') + def test_start_test_quota_exceeded_no_db_record( + self, mock_gcp_instance, mock_test_progress, mock_g, mock_open_file, + mock_zipfile, mock_requests_get, mock_find_artifact, + mock_create_instance, mock_wait_for_operation, mock_mark_failed, mock_log): + """Test that QUOTA_EXCEEDED error prevents gcp_instance record creation. + + This is the exact scenario from test #7768: GCP operation returns + successfully but completes with QUOTA_EXCEEDED error. + """ + from mod_ci.controllers import start_test + + self._setup_start_test_mocks(mock_g, mock_gcp_instance, mock_test_progress, + mock_find_artifact, mock_requests_get, mock_zipfile) - # But wait_for_operation returns QUOTA_EXCEEDED error - # This is the exact error from test #7768 + # VM creation returns operation ID, but wait_for_operation returns error + mock_create_instance.return_value = {'name': 'op-123', 'status': 'RUNNING'} mock_wait_for_operation.return_value = { 'status': 'DONE', - 'error': { - 'errors': [{ - 'code': 'QUOTA_EXCEEDED', - 'message': "Quota 'IN_USE_ADDRESSES' exceeded. Limit: 8.0 in region us-central1." - }] - }, - 'httpErrorStatusCode': 403, - 'httpErrorMessage': 'FORBIDDEN' + 'error': {'errors': [{'code': 'QUOTA_EXCEEDED', 'message': 'Quota exceeded'}]}, + 'httpErrorStatusCode': 403 } - start_test(MagicMock(), self.app, mock_g.db, repository, test, "token") + start_test(MagicMock(), self.app, mock_g.db, MagicMock(), Test.query.first(), "token") - # Should call mark_test_failed mock_mark_failed.assert_called_once() - - # Should NOT add a gcp_instance record to the database mock_g.db.add.assert_not_called() @mock.patch('run.log') @@ -3925,38 +3907,15 @@ def test_start_test_vm_verified_creates_db_record( """Test that successful VM creation creates gcp_instance record.""" from mod_ci.controllers import start_test - # Mock locking checks - mock_gcp_instance.query.filter.return_value.first.return_value = None - mock_test_progress.query.filter.return_value.first.return_value = None - mock_query = create_mock_db_query(mock_g) - mock_query.c.got = MagicMock() - - test = Test.query.first() - repository = MagicMock() - - # Mock successful artifact download - mock_artifact = MagicMock() - mock_artifact.name = 'CCExtractor Linux build' - mock_artifact.archive_download_url = 'https://example.com/artifact.zip' - mock_find_artifact.return_value = mock_artifact - - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.content = b'fake zip content' - mock_requests_get.return_value = mock_response - - mock_zip = MagicMock() - mock_zipfile.return_value.__enter__ = MagicMock(return_value=mock_zip) - mock_zipfile.return_value.__exit__ = MagicMock(return_value=False) + self._setup_start_test_mocks(mock_g, mock_gcp_instance, mock_test_progress, + mock_find_artifact, mock_requests_get, mock_zipfile) - # VM creation succeeds mock_create_instance.return_value = {'name': 'op-123', 'status': 'RUNNING'} - mock_wait_for_operation.return_value = {'status': 'DONE'} # Success, no error + mock_wait_for_operation.return_value = {'status': 'DONE'} mock_commit.return_value = True - start_test(MagicMock(), self.app, mock_g.db, repository, test, "token") + start_test(MagicMock(), self.app, mock_g.db, MagicMock(), Test.query.first(), "token") - # Should add the gcp_instance record mock_g.db.add.assert_called_once() @mock.patch('run.log') @@ -3977,36 +3936,13 @@ def test_start_test_operation_timeout_creates_db_record( """Test that operation timeout still creates record (VM may be starting slowly).""" from mod_ci.controllers import start_test - # Mock locking checks - mock_gcp_instance.query.filter.return_value.first.return_value = None - mock_test_progress.query.filter.return_value.first.return_value = None - mock_query = create_mock_db_query(mock_g) - mock_query.c.got = MagicMock() - - test = Test.query.first() - repository = MagicMock() - - # Mock successful artifact download - mock_artifact = MagicMock() - mock_artifact.name = 'CCExtractor Linux build' - mock_artifact.archive_download_url = 'https://example.com/artifact.zip' - mock_find_artifact.return_value = mock_artifact - - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.content = b'fake zip content' - mock_requests_get.return_value = mock_response - - mock_zip = MagicMock() - mock_zipfile.return_value.__enter__ = MagicMock(return_value=mock_zip) - mock_zipfile.return_value.__exit__ = MagicMock(return_value=False) + self._setup_start_test_mocks(mock_g, mock_gcp_instance, mock_test_progress, + mock_find_artifact, mock_requests_get, mock_zipfile) - # VM creation started but verification timed out (VM still being created) mock_create_instance.return_value = {'name': 'op-123', 'status': 'RUNNING'} - mock_wait_for_operation.return_value = {'status': 'TIMEOUT'} # Timeout, no error + mock_wait_for_operation.return_value = {'status': 'TIMEOUT'} mock_commit.return_value = True - start_test(MagicMock(), self.app, mock_g.db, repository, test, "token") + start_test(MagicMock(), self.app, mock_g.db, MagicMock(), Test.query.first(), "token") - # Should still add the gcp_instance record (optimistic for slow VMs) mock_g.db.add.assert_called_once() From 4da0e48ac6acc9c40d9a83af7b1298977f9c6bf0 Mon Sep 17 00:00:00 2001 From: Carlos Date: Sun, 11 Jan 2026 09:31:20 +0100 Subject: [PATCH 3/3] fix: correct indentation for pycodestyle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- tests/test_ci/test_controllers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_ci/test_controllers.py b/tests/test_ci/test_controllers.py index 2046bbb8..f28e8398 100644 --- a/tests/test_ci/test_controllers.py +++ b/tests/test_ci/test_controllers.py @@ -3828,7 +3828,7 @@ class TestVMCreationVerification(BaseTestCase): """ def _setup_start_test_mocks(self, mock_g, mock_gcp_instance, mock_test_progress, - mock_find_artifact, mock_requests_get, mock_zipfile): + mock_find_artifact, mock_requests_get, mock_zipfile): """Set up common mocks for start_test VM creation verification tests.""" # Mock locking checks to return None (no existing instances/progress) mock_gcp_instance.query.filter.return_value.first.return_value = None @@ -3874,7 +3874,7 @@ def test_start_test_quota_exceeded_no_db_record( from mod_ci.controllers import start_test self._setup_start_test_mocks(mock_g, mock_gcp_instance, mock_test_progress, - mock_find_artifact, mock_requests_get, mock_zipfile) + mock_find_artifact, mock_requests_get, mock_zipfile) # VM creation returns operation ID, but wait_for_operation returns error mock_create_instance.return_value = {'name': 'op-123', 'status': 'RUNNING'} @@ -3908,7 +3908,7 @@ def test_start_test_vm_verified_creates_db_record( from mod_ci.controllers import start_test self._setup_start_test_mocks(mock_g, mock_gcp_instance, mock_test_progress, - mock_find_artifact, mock_requests_get, mock_zipfile) + mock_find_artifact, mock_requests_get, mock_zipfile) mock_create_instance.return_value = {'name': 'op-123', 'status': 'RUNNING'} mock_wait_for_operation.return_value = {'status': 'DONE'} @@ -3937,7 +3937,7 @@ def test_start_test_operation_timeout_creates_db_record( from mod_ci.controllers import start_test self._setup_start_test_mocks(mock_g, mock_gcp_instance, mock_test_progress, - mock_find_artifact, mock_requests_get, mock_zipfile) + mock_find_artifact, mock_requests_get, mock_zipfile) mock_create_instance.return_value = {'name': 'op-123', 'status': 'RUNNING'} mock_wait_for_operation.return_value = {'status': 'TIMEOUT'}