diff --git a/app/api/v1/admin/tests.py b/app/api/v1/admin/tests.py index de49c475..cdda259a 100644 --- a/app/api/v1/admin/tests.py +++ b/app/api/v1/admin/tests.py @@ -3,13 +3,14 @@ Test Runner API Endpoints RESTful API for running pytest and viewing test results """ -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, BackgroundTasks, Depends, Query from pydantic import BaseModel, Field from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db from app.services.test_runner_service import test_runner_service +from app.tasks.test_runner_tasks import execute_test_run from models.database.user import User router = APIRouter() @@ -96,27 +97,37 @@ class TestDashboardStatsResponse(BaseModel): @router.post("/run", response_model=TestRunResponse) async def run_tests( + background_tasks: BackgroundTasks, request: RunTestsRequest | None = None, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_api), ): """ - Run pytest and store results + Start a pytest run in the background - Requires admin authentication. Runs pytest on the specified path - and stores results in the database. + Requires admin authentication. Creates a test run record and starts + pytest execution in the background. Returns immediately with the run ID. + Poll GET /runs/{run_id} to check status. """ test_path = request.test_path if request else "tests" extra_args = request.extra_args if request else None - run = test_runner_service.run_tests( + # Create the test run record + run = test_runner_service.create_test_run( db, test_path=test_path, triggered_by=f"manual:{current_user.username}", - extra_args=extra_args, ) db.commit() + # Start background execution + background_tasks.add_task( + execute_test_run, + run.id, + test_path, + extra_args, + ) + return TestRunResponse( id=run.id, timestamp=run.timestamp.isoformat(), diff --git a/app/services/test_runner_service.py b/app/services/test_runner_service.py index 8bfc1e5e..b01aeb46 100644 --- a/app/services/test_runner_service.py +++ b/app/services/test_runner_service.py @@ -25,6 +25,25 @@ class TestRunnerService: def __init__(self): self.project_root = Path(__file__).parent.parent.parent + def create_test_run( + self, + db: Session, + test_path: str = "tests", + triggered_by: str = "manual", + ) -> TestRun: + """Create a test run record without executing tests""" + test_run = TestRun( + timestamp=datetime.now(UTC), + triggered_by=triggered_by, + test_path=test_path, + status="running", + git_commit_hash=self._get_git_commit(), + git_branch=self._get_git_branch(), + ) + db.add(test_run) + db.flush() + return test_run + def run_tests( self, db: Session, @@ -33,7 +52,7 @@ class TestRunnerService: extra_args: list[str] | None = None, ) -> TestRun: """ - Run pytest and store results in database + Run pytest synchronously and store results in database Args: db: Database session @@ -44,21 +63,19 @@ class TestRunnerService: Returns: TestRun object with results """ - # Create test run record - test_run = TestRun( - timestamp=datetime.now(UTC), - triggered_by=triggered_by, - test_path=test_path, - status="running", - ) - db.add(test_run) - db.flush() # Get the ID + test_run = self.create_test_run(db, test_path, triggered_by) + self._execute_tests(db, test_run, test_path, extra_args) + return test_run + def _execute_tests( + self, + db: Session, + test_run: TestRun, + test_path: str, + extra_args: list[str] | None, + ) -> None: + """Execute pytest and update the test run record""" try: - # Get git info - test_run.git_commit_hash = self._get_git_commit() - test_run.git_branch = self._get_git_branch() - # Build pytest command with JSON output with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: json_report_path = f.name @@ -66,7 +83,7 @@ class TestRunnerService: pytest_args = [ "python", "-m", "pytest", test_path, - f"--json-report", + "--json-report", f"--json-report-file={json_report_path}", "-v", "--tb=short", @@ -120,8 +137,6 @@ class TestRunnerService: test_run.status = "error" logger.error(f"Error running tests: {e}") - return test_run - def _process_json_report(self, db: Session, test_run: TestRun, report: dict): """Process pytest-json-report output""" summary = report.get("summary", {}) diff --git a/app/tasks/test_runner_tasks.py b/app/tasks/test_runner_tasks.py new file mode 100644 index 00000000..cd626b7e --- /dev/null +++ b/app/tasks/test_runner_tasks.py @@ -0,0 +1,62 @@ +# app/tasks/test_runner_tasks.py +"""Background tasks for test runner.""" + +import logging +from datetime import UTC, datetime + +from app.core.database import SessionLocal +from app.services.test_runner_service import test_runner_service +from models.database.test_run import TestRun + +logger = logging.getLogger(__name__) + + +async def execute_test_run( + run_id: int, + test_path: str = "tests", + extra_args: list[str] | None = None, +): + """Background task to execute pytest tests. + + Args: + run_id: ID of the TestRun record + test_path: Path to tests (relative to project root) + extra_args: Additional pytest arguments + """ + db = SessionLocal() + test_run = None + + try: + # Get the test run record + test_run = db.query(TestRun).filter(TestRun.id == run_id).first() + if not test_run: + logger.error(f"Test run {run_id} not found") + return + + logger.info(f"Starting test execution: Run {run_id}, Path: {test_path}") + + # Execute the tests + test_runner_service._execute_tests(db, test_run, test_path, extra_args) + db.commit() + + logger.info( + f"Test run {run_id} completed: " + f"status={test_run.status}, passed={test_run.passed}, " + f"failed={test_run.failed}, duration={test_run.duration_seconds:.1f}s" + ) + + except Exception as e: + logger.error(f"Test run {run_id} failed: {e}", exc_info=True) + if test_run is not None: + try: + test_run.status = "error" + db.commit() + except Exception as commit_error: + logger.error(f"Failed to update test run status: {commit_error}") + db.rollback() + finally: + if hasattr(db, "close") and callable(db.close): + try: + db.close() + except Exception as close_error: + logger.error(f"Error closing database session: {close_error}") diff --git a/app/templates/admin/testing-dashboard.html b/app/templates/admin/testing-dashboard.html index f943e797..3e51a14d 100644 --- a/app/templates/admin/testing-dashboard.html +++ b/app/templates/admin/testing-dashboard.html @@ -23,6 +23,33 @@ {{ alert_dynamic(type='success', message_var='successMessage', show_condition='successMessage') }} + +
Running tests...
+Tests are executing in the background. You can leave this page and come back.
+0s
+elapsed
+