Skip to content

Commit a48846c

Browse files
author
Gustavo Flores
committed
feat: add hardware selectors and revision API
1 parent b57ace3 commit a48846c

9 files changed

Lines changed: 584 additions & 0 deletions

File tree

backend/kernelCI_app/queries/hardware.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,134 @@ 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+
SELECT
209+
qr.tree_name,
210+
qr.git_repository_url,
211+
qr.git_repository_branch,
212+
qr.git_commit_hash,
213+
qr.git_commit_name,
214+
qr.latest_start_time,
215+
MAX(qr.latest_start_time) OVER (
216+
PARTITION BY
217+
qr.tree_name,
218+
qr.git_repository_url,
219+
qr.git_repository_branch
220+
) AS branch_latest_start_time
221+
FROM
222+
qualified_revisions qr
223+
ORDER BY
224+
qr.tree_name ASC,
225+
branch_latest_start_time DESC,
226+
qr.latest_start_time DESC
227+
"""
228+
229+
with connection.cursor() as cursor:
230+
cursor.execute(query, params)
231+
return dict_fetchall(cursor)
232+
233+
234+
def get_hardware_listing_data_by_revision(
235+
*,
236+
origin: str,
237+
tree_name: str,
238+
git_repository_url: str,
239+
git_repository_branch: str,
240+
git_commit_hash: str,
241+
) -> list[tuple]:
242+
count_clauses = _get_hardware_listing_count_clauses()
243+
params = {
244+
"origin": origin,
245+
"tree_name": tree_name,
246+
"git_repository_url": git_repository_url,
247+
"git_repository_branch": git_repository_branch,
248+
"git_commit_hash": git_commit_hash,
249+
}
250+
251+
query = f"""
252+
WITH relevant_tests AS (
253+
SELECT
254+
tests.environment_compatible,
255+
tests.environment_misc ->> 'platform' AS platform,
256+
tests.status,
257+
tests.path,
258+
tests.id,
259+
b.id AS build_id,
260+
b.status AS build_status
261+
FROM
262+
checkouts c
263+
INNER JOIN builds b ON b.checkout_id = c.id
264+
INNER JOIN tests ON tests.build_id = b.id
265+
WHERE
266+
c.tree_name = %(tree_name)s
267+
AND c.git_repository_url = %(git_repository_url)s
268+
AND c.git_repository_branch = %(git_repository_branch)s
269+
AND c.git_commit_hash = %(git_commit_hash)s
270+
AND tests.origin = %(origin)s
271+
AND tests.environment_misc ->> 'platform' IS NOT NULL
272+
),
273+
compatible_values AS (
274+
SELECT
275+
platform,
276+
UNNEST(environment_compatible) AS compatible
277+
FROM
278+
relevant_tests
279+
WHERE
280+
environment_compatible IS NOT NULL
281+
),
282+
compatible_agg AS (
283+
SELECT
284+
platform,
285+
ARRAY_AGG(DISTINCT compatible ORDER BY compatible) AS hardware
286+
FROM
287+
compatible_values
288+
GROUP BY
289+
platform
290+
)
291+
SELECT
292+
relevant_tests.platform,
293+
compatible_agg.hardware,
294+
{count_clauses}
295+
FROM
296+
relevant_tests
297+
LEFT JOIN compatible_agg ON compatible_agg.platform = relevant_tests.platform
298+
GROUP BY
299+
relevant_tests.platform,
300+
compatible_agg.hardware
301+
ORDER BY
302+
relevant_tests.platform ASC
303+
"""
304+
305+
with connection.cursor() as cursor:
306+
cursor.execute(query, params)
307+
return cursor.fetchall()
308+
309+
182310
def get_hardware_listing_data_bulk(
183311
keys: list[tuple[str, str]],
184312
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
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from datetime import datetime
2+
from typing import Annotated
3+
4+
from pydantic import BaseModel, BeforeValidator, Field
5+
6+
from kernelCI_app.constants.general import DEFAULT_ORIGIN
7+
from kernelCI_app.constants.localization import DocStrings
8+
9+
10+
class HardwareSelectorRevision(BaseModel):
11+
git_commit_hash: str
12+
git_commit_name: str | None = None
13+
start_time: datetime
14+
15+
16+
class HardwareSelectorBranch(BaseModel):
17+
git_repository_url: str
18+
git_repository_branch: str
19+
latest_start_time: datetime
20+
revisions: list[HardwareSelectorRevision]
21+
22+
23+
class HardwareSelectorTree(BaseModel):
24+
tree_name: str
25+
latest_start_time: datetime
26+
branches: list[HardwareSelectorBranch]
27+
28+
29+
class HardwareSelectorsResponse(BaseModel):
30+
trees: list[HardwareSelectorTree]
31+
32+
33+
class HardwareSelectorsQueryParamsDocumentationOnly(BaseModel):
34+
origin: Annotated[
35+
str,
36+
Field(
37+
default=DEFAULT_ORIGIN,
38+
description=DocStrings.HARDWARE_LISTING_ORIGIN_DESCRIPTION,
39+
),
40+
]
41+
42+
43+
class HardwareSelectorsQueryParams(BaseModel):
44+
origin: Annotated[
45+
str,
46+
Field(default=DEFAULT_ORIGIN),
47+
BeforeValidator(lambda o: DEFAULT_ORIGIN if o is None else o),
48+
]

0 commit comments

Comments
 (0)