diff --git a/backend/kernelCI_app/queries/test.py b/backend/kernelCI_app/queries/test.py index 7a094122..0bdb7f98 100644 --- a/backend/kernelCI_app/queries/test.py +++ b/backend/kernelCI_app/queries/test.py @@ -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, @@ -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: diff --git a/backend/kernelCI_app/typeModels/testDetails.py b/backend/kernelCI_app/typeModels/testDetails.py index cdd861b8..eb3e38ea 100644 --- a/backend/kernelCI_app/typeModels/testDetails.py +++ b/backend/kernelCI_app/typeModels/testDetails.py @@ -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, @@ -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 @@ -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 diff --git a/backend/kernelCI_app/unitTests/testStatusHistory_test.py b/backend/kernelCI_app/unitTests/testStatusHistory_test.py new file mode 100644 index 00000000..b43520f1 --- /dev/null +++ b/backend/kernelCI_app/unitTests/testStatusHistory_test.py @@ -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 + ) diff --git a/backend/kernelCI_app/unitTests/utils/fields/tests.py b/backend/kernelCI_app/unitTests/utils/fields/tests.py index 24be8061..3d8863a8 100644 --- a/backend/kernelCI_app/unitTests/utils/fields/tests.py +++ b/backend/kernelCI_app/unitTests/utils/fields/tests.py @@ -18,6 +18,11 @@ "environment_misc", "tree_name", "git_repository_branch", + "origin", + "field_timestamp", +] + +status_history_expected_fields = [ "status_history", "regression_type", ] diff --git a/backend/kernelCI_app/unitTests/utils/testClient.py b/backend/kernelCI_app/unitTests/utils/testClient.py index a3b0f54b..206cd98b 100644 --- a/backend/kernelCI_app/unitTests/utils/testClient.py +++ b/backend/kernelCI_app/unitTests/utils/testClient.py @@ -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 @@ -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) diff --git a/backend/kernelCI_app/urls.py b/backend/kernelCI_app/urls.py index aa4e46d2..c6217336 100644 --- a/backend/kernelCI_app/urls.py +++ b/backend/kernelCI_app/urls.py @@ -17,6 +17,11 @@ def view_cache(view): urlpatterns = [ + path( + "test/status-history", + view_cache(views.TestStatusHistory), + name="testStatusHistory", + ), path("test/", view_cache(views.TestDetails), name="testDetails"), path("tree/", view_cache(views.TreeView), name="tree"), path("tree-fast/", view_cache(views.TreeViewFast), name="tree-fast"), diff --git a/backend/kernelCI_app/views/testDetailsView.py b/backend/kernelCI_app/views/testDetailsView.py index cf3a6bdb..1ea34b8b 100644 --- a/backend/kernelCI_app/views/testDetailsView.py +++ b/backend/kernelCI_app/views/testDetailsView.py @@ -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 @@ -14,48 +11,10 @@ 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: @@ -63,30 +22,8 @@ def get(self, _request, test_id: str) -> 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) diff --git a/backend/kernelCI_app/views/testStatusHistoryView.py b/backend/kernelCI_app/views/testStatusHistoryView.py new file mode 100644 index 00000000..669e55e6 --- /dev/null +++ b/backend/kernelCI_app/views/testStatusHistoryView.py @@ -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()) diff --git a/backend/requests/test-details-get.sh b/backend/requests/test-details-get.sh index 46d4187d..185c5925 100644 --- a/backend/requests/test-details-get.sh +++ b/backend/requests/test-details-get.sh @@ -3,11 +3,11 @@ http 'http://localhost:8000/api/test/maestro:67a182c5661a7bc8748b9905' # HTTP/1.1 200 OK # Allow: GET, HEAD, OPTIONS # Cache-Control: max-age=0 -# Content-Length: 18610 +# Content-Length: 18677 # Content-Type: application/json # Cross-Origin-Opener-Policy: same-origin -# Date: Wed, 05 Feb 2025 17:35:56 GMT -# Expires: Wed, 05 Feb 2025 17:35:56 GMT +# Date: Mon, 10 Mar 2025 19:59:02 GMT +# Expires: Mon, 10 Mar 2025 19:59:02 GMT # Referrer-Policy: same-origin # Server: WSGIServer/0.2 CPython/3.12.7 # Vary: Accept, Cookie, origin @@ -23,17 +23,19 @@ http 'http://localhost:8000/api/test/maestro:67a182c5661a7bc8748b9905' # "environment_misc": { # "platform": "kubernetes" # }, +# "field_timestamp": "2025-02-04T03:02:01.190150Z", # "git_commit_hash": "170c9fb62732486d54f4876cfee81654f217364c", # "git_commit_tags": [], # "git_repository_branch": "android15-6.6", # "git_repository_url": "https://android.googlesource.com/kernel/common", # "id": "maestro:67a182c5661a7bc8748b9905", -# "log_excerpt": "n_order_2\naction_order_1\n ... (Too big to show here) +# "log_excerpt": "n_order_2\naction_order_1\n ok 12 kunit_resource_test_action_ordering\n# kunit-resource-test: pass:12 fail:0 skip:0 total:12\n# Totals: pass:12 fail:0 skip:0 total:12\nok 13 kunit-resource-test\n KTAP version 1\n # Subtest: kunit-log-test\n # module: kunit_test\n 1..2\nput this in log.\nthis too.\nadd to suite log.\nalong with this.\n ok 1 kunit_log_test\n # kunit_log_newline_test: Add newline\n ok 2 kunit_log_newline_test\n# kunit-log-test: pass:2 fail:0 skip:0 total:2\n# Totals: pass:2 fail:0 skip:0 total:2\nok 14 kunit-log-test\n KTAP version 1\n # Subtest: kunit_status\n # module: kunit_test\n 1..2\n ok 1 kunit_status_set_failure_test\n ok 2 kunit_status_mark_skipped_test\n# kunit_status: pass:2 fail:0 skip:0 total:2\n# Totals: pass:2 fail:0 skip:0 total:2\nok 15 kunit_status\n KTAP version 1\n # Subtest: kunit_current\n # module: kunit_test\n 1..2\n ok 1 kunit_current_test\n # fake test: lib/kunit/kunit-test.c:641: This should make `fake` test fail.\n ok 2 kunit_current_fail_test\n# kunit_current: pass:2 fail:0 skip:0 total:2\n# Totals: pass:2 fail:0 skip:0 total:2\nok 16 kunit_current\n KTAP version 1\n # Subtest: string-stream-test\n # module: string_stream_test\n 1..3\n ok 1 string_stream_test_empty_on_creation\n ok 2 string_stream_test_not_empty_after_add\n ok 3 string_stream_test_get_string\n# string-stream-test: pass:3 fail:0 skip:0 total:3\n# Totals: pass:3 fail:0 skip:0 total:3\nok 17 string-stream-test\n # example: initializing suite\n KTAP version 1\n # Subtest: example\n # module: kunit_example_test\n 1..7\n # example_simple_test: initializing\n # example_simple_test: cleaning up\n ok 1 example_simple_test\n # example_skip_test: initializing\n # example_skip_test: You should not see a line below.\n # example_skip_test: cleaning up\n ok 2 example_skip_test # SKIP this test should be skipped\n # example_mark_skipped_test: initializing\n # example_mark_skipped_test: You should see a line below.\n # example_mark_skipped_test: You should see this line.\n # example_mark_skipped_test: cleaning up\n ok 3 example_mark_skipped_test # SKIP this test should be skipped\n # example_all_expect_macros_test: initializing\n # example_all_expect_macros_test: cleaning up\n ok 4 example_all_expect_macros_test\n # example_static_stub_test: initializing\n # example_static_stub_test: cleaning up\n ok 5 example_static_stub_test\n KTAP version 1\n # Subtest: example_params_test\n # example_params_test: initializing\n # example_params_test: cleaning up\n ok 1 example value 2\n # example_params_test: initializing\n # example_params_test: cleaning up\n ok 2 example value 1\n # example_params_test: initializing\n # example_params_test: cleaning up\n ok 3 example value 0 # SKIP unsupported param value\n # example_params_test: pass:2 fail:0 skip:1 total:3\n ok 6 example_params_test\n # example_slow_test: initializing\n # example_slow_test: cleaning up\n # example_slow_test.speed: slow\n ok 7 example_slow_test\n # example: exiting suite\n# example: pass:5 fail:0 skip:2 total:7\n# Totals: pass:6 fail:0 skip:3 total:9\nok 18 example\n KTAP version 1\n # Subtest: bitfields\n # module: bitfield_kunit\n 1..2\n ok 1 test_bitfields_constants\n ok 2 test_bitfields_variables\n# bitfields: pass:2 fail:0 skip:0 total:2\n# Totals: pass:2 fail:0 skip:0 total:2\nok 19 bitfields\n KTAP version 1\n # Subtest: checksum\n # module: checksum_kunit\n 1..3\n ok 1 test_csum_fixed_random_inputs\n ok 2 test_csum_all_carry_inputs\n ok 3 test_csum_no_carry_inputs\n# checksum: pass:3 fail:0 skip:0 total:3\n# Totals: pass:3 fail:0 skip:0 total:3\nok 20 checksum\n KTAP version 1\n # Subtest: list-kunit-test\n # module: list_test\n 1..39\n ok 1 list_test_list_init\n ok 2 list_test_list_add\n ok 3 list_test_list_add_tail\n ok 4 list_test_list_del\n ok 5 list_test_list_replace\n ok 6 list_test_list_replace_init\n ok 7 list_test_list_swap\n ok 8 list_test_list_del_init\n ok 9 list_test_list_del_init_careful\n ok 10 list_test_list_move\n ok 11 list_test_list_move_tail\n ok 12 list_test_list_bulk_move_tail\n ok 13 list_test_list_is_head\n ok 14 list_test_list_is_first\n ok 15 list_test_list_is_last\n ok 16 list_test_list_empty\n ok 17 list_test_list_empty_careful\n ok 18 list_test_list_rotate_left\n ok 19 list_test_list_rotate_to_front\n ok 20 list_test_list_is_singular\n ok 21 list_test_list_cut_position\n ok 22 list_test_list_cut_before\n ok 23 list_test_list_splice\n ok 24 list_test_list_splice_tail\n ok 25 list_test_list_splice_init\n ok 26 list_test_list_splice_tail_init\n ok 27 list_test_list_entry\n ok 28 list_test_list_entry_is_head\n ok 29 list_test_list_first_entry\n ok 30 list_test_list_last_entry\n ok 31 list_test_list_first_entry_or_null\n ok 32 list_test_list_next_entry\n ok 33 list_test_list_prev_entry\n ok 34 list_test_list_for_each\n ok 35 list_test_list_for_each_prev\n ok 36 list_test_list_for_each_safe\n ok 37 list_test_list_for_each_prev_safe\n ok 38 list_test_list_for_each_entry\n ok 39 list_test_list_for_each_entry_reverse\n# list-kunit-test: pass:39 fail:0 skip:0 total:39\n# Totals: pass:39 fail:0 skip:0 total:39\nok 21 list-kunit-test\n KTAP version 1\n # Subtest: hlist\n # module: list_test\n 1..18\n ok 1 hlist_test_init\n ok 2 hlist_test_unhashed\n ok 3 hlist_test_unhashed_lockless\n ok 4 hlist_test_del\n ok 5 hlist_test_del_init\n ok 6 hlist_test_add\n ok 7 hlist_test_fake\n ok 8 hlist_test_is_singular_node\n ok 9 hlist_test_empty\n ok 10 hlist_test_move_list\n ok 11 hlist_test_entry\n ok 12 hlist_test_entry_safe\n ok 13 hlist_test_for_each\n ok 14 hlist_test_for_each_safe\n ok 15 hlist_test_for_each_entry\n ok 16 hlist_test_for_each_entry_continue\n ok 17 hlist_test_for_each_entry_from\n ok 18 hlist_test_for_each_entry_safe\n# hlist: pass:18 fail:0 skip:0 total:18\n# Totals: pass:18 fail:0 skip:0 total:18\nok 22 hlist\n KTAP version 1\n # Subtest: klist\n # module: list_test\n 1..8\n ok 1 klist_test_add_tail\n ok 2 klist_test_add_head\n ok 3 klist_test_add_behind\n ok 4 klist_test_add_before\n ok 5 klist_test_del_refcount_greater_than_zero\n ok 6 klist_test_del_refcount_zero\n ok 7 klist_test_remove\n ok 8 klist_test_node_attached\n# klist: pass:8 fail:0 skip:0 total:8\n# Totals: pass:8 fail:0 skip:0 total:8\nok 23 klist\n KTAP version 1\n # Subtest: hashtable\n # module: hashtable_test\n 1..9\n ok 1 hashtable_test_hash_init\n ok 2 hashtable_test_hash_empty\n ok 3 hashtable_test_hash_hashed\n ok 4 hashtable_test_hash_add\n ok 5 hashtable_test_hash_del\n ok 6 hashtable_test_hash_for_each\n ok 7 hashtable_test_hash_for_each_safe\n ok 8 hashtable_test_hash_for_each_possible\n ok 9 hashtable_test_hash_for_each_possible_safe\n# hashtable: pass:9 fail:0 skip:0 total:9\n# Totals: pass:9 fail:0 skip:0 total:9\nok 24 hashtable\n KTAP version 1\n # Subtest: bits-test\n # module: test_bits\n 1..3\n ok 1 genmask_test\n ok 2 genmask_ull_test\n ok 3 genmask_input_check_test\n# bits-test: pass:3 fail:0 skip:0 total:3\n# Totals: pass:3 fail:0 skip:0 total:3\nok 25 bits-test\n KTAP version 1\n # Subtest: cmdline\n # module: cmdline_kunit\n 1..4\n ok 1 cmdline_test_noint\n ok 2 cmdline_test_lead_int\n ok 3 cmdline_test_tail_int\n ok 4 cmdline_test_range\n# cmdline: pass:4 fail:0 skip:0 total:4\n# Totals: pass:4 fail:0 skip:0 total:4\nok 26 cmdline\n KTAP version 1\n # Subtest: slub_test\n # module: slub_kunit\n 1..6\n ok 1 test_clobber_zone\n ok 2 test_next_pointer\n ok 3 test_first_word\n ok 4 test_clobber_50th_byte\n ok 5 test_clobber_redzone_free\nstackdepot: allocating hash table of 65536 entries via kvcalloc\n ok 6 test_kmalloc_redzone_access\n# slub_test: pass:6 fail:0 skip:0 total:6\n# Totals: pass:6 fail:0 skip:0 total:6\nok 27 slub_test\n KTAP version 1\n # Subtest: memcpy\n # module: memcpy_kunit\n 1..7\n # memset_test: ok: memset() direct assignment\n # memset_test: ok: memset() complete overwrite\n # memset_test: ok: memset() middle overwrite\n # memset_test: ok: memset() argument side-effects\n # memset_test: ok: memset() memset_after()\n # memset_test: ok: memset() memset_startat()\n ok 1 memset_test\n # memcpy_test: ok: memcpy() static initializers\n # memcpy_test: ok: memcpy() direct assignment\n # memcpy_test: ok: memcpy() complete overwrite\n # memcpy_test: ok: memcpy() middle overwrite\n # memcpy_test: ok: memcpy() argument side-effects\n ok 2 memcpy_test\ninput: ImExPS/2 Generic Explorer Mouse as /devices/platform/i8042/serio1/input/input2\n # memcpy_large_test.speed: slow\n ok 3 memcpy_large_test\n # memmove_test: ok: memmove() static initializers\n # memmove_test: ok: memmove() direct assignment\n # memmove_test: ok: memmove() complete overwrite\n # memmove_test: ok: memmove() middle overwrite\n # memmove_test: ok: memmove() argument side-effects\n # memmove_test: ok: memmove() overlapping write\n # memmove_test.speed: slow\n ok 4 memmove_test\n # memmove_large_test.speed: slow\n ok 5 memmove_large_test\n # memmove_overlap_test.speed: slow\n ok 6 memmove_overlap_test\n ok 7 strtomem_test\n# memcpy: pass:7 fail:0 skip:0 total:7\n# Totals: pass:7 fail:0 skip:0 total:7\nok 28 memcpy\n KTAP version 1\n # Subtest: is_signed_type\n # module: is_signed_type_kunit\n 1..1\n ok 1 is_signed_type_test\nok 29 is_signed_type\n KTAP version 1\n # Subtest: overflow\n # module: overflow_kunit\n 1..21\n # u8_u8__u8_overflow_test: 18 u8_u8__u8 arithmetic tests finished\n ok 1 u8_u8__u8_overflow_test\n # s8_s8__s8_overflow_test: 19 s8_s8__s8 arithmetic tests finished\n ok 2 s8_s8__s8_overflow_test\n # u16_u16__u16_overflow_test: 17 u16_u16__u16 arithmetic tests finished\n ok 3 u16_u16__u16_overflow_test\n # s16_s16__s16_overflow_test: 17 s16_s16__s16 arithmetic tests finished\n ok 4 s16_s16__s16_overflow_test\n # u32_u32__u32_overflow_test: 17 u32_u32__u32 arithmetic tests finished\n ok 5 u32_u32__u32_overflow_test\n # s32_s32__s32_overflow_test: 17 s32_s32__s32 arithmetic tests finished\n ok 6 s32_s32__s32_overflow_test\n # u64_u64__u64_overflow_test: 17 u64_u64__u64 arithmetic tests finished\n ok 7 u64_u64__u64_overflow_test\n # s64_s64__s64_overflow_test: 21 s64_s64__s64 arithmetic tests finished\n ok 8 s64_s64__s64_overflow_test\n # u32_u32__int_overflow_test: 2 u32_u32__int arithmetic tests finished\n ok 9 u32_u32__int_overflow_test\n # u32_u32__u8_overflow_test: 3 u32_u32__u8 arithmetic tests finished\n ok 10 u32_u32__u8_overflow_test\n # u8_u8__int_overflow_test: 3 u8_u8__int arithmetic tests finished\n ok 11 u8_u8__int_overflow_test\n # int_int__u8_overflow_test: 3 int_int__u8 arithmetic tests finished\n ok 12 int_int__u8_overflow_test\n # shift_sane_test: 36 sane shift tests finished\n ok 13 shift_sane_test\n # shift_overflow_test: 25 overflow shift tests finished\n ok 14 shift_overflow_test\n # shift_truncate_test: 27 truncate shift tests finished\n ok 15 shift_truncate_test\n # shift_nonsense_test: 25 nonsense shift tests finished\n ok 16 shift_nonsense_test\n # overflow_allocation_test: 11 allocation overflow tests finished\n ok 17 overflow_allocation_test\n # overflow_size_helpers_test: 43 overflow size helper tests finished\n ok 18 overflow_size_helpers_test\n # overflows_type_test: 658 overflows_type() tests finished\n ok 19 overflows_type_test\n # same_type_test: 0 __same_type() tests finished\n ok 20 same_type_test\n # castable_to_type_test: 103 castable_to_type() tests finished\n ok 21 castable_to_type_test\n# overflow: pass:21 fail:0 skip:0 total:21\n# Totals: pass:21 fail:0 skip:0 total:21\nok 30 overflow\n KTAP version 1\n # Subtest: stackinit\n # module: stackinit_kunit\n 1..65\n ok 1 test_u8_zero\n ok 2 test_u16_zero\n ok 3 test_u32_zero\n ok 4 test_u64_zero\n ok 5 test_char_array_zero\n ok 6 test_small_hole_zero\n ok 7 test_big_hole_zero\n ok 8 test_trailing_hole_zero\n ok 9 test_packed_zero\n ok 10 test_small_hole_dynamic_partial\n ok 11 test_big_hole_dynamic_partial\n ok 12 test_trailing_hole_dynamic_partial\n ok 13 test_packed_dynamic_partial\n ok 14 test_small_hole_assigned_dynamic_partial\n ok 15 test_big_hole_assigned_dynamic_partial\n ok 16 test_trailing_hole_assigned_dynamic_partial\n ok 17 test_packed_assigned_dynamic_partial\n ok 18 test_small_hole_static_partial\n ok 19 test_big_hole_static_partial\n ok 20 test_trailing_hole_static_partial\n ok 21 test_packed_static_partial\n ok 22 test_small_hole_static_all\n ok 23 test_big_hole_static_all\n ok 24 test_trailing_hole_static_all\n ok 25 test_packed_static_all\n ok 26 test_small_hole_dynamic_all\n ok 27 test_big_hole_dynamic_all\n ok 28 test_trailing_hole_dynamic_all\n ok 29 test_packed_dynamic_all\n ok 30 test_small_hole_runtime_partial\n ok 31 test_big_hole_runtime_partial\n ok 32 test_trailing_hole_runtime_partial\n ok 33 test_packed_runtime_partial\n ok 34 test_small_hole_runtime_all\n ok 35 test_big_hole_runtime_all\n ok 36 test_trailing_hole_runtime_all\n ok 37 test_packed_runtime_all\n ok 38 test_small_hole_assigned_static_partial\n ok 39 test_big_hole_assigned_static_partial\n ok 40 test_trailing_hole_assigned_static_partial\n ok 41 test_packed_assigned_static_partial\n ok 42 test_small_hole_assigned_static_all\n ok 43 test_big_hole_assigned_static_all\n ok 44 test_trailing_hole_assigned_static_all\n ok 45 test_packed_assigned_static_all\n ok 46 test_small_hole_assigned_dynamic_all\n ok 47 test_big_hole_assigned_dynamic_all\n ok 48 test_trailing_hole_assigned_dynamic_all\n ok 49 test_packed_assigned_dynamic_all\n ok 50 test_small_hole_assigned_copy # SKIP XFAIL uninit bytes: 3\n ok 51 test_big_hole_assigned_copy # SKIP XFAIL uninit bytes: 124\n ok 52 test_trailing_hole_assigned_copy # SKIP XFAIL uninit bytes: 7\n ok 53 test_packed_assigned_copy\n ok 54 test_u8_none\n ok 55 test_u16_none\n ok 56 test_u32_none\n ok 57 test_u64_none\n ok 58 test_char_array_none\n ok 59 test_switch_1_none # SKIP XFAIL uninit bytes: 80\n ok 60 test_switch_2_none # SKIP XFAIL uninit bytes: 12\n ok 61 test_small_hole_none\n ok 62 test_big_hole_none\n ok 63 test_trailing_hole_none\n ok 64 test_packed_none\n ok 65 test_user\n# stackinit: pass:60 fail:0 skip:5 total:65\n# Totals: pass:60 fail:0 skip:5 total:65\nok 31 stackinit\n KTAP version 1\n # Subtest: strcat\n # module: strcat_kunit\n 1..3\n ok 1 strcat_test\n ok 2 strncat_test\n ok 3 strlcat_test\n# strcat: pass:3 fail:0 skip:0 total:3\n# Totals: pass:3 fail:0 skip:0 total:3\nok 32 strcat\n KTAP version 1\n # Subtest: strscpy\n # module: strscpy_kunit\n 1..1\n ok 1 strscpy_test\nok 33 strscpy\n KTAP version 1\n # Subtest: siphash\n # module: siphash_kunit\n 1..1\n ok 1 siphash_test\nok 34 siphash\n KTAP version 1\n # Subtest: qos-kunit-test\n # module: qos_test\n 1..3\n ok 1 freq_qos_test_min\n ok 2 freq_qos_test_maxdef\n ok 3 freq_qos_test_readd\n# qos-kunit-test: pass:3 fail:0 skip:0 total:3\n# Totals: pass:3 fail:0 skip:0 total:3\nok 35 qos-kunit-test\n KTAP version 1\n # Subtest: property-entry\n # module: property_entry_test\n 1..7\n ok 1 pe_test_uints\n ok 2 pe_test_uint_arrays\n ok 3 pe_test_strings\n ok 4 pe_test_bool\n ok 5 pe_test_move_inline_u8\n ok 6 pe_test_move_inline_str\n ok 7 pe_test_reference\n# property-entry: pass:7 fail:0 skip:0 total:7\n# Totals: pass:7 fail:0 skip:0 total:7\nok 36 property-entry\n KTAP version 1\n # Subtest: input_core\n # module: input_test\n 1..4\ninput: Test input device as /devices/virtual/input/input3\n ok 1 input_test_polling\ninput: Test input device as /devices/virtual/input/input4\n ok 2 input_test_timestamp\ninput: Test input device as /devices/virtual/input/input5\n ok 3 input_test_match_device_id\ninput: Test input device as /devices/virtual/input/input6\n ok 4 input_test_grab\n# input_core: pass:4 fail:0 skip:0 total:4\n# Totals: pass:4 fail:0 skip:0 total:4\nok 37 input_core\nreboot: Restarting system\nreboot: machine restart\n", # "log_url": "https://kciapistagingstorage1.file.core.windows.net/production/kunit-x86_64-67a17e87661a7bc8748b9409/test_log?sv=2022-11-02&ss=f&srt=sco&sp=r&se=2026-10-18T13:36:18Z&st=2024-10-17T05:36:18Z&spr=https&sig=xFxYOOh5uXJWeN9I3YKAUvpGGQivo89HKZbD78gcxvc%3D", # "misc": { # "arch": "x86_64", # "runtime": "k8s-gke-eu-west4" # }, +# "origin": "maestro", # "output_files": [ # { # "name": "tarball", diff --git a/backend/requests/test-status-history-get.sh b/backend/requests/test-status-history-get.sh new file mode 100644 index 00000000..2c45a645 --- /dev/null +++ b/backend/requests/test-status-history-get.sh @@ -0,0 +1,81 @@ +http 'http://localhost:8000/api/test/status-history?path=fluster.debian.v4l2.gstreamer_av1.validate-fluster-results&origin=maestro&git_repository_url=https:%2F%2Fgit.kernel.org%2Fpub%2Fscm%2Flinux%2Fkernel%2Fgit%2Ftorvalds%2Flinux.git&git_repository_branch=master&platform=mt8195-cherry-tomato-r2¤t_test_timestamp=2025-03-10T01:52:01.230777Z' + +# HTTP/1.1 200 OK +# Allow: GET, HEAD, OPTIONS +# Cache-Control: max-age=0 +# Content-Length: 1719 +# Content-Type: application/json +# Cross-Origin-Opener-Policy: same-origin +# Date: Mon, 10 Mar 2025 19:54:14 GMT +# Expires: Mon, 10 Mar 2025 19:54:14 GMT +# Referrer-Policy: same-origin +# Server: WSGIServer/0.2 CPython/3.12.7 +# Vary: Accept, Cookie, origin +# X-Content-Type-Options: nosniff +# X-Frame-Options: DENY + +# { +# "regression_type": "unstable", +# "status_history": [ +# { +# "field_timestamp": "2025-03-10T01:52:01.230777Z", +# "git_commit_hash": "80e54e84911a923c40d7bee33a34c1b4be148d7a", +# "id": "maestro:67ce452318018371957dbf70", +# "status": "FAIL" +# }, +# { +# "field_timestamp": "2025-03-09T22:25:05.510433Z", +# "git_commit_hash": "0dc1f314f854257eb64dcea604a42a55225453a9", +# "id": "maestro:67cdfbb418018371957c8ad1", +# "status": "PASS" +# }, +# { +# "field_timestamp": "2025-03-09T09:34:09.140715Z", +# "git_commit_hash": "2e51e0ac575c2095da869ea62d406f617550e6ed", +# "id": "maestro:67cc9ce518018371957a5983", +# "status": "PASS" +# }, +# { +# "field_timestamp": "2025-03-09T05:38:44.649757Z", +# "git_commit_hash": "2a520073e74fbb956b5564818fc5529dcc7e9f0e", +# "id": "maestro:67cbd2ec180183719579a321", +# "status": "PASS" +# }, +# { +# "field_timestamp": "2025-03-09T04:22:13.548635Z", +# "git_commit_hash": "21e4543a2e2f8538373d1d19264c4bae6f13e798", +# "id": "maestro:67cb8431180183719578aa98", +# "status": "FAIL" +# }, +# { +# "field_timestamp": "2025-03-08T23:05:14.256486Z", +# "git_commit_hash": "0f52fd4f67c67f7f2ea3063c627e466255f027fd", +# "id": "maestro:67ca566a180183719574464c", +# "status": "FAIL" +# }, +# { +# "field_timestamp": "2025-03-08T21:31:56.959566Z", +# "git_commit_hash": "7f0e9ee5e44887272627d0fcde0b19a675daf597", +# "id": "maestro:67ca05601801837195720596", +# "status": "PASS" +# }, +# { +# "field_timestamp": "2025-03-04T07:01:03.088880Z", +# "git_commit_hash": "99fa936e8e4f117d62f229003c9799686f74cebc", +# "id": "maestro:67c60821460b36c9f254aab0", +# "status": "FAIL" +# }, +# { +# "field_timestamp": "2025-03-02T19:36:06.013938Z", +# "git_commit_hash": "b91872c56940950a6a0852e499d249c3091d4284", +# "id": "maestro:67c4b17d3218f15c74c2a87f", +# "status": "PASS" +# }, +# { +# "field_timestamp": "2025-02-27T22:10:01.952215Z", +# "git_commit_hash": "1e15510b71c99c6e49134d756df91069f7d18141", +# "id": "maestro:67c0e25e7d9799de2b4dcab6", +# "status": "PASS" +# } +# ] +# } diff --git a/backend/schema.yml b/backend/schema.yml index 2169935c..69873627 100644 --- a/backend/schema.yml +++ b/backend/schema.yml @@ -688,6 +688,22 @@ paths: schema: $ref: '#/components/schemas/DetailsIssuesResponse' description: '' + /api/test/status-history: + get: + operationId: test_status_history_retrieve + tags: + - test + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/TestStatusHistoryResponse' + description: '' /api/tree/: get: operationId: tree_retrieve @@ -2638,13 +2654,10 @@ components: $ref: '#/components/schemas/Checkout__GitCommitTags' tree_name: $ref: '#/components/schemas/Checkout__TreeName' - status_history: - items: - $ref: '#/components/schemas/TestStatusHistoryItem' - title: Status History - type: array - regression_type: - $ref: '#/components/schemas/PossibleRegressionType' + origin: + $ref: '#/components/schemas/Origin' + field_timestamp: + $ref: '#/components/schemas/Timestamp' required: - id - build_id @@ -2665,8 +2678,8 @@ components: - git_repository_url - git_commit_tags - tree_name - - status_history - - regression_type + - origin + - field_timestamp title: TestDetailsResponse type: object TestHistoryItem: @@ -2800,6 +2813,20 @@ components: - git_commit_hash title: TestStatusHistoryItem type: object + TestStatusHistoryResponse: + properties: + status_history: + items: + $ref: '#/components/schemas/TestStatusHistoryItem' + title: Status History + type: array + regression_type: + $ref: '#/components/schemas/PossibleRegressionType' + required: + - status_history + - regression_type + title: TestStatusHistoryResponse + type: object TestSummary: properties: status: @@ -2874,7 +2901,10 @@ components: - type: 'null' Test__EnvironmentMisc: anyOf: - - $ref: '#/components/schemas/EnvironmentMisc' + - type: object + - items: + type: object + type: array - type: 'null' Test__Id: type: string diff --git a/dashboard/src/api/testDetails.ts b/dashboard/src/api/testDetails.ts index e60fec46..a5a71875 100644 --- a/dashboard/src/api/testDetails.ts +++ b/dashboard/src/api/testDetails.ts @@ -1,7 +1,10 @@ import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; -import type { TTestDetails } from '@/types/tree/TestDetails'; +import type { + TTestDetails, + TTestStatusHistory, +} from '@/types/tree/TestDetails'; import type { TIssue } from '@/types/issues'; @@ -27,10 +30,40 @@ const fetchTestIssues = async (testId: string): Promise => { return data; }; -export const useTestIssues = (testId: string): UseQueryResult => { +export const useTestIssues = ( + testId: string, + enabled = true, +): UseQueryResult => { return useQuery({ queryKey: ['testIssues', testId], - enabled: testId !== '', + enabled: testId !== '' && enabled, queryFn: () => fetchTestIssues(testId), }); }; + +const fetchTestStatusHistory = async ( + params?: Record, +): Promise => { + const data = await RequestData.get( + `/api/test/status-history`, + { + params, + }, + ); + return data; +}; + +export const useTestStatusHistory = (params?: { + path?: string; + origin?: string; + git_repository_url?: string; + git_repository_branch?: string; + platform?: string; + current_test_timestamp?: string; +}): UseQueryResult => { + return useQuery({ + queryKey: ['testDetailsData', params], + queryFn: () => fetchTestStatusHistory(params), + enabled: params !== undefined && Object.keys(params).length > 0, + }); +}; diff --git a/dashboard/src/components/TestDetails/TestDetails.tsx b/dashboard/src/components/TestDetails/TestDetails.tsx index 373a4ff4..095da116 100644 --- a/dashboard/src/components/TestDetails/TestDetails.tsx +++ b/dashboard/src/components/TestDetails/TestDetails.tsx @@ -13,10 +13,19 @@ import { import { MdChevronRight } from 'react-icons/md'; +import type { UseQueryResult } from '@tanstack/react-query'; + import { shouldTruncate, truncateBigText, valueOrEmpty } from '@/lib/string'; -import type { TTestDetails } from '@/types/tree/TestDetails'; +import type { + TTestDetails, + TTestStatusHistory, +} from '@/types/tree/TestDetails'; import { Sheet } from '@/components/Sheet'; -import { useTestDetails, useTestIssues } from '@/api/testDetails'; +import { + useTestDetails, + useTestIssues, + useTestStatusHistory, +} from '@/api/testDetails'; import { RedirectFrom } from '@/types/general'; @@ -57,10 +66,16 @@ import { StatusHistoryItem } from './StatusHistoryItem'; const TestDetailsSections = ({ test, + statusHistory, + statusHistoryStatus, + statusHistoryError, setSheetType, setJsonContent, }: { test: TTestDetails; + statusHistory?: TTestStatusHistory; + statusHistoryStatus: UseQueryResult['status']; + statusHistoryError: UseQueryResult['error']; setSheetType: Dispatch>; setJsonContent: Dispatch>; }): JSX.Element => { @@ -130,22 +145,39 @@ const TestDetailsSections = ({ [setSheetType], ); - const regressionSection: ISection = useMemo(() => { + const regressionData: JSX.Element[] | undefined = useMemo(() => { + return statusHistory?.status_history.reverse().map((historyItem, index) => { + return ( + + + {index !== statusHistory.status_history.length - 1 && ( + + )} + + ); + }); + }, [statusHistory?.status_history]); + + const regressionSection: ISection | undefined = useMemo(() => { + if (statusHistoryStatus === 'error') { + return; + } + return { title: formatMessage({ id: 'testDetails.statusHistory' }), rightIcon: ( ), - subtitle: ( + subtitle: statusHistory && (
- {test.regression_type} + {statusHistory?.regression_type} @@ -158,25 +190,34 @@ const TestDetailsSections = ({ infos: [ { children: ( -
- {test.status_history.reverse().map((historyItem, index) => { - return ( - - - {index !== test.status_history.length - 1 && ( - - )} - - ); - })} -
+ + } + > +
{regressionData}
+
), }, ], }, ], }; - }, [formatMessage, test.regression_type, test.status_history]); + }, [ + formatMessage, + regressionData, + statusHistory, + statusHistoryError?.message, + statusHistoryStatus, + ]); const generalSection: ISection = useMemo(() => { return { @@ -363,7 +404,26 @@ const TestDetails = ({ breadcrumb }: TestsDetailsProps): JSX.Element => { data: issueData, status: issueStatus, error: issueError, - } = useTestIssues(testId ?? ''); + } = useTestIssues(testId ?? '', data !== undefined); + const { + data: statusHistoryData, + status: statusHistoryStatus, + error: statusHistoryError, + } = useTestStatusHistory( + data !== undefined + ? { + path: data.path, + origin: data.origin, + git_repository_url: data.git_repository_url, + git_repository_branch: data.git_repository_branch, + platform: + typeof data.environment_misc?.['platform'] === 'string' + ? data.environment_misc['platform'] + : undefined, + current_test_timestamp: data.field_timestamp, + } + : undefined, + ); const [sheetType, setSheetType] = useState('log'); const [jsonContent, setJsonContent] = useState(); @@ -401,6 +461,9 @@ const TestDetails = ({ breadcrumb }: TestsDetailsProps): JSX.Element => { {data && ( diff --git a/dashboard/src/types/tree/TestDetails.tsx b/dashboard/src/types/tree/TestDetails.tsx index 1412e14f..cb36ade1 100644 --- a/dashboard/src/types/tree/TestDetails.tsx +++ b/dashboard/src/types/tree/TestDetails.tsx @@ -1,19 +1,5 @@ import type { Status } from '@/types/database'; -export type TestStatusHistoryItem = { - field_timestamp: Date; - id: string; - status: Status; - git_commit_hash: string; -}; - -type PossibleRegressionType = - | 'regression' - | 'fixed' - | 'unstable' - | 'pass' - | 'fail'; - export type TTestDetails = { architecture: string; build_id: string; @@ -34,6 +20,25 @@ export type TTestDetails = { misc?: Record; output_files?: Record; tree_name?: string; + origin?: string; + field_timestamp: string; +}; + +export type TestStatusHistoryItem = { + field_timestamp: string; + id: string; + status: Status; + git_commit_hash: string; +}; + +type PossibleRegressionType = + | 'regression' + | 'fixed' + | 'unstable' + | 'pass' + | 'fail'; + +export type TTestStatusHistory = { status_history: TestStatusHistoryItem[]; regression_type: PossibleRegressionType; };