Skip to content

Commit 6b46d3f

Browse files
feat: use tree_tests_rollup for tree details summary (#1842)
* feat: implement rollup data processing and querying * test: add tests rollup seeding for integration tests * test: add unit tests for tree details rollup helpers
1 parent 654fed2 commit 6b46d3f

11 files changed

Lines changed: 1257 additions & 93 deletions

File tree

backend/kernelCI_app/helpers/treeDetails.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ def process_test_summary(instance, row_data):
390390
instance.testStatusSummary.get(test_status, 0) + 1
391391
)
392392

393-
arch_key = "%s-%s" % (build_arch, build_compiler)
393+
arch_key = (build_arch, build_compiler)
394394
arch_summary = instance.test_arch_summary.get(
395395
arch_key,
396396
{"arch": build_arch, "compiler": build_compiler, "status": {}},
@@ -442,7 +442,7 @@ def process_boots_summary(instance, row_data: dict[str, Any]) -> None:
442442
instance.bootStatusSummary.get(test_status, 0) + 1
443443
)
444444

445-
arch_key = "%s-%s" % (build_arch, build_compiler)
445+
arch_key = (build_arch, build_compiler)
446446
arch_summary = instance.bootArchSummary.get(
447447
arch_key,
448448
{"arch": build_arch, "compiler": build_compiler, "status": {}},
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
from kernelCI_app.constants.process_pending import ROLLUP_STATUS_FIELDS
2+
from kernelCI_app.constants.general import UNCATEGORIZED_STRING, UNKNOWN_STRING
3+
from kernelCI_app.helpers.commonDetails import PossibleTabs, add_unfiltered_issue
4+
from kernelCI_app.helpers.filters import (
5+
is_status_failure,
6+
should_filter_test_issue,
7+
should_increment_build_issue,
8+
should_increment_test_issue,
9+
)
10+
from kernelCI_app.helpers.misc import handle_misc
11+
from kernelCI_app.typeModels.common import StatusCount
12+
from kernelCI_app.typeModels.databases import (
13+
FAIL_STATUS,
14+
NULL_STATUS,
15+
build_fail_status_list,
16+
failure_status_list,
17+
)
18+
from kernelCI_app.typeModels.issues import Issue, IssueDict
19+
from kernelCI_app.utils import create_issue_typed
20+
21+
ROLLUP_TEST_ID = "rollup_test"
22+
23+
24+
def normalize_build_dict(row_dict: dict) -> dict:
25+
"""Normalize a dict row from get_tree_details_builds into the shape
26+
expected by existing build-processing helpers."""
27+
row_dict["build_misc"] = handle_misc(row_dict.get("build_misc"))
28+
29+
defaults = {
30+
"build_status": NULL_STATUS,
31+
"build_architecture": UNKNOWN_STRING,
32+
"build_compiler": UNKNOWN_STRING,
33+
"build_config_name": UNKNOWN_STRING,
34+
}
35+
for key, default in defaults.items():
36+
row_dict.setdefault(key, default)
37+
38+
if row_dict.get("issue_id") is None and is_status_failure(
39+
row_dict["build_status"], build_fail_status_list
40+
):
41+
row_dict["issue_id"] = UNCATEGORIZED_STRING
42+
43+
return row_dict
44+
45+
46+
def process_build_filters(instance, row_data: dict) -> None:
47+
"""Populate global filter sets and unfiltered build issues from a build row."""
48+
build_status = row_data["build_status"]
49+
issue_id = row_data.get("issue_id")
50+
issue_version = row_data.get("issue_version")
51+
incident_test_id = row_data.get("incident_test_id")
52+
53+
instance.global_configs.add(row_data["build_config_name"])
54+
instance.global_architectures.add(row_data["build_architecture"])
55+
instance.global_compilers.add(row_data["build_compiler"])
56+
instance.unfiltered_origins["build"].add(row_data["build_origin"])
57+
58+
if (build_misc := row_data["build_misc"]) is not None:
59+
instance.unfiltered_labs["build"].add(build_misc.get("lab", UNKNOWN_STRING))
60+
61+
build_issue_id, build_issue_version, is_build_issue = should_increment_build_issue(
62+
issue_id=issue_id,
63+
issue_version=issue_version,
64+
incident_test_id=incident_test_id,
65+
build_status=build_status,
66+
)
67+
68+
add_unfiltered_issue(
69+
issue_id=build_issue_id,
70+
issue_version=build_issue_version,
71+
should_increment=is_build_issue,
72+
issue_set=instance.unfiltered_build_issues,
73+
unknown_issue_flag_dict=instance.unfiltered_uncategorized_issue_flags,
74+
unknown_issue_flag_tab="build",
75+
is_failed_task=build_status == FAIL_STATUS,
76+
)
77+
78+
79+
def process_rollup_filters(instance, row_dict: dict) -> None:
80+
"""Populate unfiltered issue/origin/lab sets from a rollup row."""
81+
issue_id = row_dict.get("issue_id")
82+
issue_version = row_dict.get("issue_version")
83+
is_boot_row = row_dict["is_boot"]
84+
85+
incident_test_id = ROLLUP_TEST_ID if issue_id is not None else None
86+
87+
test_issue_id, test_issue_version, is_test_issue = should_increment_test_issue(
88+
issue_id=issue_id,
89+
issue_version=issue_version,
90+
incident_test_id=incident_test_id,
91+
)
92+
93+
if is_boot_row:
94+
issue_set = instance.unfiltered_boot_issues
95+
origin_set = instance.unfiltered_origins["boot"]
96+
lab_set = instance.unfiltered_labs["boot"]
97+
flag_tab: PossibleTabs = "boot"
98+
else:
99+
issue_set = instance.unfiltered_test_issues
100+
origin_set = instance.unfiltered_origins["test"]
101+
lab_set = instance.unfiltered_labs["test"]
102+
flag_tab: PossibleTabs = "test"
103+
104+
has_failures = row_dict.get("issue_uncategorized", False)
105+
add_unfiltered_issue(
106+
issue_id=test_issue_id,
107+
issue_version=test_issue_version,
108+
should_increment=is_test_issue,
109+
issue_set=issue_set,
110+
is_failed_task=has_failures,
111+
unknown_issue_flag_dict=instance.unfiltered_uncategorized_issue_flags,
112+
unknown_issue_flag_tab=flag_tab,
113+
)
114+
115+
test_origin = row_dict.get("test_origin", UNKNOWN_STRING) or UNKNOWN_STRING
116+
test_lab = row_dict.get("test_lab", UNKNOWN_STRING) or UNKNOWN_STRING
117+
origin_set.add(test_origin)
118+
lab_set.add(test_lab)
119+
120+
121+
def rollup_test_or_boot_filtered_out(
122+
instance,
123+
*,
124+
row_dict: dict,
125+
is_boot_row: bool,
126+
) -> bool:
127+
"""Check if a rollup row is filtered by test/boot filters."""
128+
issue_id = row_dict.get("issue_id")
129+
issue_version = row_dict.get("issue_version")
130+
131+
if issue_id is None and row_dict.get("issue_uncategorized", False):
132+
issue_id = UNCATEGORIZED_STRING
133+
134+
incident_test_id = ROLLUP_TEST_ID if issue_id is not None else None
135+
path_group = row_dict.get("path_group", UNKNOWN_STRING)
136+
test_origin = row_dict.get("test_origin")
137+
test_platform = row_dict.get("test_platform")
138+
139+
if is_boot_row:
140+
path_filter = instance.filters.filterBootPath
141+
issue_filters = instance.filters.filterIssues["boot"]
142+
platform_filters = instance.filters.filterPlatforms["boot"]
143+
origin_filters = instance.filters.filter_boot_origin
144+
else:
145+
path_filter = instance.filters.filterTestPath
146+
issue_filters = instance.filters.filterIssues["test"]
147+
platform_filters = instance.filters.filterPlatforms["test"]
148+
origin_filters = instance.filters.filter_test_origin
149+
150+
if path_filter != "" and path_filter not in path_group:
151+
return True
152+
153+
if should_filter_test_issue(
154+
issue_filters=issue_filters,
155+
issue_id=issue_id,
156+
issue_version=issue_version,
157+
incident_test_id=incident_test_id,
158+
):
159+
return True
160+
161+
if platform_filters and test_platform not in platform_filters:
162+
return True
163+
164+
if origin_filters and test_origin not in origin_filters:
165+
return True
166+
167+
return False
168+
169+
170+
def _get_rollup_status_filter(instance, *, is_boot_row: bool) -> set[str]:
171+
"""Return the active status filter set for boots or tests."""
172+
if is_boot_row:
173+
return set(instance.filters.filterBootStatus)
174+
return set(instance.filters.filterTestStatus)
175+
176+
177+
def process_rollup_summary(instance, *, row_dict: dict, is_boot_row: bool) -> None:
178+
"""Accumulate pre-aggregated test/boot counts from a rollup row into
179+
the instance's summary accumulators."""
180+
build_config = row_dict["build_config_name"]
181+
build_arch = row_dict["build_architecture"]
182+
build_compiler = row_dict["build_compiler"]
183+
hardware_key = row_dict["hardware_key"]
184+
test_platform = row_dict.get("test_platform")
185+
test_origin = row_dict.get("test_origin", UNKNOWN_STRING)
186+
test_lab = row_dict.get("test_lab", UNKNOWN_STRING) or UNKNOWN_STRING
187+
188+
status_filter = _get_rollup_status_filter(instance, is_boot_row=is_boot_row)
189+
190+
# Skip row entirely if status filter excludes all its statuses
191+
if status_filter:
192+
has_matching_status = any(
193+
row_dict.get(field_name, 0) > 0 and status_name in status_filter
194+
for status_name, field_name in ROLLUP_STATUS_FIELDS.items()
195+
)
196+
if not has_matching_status:
197+
return
198+
199+
if is_boot_row:
200+
status_summary = instance.bootStatusSummary
201+
arch_summary_map = instance.bootArchSummary
202+
config_map = instance.bootConfigs
203+
platforms_failing = instance.bootPlatformsFailing
204+
env_compatible = instance.bootEnvironmentCompatible
205+
env_misc = instance.bootEnvironmentMisc
206+
origin_summary = instance.boot_summary["origins"]
207+
typed_summary = instance.boot_summary_typed
208+
else:
209+
status_summary = instance.testStatusSummary
210+
arch_summary_map = instance.test_arch_summary
211+
config_map = instance.test_configs
212+
platforms_failing = instance.testPlatformsWithErrors
213+
env_compatible = instance.testEnvironmentCompatible
214+
env_misc = instance.testEnvironmentMisc
215+
origin_summary = instance.test_summary["origins"]
216+
typed_summary = instance.test_summary_typed
217+
218+
arch_key = (build_arch, build_compiler)
219+
arch_entry = arch_summary_map.setdefault(
220+
arch_key,
221+
{"arch": build_arch, "compiler": build_compiler, "status": {}},
222+
)
223+
config_entry = config_map.setdefault(build_config, {})
224+
225+
is_env_compatible = hardware_key not in (UNKNOWN_STRING, test_platform)
226+
227+
for status_name, field_name in ROLLUP_STATUS_FIELDS.items():
228+
count = row_dict.get(field_name, 0)
229+
if count <= 0:
230+
continue
231+
if status_filter and status_name not in status_filter:
232+
continue
233+
234+
status_summary[status_name] = status_summary.get(status_name, 0) + count
235+
arch_entry["status"][status_name] = (
236+
arch_entry["status"].get(status_name, 0) + count
237+
)
238+
config_entry[status_name] = config_entry.get(status_name, 0) + count
239+
240+
if is_env_compatible:
241+
env_compatible[hardware_key][status_name] += count
242+
else:
243+
env_misc[test_platform][status_name] += count
244+
245+
origin_entry = origin_summary.setdefault(test_origin, StatusCount())
246+
setattr(
247+
origin_entry,
248+
status_name,
249+
getattr(origin_entry, status_name) + count,
250+
)
251+
252+
lab_entry = typed_summary.labs.setdefault(test_lab, StatusCount())
253+
setattr(lab_entry, status_name, getattr(lab_entry, status_name) + count)
254+
255+
if is_status_failure(status_name):
256+
platforms_failing.add(test_platform)
257+
258+
259+
def _ensure_issue_in_table(
260+
*,
261+
instance,
262+
issue_id: str,
263+
issue_version: int,
264+
issue_comment: str | None,
265+
issue_report_url: str | None,
266+
is_boot_row: bool,
267+
) -> Issue:
268+
if is_boot_row:
269+
table: IssueDict = instance.boot_issues_dict
270+
else:
271+
table: IssueDict = instance.test_issues_dict
272+
273+
issue: Issue | None = table.get((issue_id, issue_version))
274+
if issue is None:
275+
issue = create_issue_typed(
276+
issue_id=issue_id,
277+
issue_version=issue_version,
278+
issue_comment=issue_comment,
279+
issue_report_url=issue_report_url,
280+
starting_count_status=None,
281+
autoincrement=False,
282+
)
283+
table[(issue_id, issue_version)] = issue
284+
return issue
285+
286+
287+
def process_rollup_issues(instance, *, row_dict: dict, is_boot_row: bool) -> None:
288+
"""Accumulate issue information from a rollup row."""
289+
issue_id = row_dict.get("issue_id")
290+
issue_version = row_dict.get("issue_version")
291+
issue_comment = row_dict.get("issue_comment")
292+
issue_report_url = row_dict.get("issue_report_url")
293+
294+
incident_test_id = ROLLUP_TEST_ID if issue_id is not None else None
295+
296+
test_issue_id, test_issue_version, can_insert_issue = should_increment_test_issue(
297+
issue_id=issue_id,
298+
issue_version=issue_version,
299+
incident_test_id=incident_test_id,
300+
)
301+
302+
failure_counts = [
303+
(status, row_dict.get(ROLLUP_STATUS_FIELDS[status], 0))
304+
for status in failure_status_list
305+
]
306+
307+
if test_issue_id and test_issue_version is not None and can_insert_issue:
308+
current_issue = _ensure_issue_in_table(
309+
instance=instance,
310+
issue_id=test_issue_id,
311+
issue_version=test_issue_version,
312+
issue_comment=issue_comment,
313+
issue_report_url=issue_report_url,
314+
is_boot_row=is_boot_row,
315+
)
316+
317+
for status, count in failure_counts:
318+
current_issue.incidents_info.increment(status, count)
319+
elif row_dict.get("issue_uncategorized", False):
320+
status_filter = _get_rollup_status_filter(instance, is_boot_row=is_boot_row)
321+
if not status_filter or "FAIL" in status_filter:
322+
fail_count = row_dict.get("fail_tests", 0)
323+
if is_boot_row:
324+
instance.failed_boots_with_unknown_issues += fail_count
325+
else:
326+
instance.failed_tests_with_unknown_issues += fail_count

0 commit comments

Comments
 (0)