""" Test Registry - CI/CD Integration Endpoints Endpoints for receiving results from CI/CD pipelines. """ from datetime import datetime from typing import Dict from fastapi import APIRouter, BackgroundTasks from ...database import get_db_session from ...repository import TestRepository from ..api_models import CIResultRequest from ..config import ( get_test_runs, get_persisted_results, is_postgres_available, ) router = APIRouter() @router.post("/ci-result") async def receive_ci_result(result: CIResultRequest, background_tasks: BackgroundTasks): """ Empfaengt Test-Ergebnisse von der CI/CD-Pipeline. Wird vom report-test-results Step in .woodpecker/main.yml aufgerufen. Flow: 1. Pipeline fuehrt Tests aus und sammelt JSON-Ergebnisse 2. Pipeline sendet detaillierte Ergebnisse pro Service hierher 3. Dieser Endpoint speichert in PostgreSQL 4. Dashboard zeigt die Daten an test_results Format: { "service": "consent-service", "framework": "go", "total": 57, "passed": 57, "failed": 0, "skipped": 0, "coverage": 75.5 } """ test_runs = get_test_runs() persisted_results = get_persisted_results() # Extrahiere Service-spezifische Daten aus test_results tr = result.test_results or {} service_name = tr.get("service", "ci-pipeline") framework = tr.get("framework", "unknown") total = tr.get("total", 0) passed = tr.get("passed", 0) failed = tr.get("failed", 0) skipped = tr.get("skipped", 0) coverage = tr.get("coverage", 0) # Log zur Debugging print(f"[CI-RESULT] Pipeline {result.pipeline_id} - Service: {service_name}") print(f"[CI-RESULT] Tests: {passed}/{total} passed, {failed} failed, {skipped} skipped") print(f"[CI-RESULT] Coverage: {coverage}%, Commit: {result.commit[:8]}") # Speichere in PostgreSQL wenn verfuegbar if is_postgres_available(): try: with get_db_session() as db: repo = TestRepository(db) # Erstelle eindeutige Run-ID pro Service run_id = f"ci-{result.pipeline_id}-{service_name}" # Erstelle Test-Run Eintrag run = repo.create_run( run_id=run_id, service=service_name, framework=framework, triggered_by="ci", git_commit=result.commit[:8] if result.commit else None, git_branch=result.branch ) # Markiere als abgeschlossen mit detaillierten Zahlen status = "passed" if failed == 0 else "failed" repo.complete_run( run_id=run_id, status=status, total_tests=total, passed_tests=passed, failed_tests=failed, skipped_tests=skipped, duration_seconds=0 ) print(f"[CI-RESULT] Stored as run_id: {run_id}, status: {status}") # WICHTIG: Aktualisiere den In-Memory Cache fuer sofortige Frontend-Updates persisted_results[service_name] = { "total": total, "passed": passed, "failed": failed, "last_run": datetime.utcnow().isoformat(), "status": status, "failed_test_ids": [] } print(f"[CI-RESULT] Updated cache for {service_name}: {passed}/{total} passed") # Bei fehlgeschlagenen Tests: Backlog-Eintrag erstellen if failed > 0: background_tasks.add_task( _create_backlog_entry, service_name, framework, failed, result.pipeline_id, result.commit, result.branch ) else: # Alle Tests bestanden: Schließe offene Backlog-Einträge background_tasks.add_task( _close_backlog_entry, service_name, result.pipeline_id, result.commit ) return { "received": True, "run_id": run_id, "service": service_name, "pipeline_id": result.pipeline_id, "status": status, "tests": {"total": total, "passed": passed, "failed": failed}, "stored_in": "postgres" } except Exception as e: print(f"[CI-RESULT] PostgreSQL Error: {e}") # Fallback auf Memory-Storage pass # Memory-Fallback ci_run = { "id": f"ci-{result.pipeline_id}", "pipeline_id": result.pipeline_id, "commit": result.commit, "branch": result.branch, "status": result.status, "timestamp": datetime.now().isoformat(), "test_results": result.test_results } test_runs.append(ci_run) return { "received": True, "pipeline_id": result.pipeline_id, "status": result.status, "stored_in": "memory" } async def _create_backlog_entry( service_name: str, framework: str, failed_count: int, pipeline_id: str, commit: str, branch: str ): """ Background-Task: Erstellt Backlog-Eintraege fuer fehlgeschlagene Tests. Wird asynchron aufgerufen wenn Tests fehlgeschlagen sind. """ from ...db_models import FailedTestBacklogDB print(f"[CI-RESULT] Creating backlog entry for {service_name}: {failed_count} failed tests") if is_postgres_available(): try: with get_db_session() as db: now = datetime.utcnow() # Pruefe ob schon ein offener Backlog-Eintrag fuer diesen Service existiert existing = db.query(FailedTestBacklogDB).filter( FailedTestBacklogDB.service == service_name, FailedTestBacklogDB.status == "open" ).first() if existing: # Aktualisiere existierenden Eintrag existing.last_failed_at = now existing.failure_count += 1 existing.error_message = f"{failed_count} Tests fehlgeschlagen in Pipeline {pipeline_id} (Branch: {branch})" db.commit() print(f"[CI-RESULT] Updated existing backlog entry (ID: {existing.id})") else: # Neuen Eintrag erstellen backlog = FailedTestBacklogDB( test_name=f"{service_name} Tests", test_file=f"{service_name}/", service=service_name, framework=framework, error_message=f"{failed_count} Tests fehlgeschlagen in Pipeline {pipeline_id} (Branch: {branch})", error_type="TEST_FAILURE", first_failed_at=now, last_failed_at=now, failure_count=1, status="open", priority="high" if failed_count > 5 else "medium" ) db.add(backlog) db.commit() print(f"[CI-RESULT] Created new backlog entry (ID: {backlog.id})") except Exception as e: print(f"[CI-RESULT] Error creating backlog entry: {e}") async def _close_backlog_entry( service_name: str, pipeline_id: str, commit: str ): """ Background-Task: Schließt Backlog-Einträge wenn alle Tests bestanden. Wird asynchron aufgerufen wenn Tests erfolgreich waren. """ from ...db_models import FailedTestBacklogDB print(f"[CI-RESULT] Checking for open backlog entries to close for {service_name}") if is_postgres_available(): try: with get_db_session() as db: now = datetime.utcnow() # Finde offene Backlog-Einträge für diesen Service open_entries = db.query(FailedTestBacklogDB).filter( FailedTestBacklogDB.service == service_name, FailedTestBacklogDB.status == "open" ).all() for entry in open_entries: entry.status = "resolved" entry.resolved_at = now entry.resolution_commit = commit[:8] if commit else None entry.resolution_notes = f"Automatisch geschlossen - alle Tests in Pipeline {pipeline_id} bestanden" print(f"[CI-RESULT] Auto-closed backlog entry (ID: {entry.id}) for {service_name}") if open_entries: db.commit() print(f"[CI-RESULT] Closed {len(open_entries)} backlog entries for {service_name}") else: print(f"[CI-RESULT] No open backlog entries for {service_name}") except Exception as e: print(f"[CI-RESULT] Error closing backlog entries: {e}") async def _fetch_and_store_failed_tests(pipeline_id: str, commit: str, branch: str): """ Legacy Background-Task fuer generische Pipeline-Fehler. """ from ...db_models import FailedTestBacklogDB print(f"[CI-RESULT] Fetching failed test details for pipeline {pipeline_id}") if is_postgres_available(): try: with get_db_session() as db: now = datetime.utcnow() backlog = FailedTestBacklogDB( test_name=f"CI Pipeline {pipeline_id}", test_file=".woodpecker/main.yml", service="ci-pipeline", framework="woodpecker", error_message=f"Pipeline {pipeline_id} fehlgeschlagen auf Branch {branch}", error_type="CI_FAILURE", first_failed_at=now, last_failed_at=now, failure_count=1, status="open", priority="high" ) db.add(backlog) db.commit() print(f"[CI-RESULT] Added pipeline failure to backlog (ID: {backlog.id})") except Exception as e: print(f"[CI-RESULT] Error adding to backlog: {e}")