Skip to content

Commit

Permalink
Rerun artefact test executions (#173)
Browse files Browse the repository at this point in the history
* add rerun artefact test executions endpoint

* Add filters to rerun test executions of an artefact

* Handle review decision filter with mismatch order
  • Loading branch information
omar-selo committed May 8, 2024
1 parent dc5b80b commit 5cc1685
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 32 deletions.
68 changes: 50 additions & 18 deletions backend/test_observer/controllers/artefacts/artefacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,36 @@
from sqlalchemy.orm import Session, joinedload

from test_observer.data_access import queries
from test_observer.data_access.models import Artefact, ArtefactBuild, TestExecution
from test_observer.data_access.models import (
Artefact,
ArtefactBuild,
TestExecution,
TestExecutionRerunRequest,
)
from test_observer.data_access.models_enums import ArtefactStatus, FamilyName
from test_observer.data_access.repository import get_artefacts_by_family
from test_observer.data_access.repository import get_artefacts_by_family, get_or_create
from test_observer.data_access.setup import get_db

from .logic import (
are_all_test_executions_approved,
is_there_a_rejected_test_execution,
)
from .models import ArtefactBuildDTO, ArtefactDTO, ArtefactPatch
from .models import (
ArtefactBuildDTO,
ArtefactDTO,
ArtefactPatch,
RerunArtefactTestExecutionsRequest,
)

router = APIRouter(tags=["artefacts"])


router = APIRouter()
def _get_artefact_from_db(artefact_id: int, db: Session = Depends(get_db)) -> Artefact:
a = db.get(Artefact, artefact_id)
if a is None:
msg = f"Artefact with id {artefact_id} not found"
raise HTTPException(status_code=404, detail=msg)
return a


@router.get("", response_model=list[ArtefactDTO])
Expand Down Expand Up @@ -61,29 +79,20 @@ def get_artefacts(family: FamilyName | None = None, db: Session = Depends(get_db


@router.get("/{artefact_id}", response_model=ArtefactDTO)
def get_artefact(artefact_id: int, db: Session = Depends(get_db)):
"""Get an artefact by id"""
artefact = db.get(Artefact, artefact_id)

if artefact is None:
raise HTTPException(status_code=404, detail="Artefact not found")

def get_artefact(artefact: Artefact = Depends(_get_artefact_from_db)):
return artefact


@router.patch("/{artefact_id}", response_model=ArtefactDTO)
def patch_artefact(
artefact_id: int, request: ArtefactPatch, db: Session = Depends(get_db)
request: ArtefactPatch,
db: Session = Depends(get_db),
artefact: Artefact = Depends(_get_artefact_from_db),
):
artefact = db.get(Artefact, artefact_id)

if not artefact:
raise HTTPException(status_code=404, detail="Artefact not found")

latest_builds = list(
db.scalars(
queries.latest_artefact_builds.where(
ArtefactBuild.artefact_id == artefact_id
ArtefactBuild.artefact_id == artefact.id
).options(joinedload(ArtefactBuild.test_executions))
).unique()
)
Expand Down Expand Up @@ -137,3 +146,26 @@ def get_artefact_builds(artefact_id: int, db: Session = Depends(get_db)):
)

return latest_builds


@router.post("/{artefact_id}/reruns")
def rerun_artefact_test_executions(
request: RerunArtefactTestExecutionsRequest | None = None,
artefact: Artefact = Depends(_get_artefact_from_db),
db: Session = Depends(get_db),
):
latest_builds = db.scalars(
queries.latest_artefact_builds.where(ArtefactBuild.artefact_id == artefact.id)
)
test_executions = (te for ab in latest_builds for te in ab.test_executions)

if request:
if status := request.test_execution_status:
test_executions = (te for te in test_executions if te.status == status)
if (decision := request.test_execution_review_decision) is not None:
test_executions = (
te for te in test_executions if set(te.review_decision) == decision
)

for te in test_executions:
get_or_create(db, TestExecutionRerunRequest, {"test_execution_id": te.id})
5 changes: 5 additions & 0 deletions backend/test_observer/controllers/artefacts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,8 @@ class ArtefactBuildDTO(BaseModel):

class ArtefactPatch(BaseModel):
status: ArtefactStatus


class RerunArtefactTestExecutionsRequest(BaseModel):
test_execution_status: TestExecutionStatus | None = None
test_execution_review_decision: set[TestExecutionReviewDecision] | None = None
114 changes: 100 additions & 14 deletions backend/tests/controllers/artefacts/test_artefacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@

from fastapi.testclient import TestClient

from test_observer.data_access.models import TestExecution
from test_observer.data_access.models_enums import (
ArtefactStatus,
TestExecutionReviewDecision,
TestExecutionStatus,
)
from tests.data_generator import DataGenerator

Expand Down Expand Up @@ -271,31 +273,23 @@ def test_artefact_signoff_approve(test_client: TestClient, generator: DataGenera


def test_artefact_signoff_disallow_approve(
test_client: TestClient, generator: DataGenerator
test_client: TestClient, test_execution: TestExecution
):
a = generator.gen_artefact("candidate")
ab = generator.gen_artefact_build(a)
e = generator.gen_environment()
generator.gen_test_execution(ab, e)

artefact_id = test_execution.artefact_build.artefact_id
response = test_client.patch(
f"/v1/artefacts/{a.id}",
f"/v1/artefacts/{artefact_id}",
json={"status": ArtefactStatus.APPROVED},
)

assert response.status_code == 400


def test_artefact_signoff_disallow_reject(
test_client: TestClient, generator: DataGenerator
test_client: TestClient, test_execution: TestExecution
):
a = generator.gen_artefact("candidate")
ab = generator.gen_artefact_build(a)
e = generator.gen_environment()
generator.gen_test_execution(ab, e)

artefact_id = test_execution.artefact_build.artefact_id
response = test_client.patch(
f"/v1/artefacts/{a.id}",
f"/v1/artefacts/{artefact_id}",
json={"status": ArtefactStatus.MARKED_AS_FAILED},
)

Expand Down Expand Up @@ -349,3 +343,95 @@ def test_artefact_signoff_ignore_old_build_on_reject(
)

assert response.status_code == 400


def test_rerun_all_artefact_test_executions(
test_client: TestClient, test_execution: TestExecution
):
artefact_id = test_execution.artefact_build.artefact_id

response = test_client.post(f"/v1/artefacts/{artefact_id}/reruns")

assert response.status_code == 200
assert test_execution.rerun_request


def test_rerun_skips_test_executions_of_old_builds(
test_client: TestClient, generator: DataGenerator
):
a = generator.gen_artefact("candidate")
ab1 = generator.gen_artefact_build(a, revision=1)
ab2 = generator.gen_artefact_build(a, revision=2)
e = generator.gen_environment()
te1 = generator.gen_test_execution(ab1, e)
te2 = generator.gen_test_execution(ab2, e)

response = test_client.post(f"/v1/artefacts/{a.id}/reruns")

assert response.status_code == 200
assert te1.rerun_request is None
assert te2.rerun_request


def test_rerun_failed_artefact_test_executions(
test_client: TestClient, generator: DataGenerator
):
a = generator.gen_artefact("candidate")
ab = generator.gen_artefact_build(a)
e1 = generator.gen_environment(name="laptop")
e2 = generator.gen_environment(name="server")
te1 = generator.gen_test_execution(ab, e1)
te2 = generator.gen_test_execution(ab, e2, status=TestExecutionStatus.FAILED)

response = test_client.post(
f"/v1/artefacts/{a.id}/reruns",
json={"test_execution_status": TestExecutionStatus.FAILED},
)

assert response.status_code == 200
assert te1.rerun_request is None
assert te2.rerun_request


def test_rerun_undecided_artefact_test_executions(
test_client: TestClient, generator: DataGenerator
):
a = generator.gen_artefact("candidate")
ab = generator.gen_artefact_build(a)
e1 = generator.gen_environment(name="laptop")
e2 = generator.gen_environment(name="server")
te1 = generator.gen_test_execution(
ab, e1, review_decision=[TestExecutionReviewDecision.APPROVED_ALL_TESTS_PASS]
)
te2 = generator.gen_test_execution(ab, e2, review_decision=[])

response = test_client.post(
f"/v1/artefacts/{a.id}/reruns",
json={"test_execution_review_decision": []},
)

assert response.status_code == 200
assert te1.rerun_request is None
assert te2.rerun_request


def test_rerun_filters_ignore_review_decisions_order(
test_client: TestClient, test_execution: TestExecution
):
test_execution.review_decision = [
TestExecutionReviewDecision.APPROVED_INCONSISTENT_TEST,
TestExecutionReviewDecision.APPROVED_FAULTY_HARDWARE,
]

response = test_client.post(
f"/v1/artefacts/{test_execution.artefact_build.artefact_id}/reruns",
json={
"test_execution_review_decision": [
TestExecutionReviewDecision.APPROVED_FAULTY_HARDWARE,
TestExecutionReviewDecision.APPROVED_INCONSISTENT_TEST,
]
},
)

assert response.status_code == 200
assert test_execution.rerun_request

0 comments on commit 5cc1685

Please sign in to comment.