Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions backend/kernelCI_app/queries/notifications.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import sys
from concurrent.futures import ThreadPoolExecutor
from datetime import date, datetime, time, timedelta, timezone
from typing import Any
from typing import Any, Optional

from django.db import connection, connections
from pydantic import ValidationError
Expand Down Expand Up @@ -425,13 +425,15 @@ def get_checkout_summary_data(
tuple_params: list[tuple[str, str, str]],
interval_min="5 hours",
interval_max="29 hours",
tree_name: Optional[str] = None,
) -> list[dict]:
"""Queries for the checkout and status count data similarly to tree_listing but
using a list of parameters for the filtering

Parameters:
tuple_params: a list of tuples (str, str, str)
representing (git_repository_branch, git_repository_url, origin)
tree_name: optional filter so trees sharing branch/git_url stay distinct

Returns:
out: a list of dicts with the records found.
Expand All @@ -440,6 +442,8 @@ def get_checkout_summary_data(
if not tuple_params:
return []

tree_name_filter = "AND C.TREE_NAME = %s" if tree_name is not None else ""

with_clause = f"""
WITH
ORDERED_CHECKOUTS_BY_TREE AS (
Expand All @@ -448,6 +452,7 @@ def get_checkout_summary_data(
C.GIT_REPOSITORY_URL,
C.GIT_COMMIT_HASH,
C.ORIGIN,
C.TREE_NAME,
ROW_NUMBER() OVER (
PARTITION BY
C.GIT_REPOSITORY_BRANCH,
Expand All @@ -469,13 +474,15 @@ def get_checkout_summary_data(
WHERE
C.START_TIME >= NOW() - INTERVAL %s
AND C.START_TIME <= NOW() - INTERVAL %s
{tree_name_filter}
),
FIRST_TREE_CHECKOUT AS (
SELECT
GIT_REPOSITORY_BRANCH,
GIT_REPOSITORY_URL,
GIT_COMMIT_HASH,
ORIGIN
ORIGIN,
TREE_NAME
FROM
ORDERED_CHECKOUTS_BY_TREE
WHERE
Expand All @@ -489,6 +496,7 @@ def get_checkout_summary_data(
AND checkouts.git_repository_url IS NOT DISTINCT FROM FTC.GIT_REPOSITORY_URL
AND checkouts.git_commit_hash = FTC.GIT_COMMIT_HASH
AND checkouts.origin = FTC.ORIGIN
AND checkouts.tree_name IS NOT DISTINCT FROM FTC.TREE_NAME
)
"""

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

params = flattened_list + [interval_max, interval_min]
if tree_name is not None:
params.append(tree_name)

with connection.cursor() as cursor:
cursor.execute(
query,
flattened_list
+ [
interval_max,
interval_min,
],
)
cursor.execute(query, params)
return dict_fetchall(cursor=cursor)


Expand Down
4 changes: 3 additions & 1 deletion backend/kernelCI_app/tests/factories/checkout_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ class Meta:

git_commit_hash = factory.LazyAttribute(
lambda obj: (
obj.id if Checkout.is_known_checkout(obj.id) else f"commit_{obj.id[:8]}"
Checkout.get_git_commit_hash(obj.id)
if Checkout.is_known_checkout(obj.id)
else f"commit_{obj.id[:8]}"
)
)

Expand Down
8 changes: 8 additions & 0 deletions backend/kernelCI_app/tests/factories/mocks/checkout.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ def get_git_branch(cls, checkout_id: str):
checkout_data = TREE_DATA.get(checkout_id)
return checkout_data.get("git_branch") if checkout_data else None

@classmethod
def get_git_commit_hash(cls, checkout_id: str):
"""Get git commit hash for a checkout (defaults to checkout_id)."""
checkout_data = TREE_DATA.get(checkout_id)
if not checkout_data:
return None
return checkout_data.get("git_commit_hash", checkout_id)

@classmethod
def get_tree_name(cls, checkout_id: str):
"""Get tree name for a checkout."""
Expand Down
24 changes: 23 additions & 1 deletion backend/kernelCI_app/tests/factories/mocks/fixtures/tree_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
Tree data fixtures for test factories.
"""

from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone

# Evaluated at seed import so tree-report age windows (hours from NOW) still match.
_SEED_NOW = datetime.now(timezone.utc)

TREE_DATA = {
"a1c24ab822793eb513351686f631bd18952b7870": { # ARM64_TREE
Expand Down Expand Up @@ -167,4 +170,23 @@
), # failed_tests_build
"hardware_platform": None,
},
# https://github.com/kernelci/dashboard/issues/1460 — same url/branch/hash, different tree_name
"issue1460_older_checkout": {
"origin": "maestro",
"git_url": "https://example.com/kernelci-dashboard-issue-1460.git",
"git_branch": "issue-1460-branch",
"tree_name": "issue1460_older",
"git_commit_hash": "issue1460sharedhash00000000000000000000",
"start_time": _SEED_NOW - timedelta(hours=5),
"hardware_platform": None,
},
"issue1460_newer_checkout": {
"origin": "maestro",
"git_url": "https://example.com/kernelci-dashboard-issue-1460.git",
"git_branch": "issue-1460-branch",
"tree_name": "issue1460_newer",
"git_commit_hash": "issue1460sharedhash00000000000000000000",
"start_time": _SEED_NOW - timedelta(hours=1),
"hardware_platform": None,
},
}
29 changes: 29 additions & 0 deletions backend/kernelCI_app/tests/integrationTests/treeReport_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from http import HTTPStatus

from kernelCI_app.tests.utils.client.treeClient import TreeClient
from kernelCI_app.utils import string_to_json

client = TreeClient()

# Seeded in tree_data.py (issue1460_*): same git_url/branch/commit hash, different tree_name.
ORIGIN = "maestro"
GIT_URL = "https://example.com/kernelci-dashboard-issue-1460.git"
GIT_BRANCH = "issue-1460-branch"
OLDER_TREE_NAME = "issue1460_older"
NEWER_TREE_NAME = "issue1460_newer"


def test_tree_report_returns_requested_tree_name():
response = client.get_tree_report(
query={
"origin": ORIGIN,
"git_url": GIT_URL,
"git_branch": GIT_BRANCH,
"tree_name": OLDER_TREE_NAME,
}
)
assert response.status_code == HTTPStatus.OK
content = string_to_json(response.content.decode())

assert f"/tree/{OLDER_TREE_NAME}/" in content["dashboard_url"]
assert NEWER_TREE_NAME not in content["dashboard_url"]
5 changes: 5 additions & 0 deletions backend/kernelCI_app/tests/utils/client/treeClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ def get_tree_listing(self, *, query: dict) -> requests.Response:
url = self.get_endpoint(path=path, query=query)
return requests.get(url)

def get_tree_report(self, *, query: dict) -> requests.Response:
path = reverse("treeReportView")
url = self.get_endpoint(path=path, query=query)
return requests.get(url)

def get_tree_latest(
self, *, tree_name: str, git_branch: str, query: dict
) -> requests.Response:
Expand Down
6 changes: 5 additions & 1 deletion backend/kernelCI_app/typeModels/treeReport.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import datetime
from typing import TypedDict
from typing import Optional, TypedDict

from pydantic import BaseModel, Field
from typing_extensions import Annotated
Expand Down Expand Up @@ -35,6 +35,10 @@ class TreeReportQueryParameters(BaseModel):
str,
Field(description=DocStrings.TREE_QUERY_GIT_URL_DESCRIPTION),
]
tree_name: Annotated[
Optional[str],
Field(default=None, description=DocStrings.TREE_NAME_PATH_DESCRIPTION),
] = None
path: Annotated[
list[str],
Field(
Expand Down
2 changes: 2 additions & 0 deletions backend/kernelCI_app/views/treeReportView.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def get(self, request: HttpRequest):
origin=request.GET.get("origin"),
git_branch=request.GET.get("git_branch"),
git_url=request.GET.get("git_url"),
tree_name=request.GET.get("tree_name"),
path=request.GET.getlist("path"),
group_size=request.GET.get("group_size"),
min_age_in_hours=request.GET.get("min_age_in_hours"),
Expand All @@ -61,6 +62,7 @@ def get(self, request: HttpRequest):
tuple_params=[tree_key],
interval_min=min_query_interval,
interval_max=max_query_interval,
tree_name=params.tree_name or None,
)
if not records:
return create_api_error_response(
Expand Down
104 changes: 26 additions & 78 deletions backend/requests/tree-report-get.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,80 +1,28 @@
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
#!/usr/bin/env bash
# Usage:
# ./tree-report-get.sh
# 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
# MIN_AGE_IN_HOURS=24 MAX_AGE_IN_HOURS=48 ./tree-report-get.sh
# Env: GIT_BRANCH, GIT_URL, ORIGIN, TREE_NAME (set TREE_NAME= empty to omit),
# MIN_AGE_IN_HOURS (default 0), MAX_AGE_IN_HOURS (default 24)

# HTTP/1.1 200 OK
# Allow: GET, HEAD, OPTIONS
# Content-Length: 3677
# Content-Type: application/json
# Cross-Origin-Opener-Policy: same-origin
# Date: Wed, 02 Jul 2025 17:57:35 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
GIT_BRANCH="${GIT_BRANCH:-master}"
GIT_URL="${GIT_URL:-https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git}"
ORIGIN="${ORIGIN:-maestro}"
TREE_NAME="${TREE_NAME-mainline}"
MIN_AGE_IN_HOURS="${MIN_AGE_IN_HOURS:-0}"
MAX_AGE_IN_HOURS="${MAX_AGE_IN_HOURS:-24}"

# {
# "boot_status_summary": {
# "done_count": 0,
# "error_count": 0,
# "fail_count": 0,
# "miss_count": 13,
# "null_count": 0,
# "pass_count": 9,
# "skip_count": 0
# },
# "build_status_summary": {
# "DONE": 0,
# "ERROR": 0,
# "FAIL": 0,
# "MISS": 0,
# "NULL": 0,
# "PASS": 9,
# "SKIP": 0
# },
# "checkout_start_time": "2025-07-01T15:08:26.610000Z",
# "commit_hash": "3c795c3404e82c4db5c69317847dc5bbafbb368b",
# "dashboard_url": "https://d.kernelci.org/tree/arm64/for-kernelci/3c795c3404e82c4db5c69317847dc5bbafbb368b?o=maestro",
# "fixed_regressions": {},
# "git_branch": "for-kernelci",
# "git_url": "https://git.kernel.org/pub/scm/linux/kernel/git/arm64/linux.git",
# "origin": "maestro",
# "possible_regressions": {},
# "test_status_summary": {
# "done_count": 0,
# "error_count": 0,
# "fail_count": 0,
# "miss_count": 0,
# "null_count": 62,
# "pass_count": 592,
# "skip_count": 0
# },
# "unstable_tests": {
# "bcm2711-rpi-4-b": {
# "defconfig+lab-setup+kselftest": {
# "boot": [
# {
# "id": "maestro:686413d35c2cf25042f65ec8",
# "start_time": "2025-07-01T16:58:59.086000Z",
# "status": "MISS"
# },
# {
# "id": "maestro:686413cf5c2cf25042f65ead",
# "start_time": "2025-07-01T16:58:55.436000Z",
# "status": "PASS"
# },
# {
# "id": "maestro:6862e6f15c2cf25042f38ba1",
# "start_time": "2025-06-30T19:35:13.117000Z",
# "status": "PASS"
# },
# {
# "id": "maestro:6862e6ed5c2cf25042f38b85",
# "start_time": "2025-06-30T19:35:09.018000Z",
# "status": "PASS"
# }
# ]
# }
# },
# ...
# }
# }
args=(
"http://localhost:8000/api/tree-report/"
"git_branch==${GIT_BRANCH}"
"git_url==${GIT_URL}"
"origin==${ORIGIN}"
"min_age_in_hours==${MIN_AGE_IN_HOURS}"
"max_age_in_hours==${MAX_AGE_IN_HOURS}"
)
if [[ -n "${TREE_NAME}" ]]; then
args+=("tree_name==${TREE_NAME}")
fi

http "${args[@]}"
9 changes: 9 additions & 0 deletions backend/schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,15 @@ paths:
title: Path
type: array
description: A list of test paths to query for. SQL Wildcard can be used.
- in: query
name: tree_name
schema:
anyOf:
- type: string
- type: 'null'
default: null
title: Tree Name
description: Name of the tree
tags:
- tree-report
security:
Expand Down
Loading