Skip to content

Commit

Permalink
Add testresult report (#127)
Browse files Browse the repository at this point in the history
* Make artefact track, store, series and repo not nullable

* Add testresults report endpoint

* Convert helper test function to fixtures

* Refactor test data generation

* Move create_artefact helper into DataGenerator

* Return only latest artefact builds in testresults report

* Fix migration

* Reflect not nullability through artefact dto

* Fix frontend after some artefact fields became not nullable
  • Loading branch information
omar-selo authored Feb 21, 2024
1 parent f9108c1 commit 2021956
Show file tree
Hide file tree
Showing 19 changed files with 555 additions and 131 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Make artefact track, store, series and repo not nullable
Revision ID: 654e57018d35
Revises: bb2a51214402
Create Date: 2024-02-20 08:19:37.117290+00:00
"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "654e57018d35"
down_revision = "bb2a51214402"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.drop_constraint("unique_deb", "artefact", type_="unique")
op.create_index(
"unique_deb",
"artefact",
["name", "version", "series", "repo"],
unique=True,
postgresql_where=sa.text("series != '' AND repo != ''"),
)
op.drop_constraint("unique_snap", "artefact", type_="unique")
op.create_index(
"unique_snap",
"artefact",
["name", "version", "track"],
unique=True,
postgresql_where=sa.text("track != ''"),
)

op.execute("UPDATE artefact SET track = '' WHERE track is NULL")
op.execute("UPDATE artefact SET store = '' WHERE store is NULL")
op.execute("UPDATE artefact SET series = '' WHERE series is NULL")
op.execute("UPDATE artefact SET repo = '' WHERE repo is NULL")

op.alter_column("artefact", "track", existing_type=sa.VARCHAR(), nullable=False)
op.alter_column("artefact", "store", existing_type=sa.VARCHAR(), nullable=False)
op.alter_column("artefact", "series", existing_type=sa.VARCHAR(), nullable=False)
op.alter_column("artefact", "repo", existing_type=sa.VARCHAR(), nullable=False)


def downgrade() -> None:
op.alter_column("artefact", "repo", existing_type=sa.VARCHAR(), nullable=True)
op.alter_column("artefact", "series", existing_type=sa.VARCHAR(), nullable=True)
op.alter_column("artefact", "store", existing_type=sa.VARCHAR(), nullable=True)
op.alter_column("artefact", "track", existing_type=sa.VARCHAR(), nullable=True)

op.execute("UPDATE artefact SET repo = NULL WHERE repo = ''")
op.execute("UPDATE artefact SET series = NULL WHERE series = ''")
op.execute("UPDATE artefact SET store = NULL WHERE store = ''")
op.execute("UPDATE artefact SET track = NULL WHERE track = ''")

op.drop_index(
"unique_snap", table_name="artefact", postgresql_where=sa.text("track != ''")
)
op.create_unique_constraint("unique_snap", "artefact", ["name", "version", "track"])
op.drop_index(
"unique_deb",
table_name="artefact",
postgresql_where=sa.text("series != '' AND repo != ''"),
)
op.create_unique_constraint(
"unique_deb", "artefact", ["name", "version", "series", "repo"]
)
8 changes: 4 additions & 4 deletions backend/test_observer/controllers/artefacts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ class ArtefactDTO(BaseModel):
id: int
name: str
version: str
track: str | None
store: str | None
series: str | None
repo: str | None
track: str
store: str
series: str
repo: str
stage: str = Field(validation_alias=AliasPath("stage", "name"))
status: ArtefactStatus
assignee: UserDTO | None
Expand Down
81 changes: 81 additions & 0 deletions backend/test_observer/controllers/reports/reports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import csv
from datetime import datetime

from fastapi import APIRouter, Depends
from fastapi.responses import FileResponse
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.orm.attributes import InstrumentedAttribute

from test_observer.data_access.models import (
Artefact,
ArtefactBuild,
Environment,
Family,
Stage,
TestCase,
TestExecution,
TestResult,
)
from test_observer.data_access.setup import get_db

router = APIRouter()

TESTRESULTS_REPORT_COLUMNS: list[InstrumentedAttribute] = [
Family.name,
Artefact.name,
Artefact.version,
Artefact.status,
Artefact.track,
Artefact.series,
Artefact.repo,
TestExecution.status,
TestExecution.review_decision,
TestExecution.review_comment,
Environment.name,
Environment.architecture,
TestCase.name,
TestCase.category,
TestResult.status,
]


@router.get("/testresults", response_class=FileResponse)
def get_testresults_report(
start_date: datetime = datetime.min,
end_date: datetime | None = None,
db: Session = Depends(get_db),
):
"""
Returns a csv report detailing all artefacts within a given date range. Together
with their test executions and test results in csv format.
"""
if end_date is None:
end_date = datetime.now()

latest_builds = (
select(ArtefactBuild)
.distinct(ArtefactBuild.artefact_id, ArtefactBuild.architecture)
.order_by(ArtefactBuild.artefact_id, ArtefactBuild.architecture)
.subquery()
)

cursor = db.execute(
select(*TESTRESULTS_REPORT_COLUMNS)
.join_from(Family, Stage)
.join_from(Stage, Artefact)
.join_from(Artefact, latest_builds)
.join_from(latest_builds, TestExecution)
.join_from(TestExecution, Environment)
.join_from(TestExecution, TestResult)
.join_from(TestResult, TestCase)
.where(Artefact.created_at >= start_date, Artefact.created_at <= end_date)
)

filename = "testresults_report.csv"
with open(filename, "w") as csvfile:
writer = csv.writer(csvfile)
writer.writerow(TESTRESULTS_REPORT_COLUMNS)
writer.writerows(cursor)

return filename
2 changes: 2 additions & 0 deletions backend/test_observer/controllers/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@
from .application import version
from .artefacts import artefacts
from .promoter import promoter
from .reports import reports
from .test_executions import test_executions

router = APIRouter()
router.include_router(promoter.router)
router.include_router(version.router, prefix="/v1/version")
router.include_router(test_executions.router, prefix="/v1/test-executions")
router.include_router(artefacts.router, prefix="/v1/artefacts")
router.include_router(reports.router, prefix="/v1/reports")


@router.get("/")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ def start_test_execution(
filter_kwargs={
"name": request.name,
"version": request.version,
"track": request.track,
"store": request.store,
"series": request.series,
"repo": request.repo,
"track": request.track if request.track is not None else "",
"store": request.store if request.store is not None else "",
"series": request.series if request.series is not None else "",
"repo": request.repo if request.repo is not None else "",
},
creation_kwargs={"stage_id": stage.id},
)
Expand Down
27 changes: 21 additions & 6 deletions backend/test_observer/data_access/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,10 @@ class Artefact(Base):

name: Mapped[str] = mapped_column(String(200), index=True)
version: Mapped[str]
track: Mapped[str | None]
store: Mapped[str | None]
series: Mapped[str | None]
repo: Mapped[str | None]
track: Mapped[str] = mapped_column(default="")
store: Mapped[str] = mapped_column(default="")
series: Mapped[str] = mapped_column(default="")
repo: Mapped[str] = mapped_column(default="")
# Relationships
stage_id: Mapped[int] = mapped_column(ForeignKey("stage.id", ondelete="CASCADE"))
stage: Mapped[Stage] = relationship(back_populates="artefacts")
Expand All @@ -152,8 +152,23 @@ class Artefact(Base):
status: Mapped[ArtefactStatus] = mapped_column(default=ArtefactStatus.UNDECIDED)

__table_args__ = (
UniqueConstraint("name", "version", "track", name="unique_snap"),
UniqueConstraint("name", "version", "series", "repo", name="unique_deb"),
Index(
"unique_snap",
"name",
"version",
"track",
postgresql_where=column("track") != "",
unique=True,
),
Index(
"unique_deb",
"name",
"version",
"series",
"repo",
postgresql_where=(column("series") != "") & (column("repo") != ""),
unique=True,
),
)

def __repr__(self) -> str:
Expand Down
4 changes: 2 additions & 2 deletions backend/test_observer/data_access/models_enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@
from enum import Enum


class FamilyName(Enum):
class FamilyName(str, Enum):
SNAP = "snap"
DEB = "deb"


class TestExecutionStatus(Enum):
class TestExecutionStatus(str, Enum):
__test__ = False

NOT_STARTED = "NOT_STARTED"
Expand Down
20 changes: 3 additions & 17 deletions backend/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"""Fixtures for testing"""


from collections.abc import Callable
from os import environ

import pytest
Expand All @@ -34,9 +33,9 @@
drop_database,
)

from test_observer.data_access.models import User
from test_observer.data_access.setup import get_db
from test_observer.main import app
from tests.data_generator import DataGenerator


@pytest.fixture(scope="session")
Expand Down Expand Up @@ -93,18 +92,5 @@ def test_client(db_session: Session) -> TestClient:


@pytest.fixture
def create_user(db_session: Session) -> Callable[..., User]:
def _create_user(**kwargs) -> User:
user = User(
**{
"name": "John Doe",
"launchpad_handle": "jd",
"launchpad_email": "[email protected]",
**kwargs,
}
)
db_session.add(user)
db_session.commit()
return user

return _create_user
def generator(db_session: Session) -> DataGenerator:
return DataGenerator(db_session)
Loading

0 comments on commit 2021956

Please sign in to comment.