Skip to content

Commit 0f1dc3f

Browse files
committed
fix(hardware): list boards in time window and load test origins from hardware_status
* Non longer limiting hardware listing to latest checkout. * Hardcoded TEST_ORIGINS also omitted origins from the Hardware dropdown. * Dynamic origins list from hardware_status. Closes #1983 Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi>
1 parent 50b656f commit 0f1dc3f

4 files changed

Lines changed: 98 additions & 33 deletions

File tree

backend/kernelCI_app/queries/hardware.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,41 @@
55

66
from kernelCI_app.cache import get_query_cache, set_query_cache
77
from kernelCI_app.helpers.database import dict_fetchall
8+
from kernelCI_app.queries.checkout import ORIGINS_CACHE_TIMEOUT
89
from kernelCI_app.queries.duration import (
910
get_boot_test_duration_clause,
1011
get_build_duration_clause,
1112
)
1213
from kernelCI_app.typeModels.hardwareDetails import CommitHead, Tree
1314

1415

16+
def get_test_origins(interval_in_days: int) -> list[str]:
17+
"""Distinct test origins from hardware_status in the given interval."""
18+
cache_key = f"test_origins_hardware_status_{interval_in_days}"
19+
cached = get_query_cache(key=cache_key)
20+
if cached is not None:
21+
return cached
22+
23+
query = """
24+
SELECT DISTINCT test_origin AS origin
25+
FROM hardware_status
26+
WHERE start_time >= CURRENT_DATE - INTERVAL '%(interval_in_days)s days'
27+
ORDER BY origin
28+
"""
29+
30+
with connection.cursor() as cursor:
31+
cursor.execute(query, {"interval_in_days": interval_in_days})
32+
origins = [row[0] for row in cursor.fetchall() if row[0]]
33+
34+
if origins:
35+
set_query_cache(
36+
key=cache_key,
37+
rows=origins,
38+
timeout=ORIGINS_CACHE_TIMEOUT,
39+
)
40+
return origins
41+
42+
1543
def _get_hardware_tree_heads_clause(*, id_only: bool) -> str:
1644
"""Returns the tree_heads for the hardware queries,
1745
where the checkout is not filtered by origin.
@@ -370,16 +398,10 @@ def get_hardware_listing_data_from_status_table(
370398
SUM(test_inc) AS test_null
371399
FROM
372400
hardware_status
373-
INNER JOIN
374-
latest_checkout
375-
ON
376-
hardware_status.checkout_id = latest_checkout.checkout_id
377-
AND
378-
latest_checkout.start_time >= %(start_date)s
379-
AND
380-
latest_checkout.start_time <= %(end_date)s
381401
WHERE
382402
hardware_status.test_origin = %(origin)s
403+
AND hardware_status.start_time >= %(start_date)s
404+
AND hardware_status.start_time <= %(end_date)s
383405
GROUP BY
384406
platform,
385407
compatibles

backend/kernelCI_app/tests/unitTests/queries/hardware_queries_test.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from datetime import datetime
22
from unittest.mock import patch
33

4+
from kernelCI_app.queries.checkout import ORIGINS_CACHE_TIMEOUT
45
from kernelCI_app.queries.hardware import (
56
_generate_query_params,
67
get_hardware_commit_history,
78
get_hardware_details_data,
89
get_hardware_trees_data,
10+
get_test_origins,
911
query_records,
1012
)
1113
from kernelCI_app.tests.unitTests.queries.conftest import (
@@ -18,6 +20,56 @@
1820
END_DATE = datetime(2025, 11, 12)
1921

2022

23+
class TestGetTestOrigins:
24+
@patch("kernelCI_app.queries.hardware.get_query_cache")
25+
def test_get_test_origins_from_cache(self, mock_get_cache):
26+
cached_data = ["linaro_pull_labs", "maestro"]
27+
mock_get_cache.return_value = cached_data
28+
29+
result = get_test_origins(7)
30+
31+
assert result == cached_data
32+
33+
@patch("kernelCI_app.queries.hardware.get_query_cache")
34+
@patch("kernelCI_app.queries.hardware.set_query_cache")
35+
@patch("kernelCI_app.queries.hardware.connection")
36+
def test_get_test_origins_from_database(
37+
self, mock_connection, mock_set_cache, mock_get_cache
38+
):
39+
mock_get_cache.return_value = None
40+
mock_cursor = setup_mock_cursor(mock_connection)
41+
mock_cursor.fetchall.return_value = [
42+
("linaro_pull_labs",),
43+
("maestro",),
44+
(None,),
45+
]
46+
47+
result = get_test_origins(7)
48+
49+
assert result == ["linaro_pull_labs", "maestro"]
50+
mock_cursor.execute.assert_called_once()
51+
mock_set_cache.assert_called_once_with(
52+
key="test_origins_hardware_status_7",
53+
rows=["linaro_pull_labs", "maestro"],
54+
timeout=ORIGINS_CACHE_TIMEOUT,
55+
)
56+
57+
@patch("kernelCI_app.queries.hardware.get_query_cache")
58+
@patch("kernelCI_app.queries.hardware.set_query_cache")
59+
@patch("kernelCI_app.queries.hardware.connection")
60+
def test_get_test_origins_empty_result_does_not_cache(
61+
self, mock_connection, mock_set_cache, mock_get_cache
62+
):
63+
mock_get_cache.return_value = None
64+
mock_cursor = setup_mock_cursor(mock_connection)
65+
mock_cursor.fetchall.return_value = []
66+
67+
result = get_test_origins(7)
68+
69+
assert result == []
70+
mock_set_cache.assert_not_called()
71+
72+
2173
class TestGetHardwareDetailsData:
2274
@patch("kernelCI_app.queries.hardware.get_query_cache")
2375
def test_get_hardware_details_data_from_cache(self, mock_get_cache):

backend/kernelCI_app/tests/unitTests/views/originsView_test.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from kernelCI_app.constants.localization import ClientStrings
88
from kernelCI_app.views.originsView import (
9-
TEST_ORIGINS,
109
OriginsView,
1110
separate_origin_records,
1211
)
@@ -51,12 +50,14 @@ def setUp(self):
5150
self.view = OriginsView()
5251
self.url = "/origins"
5352

53+
@patch("kernelCI_app.views.originsView.get_test_origins")
5454
@patch("kernelCI_app.views.originsView.get_origins")
55-
def test_get_origins_success(self, mock_get_origins):
55+
def test_get_origins_success(self, mock_get_origins, mock_get_test_origins):
5656
mock_get_origins.return_value = [
5757
{"origin": "origin1", "table": "checkouts"},
5858
{"origin": "origin2", "table": "checkouts"},
5959
]
60+
mock_get_test_origins.return_value = ["maestro", "linaro_pull_labs"]
6061
request = self.factory.get(self.url)
6162
response = self.view.get(request)
6263

@@ -65,18 +66,21 @@ def test_get_origins_success(self, mock_get_origins):
6566
response.data,
6667
{
6768
"checkout_origins": ["origin1", "origin2"],
68-
"test_origins": TEST_ORIGINS,
69+
"test_origins": ["maestro", "linaro_pull_labs"],
6970
},
7071
)
72+
mock_get_test_origins.assert_called_once()
7173

74+
@patch("kernelCI_app.views.originsView.get_test_origins")
7275
@patch("kernelCI_app.views.originsView.get_origins")
73-
def test_get_origins_not_found(self, mock_get_origins):
76+
def test_get_origins_not_found(self, mock_get_origins, mock_get_test_origins):
7477
mock_get_origins.return_value = []
7578
request = self.factory.get(self.url)
7679
response = self.view.get(request)
7780

7881
self.assertEqual(response.status_code, 400)
7982
self.assertEqual(response.data, {"error": ClientStrings.NO_ORIGIN_FOUND})
83+
mock_get_test_origins.assert_not_called()
8084

8185
@patch("kernelCI_app.views.originsView.OriginsQueryParameters.model_validate")
8286
def test_get_origins_query_validation_error(self, mock_validate):
@@ -98,14 +102,16 @@ def test_get_origins_query_validation_error(self, mock_validate):
98102
self.assertEqual(response.status_code, 500)
99103
self.assertIsNotNone(response.data)
100104

105+
@patch("kernelCI_app.views.originsView.get_test_origins")
101106
@patch("kernelCI_app.views.originsView.get_origins")
102107
@patch("kernelCI_app.views.originsView.OriginsResponse")
103108
def test_get_origins_response_validation_error(
104-
self, mock_response_class, mock_get_origins
109+
self, mock_response_class, mock_get_origins, mock_get_test_origins
105110
):
106111
mock_get_origins.return_value = [
107112
{"origin": "origin1", "table": "checkouts"},
108113
]
114+
mock_get_test_origins.return_value = ["maestro"]
109115

110116
mock_error = ValidationError.from_exception_data(
111117
"test_error",

backend/kernelCI_app/views/originsView.py

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,9 @@
99
from kernelCI_app.helpers.errorHandling import create_api_error_response
1010
from kernelCI_app.helpers.logger import log_message
1111
from kernelCI_app.queries.checkout import get_origins
12+
from kernelCI_app.queries.hardware import get_test_origins
1213
from kernelCI_app.typeModels.origins import OriginsQueryParameters, OriginsResponse
1314

14-
# For now we are hardcoding test origins since fetching them dynamically in a query is taking
15-
# around 8 minutes to finish. That time will result in a timeout every request, so this is a
16-
# temporary solution. Another option would be to create a cron job that feeds a TestOriginsCache
17-
# sqlite table, from which this endpoint would fetch.
18-
#
19-
# TODO: replace with a dynamic approach
20-
TEST_ORIGINS = [
21-
"0dayci",
22-
"arm",
23-
"broonie",
24-
"linaro",
25-
"maestro",
26-
"microsoft",
27-
"redhat",
28-
"riscv",
29-
"syzbot",
30-
"ti",
31-
]
32-
3315

3416
def separate_origin_records(*, records: list[dict[str, str]]) -> set[str]:
3517
"""Iterates over the records for origins and returns a set for each table defined by those records
@@ -85,11 +67,14 @@ def get(self, request) -> Response:
8567
)
8668

8769
self.checkout_origins = separate_origin_records(records=origin_records)
70+
test_origins = get_test_origins(
71+
interval_in_days=query_parameters.interval_in_days
72+
)
8873

8974
try:
9075
valid_response = OriginsResponse(
9176
checkout_origins=sorted(self.checkout_origins),
92-
test_origins=TEST_ORIGINS,
77+
test_origins=test_origins,
9378
)
9479
except ValidationError as e:
9580
return Response(e.json(), HTTPStatus.INTERNAL_SERVER_ERROR)

0 commit comments

Comments
 (0)