Skip to content

Commit fe69fe8

Browse files
committed
fix: distinguish tree-report by tree_name for shared git url/branch
Trees that share git_url and branch could return the wrong checkout when they also share a commit hash, because tree_name was not applied in head selection or the join back to checkouts. Accept tree_name on the tree-report API and thread it through get_checkout_summary_data. Closes #1460 Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi>
1 parent b9f9d1e commit fe69fe8

10 files changed

Lines changed: 125 additions & 91 deletions

File tree

backend/kernelCI_app/queries/notifications.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import sys
22
from concurrent.futures import ThreadPoolExecutor
33
from datetime import date, datetime, time, timedelta, timezone
4-
from typing import Any
4+
from typing import Any, Optional
55

66
from django.db import connection, connections
77
from pydantic import ValidationError
@@ -425,13 +425,15 @@ def get_checkout_summary_data(
425425
tuple_params: list[tuple[str, str, str]],
426426
interval_min="5 hours",
427427
interval_max="29 hours",
428+
tree_name: Optional[str] = None,
428429
) -> list[dict]:
429430
"""Queries for the checkout and status count data similarly to tree_listing but
430431
using a list of parameters for the filtering
431432
432433
Parameters:
433434
tuple_params: a list of tuples (str, str, str)
434435
representing (git_repository_branch, git_repository_url, origin)
436+
tree_name: optional filter so trees sharing branch/git_url stay distinct
435437
436438
Returns:
437439
out: a list of dicts with the records found.
@@ -440,6 +442,8 @@ def get_checkout_summary_data(
440442
if not tuple_params:
441443
return []
442444

445+
tree_name_filter = "AND C.TREE_NAME = %s" if tree_name is not None else ""
446+
443447
with_clause = f"""
444448
WITH
445449
ORDERED_CHECKOUTS_BY_TREE AS (
@@ -448,6 +452,7 @@ def get_checkout_summary_data(
448452
C.GIT_REPOSITORY_URL,
449453
C.GIT_COMMIT_HASH,
450454
C.ORIGIN,
455+
C.TREE_NAME,
451456
ROW_NUMBER() OVER (
452457
PARTITION BY
453458
C.GIT_REPOSITORY_BRANCH,
@@ -469,13 +474,15 @@ def get_checkout_summary_data(
469474
WHERE
470475
C.START_TIME >= NOW() - INTERVAL %s
471476
AND C.START_TIME <= NOW() - INTERVAL %s
477+
{tree_name_filter}
472478
),
473479
FIRST_TREE_CHECKOUT AS (
474480
SELECT
475481
GIT_REPOSITORY_BRANCH,
476482
GIT_REPOSITORY_URL,
477483
GIT_COMMIT_HASH,
478-
ORIGIN
484+
ORIGIN,
485+
TREE_NAME
479486
FROM
480487
ORDERED_CHECKOUTS_BY_TREE
481488
WHERE
@@ -489,6 +496,7 @@ def get_checkout_summary_data(
489496
AND checkouts.git_repository_url IS NOT DISTINCT FROM FTC.GIT_REPOSITORY_URL
490497
AND checkouts.git_commit_hash = FTC.GIT_COMMIT_HASH
491498
AND checkouts.origin = FTC.ORIGIN
499+
AND checkouts.tree_name IS NOT DISTINCT FROM FTC.TREE_NAME
492500
)
493501
"""
494502

@@ -502,15 +510,12 @@ def get_checkout_summary_data(
502510
for tuple in tuple_params:
503511
flattened_list += list(tuple)
504512

513+
params = flattened_list + [interval_max, interval_min]
514+
if tree_name is not None:
515+
params.append(tree_name)
516+
505517
with connection.cursor() as cursor:
506-
cursor.execute(
507-
query,
508-
flattened_list
509-
+ [
510-
interval_max,
511-
interval_min,
512-
],
513-
)
518+
cursor.execute(query, params)
514519
return dict_fetchall(cursor=cursor)
515520

516521

backend/kernelCI_app/tests/factories/checkout_factory.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ class Meta:
6565

6666
git_commit_hash = factory.LazyAttribute(
6767
lambda obj: (
68-
obj.id if Checkout.is_known_checkout(obj.id) else f"commit_{obj.id[:8]}"
68+
Checkout.get_git_commit_hash(obj.id)
69+
if Checkout.is_known_checkout(obj.id)
70+
else f"commit_{obj.id[:8]}"
6971
)
7072
)
7173

backend/kernelCI_app/tests/factories/mocks/checkout.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ def get_git_branch(cls, checkout_id: str):
4343
checkout_data = TREE_DATA.get(checkout_id)
4444
return checkout_data.get("git_branch") if checkout_data else None
4545

46+
@classmethod
47+
def get_git_commit_hash(cls, checkout_id: str):
48+
"""Get git commit hash for a checkout (defaults to checkout_id)."""
49+
checkout_data = TREE_DATA.get(checkout_id)
50+
if not checkout_data:
51+
return None
52+
return checkout_data.get("git_commit_hash", checkout_id)
53+
4654
@classmethod
4755
def get_tree_name(cls, checkout_id: str):
4856
"""Get tree name for a checkout."""

backend/kernelCI_app/tests/factories/mocks/fixtures/tree_data.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
Tree data fixtures for test factories.
33
"""
44

5-
from datetime import datetime, timezone
5+
from datetime import datetime, timedelta, timezone
6+
7+
# Evaluated at seed import so tree-report age windows (hours from NOW) still match.
8+
_SEED_NOW = datetime.now(timezone.utc)
69

710
TREE_DATA = {
811
"a1c24ab822793eb513351686f631bd18952b7870": { # ARM64_TREE
@@ -167,4 +170,23 @@
167170
), # failed_tests_build
168171
"hardware_platform": None,
169172
},
173+
# https://github.com/kernelci/dashboard/issues/1460 — same url/branch/hash, different tree_name
174+
"issue1460_older_checkout": {
175+
"origin": "maestro",
176+
"git_url": "https://example.com/kernelci-dashboard-issue-1460.git",
177+
"git_branch": "issue-1460-branch",
178+
"tree_name": "issue1460_older",
179+
"git_commit_hash": "issue1460sharedhash00000000000000000000",
180+
"start_time": _SEED_NOW - timedelta(hours=5),
181+
"hardware_platform": None,
182+
},
183+
"issue1460_newer_checkout": {
184+
"origin": "maestro",
185+
"git_url": "https://example.com/kernelci-dashboard-issue-1460.git",
186+
"git_branch": "issue-1460-branch",
187+
"tree_name": "issue1460_newer",
188+
"git_commit_hash": "issue1460sharedhash00000000000000000000",
189+
"start_time": _SEED_NOW - timedelta(hours=1),
190+
"hardware_platform": None,
191+
},
170192
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from http import HTTPStatus
2+
3+
from kernelCI_app.tests.utils.client.treeClient import TreeClient
4+
from kernelCI_app.utils import string_to_json
5+
6+
client = TreeClient()
7+
8+
# Seeded in tree_data.py (issue1460_*): same git_url/branch/commit hash, different tree_name.
9+
ORIGIN = "maestro"
10+
GIT_URL = "https://example.com/kernelci-dashboard-issue-1460.git"
11+
GIT_BRANCH = "issue-1460-branch"
12+
OLDER_TREE_NAME = "issue1460_older"
13+
NEWER_TREE_NAME = "issue1460_newer"
14+
15+
16+
def test_tree_report_returns_requested_tree_name():
17+
response = client.get_tree_report(
18+
query={
19+
"origin": ORIGIN,
20+
"git_url": GIT_URL,
21+
"git_branch": GIT_BRANCH,
22+
"tree_name": OLDER_TREE_NAME,
23+
}
24+
)
25+
assert response.status_code == HTTPStatus.OK
26+
content = string_to_json(response.content.decode())
27+
28+
assert f"/tree/{OLDER_TREE_NAME}/" in content["dashboard_url"]
29+
assert NEWER_TREE_NAME not in content["dashboard_url"]

backend/kernelCI_app/tests/utils/client/treeClient.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ def get_tree_listing(self, *, query: dict) -> requests.Response:
1818
url = self.get_endpoint(path=path, query=query)
1919
return requests.get(url)
2020

21+
def get_tree_report(self, *, query: dict) -> requests.Response:
22+
path = reverse("treeReportView")
23+
url = self.get_endpoint(path=path, query=query)
24+
return requests.get(url)
25+
2126
def get_tree_latest(
2227
self, *, tree_name: str, git_branch: str, query: dict
2328
) -> requests.Response:

backend/kernelCI_app/typeModels/treeReport.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from datetime import datetime
2-
from typing import TypedDict
2+
from typing import Optional, TypedDict
33

44
from pydantic import BaseModel, Field
55
from typing_extensions import Annotated
@@ -35,6 +35,10 @@ class TreeReportQueryParameters(BaseModel):
3535
str,
3636
Field(description=DocStrings.TREE_QUERY_GIT_URL_DESCRIPTION),
3737
]
38+
tree_name: Annotated[
39+
Optional[str],
40+
Field(default=None, description=DocStrings.TREE_NAME_PATH_DESCRIPTION),
41+
] = None
3842
path: Annotated[
3943
list[str],
4044
Field(

backend/kernelCI_app/views/treeReportView.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def get(self, request: HttpRequest):
3535
origin=request.GET.get("origin"),
3636
git_branch=request.GET.get("git_branch"),
3737
git_url=request.GET.get("git_url"),
38+
tree_name=request.GET.get("tree_name"),
3839
path=request.GET.getlist("path"),
3940
group_size=request.GET.get("group_size"),
4041
min_age_in_hours=request.GET.get("min_age_in_hours"),
@@ -61,6 +62,7 @@ def get(self, request: HttpRequest):
6162
tuple_params=[tree_key],
6263
interval_min=min_query_interval,
6364
interval_max=max_query_interval,
65+
tree_name=params.tree_name or None,
6466
)
6567
if not records:
6668
return create_api_error_response(

backend/requests/tree-report-get.sh

100644100755
Lines changed: 26 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,28 @@
1-
http "http://localhost:8000/api/tree-report/" git_branch==for-kernelci git_url==https://git.kernel.org/pub/scm/linux/kernel/git/arm64/linux.git origin==maestro
1+
#!/usr/bin/env bash
2+
# Usage:
3+
# ./tree-report-get.sh
4+
# GIT_BRANCH=for-next TREE_NAME=mediatek GIT_URL=https://git.kernel.org/pub/scm/linux/kernel/git/mediatek/linux.git ./tree-report-get.sh
5+
# MIN_AGE_IN_HOURS=24 MAX_AGE_IN_HOURS=48 ./tree-report-get.sh
6+
# Env: GIT_BRANCH, GIT_URL, ORIGIN, TREE_NAME (set TREE_NAME= empty to omit),
7+
# MIN_AGE_IN_HOURS (default 0), MAX_AGE_IN_HOURS (default 24)
28

3-
# HTTP/1.1 200 OK
4-
# Allow: GET, HEAD, OPTIONS
5-
# Content-Length: 3677
6-
# Content-Type: application/json
7-
# Cross-Origin-Opener-Policy: same-origin
8-
# Date: Wed, 02 Jul 2025 17:57:35 GMT
9-
# Referrer-Policy: same-origin
10-
# Server: WSGIServer/0.2 CPython/3.12.7
11-
# Vary: Accept, Cookie, origin
12-
# X-Content-Type-Options: nosniff
13-
# X-Frame-Options: DENY
9+
GIT_BRANCH="${GIT_BRANCH:-master}"
10+
GIT_URL="${GIT_URL:-https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git}"
11+
ORIGIN="${ORIGIN:-maestro}"
12+
TREE_NAME="${TREE_NAME-mainline}"
13+
MIN_AGE_IN_HOURS="${MIN_AGE_IN_HOURS:-0}"
14+
MAX_AGE_IN_HOURS="${MAX_AGE_IN_HOURS:-24}"
1415

15-
# {
16-
# "boot_status_summary": {
17-
# "done_count": 0,
18-
# "error_count": 0,
19-
# "fail_count": 0,
20-
# "miss_count": 13,
21-
# "null_count": 0,
22-
# "pass_count": 9,
23-
# "skip_count": 0
24-
# },
25-
# "build_status_summary": {
26-
# "DONE": 0,
27-
# "ERROR": 0,
28-
# "FAIL": 0,
29-
# "MISS": 0,
30-
# "NULL": 0,
31-
# "PASS": 9,
32-
# "SKIP": 0
33-
# },
34-
# "checkout_start_time": "2025-07-01T15:08:26.610000Z",
35-
# "commit_hash": "3c795c3404e82c4db5c69317847dc5bbafbb368b",
36-
# "dashboard_url": "https://d.kernelci.org/tree/arm64/for-kernelci/3c795c3404e82c4db5c69317847dc5bbafbb368b?o=maestro",
37-
# "fixed_regressions": {},
38-
# "git_branch": "for-kernelci",
39-
# "git_url": "https://git.kernel.org/pub/scm/linux/kernel/git/arm64/linux.git",
40-
# "origin": "maestro",
41-
# "possible_regressions": {},
42-
# "test_status_summary": {
43-
# "done_count": 0,
44-
# "error_count": 0,
45-
# "fail_count": 0,
46-
# "miss_count": 0,
47-
# "null_count": 62,
48-
# "pass_count": 592,
49-
# "skip_count": 0
50-
# },
51-
# "unstable_tests": {
52-
# "bcm2711-rpi-4-b": {
53-
# "defconfig+lab-setup+kselftest": {
54-
# "boot": [
55-
# {
56-
# "id": "maestro:686413d35c2cf25042f65ec8",
57-
# "start_time": "2025-07-01T16:58:59.086000Z",
58-
# "status": "MISS"
59-
# },
60-
# {
61-
# "id": "maestro:686413cf5c2cf25042f65ead",
62-
# "start_time": "2025-07-01T16:58:55.436000Z",
63-
# "status": "PASS"
64-
# },
65-
# {
66-
# "id": "maestro:6862e6f15c2cf25042f38ba1",
67-
# "start_time": "2025-06-30T19:35:13.117000Z",
68-
# "status": "PASS"
69-
# },
70-
# {
71-
# "id": "maestro:6862e6ed5c2cf25042f38b85",
72-
# "start_time": "2025-06-30T19:35:09.018000Z",
73-
# "status": "PASS"
74-
# }
75-
# ]
76-
# }
77-
# },
78-
# ...
79-
# }
80-
# }
16+
args=(
17+
"http://localhost:8000/api/tree-report/"
18+
"git_branch==${GIT_BRANCH}"
19+
"git_url==${GIT_URL}"
20+
"origin==${ORIGIN}"
21+
"min_age_in_hours==${MIN_AGE_IN_HOURS}"
22+
"max_age_in_hours==${MAX_AGE_IN_HOURS}"
23+
)
24+
if [[ -n "${TREE_NAME}" ]]; then
25+
args+=("tree_name==${TREE_NAME}")
26+
fi
27+
28+
http "${args[@]}"

backend/schema.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,15 @@ paths:
11061106
title: Path
11071107
type: array
11081108
description: A list of test paths to query for. SQL Wildcard can be used.
1109+
- in: query
1110+
name: tree_name
1111+
schema:
1112+
anyOf:
1113+
- type: string
1114+
- type: 'null'
1115+
default: null
1116+
title: Tree Name
1117+
description: Name of the tree
11091118
tags:
11101119
- tree-report
11111120
security:

0 commit comments

Comments
 (0)