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

Refactor: lazy load test status history #1056

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
13 changes: 6 additions & 7 deletions backend/kernelCI_app/queries/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ def get_test_details_data(*, test_id):
)


# TODO: combine with the test_details query
def get_test_status_history(
*,
path: str,
Expand All @@ -43,17 +42,17 @@ def get_test_status_history(
platform: Optional[str],
current_test_timestamp: datetime,
):
query = Tests.objects.values(
"field_timestamp",
"id",
"status",
"build__checkout__git_commit_hash",
).filter(
query = Tests.objects.filter(
path=path,
build__checkout__origin=origin,
build__checkout__git_repository_url=git_repository_url,
build__checkout__git_repository_branch=git_repository_branch,
field_timestamp__lte=current_test_timestamp,
).values(
"field_timestamp",
"id",
"status",
"build__checkout__git_commit_hash",
)

if platform is None:
Expand Down
40 changes: 28 additions & 12 deletions backend/kernelCI_app/typeModels/testDetails.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Literal
from typing import Literal, Optional
from pydantic import BaseModel, Field

from kernelCI_app.typeModels.databases import (
Origin,
Test__Id,
Build__Id,
Test__Status,
Expand All @@ -24,17 +25,6 @@
Timestamp,
)

type PossibleRegressionType = Literal["regression", "fixed", "unstable", "pass", "fail"]


class TestStatusHistoryItem(BaseModel):
field_timestamp: Timestamp
id: Test__Id
status: Test__Status
git_commit_hash: Checkout__GitCommitHash = Field(
validation_alias="build__checkout__git_commit_hash"
)


class TestDetailsResponse(BaseModel):
id: Test__Id
Expand Down Expand Up @@ -64,5 +54,31 @@ class TestDetailsResponse(BaseModel):
validation_alias="build__checkout__git_commit_tags"
)
tree_name: Checkout__TreeName = Field(validation_alias="build__checkout__tree_name")
origin: Origin = Field(validation_alias="build__checkout__origin")
field_timestamp: Timestamp


type PossibleRegressionType = Literal["regression", "fixed", "unstable", "pass", "fail"]


class TestStatusHistoryItem(BaseModel):
field_timestamp: Timestamp
id: Test__Id
status: Test__Status
git_commit_hash: Checkout__GitCommitHash = Field(
validation_alias="build__checkout__git_commit_hash"
)


class TestStatusHistoryResponse(BaseModel):
status_history: list[TestStatusHistoryItem]
regression_type: PossibleRegressionType


class TestStatusHistoryRequest(BaseModel):
path: Test__Path = None
origin: Origin
git_repository_url: Checkout__GitRepositoryUrl = None
git_repository_branch: Checkout__GitRepositoryBranch = None
platform: Optional[str] = None
current_test_timestamp: Timestamp
57 changes: 57 additions & 0 deletions backend/kernelCI_app/unitTests/testStatusHistory_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from kernelCI_app.typeModels.testDetails import TestStatusHistoryRequest
from kernelCI_app.unitTests.utils.healthCheck import online
from kernelCI_app.unitTests.utils.testClient import TestClient
from kernelCI_app.unitTests.utils.asserts import (
assert_status_code_and_error_response,
assert_has_fields_in_response_content,
)
from kernelCI_app.unitTests.utils.fields.tests import status_history_expected_fields
from kernelCI_app.utils import string_to_json
import pytest
from http import HTTPStatus


client = TestClient()


@online
@pytest.mark.parametrize(
"params, status_code, has_error_body",
[
(
TestStatusHistoryRequest(
path="fluster.debian.v4l2.gstreamer_av1.validate-fluster-results",
origin="maestro",
git_repository_url="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git",
git_repository_branch="master",
platform="mt8195-cherry-tomato-r2",
current_test_timestamp="2025-03-10T01:52:01.230777Z",
),
HTTPStatus.OK,
False,
),
(
TestStatusHistoryRequest(
path="unexistent",
origin="maestro",
current_test_timestamp="2025-03-10T01:39:01.486560Z",
),
HTTPStatus.BAD_REQUEST,
True,
),
],
)
def test_get(params: TestStatusHistoryRequest, status_code, has_error_body):
response = client.get_test_status_history(query=params)
content = string_to_json(response.content.decode())
assert_status_code_and_error_response(
response=response,
content=content,
status_code=status_code,
should_error=has_error_body,
)

if not has_error_body:
assert_has_fields_in_response_content(
fields=status_history_expected_fields, response_content=content
)
5 changes: 5 additions & 0 deletions backend/kernelCI_app/unitTests/utils/fields/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
"environment_misc",
"tree_name",
"git_repository_branch",
"origin",
"field_timestamp",
]

status_history_expected_fields = [
"status_history",
"regression_type",
]
10 changes: 10 additions & 0 deletions backend/kernelCI_app/unitTests/utils/testClient.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import requests
from django.urls import reverse

from kernelCI_app.typeModels.testDetails import TestStatusHistoryRequest
from kernelCI_app.unitTests.utils.baseClient import BaseClient


Expand All @@ -14,3 +15,12 @@ def get_test_issues(self, *, test_id: str) -> requests.Response:
path = reverse("testIssues", kwargs={"test_id": test_id})
url = self.get_endpoint(path=path)
return requests.get(url)

def get_test_status_history(
self,
*,
query: TestStatusHistoryRequest,
) -> requests.Response:
path = reverse("testStatusHistory", args={})
url = self.get_endpoint(path=path, query=query.model_dump())
return requests.get(url)
5 changes: 5 additions & 0 deletions backend/kernelCI_app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ def view_cache(view):


urlpatterns = [
path(
"test/status-history",
view_cache(views.TestStatusHistory),
name="testStatusHistory",
),
path("test/<str:test_id>", view_cache(views.TestDetails), name="testDetails"),
path("tree/", view_cache(views.TreeView), name="tree"),
path("tree-fast/", view_cache(views.TreeViewFast), name="tree-fast"),
Expand Down
67 changes: 2 additions & 65 deletions backend/kernelCI_app/views/testDetailsView.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
from http import HTTPStatus
from typing import Optional
from kernelCI_app.helpers.errorHandling import create_api_error_response
from kernelCI_app.queries.test import get_test_details_data, get_test_status_history
from kernelCI_app.typeModels.databases import FAIL_STATUS, PASS_STATUS
from kernelCI_app.queries.test import get_test_details_data
from kernelCI_app.typeModels.testDetails import (
PossibleRegressionType,
TestDetailsResponse,
)
from drf_spectacular.utils import extend_schema
Expand All @@ -14,79 +11,19 @@


class TestDetails(APIView):
# TODO: create unit tests for this method
def process_test_status_history(
self, *, status_history: list[dict]
) -> PossibleRegressionType:
history_task: PossibleRegressionType
first_test_flag = True
status_changed = False

for test in status_history:
test_status = test["status"]
if first_test_flag:
if test_status == PASS_STATUS:
history_task = "pass"
starting_status = PASS_STATUS
opposite_status = FAIL_STATUS
elif test_status == FAIL_STATUS:
history_task = "fail"
starting_status = FAIL_STATUS
opposite_status = PASS_STATUS
else:
return "unstable"
first_test_flag = False
continue

is_inconclusive = test_status != PASS_STATUS and test_status != FAIL_STATUS

if test_status == opposite_status:
status_changed = True
if history_task == "pass":
history_task = "fixed"
elif history_task == "fail":
history_task = "regression"
if (status_changed and test_status == starting_status) or is_inconclusive:
return "unstable"

return history_task

@extend_schema(
responses=TestDetailsResponse,
)
def get(self, _request, test_id: str) -> Response:

response = get_test_details_data(test_id=test_id)

if response is None:
return create_api_error_response(
error_message="Test not found", status_code=HTTPStatus.OK
)

environment_misc = response.get("environment_misc")
platform: Optional[str] = None
if environment_misc is not None:
platform = environment_misc.get("platform")

status_history_response = get_test_status_history(
path=response["path"],
origin=response["build__checkout__origin"],
git_repository_url=response["build__checkout__git_repository_url"],
git_repository_branch=response["build__checkout__git_repository_branch"],
platform=platform,
current_test_timestamp=response["field_timestamp"],
)

regression_type = self.process_test_status_history(
status_history=status_history_response
)

try:
valid_response = TestDetailsResponse(
**response,
status_history=status_history_response,
regression_type=regression_type,
)
valid_response = TestDetailsResponse(**response)
except ValidationError as e:
return Response(data=e.json(), status=HTTPStatus.INTERNAL_SERVER_ERROR)

Expand Down
101 changes: 101 additions & 0 deletions backend/kernelCI_app/views/testStatusHistoryView.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from http import HTTPStatus

from django.http import HttpRequest
from kernelCI_app.helpers.errorHandling import create_api_error_response
from kernelCI_app.queries.test import get_test_status_history
from kernelCI_app.typeModels.databases import FAIL_STATUS, PASS_STATUS
from kernelCI_app.typeModels.testDetails import (
PossibleRegressionType,
TestStatusHistoryRequest,
TestStatusHistoryResponse,
)
from drf_spectacular.utils import extend_schema
from rest_framework.views import APIView
from rest_framework.response import Response
from pydantic import ValidationError


class TestStatusHistory(APIView):
# TODO: create unit tests for this method
def process_test_status_history(
self, *, status_history: list[dict]
) -> PossibleRegressionType:
history_task: PossibleRegressionType
first_test_flag = True
status_changed = False

for test in status_history:
test_status = test["status"]
if first_test_flag:
if test_status == PASS_STATUS:
history_task = "pass"
starting_status = PASS_STATUS
opposite_status = FAIL_STATUS
elif test_status == FAIL_STATUS:
history_task = "fail"
starting_status = FAIL_STATUS
opposite_status = PASS_STATUS
else:
return "unstable"
first_test_flag = False
continue

is_inconclusive = test_status != PASS_STATUS and test_status != FAIL_STATUS

if test_status == opposite_status:
status_changed = True
if history_task == "pass":
history_task = "fixed"
elif history_task == "fail":
history_task = "regression"
if (status_changed and test_status == starting_status) or is_inconclusive:
return "unstable"

return history_task

@extend_schema(
request=TestStatusHistoryRequest,
responses=TestStatusHistoryResponse,
)
def get(self, request: HttpRequest) -> Response:
try:
params = TestStatusHistoryRequest(
path=request.GET.get("path"),
origin=request.GET.get("origin"),
git_repository_branch=request.GET.get("git_repository_branch"),
git_repository_url=request.GET.get("git_repository_url"),
platform=request.GET.get("platform"),
current_test_timestamp=request.GET.get("current_test_timestamp"),
)
except ValidationError as e:
return Response(data=e.json(), status=HTTPStatus.BAD_REQUEST)

status_history_response = get_test_status_history(
path=params.path,
origin=params.origin,
git_repository_url=params.git_repository_url,
git_repository_branch=params.git_repository_branch,
platform=params.platform,
current_test_timestamp=params.current_test_timestamp,
)

if len(status_history_response) == 0:
# This endpoint should always return at least 1 item (the current test),
# if the result has no items it means that the request was wrong
return create_api_error_response(
error_message="Test status history not found"
)

regression_type = self.process_test_status_history(
status_history=status_history_response
)

try:
valid_response = TestStatusHistoryResponse(
status_history=status_history_response,
regression_type=regression_type,
)
except ValidationError as e:
return Response(data=e.json(), status=HTTPStatus.INTERNAL_SERVER_ERROR)

return Response(valid_response.model_dump())
Loading