Skip to content

Commit b6cc16e

Browse files
author
Gustavo Flores
committed
feat: add hardware selectors and revision API
1 parent 57874b6 commit b6cc16e

9 files changed

Lines changed: 606 additions & 0 deletions

File tree

backend/kernelCI_app/queries/hardware.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,156 @@ def get_hardware_listing_data(
179179
return cursor.fetchall()
180180

181181

182+
def get_hardware_selectors(origin: str) -> list[dict]:
183+
params = {"origin": origin}
184+
185+
query = """
186+
WITH qualified_revisions AS (
187+
SELECT
188+
c.tree_name,
189+
c.git_repository_url,
190+
c.git_repository_branch,
191+
c.git_commit_hash,
192+
c.git_commit_name,
193+
MAX(c.start_time) AS latest_start_time
194+
FROM
195+
checkouts c
196+
INNER JOIN builds b ON b.checkout_id = c.id
197+
INNER JOIN tests ON tests.build_id = b.id
198+
WHERE
199+
tests.origin = %(origin)s
200+
AND tests.environment_misc ->> 'platform' IS NOT NULL
201+
GROUP BY
202+
c.tree_name,
203+
c.git_repository_url,
204+
c.git_repository_branch,
205+
c.git_commit_hash,
206+
c.git_commit_name
207+
),
208+
ranked_revisions AS (
209+
SELECT
210+
qr.tree_name,
211+
qr.git_repository_url,
212+
qr.git_repository_branch,
213+
qr.git_commit_hash,
214+
qr.git_commit_name,
215+
qr.latest_start_time,
216+
MAX(qr.latest_start_time) OVER (
217+
PARTITION BY
218+
qr.tree_name,
219+
qr.git_repository_url,
220+
qr.git_repository_branch
221+
) AS branch_latest_start_time,
222+
ROW_NUMBER() OVER (
223+
PARTITION BY
224+
qr.tree_name,
225+
qr.git_repository_url,
226+
qr.git_repository_branch
227+
ORDER BY
228+
qr.latest_start_time DESC
229+
) AS revision_rank
230+
FROM
231+
qualified_revisions qr
232+
)
233+
SELECT
234+
tree_name,
235+
git_repository_url,
236+
git_repository_branch,
237+
git_commit_hash,
238+
git_commit_name,
239+
latest_start_time,
240+
branch_latest_start_time
241+
FROM
242+
ranked_revisions
243+
WHERE
244+
revision_rank <= 50
245+
ORDER BY
246+
tree_name ASC,
247+
branch_latest_start_time DESC,
248+
latest_start_time DESC
249+
"""
250+
251+
with connection.cursor() as cursor:
252+
cursor.execute(query, params)
253+
return dict_fetchall(cursor)
254+
255+
256+
def get_hardware_listing_data_by_revision(
257+
*,
258+
origin: str,
259+
tree_name: str,
260+
git_repository_url: str,
261+
git_repository_branch: str,
262+
git_commit_hash: str,
263+
) -> list[tuple]:
264+
count_clauses = _get_hardware_listing_count_clauses()
265+
params = {
266+
"origin": origin,
267+
"tree_name": tree_name,
268+
"git_repository_url": git_repository_url,
269+
"git_repository_branch": git_repository_branch,
270+
"git_commit_hash": git_commit_hash,
271+
}
272+
273+
query = f"""
274+
WITH relevant_tests AS (
275+
SELECT
276+
tests.environment_compatible,
277+
tests.environment_misc ->> 'platform' AS platform,
278+
tests.status,
279+
tests.path,
280+
tests.id,
281+
b.id AS build_id,
282+
b.status AS build_status
283+
FROM
284+
checkouts c
285+
INNER JOIN builds b ON b.checkout_id = c.id
286+
INNER JOIN tests ON tests.build_id = b.id
287+
WHERE
288+
c.tree_name = %(tree_name)s
289+
AND c.git_repository_url = %(git_repository_url)s
290+
AND c.git_repository_branch = %(git_repository_branch)s
291+
AND c.git_commit_hash = %(git_commit_hash)s
292+
AND tests.origin = %(origin)s
293+
AND tests.environment_misc ->> 'platform' IS NOT NULL
294+
),
295+
compatible_values AS (
296+
SELECT
297+
platform,
298+
UNNEST(environment_compatible) AS compatible
299+
FROM
300+
relevant_tests
301+
WHERE
302+
environment_compatible IS NOT NULL
303+
),
304+
compatible_agg AS (
305+
SELECT
306+
platform,
307+
ARRAY_AGG(DISTINCT compatible ORDER BY compatible) AS hardware
308+
FROM
309+
compatible_values
310+
GROUP BY
311+
platform
312+
)
313+
SELECT
314+
relevant_tests.platform,
315+
compatible_agg.hardware,
316+
{count_clauses}
317+
FROM
318+
relevant_tests
319+
LEFT JOIN compatible_agg ON compatible_agg.platform = relevant_tests.platform
320+
GROUP BY
321+
relevant_tests.platform,
322+
compatible_agg.hardware
323+
ORDER BY
324+
relevant_tests.platform ASC
325+
"""
326+
327+
with connection.cursor() as cursor:
328+
cursor.execute(query, params)
329+
return cursor.fetchall()
330+
331+
182332
def get_hardware_listing_data_bulk(
183333
keys: list[tuple[str, str]],
184334
start_date: datetime,

backend/kernelCI_app/tests/unitTests/url_patterns_test.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,11 @@ def test_tree_commit_hash_patterns(self):
133133
url = reverse("treeDetailsSummaryView", kwargs={"commit_hash": commit_hash})
134134
resolved = resolve(url)
135135
assert resolved.kwargs["commit_hash"] == commit_hash
136+
137+
def test_hardware_selectors_route(self):
138+
url = reverse("hardwareSelectors")
139+
resolved = resolve(url)
140+
141+
assert url == "/api/hardware/selectors/"
142+
assert resolved.url_name == "hardwareSelectors"
143+
assert "hardware_id" not in resolved.kwargs
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from http import HTTPStatus
2+
from unittest.mock import patch
3+
4+
from django.test.testcases import SimpleTestCase
5+
from rest_framework.test import APIRequestFactory
6+
7+
from kernelCI_app.views.hardwareByRevisionView import HardwareByRevisionView
8+
9+
10+
class TestHardwareByRevisionView(SimpleTestCase):
11+
def setUp(self):
12+
self.factory = APIRequestFactory()
13+
self.view = HardwareByRevisionView()
14+
self.url = "/hardware-by-revision"
15+
self.required_query_params = {
16+
"origin": "origin1",
17+
"tree_name": "mainline",
18+
"git_repository_url": "https://example.com/linux.git",
19+
"git_repository_branch": "master",
20+
"git_commit_hash": "abc123",
21+
}
22+
23+
@patch(
24+
"kernelCI_app.views.hardwareByRevisionView.get_hardware_listing_data_by_revision"
25+
)
26+
def test_get_hardware_listing_by_revision_success(
27+
self, mock_get_hardware_listing_data_by_revision
28+
):
29+
mock_get_hardware_listing_data_by_revision.return_value = [
30+
("platform1", ["hardware1"], *range(22)),
31+
]
32+
33+
request = self.factory.get(self.url, self.required_query_params)
34+
response = self.view.get(request)
35+
36+
self.assertEqual(response.status_code, HTTPStatus.OK)
37+
self.assertEqual(response.data["hardware"][0]["platform"], "platform1")
38+
39+
def test_get_hardware_listing_by_revision_missing_query_params_returns_bad_request(
40+
self,
41+
):
42+
request = self.factory.get(self.url, {"origin": "origin1"})
43+
response = self.view.get(request)
44+
45+
self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST)
46+
self.assertIn("tree_name", response.data)
47+
self.assertIn("git_repository_url", response.data)
48+
self.assertIn("git_repository_branch", response.data)
49+
self.assertIn("git_commit_hash", response.data)
50+
51+
@patch(
52+
"kernelCI_app.views.hardwareByRevisionView.get_hardware_listing_data_by_revision"
53+
)
54+
def test_get_hardware_listing_by_revision_empty_response(
55+
self, mock_get_hardware_listing_data_by_revision
56+
):
57+
mock_get_hardware_listing_data_by_revision.return_value = []
58+
59+
request = self.factory.get(self.url, self.required_query_params)
60+
response = self.view.get(request)
61+
62+
self.assertEqual(response.status_code, HTTPStatus.OK)
63+
self.assertEqual(response.data, {"hardware": []})
64+
65+
@patch(
66+
"kernelCI_app.views.hardwareByRevisionView.get_hardware_listing_data_by_revision"
67+
)
68+
def test_get_hardware_listing_by_revision_sanitize_validation_error_returns_internal_server_error(
69+
self, mock_get_hardware_listing_data_by_revision
70+
):
71+
mock_get_hardware_listing_data_by_revision.return_value = [
72+
(None, "hardware1", *range(22)),
73+
]
74+
75+
request = self.factory.get(self.url, self.required_query_params)
76+
response = self.view.get(request)
77+
78+
self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR)
79+
self.assertIn("platform", response.data)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from datetime import datetime
2+
from http import HTTPStatus
3+
from unittest.mock import patch
4+
5+
from django.test.testcases import SimpleTestCase
6+
from rest_framework.test import APIRequestFactory
7+
8+
from kernelCI_app.constants.general import DEFAULT_ORIGIN
9+
from kernelCI_app.views.hardwareSelectorsView import HardwareSelectorsView
10+
11+
12+
class TestHardwareSelectorsView(SimpleTestCase):
13+
def setUp(self):
14+
self.factory = APIRequestFactory()
15+
self.view = HardwareSelectorsView()
16+
self.url = "/hardware/selectors"
17+
18+
@patch("kernelCI_app.views.hardwareSelectorsView.get_hardware_selectors")
19+
def test_get_hardware_selectors_success(self, mock_get_hardware_selectors):
20+
mock_get_hardware_selectors.return_value = [
21+
{
22+
"tree_name": "mainline",
23+
"git_repository_url": "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git",
24+
"git_repository_branch": "master",
25+
"git_commit_hash": "abc123",
26+
"git_commit_name": "v6.12-rc1",
27+
"latest_start_time": datetime(2026, 1, 1, 10, 0, 0),
28+
"branch_latest_start_time": datetime(2026, 1, 1, 10, 0, 0),
29+
}
30+
]
31+
32+
request = self.factory.get(self.url, {"origin": "origin1"})
33+
response = self.view.get(request)
34+
35+
self.assertEqual(response.status_code, HTTPStatus.OK)
36+
self.assertEqual(response.data["trees"][0]["tree_name"], "mainline")
37+
self.assertEqual(
38+
response.data["trees"][0]["branches"][0]["revisions"][0]["git_commit_hash"],
39+
"abc123",
40+
)
41+
mock_get_hardware_selectors.assert_called_once_with(origin="origin1")
42+
43+
@patch("kernelCI_app.views.hardwareSelectorsView.get_hardware_selectors")
44+
def test_get_hardware_selectors_defaults_origin(self, mock_get_hardware_selectors):
45+
mock_get_hardware_selectors.return_value = []
46+
47+
request = self.factory.get(self.url)
48+
response = self.view.get(request)
49+
50+
self.assertEqual(response.status_code, HTTPStatus.OK)
51+
self.assertEqual(response.data, {"trees": []})
52+
mock_get_hardware_selectors.assert_called_once_with(origin=DEFAULT_ORIGIN)
53+
54+
@patch("kernelCI_app.views.hardwareSelectorsView.get_hardware_selectors")
55+
def test_get_hardware_selectors_empty_response(self, mock_get_hardware_selectors):
56+
mock_get_hardware_selectors.return_value = []
57+
58+
request = self.factory.get(self.url, {"origin": "origin1"})
59+
response = self.view.get(request)
60+
61+
self.assertEqual(response.status_code, HTTPStatus.OK)
62+
self.assertEqual(response.data, {"trees": []})
63+
64+
@patch("kernelCI_app.views.hardwareSelectorsView.get_hardware_selectors")
65+
def test_get_hardware_selectors_sanitize_validation_error_returns_internal_server_error(
66+
self, mock_get_hardware_selectors
67+
):
68+
mock_get_hardware_selectors.return_value = [
69+
{
70+
"tree_name": "mainline",
71+
"git_repository_url": "https://example.com/linux.git",
72+
"git_repository_branch": "master",
73+
"git_commit_hash": None,
74+
"git_commit_name": "v6.12-rc1",
75+
"latest_start_time": datetime(2026, 1, 1, 10, 0, 0),
76+
"branch_latest_start_time": datetime(2026, 1, 1, 10, 0, 0),
77+
}
78+
]
79+
80+
request = self.factory.get(self.url, {"origin": "origin1"})
81+
response = self.view.get(request)
82+
83+
self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR)
84+
self.assertIn("git_commit_hash", response.data)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from typing import Annotated
2+
3+
from pydantic import BaseModel, BeforeValidator, Field
4+
5+
from kernelCI_app.constants.general import DEFAULT_ORIGIN
6+
from kernelCI_app.constants.localization import DocStrings
7+
8+
9+
class HardwareListingByRevisionQueryParamsDocumentationOnly(BaseModel):
10+
origin: Annotated[
11+
str,
12+
Field(
13+
default=DEFAULT_ORIGIN,
14+
description=DocStrings.HARDWARE_LISTING_ORIGIN_DESCRIPTION,
15+
),
16+
]
17+
tree_name: str = Field(description=DocStrings.TREE_NAME_PATH_DESCRIPTION)
18+
git_repository_url: str = Field(
19+
description=DocStrings.TREE_QUERY_GIT_URL_DESCRIPTION
20+
)
21+
git_repository_branch: str = Field(
22+
description=DocStrings.DEFAULT_GIT_BRANCH_DESCRIPTION
23+
)
24+
git_commit_hash: str = Field(description=DocStrings.COMMIT_HASH_PATH_DESCRIPTION)
25+
26+
27+
class HardwareListingByRevisionQueryParams(BaseModel):
28+
origin: Annotated[
29+
str,
30+
Field(default=DEFAULT_ORIGIN),
31+
BeforeValidator(lambda o: DEFAULT_ORIGIN if o is None else o),
32+
]
33+
tree_name: str
34+
git_repository_url: str
35+
git_repository_branch: str
36+
git_commit_hash: str

0 commit comments

Comments
 (0)