Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rerun artefact test executions #173

Merged
merged 3 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding the tag here 😄



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
Loading