diff --git a/backend/kernelCI_app/helpers/filters.py b/backend/kernelCI_app/helpers/filters.py index 8dc0dea0d..210c7c188 100644 --- a/backend/kernelCI_app/helpers/filters.py +++ b/backend/kernelCI_app/helpers/filters.py @@ -353,6 +353,10 @@ def __init__(self, data: Dict, process_body=False) -> None: self.filter_boot_origin: set[str] = set() self.filter_test_origin: set[str] = set() + self.filter_build_lab: set[str] = set() + self.filter_boot_lab: set[str] = set() + self.filter_test_lab: set[str] = set() + self.filter_handlers: FilterHandlers = { "boot.status": self._handle_boot_status, "boot.duration": self._handle_boot_duration, @@ -378,6 +382,9 @@ def __init__(self, data: Dict, process_body=False) -> None: "build.origin": self._handle_build_origin, "boot.origin": self._handle_boot_origin, "test.origin": self._handle_test_origin, + "build.lab": self._handle_build_lab, + "boot.lab": self._handle_boot_lab, + "test.lab": self._handle_test_lab, } self.filters: List[FilterParams.ParsedFilter] = [] @@ -486,6 +493,15 @@ def _handle_boot_origin(self, current_filter: ParsedFilter) -> None: def _handle_test_origin(self, current_filter: ParsedFilter) -> None: self.filter_test_origin.add(current_filter["value"]) + def _handle_build_lab(self, current_filter: ParsedFilter) -> None: + self.filter_build_lab.add(current_filter["value"]) + + def _handle_boot_lab(self, current_filter: ParsedFilter) -> None: + self.filter_boot_lab.add(current_filter["value"]) + + def _handle_test_lab(self, current_filter: ParsedFilter) -> None: + self.filter_test_lab.add(current_filter["value"]) + def _process_filters(self): try: for current_filter in self.filters: @@ -603,6 +619,7 @@ def is_build_filtered_out( issue_version: Optional[int], incident_test_id: Optional[str], build_origin: Optional[str] = None, + build_lab: Optional[str] = None, ) -> bool: return ( ( @@ -637,6 +654,10 @@ def is_build_filtered_out( build_status=build_status, ) ) + or ( + len(self.filter_build_lab) > 0 + and (build_lab not in self.filter_build_lab) + ) ) def is_record_filtered_out( @@ -694,6 +715,7 @@ def is_boot_filtered_out( incident_test_id: Optional[str] = "incident_test_id", platform: Optional[str] = None, origin: Optional[str] = None, + lab: Optional[str] = None, ) -> bool: if ( (self.filterBootPath != "" and (self.filterBootPath not in path)) @@ -729,6 +751,7 @@ def is_boot_filtered_out( len(self.filter_boot_origin) > 0 and (origin not in self.filter_boot_origin) ) + or (len(self.filter_boot_lab) > 0 and (lab not in self.filter_boot_lab)) ): return True @@ -745,6 +768,7 @@ def is_test_filtered_out( incident_test_id: Optional[str] = "incident_test_id", platform: Optional[str] = None, origin: Optional[str] = None, + lab: Optional[str] = None, ) -> bool: if ( (self.filterTestPath != "" and (self.filterTestPath not in path)) @@ -780,6 +804,7 @@ def is_test_filtered_out( len(self.filter_test_origin) > 0 and (origin not in self.filter_test_origin) ) + or (len(self.filter_test_lab) > 0 and (lab not in self.filter_test_lab)) ): return True diff --git a/backend/kernelCI_app/helpers/hardwareDetails.py b/backend/kernelCI_app/helpers/hardwareDetails.py index 804dcceb6..efa449c93 100644 --- a/backend/kernelCI_app/helpers/hardwareDetails.py +++ b/backend/kernelCI_app/helpers/hardwareDetails.py @@ -226,6 +226,7 @@ def generate_build_summary_typed() -> BuildSummary: configs={}, issues=[], unknown_issues=0, + labs={}, ) @@ -239,6 +240,7 @@ def generate_test_summary_typed() -> TestSummary: unknown_issues=0, fail_reasons={}, failed_platforms=set(), + labs={}, ) @@ -298,7 +300,11 @@ def handle_test_history( environment_misc=EnvironmentMisc(platform=record["test_platform"]), tree_name=record["build__checkout__tree_name"], git_repository_branch=record["build__checkout__git_repository_branch"], - lab=record_misc.get("runtime") if record_misc else None, + lab=( + record_misc.get("runtime", UNKNOWN_STRING) + if record_misc + else UNKNOWN_STRING + ), ) task.append(test_history_item) diff --git a/backend/kernelCI_app/helpers/treeDetails.py b/backend/kernelCI_app/helpers/treeDetails.py index a8355938e..eb33f9c25 100644 --- a/backend/kernelCI_app/helpers/treeDetails.py +++ b/backend/kernelCI_app/helpers/treeDetails.py @@ -112,7 +112,9 @@ def get_current_row_data(current_row: dict) -> dict: ) current_row_data["test_platform"] = environment_misc.get("platform") test_misc = sanitize_dict(current_row_data["test_misc"]) - test_runtime_lab = test_misc.get("runtime") if test_misc is not None else None + test_runtime_lab = UNKNOWN_STRING + if test_misc is not None: + test_runtime_lab = test_misc.get("runtime", UNKNOWN_STRING) if current_row_data["test_status"] is None: current_row_data["test_status"] = NULL_STATUS @@ -286,6 +288,9 @@ def decide_if_is_build_filtered_out(instance, row_data): build_duration = row_data["build_duration"] incident_test_id = row_data["incident_test_id"] build_origin = row_data["build_origin"] + build_lab = UNKNOWN_STRING + if row_data.get("build_misc") is not None: + build_lab = row_data["build_misc"].get("lab", UNKNOWN_STRING) is_build_filtered_out = instance.filters.is_build_filtered_out( build_status=build_status, @@ -294,6 +299,7 @@ def decide_if_is_build_filtered_out(instance, row_data): issue_version=issue_version, incident_test_id=incident_test_id, build_origin=build_origin, + build_lab=build_lab, ) return is_build_filtered_out @@ -306,6 +312,7 @@ def decide_if_is_boot_filtered_out(instance, row_data): test_path = row_data["test_path"] incident_test_id = row_data["incident_test_id"] origin = row_data["test_origin"] + lab = row_data["history_item"].get("lab", UNKNOWN_STRING) return instance.filters.is_boot_filtered_out( duration=test_duration, @@ -315,6 +322,7 @@ def decide_if_is_boot_filtered_out(instance, row_data): status=test_status, incident_test_id=incident_test_id, origin=origin, + lab=lab, ) @@ -337,6 +345,7 @@ def decide_if_is_test_filtered_out(instance, row_data): test_path = row_data["test_path"] incident_test_id = row_data["incident_test_id"] origin = row_data["test_origin"] + lab = row_data["history_item"].get("lab", UNKNOWN_STRING) return instance.filters.is_test_filtered_out( duration=test_duration, @@ -346,6 +355,7 @@ def decide_if_is_test_filtered_out(instance, row_data): status=test_status, incident_test_id=incident_test_id, origin=origin, + lab=lab, ) @@ -371,6 +381,8 @@ def process_test_summary(instance, row_data): test_platform = row_data["test_platform"] test_error = row_data["test_error"] test_environment_compatible = row_data["test_environment_compatible"] + test_origin = row_data["test_origin"] + test_lab = row_data["history_item"].get("lab", UNKNOWN_STRING) instance.testStatusSummary[test_status] = ( instance.testStatusSummary.get(test_status, 0) + 1 @@ -402,13 +414,18 @@ def process_test_summary(instance, row_data): instance.testEnvironmentMisc[test_platform][test_status] += 1 increment_test_origin_summary( - test_origin=row_data["test_origin"], + test_origin=test_origin, test_status=test_status, origin_summary=instance.test_summary["origins"], ) + if instance.test_summary_typed.labs.get(test_lab) is None: + instance.test_summary_typed.labs[test_lab] = StatusCount() + instance.test_summary_typed.labs[test_lab].increment(test_status) -def process_boots_summary(instance, row_data): + +# TODO: use types instead of all dicts, receive specific fields instead of entire row_data +def process_boots_summary(instance, row_data: dict[str, Any]) -> None: test_status = row_data["test_status"] build_config = row_data["build_config_name"] build_arch = row_data["build_architecture"] @@ -416,6 +433,8 @@ def process_boots_summary(instance, row_data): test_platform = row_data["test_platform"] test_error = row_data["test_error"] test_environment_compatible = row_data["test_environment_compatible"] + test_origin = row_data["test_origin"] + test_lab = row_data["history_item"].get("lab", UNKNOWN_STRING) instance.bootStatusSummary[test_status] = ( instance.bootStatusSummary.get(test_status, 0) + 1 @@ -447,11 +466,15 @@ def process_boots_summary(instance, row_data): instance.bootEnvironmentMisc[test_platform][test_status] += 1 increment_test_origin_summary( - test_origin=row_data["test_origin"], + test_origin=test_origin, test_status=test_status, origin_summary=instance.boot_summary["origins"], ) + if instance.boot_summary_typed.labs.get(test_lab) is None: + instance.boot_summary_typed.labs[test_lab] = StatusCount() + instance.boot_summary_typed.labs[test_lab].increment(test_status) + def process_filters(instance, row_data: dict) -> None: issue_id = row_data["issue_id"] @@ -464,6 +487,8 @@ def process_filters(instance, row_data: dict) -> None: instance.global_architectures.add(row_data["build_architecture"]) instance.global_compilers.add(row_data["build_compiler"]) instance.unfiltered_origins["build"].add(row_data["build_origin"]) + if (build_misc := row_data["build_misc"]) is not None: + instance.unfiltered_labs["build"].add(build_misc.get("lab", UNKNOWN_STRING)) build_issue_id, build_issue_version, is_build_issue = ( should_increment_build_issue( @@ -495,10 +520,12 @@ def process_filters(instance, row_data: dict) -> None: if is_boot(row_data["test_path"]): issue_set = instance.unfiltered_boot_issues origin_set = instance.unfiltered_origins["boot"] + lab_set = instance.unfiltered_labs["boot"] flag_tab: PossibleTabs = "boot" else: issue_set = instance.unfiltered_test_issues origin_set = instance.unfiltered_origins["test"] + lab_set = instance.unfiltered_labs["test"] flag_tab: PossibleTabs = "test" is_failed_test = row_data["test_status"] == FAIL_STATUS @@ -513,3 +540,5 @@ def process_filters(instance, row_data: dict) -> None: ) origin_set.add(row_data["test_origin"]) + if (history_item := row_data.get("history_item")) is not None: + lab_set.add(history_item.get("lab", UNKNOWN_STRING)) diff --git a/backend/kernelCI_app/queries/tree.py b/backend/kernelCI_app/queries/tree.py index a779d7fad..ce6a5fe82 100644 --- a/backend/kernelCI_app/queries/tree.py +++ b/backend/kernelCI_app/queries/tree.py @@ -597,6 +597,7 @@ def get_tree_commit_history( t.environment_compatible, t.environment_misc, t.origin, + t.misc->>'runtime' AS test_lab, b.id AS build_id, b.misc AS build_misc, t.id AS test_id, diff --git a/backend/kernelCI_app/tests/integrationTests/hardwareDetailsSummary_test.py b/backend/kernelCI_app/tests/integrationTests/hardwareDetailsSummary_test.py index d96f2b163..85c163d7b 100644 --- a/backend/kernelCI_app/tests/integrationTests/hardwareDetailsSummary_test.py +++ b/backend/kernelCI_app/tests/integrationTests/hardwareDetailsSummary_test.py @@ -431,6 +431,7 @@ def test_invalid_filters(invalid_filters_input): "DONE": 0, }, "unknown_issues": 0, + "labs": {}, } empty_build = { @@ -448,6 +449,7 @@ def test_invalid_filters(invalid_filters_input): "DONE": 0, }, "unknown_issues": 0, + "labs": {}, } empty_summary = { diff --git a/backend/kernelCI_app/tests/integrationTests/treeDetailsSummary_test.py b/backend/kernelCI_app/tests/integrationTests/treeDetailsSummary_test.py index 29374fe51..50ccdc386 100644 --- a/backend/kernelCI_app/tests/integrationTests/treeDetailsSummary_test.py +++ b/backend/kernelCI_app/tests/integrationTests/treeDetailsSummary_test.py @@ -404,6 +404,7 @@ def test_invalid_filters(invalid_filters_input): "DONE": 0, }, "unknown_issues": 0, + "labs": {}, } empty_build = { @@ -421,6 +422,7 @@ def test_invalid_filters(invalid_filters_input): "DONE": 0, }, "unknown_issues": 0, + "labs": {}, } empty_summary = { diff --git a/backend/kernelCI_app/tests/unitTests/helpers/fixtures/hardware_details_data.py b/backend/kernelCI_app/tests/unitTests/helpers/fixtures/hardware_details_data.py index a5c2901a9..53b215020 100644 --- a/backend/kernelCI_app/tests/unitTests/helpers/fixtures/hardware_details_data.py +++ b/backend/kernelCI_app/tests/unitTests/helpers/fixtures/hardware_details_data.py @@ -114,6 +114,7 @@ def create_test_summary(**overrides): failed_platforms={"x86_64", "arm64"}, environment_compatible={"hardware1": StatusCount()}, environment_misc={"x86_64": StatusCount()}, + labs={}, ) for key, value in overrides.items(): diff --git a/backend/kernelCI_app/tests/unitTests/helpers/fixtures/tree_details_data.py b/backend/kernelCI_app/tests/unitTests/helpers/fixtures/tree_details_data.py index 091e678ad..6be4be616 100644 --- a/backend/kernelCI_app/tests/unitTests/helpers/fixtures/tree_details_data.py +++ b/backend/kernelCI_app/tests/unitTests/helpers/fixtures/tree_details_data.py @@ -81,6 +81,9 @@ def create_summary_row_data(**overrides): "test_error": "Test error", "test_environment_compatible": "hardware1", "test_origin": "test", + "history_item": { + "lab": "test_runtime_lab", + }, } base_data.update(overrides) return base_data @@ -102,6 +105,10 @@ def create_filter_row_data(**overrides): "build_compiler": "gcc", "build_origin": "build_origin", "test_origin": "test_origin", + "build_misc": {"lab": "lab1"}, + "history_item": { + "lab": "test_runtime_lab", + }, } base_data.update(overrides) return base_data diff --git a/backend/kernelCI_app/tests/unitTests/helpers/hardwareDetails_helpers_test.py b/backend/kernelCI_app/tests/unitTests/helpers/hardwareDetails_helpers_test.py index b51598783..431f4dff2 100644 --- a/backend/kernelCI_app/tests/unitTests/helpers/hardwareDetails_helpers_test.py +++ b/backend/kernelCI_app/tests/unitTests/helpers/hardwareDetails_helpers_test.py @@ -617,6 +617,7 @@ def test_handle_test_summary( unknown_issues=0, fail_reasons={}, failed_platforms=set(), + labs={}, ) issue_dict = {} @@ -1317,6 +1318,7 @@ def test_format_issue_summary_for_response(self, mock_convert): unknown_issues=0, fail_reasons={}, failed_platforms=set(), + labs={}, ) tests_summary = TestSummary( status=StatusCount(), @@ -1327,6 +1329,7 @@ def test_format_issue_summary_for_response(self, mock_convert): unknown_issues=0, fail_reasons={}, failed_platforms=set(), + labs={}, ) issue_dicts = { diff --git a/backend/kernelCI_app/tests/unitTests/helpers/treeDetails_test.py b/backend/kernelCI_app/tests/unitTests/helpers/treeDetails_test.py index c1fbef365..7ad3b83f4 100644 --- a/backend/kernelCI_app/tests/unitTests/helpers/treeDetails_test.py +++ b/backend/kernelCI_app/tests/unitTests/helpers/treeDetails_test.py @@ -552,6 +552,7 @@ def test_decide_if_is_build_filtered_out(self): "build_duration": 100, "incident_test_id": "test123", "build_origin": "test", + "build_misc": {"lab": "build_lab"}, } result = decide_if_is_build_filtered_out(instance, row_data) @@ -564,6 +565,7 @@ def test_decide_if_is_build_filtered_out(self): issue_version=1, incident_test_id="test123", build_origin="test", + build_lab="build_lab", ) @@ -581,6 +583,7 @@ def test_decide_if_is_boot_filtered_out(self): "test_path": "boot.test", "incident_test_id": "test123", "test_origin": "test", + "history_item": {"lab": "boot_lab"}, } result = decide_if_is_boot_filtered_out(instance, row_data) @@ -594,6 +597,7 @@ def test_decide_if_is_boot_filtered_out(self): status="FAIL", incident_test_id="test123", origin="test", + lab="boot_lab", ) @@ -637,6 +641,7 @@ def test_decide_if_is_test_filtered_out(self): "test_path": "test.specific", "incident_test_id": "test123", "test_origin": "test", + "history_item": {"lab": "test_lab"}, } result = decide_if_is_test_filtered_out(instance, row_data) @@ -650,6 +655,7 @@ def test_decide_if_is_test_filtered_out(self): status="FAIL", incident_test_id="test123", origin="test", + lab="test_lab", ) diff --git a/backend/kernelCI_app/tests/utils/fields/hardware.py b/backend/kernelCI_app/tests/utils/fields/hardware.py index c1486b885..f259d01d1 100644 --- a/backend/kernelCI_app/tests/utils/fields/hardware.py +++ b/backend/kernelCI_app/tests/utils/fields/hardware.py @@ -18,6 +18,7 @@ "configs", "issues", "unknown_issues", + "labs", ] hardware_test_summary = [ "status", @@ -29,6 +30,7 @@ "environment_misc", "fail_reasons", "failed_platforms", + "labs", ] hardware_history_checkouts = [ diff --git a/backend/kernelCI_app/typeModels/commonDetails.py b/backend/kernelCI_app/typeModels/commonDetails.py index 28ee81393..7335c4a42 100644 --- a/backend/kernelCI_app/typeModels/commonDetails.py +++ b/backend/kernelCI_app/typeModels/commonDetails.py @@ -102,6 +102,7 @@ class TestSummary(BaseModel): environment_compatible: Optional[dict] = None environment_misc: Optional[dict] = None platforms: Optional[dict[str, StatusCount]] = None + labs: dict[str, StatusCount] class BaseBuildSummary(BaseModel): @@ -109,6 +110,7 @@ class BaseBuildSummary(BaseModel): origins: dict[str, StatusCount] = Field(default_factory=dict) architectures: dict[str, BuildArchitectures] = Field(default_factory=dict) configs: dict[str, StatusCount] = Field(default_factory=dict) + labs: dict[str, StatusCount] = Field(default_factory=dict) class BuildSummary(BaseBuildSummary): @@ -132,6 +134,7 @@ class LocalFilters(BaseModel): issues: list[tuple[str, Optional[int]]] origins: list[str] has_unknown_issue: bool + labs: list[str] class DetailsFilters(BaseModel): diff --git a/backend/kernelCI_app/viewCommon.py b/backend/kernelCI_app/viewCommon.py index df5ebebed..a17da8921 100644 --- a/backend/kernelCI_app/viewCommon.py +++ b/backend/kernelCI_app/viewCommon.py @@ -1,3 +1,4 @@ +from kernelCI_app.constants.general import UNKNOWN_STRING from kernelCI_app.typeModels.common import StatusCount from kernelCI_app.typeModels.commonDetails import ( BaseBuildSummary, @@ -16,6 +17,7 @@ def create_details_build_summary(builds: list[BuildHistoryItem]) -> BaseBuildSum config_summ: dict[str, StatusCount] = {} arch_summ: dict[str, BuildArchitectures] = {} origin_summ: dict[str, StatusCount] = {} + labs_summ: dict[str, StatusCount] = {} for build in builds: status_key = build.status @@ -37,9 +39,15 @@ def create_details_build_summary(builds: list[BuildHistoryItem]) -> BaseBuildSum status = origin_summ.setdefault(origin, StatusCount()) _increment_status(status, status_key) + if build.misc is not None and isinstance(build.misc, dict): + lab = build.misc.get("lab", UNKNOWN_STRING) + status = labs_summ.setdefault(lab, StatusCount()) + _increment_status(status, status_key) + return BaseBuildSummary( status=status_summ, configs=config_summ, architectures=arch_summ, origins=origin_summ, + labs=labs_summ, ) diff --git a/backend/kernelCI_app/views/hardwareDetailsSummaryView.py b/backend/kernelCI_app/views/hardwareDetailsSummaryView.py index 5a46f6daa..41baf258e 100644 --- a/backend/kernelCI_app/views/hardwareDetailsSummaryView.py +++ b/backend/kernelCI_app/views/hardwareDetailsSummaryView.py @@ -128,7 +128,13 @@ def __init__(self): self.tree_status_summary = defaultdict(generate_tree_status_summary_dict) self.compatibles: List[str] = [] - self.unfiltered_origins: dict[str, set[str]] = { + self.unfiltered_origins: dict[PossibleTabs, set[str]] = { + "build": set(), + "boot": set(), + "test": set(), + } + + self.unfiltered_labs: dict[PossibleTabs, set[str]] = { "build": set(), "boot": set(), "test": set(), @@ -344,6 +350,7 @@ def post(self, request, hardware_id) -> Response: "build" ], origins=sorted(self.unfiltered_origins["build"]), + labs=self.unfiltered_labs["build"], ), boots=HardwareTestLocalFilters( issues=list(self.unfiltered_boot_issues), @@ -352,6 +359,7 @@ def post(self, request, hardware_id) -> Response: "boot" ], origins=sorted(self.unfiltered_origins["boot"]), + labs=self.unfiltered_labs["boot"], ), tests=HardwareTestLocalFilters( issues=list(self.unfiltered_test_issues), @@ -360,6 +368,7 @@ def post(self, request, hardware_id) -> Response: "test" ], origins=sorted(self.unfiltered_origins["test"]), + labs=self.unfiltered_labs["test"], ), ), common=HardwareCommon( diff --git a/backend/kernelCI_app/views/hardwareDetailsView.py b/backend/kernelCI_app/views/hardwareDetailsView.py index 1b4882212..ee6e4834c 100644 --- a/backend/kernelCI_app/views/hardwareDetailsView.py +++ b/backend/kernelCI_app/views/hardwareDetailsView.py @@ -110,7 +110,13 @@ def __init__(self): self.tree_status_summary = defaultdict(generate_tree_status_summary_dict) - self.unfiltered_origins: dict[str, set[str]] = { + self.unfiltered_origins: dict[PossibleTabs, set[str]] = { + "build": set(), + "boot": set(), + "test": set(), + } + + self.unfiltered_labs: dict[PossibleTabs, set[str]] = { "build": set(), "boot": set(), "test": set(), @@ -313,6 +319,7 @@ def post(self, request, hardware_id) -> Response: configs=self.base_build_summary.configs, issues=self.builds["issues"], unknown_issues=self.builds["failedWithUnknownIssues"], + labs={}, ), boots=TestSummary( status=self.boots["statusSummary"], @@ -324,6 +331,7 @@ def post(self, request, hardware_id) -> Response: platforms=self.boots["platforms"], fail_reasons=self.boots["failReasons"], failed_platforms=list(self.boots["platformsFailing"]), + labs={}, ), tests=TestSummary( status=self.tests["statusSummary"], @@ -335,6 +343,7 @@ def post(self, request, hardware_id) -> Response: platforms=self.tests["platforms"], fail_reasons=self.tests["failReasons"], failed_platforms=list(self.tests["platformsFailing"]), + labs={}, ), ), filters=HardwareDetailsFilters( @@ -349,6 +358,7 @@ def post(self, request, hardware_id) -> Response: "build" ], origins=sorted(self.unfiltered_origins["build"]), + labs=self.unfiltered_labs["build"], ), boots=HardwareTestLocalFilters( issues=list(self.unfiltered_boot_issues), @@ -357,6 +367,7 @@ def post(self, request, hardware_id) -> Response: "boot" ], origins=sorted(self.unfiltered_origins["boot"]), + labs=self.unfiltered_labs["boot"], ), tests=HardwareTestLocalFilters( issues=list(self.unfiltered_test_issues), @@ -365,6 +376,7 @@ def post(self, request, hardware_id) -> Response: "test" ], origins=sorted(self.unfiltered_origins["test"]), + labs=self.unfiltered_labs["test"], ), ), common=HardwareCommon( diff --git a/backend/kernelCI_app/views/treeCommitsHistory.py b/backend/kernelCI_app/views/treeCommitsHistory.py index 33d01a054..bdf99fb98 100644 --- a/backend/kernelCI_app/views/treeCommitsHistory.py +++ b/backend/kernelCI_app/views/treeCommitsHistory.py @@ -19,7 +19,7 @@ TREE_NAME_PATH_PARAM, ) from kernelCI_app.typeModels.databases import FAIL_STATUS, NULL_STATUS, StatusValues -from kernelCI_app.utils import is_boot +from kernelCI_app.utils import is_boot, sanitize_dict from rest_framework.views import APIView from rest_framework.response import Response from drf_spectacular.utils import extend_schema @@ -65,34 +65,47 @@ def setup_filters(self): # TODO: use a pydantic model instead of a dict def sanitize_rows(self, rows: dict) -> list: - return [ - { - "git_commit_hash": row[0], - "git_commit_name": row[1], - "git_commit_tags": row[2], - "earliest_start_time": row[3], - "build_duration": row[4], - "architecture": row[5], - "compiler": row[6], - "config_name": row[7], - "build_status": NULL_STATUS if row[8] is None else row[8], - "build_origin": row[9], - "test_path": row[10], - "test_status": row[11], - "test_duration": row[12], - "hardware_compatibles": row[13], - "test_environment_misc": row[14], - "test_origin": row[15], - "build_id": row[16], - "build_misc": row[17], - "test_id": row[18], - "incidents_id": row[19], - "incidents_test_id": row[20], - "issue_id": row[21], - "issue_version": row[22], - } - for row in rows - ] + result = [] + for row in rows: + build_misc = row[18] + sanitized_build_misc = sanitize_dict(build_misc) + build_lab = ( + sanitized_build_misc.get("lab", UNKNOWN_STRING) + if sanitized_build_misc + else UNKNOWN_STRING + ) + + result.append( + { + "git_commit_hash": row[0], + "git_commit_name": row[1], + "git_commit_tags": row[2], + "earliest_start_time": row[3], + "build_duration": row[4], + "architecture": row[5], + "compiler": row[6], + "config_name": row[7], + "build_status": NULL_STATUS if row[8] is None else row[8], + "build_origin": row[9], + "test_path": row[10], + "test_status": row[11], + "test_duration": row[12], + "hardware_compatibles": row[13], + "test_environment_misc": row[14], + "test_origin": row[15], + "test_lab": row[16], + "build_id": row[17], + "build_misc": build_misc, + "test_id": row[19], + "incidents_id": row[20], + "incidents_test_id": row[21], + "issue_id": row[22], + "issue_version": row[23], + "build_lab": build_lab, + } + ) + + return result def _create_commit_entry(self) -> dict: empty_status_dict = { @@ -123,6 +136,7 @@ def _process_builds_count( incident_test_id: Optional[str], key: str, build_origin: str, + build_lab: str, ) -> None: is_filtered_out = self.filterParams.is_build_filtered_out( duration=duration, @@ -131,6 +145,7 @@ def _process_builds_count( issue_version=issue_version, incident_test_id=incident_test_id, build_origin=build_origin, + build_lab=build_lab, ) if is_filtered_out: return @@ -152,6 +167,7 @@ def _process_boots_count( issue_version: int, incident_test_id: str, test_origin: str, + lab: str, ) -> None: is_boot_filter_out = self.filterParams.is_boot_filtered_out( duration=test_duration, @@ -161,6 +177,7 @@ def _process_boots_count( status=test_status, incident_test_id=incident_test_id, origin=test_origin, + lab=lab, ) is_boot_processed = test_id in self.processed_tests @@ -184,6 +201,7 @@ def _process_nonboots_count( issue_version: int, incident_test_id: str, test_origin: str, + lab: str, ) -> None: is_nonboot_filter_out = self.filterParams.is_test_filtered_out( duration=test_duration, @@ -193,6 +211,7 @@ def _process_nonboots_count( status=test_status, incident_test_id=incident_test_id, origin=test_origin, + lab=lab, ) is_test_processed = test_id in self.processed_tests @@ -251,8 +270,8 @@ def _process_tests(self, row: dict) -> None: incident_test_id = row["incidents_test_id"] build_status = row["build_status"] test_origin = row["test_origin"] - commit_hash = row["git_commit_hash"] + test_lab = row["test_lab"] if issue_id is None and ( build_status in [FAIL_STATUS, NULL_STATUS] or test_status == FAIL_STATUS @@ -273,6 +292,7 @@ def _process_tests(self, row: dict) -> None: issue_version=issue_version, incident_test_id=incident_test_id, test_origin=test_origin, + lab=test_lab, ) else: self._process_nonboots_count( @@ -285,12 +305,14 @@ def _process_tests(self, row: dict) -> None: issue_version=issue_version, incident_test_id=incident_test_id, test_origin=test_origin, + lab=test_lab, ) def _process_builds(self, row: dict) -> None: build_id = row["build_id"] commit_hash = row["git_commit_hash"] build_origin = row["build_origin"] + build_lab = row["build_lab"] key = f"{build_id}_{commit_hash}" @@ -308,6 +330,7 @@ def _process_builds(self, row: dict) -> None: incident_test_id=row["incidents_test_id"], key=key, build_origin=build_origin, + build_lab=build_lab, ) def _is_record_in_time_period(self, start_time: datetime) -> bool: diff --git a/backend/kernelCI_app/views/treeDetailsSummaryView.py b/backend/kernelCI_app/views/treeDetailsSummaryView.py index b408105fd..1637bf62c 100644 --- a/backend/kernelCI_app/views/treeDetailsSummaryView.py +++ b/backend/kernelCI_app/views/treeDetailsSummaryView.py @@ -1,5 +1,6 @@ from typing import Any, Dict, Optional from django.http import HttpRequest +from kernelCI_app.helpers.hardwareDetails import generate_test_summary_typed from pydantic import ValidationError from rest_framework.response import Response from kernelCI_app.constants.localization import ClientStrings @@ -111,13 +112,21 @@ def __init__(self): "test": set(), } + self.unfiltered_labs: dict[PossibleTabs, set[str]] = { + "build": set(), + "boot": set(), + "test": set(), + } + # TODO: move to a BuildSummary model and combine with the other fields above - self.base_build_summary = BaseBuildSummary() + self.base_build_summary = BaseBuildSummary(labs={}) # TODO: move to a TestSummary model and combine with the other fields above self.test_summary: dict[str, Any] = {"origins": {}} + self.test_summary_typed: TestSummary = generate_test_summary_typed() self.boot_summary: dict[str, Any] = {"origins": {}} + self.boot_summary_typed: TestSummary = generate_test_summary_typed() def _process_boots_test(self, row_data): test_id = row_data["test_id"] @@ -254,10 +263,7 @@ def get( ), summary=Summary( builds=BuildSummary( - status=self.base_build_summary.status, - origins=self.base_build_summary.origins, - architectures=self.base_build_summary.architectures, - configs=self.base_build_summary.configs, + **self.base_build_summary.model_dump(), issues=self.build_issues, unknown_issues=self.failed_builds_with_unknown_issues, ), @@ -272,6 +278,7 @@ def get( environment_misc=self.bootEnvironmentMisc, fail_reasons=self.bootFailReasons, failed_platforms=list(self.bootPlatformsFailing), + labs=self.boot_summary_typed.labs, ), tests=TestSummary( status=self.testStatusSummary, @@ -284,6 +291,7 @@ def get( environment_misc=self.testEnvironmentMisc, fail_reasons=self.testFailReasons, failed_platforms=list(self.testPlatformsWithErrors), + labs=self.test_summary_typed.labs, ), ), filters=DetailsFilters( @@ -298,6 +306,7 @@ def get( "build" ], origins=sorted(self.unfiltered_origins["build"]), + labs=self.unfiltered_labs["build"], ), boots=LocalFilters( issues=list(self.unfiltered_boot_issues), @@ -305,6 +314,7 @@ def get( "boot" ], origins=sorted(self.unfiltered_origins["boot"]), + labs=self.unfiltered_labs["boot"], ), tests=LocalFilters( issues=list(self.unfiltered_test_issues), @@ -312,6 +322,7 @@ def get( "test" ], origins=sorted(self.unfiltered_origins["test"]), + labs=self.unfiltered_labs["test"], ), ), ) diff --git a/backend/kernelCI_app/views/treeDetailsView.py b/backend/kernelCI_app/views/treeDetailsView.py index db0dd5679..315836897 100644 --- a/backend/kernelCI_app/views/treeDetailsView.py +++ b/backend/kernelCI_app/views/treeDetailsView.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Optional from http import HTTPStatus from django.http import HttpRequest +from kernelCI_app.helpers.hardwareDetails import generate_test_summary_typed from rest_framework.views import APIView from rest_framework.response import Response from kernelCI_app.constants.localization import ClientStrings @@ -112,13 +113,21 @@ def __init__(self): "test": set(), } + self.unfiltered_labs: dict[PossibleTabs, set[Optional[str]]] = { + "build": set(), + "boot": set(), + "test": set(), + } + # TODO: move to a BuildSummary model and combine with the other fields above self.base_build_summary = BaseBuildSummary() # TODO: move to a TestSummary model and combine with the other fields above self.test_summary: dict[str, Any] = {"origins": {}} + self.test_summary_typed: TestSummary = generate_test_summary_typed() self.boot_summary: dict[str, Any] = {"origins": {}} + self.boot_summary_typed: TestSummary = generate_test_summary_typed() def _process_boots_test(self, row_data): test_id = row_data["test_id"] @@ -263,10 +272,7 @@ def get( tests=self.testHistory, summary=Summary( builds=BuildSummary( - status=self.base_build_summary.status, - origins=self.base_build_summary.origins, - architectures=self.base_build_summary.architectures, - configs=self.base_build_summary.configs, + **self.base_build_summary.model_dump(), issues=self.build_issues, unknown_issues=self.failed_builds_with_unknown_issues, ), @@ -281,6 +287,7 @@ def get( environment_misc=self.bootEnvironmentMisc, fail_reasons=self.bootFailReasons, failed_platforms=list(self.bootPlatformsFailing), + labs=self.boot_summary_typed.labs, ), tests=TestSummary( status=self.testStatusSummary, @@ -293,6 +300,7 @@ def get( environment_misc=self.testEnvironmentMisc, fail_reasons=self.testFailReasons, failed_platforms=list(self.testPlatformsWithErrors), + labs=self.test_summary_typed.labs, ), ), common=TreeCommon( @@ -312,6 +320,7 @@ def get( "build" ], origins=sorted(self.unfiltered_origins["build"]), + labs=self.unfiltered_labs["build"], ), boots=LocalFilters( issues=list(self.unfiltered_boot_issues), @@ -319,6 +328,7 @@ def get( "boot" ], origins=sorted(self.unfiltered_origins["boot"]), + labs=self.unfiltered_labs["boot"], ), tests=LocalFilters( issues=list(self.unfiltered_test_issues), @@ -326,6 +336,7 @@ def get( "test" ], origins=sorted(self.unfiltered_origins["test"]), + labs=self.unfiltered_labs["test"], ), ), ) diff --git a/dashboard/src/components/Cards/OriginsCard.tsx b/dashboard/src/components/Cards/FilterCard.tsx similarity index 67% rename from dashboard/src/components/Cards/OriginsCard.tsx rename to dashboard/src/components/Cards/FilterCard.tsx index b9e41cca5..6ca44c763 100644 --- a/dashboard/src/components/Cards/OriginsCard.tsx +++ b/dashboard/src/components/Cards/FilterCard.tsx @@ -13,35 +13,38 @@ import type { TFilter, TFilterObjectsKeys, } from '@/types/general'; +import type { MessagesKey } from '@/locales/messages'; interface IOriginsCard { - origins: Record; + data: Record; diffFilter: TFilter; filterSection: TFilterObjectsKeys; - hideSingleOrigin?: boolean; + hideSingleValue?: boolean; + cardTitle: MessagesKey; } -const OriginsCard = ({ - origins, +const FilterCard = ({ + data, diffFilter, filterSection, - hideSingleOrigin = true, + hideSingleValue = true, + cardTitle, }: IOriginsCard): JSX.Element => { const content: JSX.Element[] = useMemo(() => { - return Object.keys(origins).map(originItem => { - const { DONE, FAIL, ERROR, MISS, PASS, SKIP, NULL } = origins[originItem]; + return Object.keys(data).map(item => { + const { DONE, FAIL, ERROR, MISS, PASS, SKIP, NULL } = data[item]; return ( ); }); - }, [origins, filterSection, diffFilter]); + }, [data, filterSection, diffFilter]); - if (hideSingleOrigin && Object.keys(origins).length === 1) { + if (hideSingleValue && Object.keys(data).length === 1) { return <>; } return ( - }> + }> {content} ); }; -export const MemoizedOriginsCard = memo(OriginsCard); +export const MemoizedFilterCard = memo(FilterCard); diff --git a/dashboard/src/locales/messages/index.ts b/dashboard/src/locales/messages/index.ts index 7e7ef0783..91dd20844 100644 --- a/dashboard/src/locales/messages/index.ts +++ b/dashboard/src/locales/messages/index.ts @@ -53,11 +53,13 @@ export const messages = { 'filter.architectureSubtitle': 'Please select one or more Architectures:', 'filter.bootDuration': 'Boot duration', 'filter.bootIssue': 'Boot issue', + 'filter.bootLab': 'Boot Lab', 'filter.bootOrigin': 'Boot origin', 'filter.bootPlatform': 'Boot Platforms', 'filter.bootStatus': 'Boot Status', 'filter.buildDuration': 'Build duration', 'filter.buildIssue': 'Build Issue', + 'filter.buildLab': 'Build Lab', 'filter.buildOrigin': 'Build origin', 'filter.buildStatus': 'Build Status', 'filter.compatiblesSubtitle': 'Please select one or more compatibles:', @@ -77,6 +79,8 @@ export const messages = { 'filter.issueCulpritSubtitle': 'Select one or more issue culprits:', 'filter.issueHasIncident': 'Has incident', 'filter.issueSubtitle': 'Please select one or more issues:', + 'filter.labs': 'Labs', + 'filter.labsSubtitle': 'Please select one or more labs:', 'filter.max': 'Max', 'filter.min': 'Min', 'filter.onlySpecificTab': 'Only affects a specific tab', @@ -90,6 +94,7 @@ export const messages = { 'filter.tableFilter': 'Status filters:', 'filter.testDuration': 'Test duration', 'filter.testIssue': 'Test issue', + 'filter.testLab': 'Test Lab', 'filter.testOrigin': 'Test origin', 'filter.testPlatform': 'Test Platforms', 'filter.testStatus': 'Test Status', diff --git a/dashboard/src/pages/TreeDetails/Tabs/Boots/BootsTab.tsx b/dashboard/src/pages/TreeDetails/Tabs/Boots/BootsTab.tsx index d5c091938..e2695499a 100644 --- a/dashboard/src/pages/TreeDetails/Tabs/Boots/BootsTab.tsx +++ b/dashboard/src/pages/TreeDetails/Tabs/Boots/BootsTab.tsx @@ -34,7 +34,7 @@ import type { TreeDetailsLazyLoaded } from '@/hooks/useTreeDetailsLazyLoadQuery' import QuerySwitcher from '@/components/QuerySwitcher/QuerySwitcher'; import { generateDiffFilter } from '@/components/Tabs/tabsUtils'; import { MemoizedSectionError } from '@/components/DetailsPages/SectionError'; -import { MemoizedOriginsCard } from '@/components/Cards/OriginsCard'; +import { MemoizedFilterCard } from '@/components/Cards/FilterCard'; import { sanitizeTreeinfo } from '@/utils/treeDetails'; import { MemoizedKcidevFooter } from '@/components/Footer/KcidevFooter'; import { getStringParam } from '@/utils/utils'; @@ -277,12 +277,21 @@ const BootsTab = ({ environmentCompatible={hardwareData} diffFilter={diffFilter} />, - , + , ], footerCards: [ ; + labs: Record; } const BuildTab = ({ @@ -146,6 +147,7 @@ const BuildTab = ({ failedBuildsWithUnknownIssues: summaryBuildsData?.unknown_issues, builds: sanitizeBuilds(fullBuildsData), origins: summaryBuildsData?.origins || {}, + labs: summaryBuildsData?.labs || {}, }), [ fullBuildsData, @@ -155,6 +157,7 @@ const BuildTab = ({ summaryBuildsData?.origins, summaryBuildsData?.status, summaryBuildsData?.unknown_issues, + summaryBuildsData?.labs, ], ); @@ -209,10 +212,19 @@ const BuildTab = ({ toggleFilterBySection={toggleFilterBySection} diffFilter={diffFilter} />, - , + , , - , + , ], footerCards: [ ; interface ITreeDetailsFilter { @@ -67,6 +70,10 @@ export const createFilter = (data: TreeDetailsSummary): TFilter => { filters.bootOrigin = {}; filters.testOrigin = {}; + filters.buildLab = {}; + filters.bootLab = {}; + filters.testLab = {}; + // Filters affecting all tabs const allFilters = data.filters.all; for (const config of allFilters.configs) { @@ -95,6 +102,9 @@ export const createFilter = (data: TreeDetailsSummary): TFilter => { for (const o of buildFilters.origins) { filters.buildOrigin[o] = false; } + for (const l of buildFilters.labs) { + filters.buildLab[l] = false; + } // Boot tab filters const bootFilters = data.filters.boots; @@ -107,6 +117,9 @@ export const createFilter = (data: TreeDetailsSummary): TFilter => { for (const o of bootFilters.origins) { filters.bootOrigin[o] = false; } + for (const l of bootFilters.labs) { + filters.bootLab[l] = false; + } // Test tab filters const testFilters = data.filters.tests; @@ -119,6 +132,9 @@ export const createFilter = (data: TreeDetailsSummary): TFilter => { for (const o of testFilters.origins) { filters.testOrigin[o] = false; } + for (const l of testFilters.labs) { + filters.testLab[l] = false; + } return filters; }; @@ -196,7 +212,27 @@ const sectionTrees: ISectionItem[] = [ sectionKey: 'testOrigin', isGlobal: false, }, + { + title: 'filter.buildLab', + subtitle: 'filter.labsSubtitle', + sectionKey: 'buildLab', + isGlobal: false, + }, + { + title: 'filter.bootLab', + subtitle: 'filter.labsSubtitle', + sectionKey: 'bootLab', + isGlobal: false, + }, + { + title: 'filter.testLab', + subtitle: 'filter.labsSubtitle', + sectionKey: 'testLab', + isGlobal: false, + }, ]; + +// TODO: some sections can be hidden if there is only 1 value for them (e.g., origins, labs) const TreeDetailsFilter = ({ paramFilter, treeUrl, diff --git a/dashboard/src/types/commonDetails.ts b/dashboard/src/types/commonDetails.ts index 86d613428..4f14818ec 100644 --- a/dashboard/src/types/commonDetails.ts +++ b/dashboard/src/types/commonDetails.ts @@ -21,6 +21,7 @@ type TestSummary = { environment_compatible?: PropertyStatusCounts; environment_misc?: PropertyStatusCounts; platforms?: PropertyStatusCounts; + labs: Record; }; type BuildSummary = { @@ -28,6 +29,7 @@ type BuildSummary = { origins: Record; architectures: Architecture; configs: Record; + labs: Record; issues: TIssue[]; unknown_issues: number; }; @@ -50,6 +52,7 @@ export type LocalFilters = { issues: IssueFilterItem[]; has_unknown_issue: boolean; origins: string[]; + labs: string[]; }; export type DetailsFilters = { diff --git a/dashboard/src/types/general.ts b/dashboard/src/types/general.ts index 3cd8a3bfb..594123932 100644 --- a/dashboard/src/types/general.ts +++ b/dashboard/src/types/general.ts @@ -163,6 +163,9 @@ export const zFilterObjectsKeys = z.enum([ 'buildOrigin', 'bootOrigin', 'testOrigin', + 'buildLab', + 'bootLab', + 'testLab', ]); export const zFilterNumberKeys = z.enum([ @@ -208,6 +211,9 @@ export const zDiffFilter = z buildOrigin: zFilterBoolValue, bootOrigin: zFilterBoolValue, testOrigin: zFilterBoolValue, + buildLab: zFilterBoolValue, + bootLab: zFilterBoolValue, + testLab: zFilterBoolValue, } satisfies Record), z.record(z.never()), ]) @@ -278,6 +284,9 @@ const requestFilters = { 'build.origin', 'boot.origin', 'test.origin', + 'build.lab', + 'boot.lab', + 'test.lab', ], issueListing: [ 'origin', @@ -321,6 +330,9 @@ export const filterFieldMap = { 'test.issue': 'testIssue', 'build.status': 'buildStatus', 'build.origin': 'buildOrigin', + 'build.lab': 'buildLab', + 'boot.lab': 'bootLab', + 'test.lab': 'testLab', origin: 'origins', 'issue.culprit': 'issueCulprits', 'issue.categories': 'issueCategories', diff --git a/dashboard/src/utils/search.ts b/dashboard/src/utils/search.ts index 6645f9be6..7effa453f 100644 --- a/dashboard/src/utils/search.ts +++ b/dashboard/src/utils/search.ts @@ -194,6 +194,9 @@ const diffFilterMinifiedParams: Record = { buildOrigin: 'buo', bootOrigin: 'boo', testOrigin: 'to', + buildLab: 'bul', + bootLab: 'bol', + testLab: 'tl', } as const satisfies Record; type MinifiedParams = Record< diff --git a/docs/IntegrationTests.md b/docs/IntegrationTests.md index 07b720819..9f2047435 100644 --- a/docs/IntegrationTests.md +++ b/docs/IntegrationTests.md @@ -252,7 +252,7 @@ docker compose -f docker-compose.test.yml run --rm test_backend python manage.py **Step 4: Start test backend** ```bash -docker compose -f docker-compose.test.yml up test_db redis -d +docker compose -f docker-compose.test.yml up --build -d ``` **Step 5: Run integration tests** @@ -266,6 +266,8 @@ TEST_BASE_URL=http://localhost:8001 poetry run pytest -m integration --use-local docker compose -f docker-compose.test.yml down --volumes --remove-orphans ``` +>![IMPORTANT] If you run the tests, see a problem and go about fixing it, remember to rebuild the docker containers at Step 4 with `--force-recreate` + ### Test Configuration The test environment uses `backend/kernelCI/test_settings.py` which: diff --git a/docs/filters.md b/docs/filters.md index eb7f57420..ac5e0d288 100644 --- a/docs/filters.md +++ b/docs/filters.md @@ -1,4 +1,6 @@ -# How filters should work +# Filter logic + +## How filters should work They should work as OR in the same category and AND between two different categories @@ -14,3 +16,26 @@ It means that: | 'FAIL' | 'arm' | False | | 'MISS' | 'arm' | True | | 'MISS' | 'x86' | False | + +## Adding a new filter + +Our filter system is currently seomwhat complex. Most of the parts are required, while some of them are unnecessary and should be addressed. For now, here are the steps for adding a new filter with our current structure. You don't necessarily need to do the backend steps first and then the frontend ones, mix and squash as sensible to a good commit split. + +### In the backend + +- Edit the summary and filter return fields of treeDetails/hardwareDetails, for both full and summary endpoints +- Add the filter logic to the FilterParams class + - Add the relevant set, the handler function, update the "field: handler" map, and update the logic in the desired functions, such as `_is_build_filtered_out` +- Edit the tree/hardware commitHistory endpoint to be affected by the filter too +- Check unit tests to see if they need updates +- Check integration tests to see if they need updates +- Update backend schema with `generate_schema.sh` script + +### In the frontend + +- Edit the type that reflects the api response to include the new fields in summary and filters +- Add the card in the frontend +- Edit `zFilterObjectsKeys` or `zFilterNumberKeys` utils types to include the new filter. Edit the other related types to them as well, the components will flag the required types. +- Edit the mapFilterToReq function to translate the new filter to the backend format +- Edit the `TreeDetailsFilter`/`HardwareDetailsFilter` modal to include a new section and to transform the data into checkboxes +- Edit the search.ts file to map the filter name to a shortened version in the url