diff --git a/backend/test_observer/controllers/artefacts/artefacts.py b/backend/test_observer/controllers/artefacts/artefacts.py index 7d0e1a20..fbefd221 100644 --- a/backend/test_observer/controllers/artefacts/artefacts.py +++ b/backend/test_observer/controllers/artefacts/artefacts.py @@ -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]) @@ -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() ) @@ -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}) diff --git a/backend/test_observer/controllers/artefacts/models.py b/backend/test_observer/controllers/artefacts/models.py index 8f22450e..78469a27 100644 --- a/backend/test_observer/controllers/artefacts/models.py +++ b/backend/test_observer/controllers/artefacts/models.py @@ -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 diff --git a/backend/tests/controllers/artefacts/test_artefacts.py b/backend/tests/controllers/artefacts/test_artefacts.py index 4d3c46c8..aaf50fb4 100644 --- a/backend/tests/controllers/artefacts/test_artefacts.py +++ b/backend/tests/controllers/artefacts/test_artefacts.py @@ -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 @@ -271,15 +273,11 @@ 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}, ) @@ -287,15 +285,11 @@ def test_artefact_signoff_disallow_approve( 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}, ) @@ -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