Files
orion/alembic/versions/82ea1b4a3ccb_add_test_run_tables.py
Samir Boulahtit e3a10b4a53 feat: add pytest testing dashboard with run history and statistics
Add a new Testing Dashboard page that replaces the old Testing Hub with
pytest integration:

- Database models for test runs, results, and collections (TestRun,
  TestResult, TestCollection)
- Test runner service that executes pytest with JSON reporting and
  stores results in the database
- REST API endpoints for running tests, viewing history, and statistics
- Dashboard UI showing pass rates, trends, tests by category, and top
  failing tests
- Alembic migration for the new test_* tables

The dashboard allows admins to:
- Run pytest directly from the UI
- View test run history with pass/fail statistics
- See trend data across recent runs
- Identify frequently failing tests
- Collect test information without running

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 23:04:41 +01:00

104 lines
4.9 KiB
Python

"""add_test_run_tables
Revision ID: 82ea1b4a3ccb
Revises: b4c5d6e7f8a9
Create Date: 2025-12-12 22:48:09.501172
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '82ea1b4a3ccb'
down_revision: Union[str, None] = 'b4c5d6e7f8a9'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Create test_collections table
op.create_table('test_collections',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('total_tests', sa.Integer(), nullable=True),
sa.Column('total_files', sa.Integer(), nullable=True),
sa.Column('total_classes', sa.Integer(), nullable=True),
sa.Column('unit_tests', sa.Integer(), nullable=True),
sa.Column('integration_tests', sa.Integer(), nullable=True),
sa.Column('performance_tests', sa.Integer(), nullable=True),
sa.Column('system_tests', sa.Integer(), nullable=True),
sa.Column('test_files', sa.JSON(), nullable=True),
sa.Column('collected_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_test_collections_id'), 'test_collections', ['id'], unique=False)
# Create test_runs table
op.create_table('test_runs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('timestamp', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('total_tests', sa.Integer(), nullable=True),
sa.Column('passed', sa.Integer(), nullable=True),
sa.Column('failed', sa.Integer(), nullable=True),
sa.Column('errors', sa.Integer(), nullable=True),
sa.Column('skipped', sa.Integer(), nullable=True),
sa.Column('xfailed', sa.Integer(), nullable=True),
sa.Column('xpassed', sa.Integer(), nullable=True),
sa.Column('coverage_percent', sa.Float(), nullable=True),
sa.Column('duration_seconds', sa.Float(), nullable=True),
sa.Column('triggered_by', sa.String(length=100), nullable=True),
sa.Column('git_commit_hash', sa.String(length=40), nullable=True),
sa.Column('git_branch', sa.String(length=100), nullable=True),
sa.Column('test_path', sa.String(length=500), nullable=True),
sa.Column('pytest_args', sa.String(length=500), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_test_runs_id'), 'test_runs', ['id'], unique=False)
op.create_index(op.f('ix_test_runs_status'), 'test_runs', ['status'], unique=False)
op.create_index(op.f('ix_test_runs_timestamp'), 'test_runs', ['timestamp'], unique=False)
# Create test_results table
op.create_table('test_results',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('run_id', sa.Integer(), nullable=False),
sa.Column('node_id', sa.String(length=500), nullable=False),
sa.Column('test_name', sa.String(length=200), nullable=False),
sa.Column('test_file', sa.String(length=300), nullable=False),
sa.Column('test_class', sa.String(length=200), nullable=True),
sa.Column('outcome', sa.String(length=20), nullable=False),
sa.Column('duration_seconds', sa.Float(), nullable=True),
sa.Column('error_message', sa.Text(), nullable=True),
sa.Column('traceback', sa.Text(), nullable=True),
sa.Column('markers', sa.JSON(), nullable=True),
sa.Column('parameters', sa.JSON(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['run_id'], ['test_runs.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_test_results_id'), 'test_results', ['id'], unique=False)
op.create_index(op.f('ix_test_results_node_id'), 'test_results', ['node_id'], unique=False)
op.create_index(op.f('ix_test_results_outcome'), 'test_results', ['outcome'], unique=False)
op.create_index(op.f('ix_test_results_run_id'), 'test_results', ['run_id'], unique=False)
def downgrade() -> None:
# Drop test_results table first (has foreign key to test_runs)
op.drop_index(op.f('ix_test_results_run_id'), table_name='test_results')
op.drop_index(op.f('ix_test_results_outcome'), table_name='test_results')
op.drop_index(op.f('ix_test_results_node_id'), table_name='test_results')
op.drop_index(op.f('ix_test_results_id'), table_name='test_results')
op.drop_table('test_results')
# Drop test_runs table
op.drop_index(op.f('ix_test_runs_timestamp'), table_name='test_runs')
op.drop_index(op.f('ix_test_runs_status'), table_name='test_runs')
op.drop_index(op.f('ix_test_runs_id'), table_name='test_runs')
op.drop_table('test_runs')
# Drop test_collections table
op.drop_index(op.f('ix_test_collections_id'), table_name='test_collections')
op.drop_table('test_collections')