Skip to content

Commit 600bf91

Browse files
authored
Sort jobs by creation in API (#5545)
* Sort jobs by creation in API * Fix tests missing new field * Fix sorting logic around child jobs
1 parent da6bdfa commit 600bf91

File tree

6 files changed

+125
-15
lines changed

6 files changed

+125
-15
lines changed

supervisor/api/jobs.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,16 @@ def _extract_job(self, request: web.Request) -> SupervisorJob:
3131
raise APINotFound("Job does not exist") from None
3232

3333
def _list_jobs(self, start: SupervisorJob | None = None) -> list[dict[str, Any]]:
34-
"""Return current job tree."""
34+
"""Return current job tree.
35+
36+
Jobs are added to cache as they are created so by default they are in oldest to newest.
37+
This is correct ordering for child jobs as it makes logical sense to present those in
38+
the order they occurred within the parent. For the list as a whole, sort from newest
39+
to oldest as its likely any client is most interested in the newer ones.
40+
"""
41+
# Initially sort oldest to newest so all child lists end up in correct order
3542
jobs_by_parent: dict[str | None, list[SupervisorJob]] = {}
36-
for job in self.sys_jobs.jobs:
43+
for job in sorted(self.sys_jobs.jobs):
3744
if job.internal:
3845
continue
3946

@@ -42,11 +49,15 @@ def _list_jobs(self, start: SupervisorJob | None = None) -> list[dict[str, Any]]
4249
else:
4350
jobs_by_parent[job.parent_id].append(job)
4451

52+
# After parent-child organization, sort the root jobs only from newest to oldest
4553
job_list: list[dict[str, Any]] = []
4654
queue: list[tuple[list[dict[str, Any]], SupervisorJob]] = (
4755
[(job_list, start)]
4856
if start
49-
else [(job_list, job) for job in jobs_by_parent.get(None, [])]
57+
else [
58+
(job_list, job)
59+
for job in sorted(jobs_by_parent.get(None, []), reverse=True)
60+
]
5061
)
5162

5263
while queue:

supervisor/jobs/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from ..exceptions import HassioError, JobNotFound, JobStartException
2020
from ..homeassistant.const import WSEvent
2121
from ..utils.common import FileConfiguration
22+
from ..utils.dt import utcnow
2223
from ..utils.sentry import capture_exception
2324
from .const import ATTR_IGNORE_CONDITIONS, FILE_CONFIG_JOBS, JobCondition
2425
from .validate import SCHEMA_JOBS_CONFIG
@@ -79,10 +80,12 @@ def as_dict(self) -> dict[str, str]:
7980
return {"type": self.type_.__name__, "message": self.message}
8081

8182

82-
@define
83+
@define(order=True)
8384
class SupervisorJob:
8485
"""Representation of a job running in supervisor."""
8586

87+
created: datetime = field(init=False, factory=utcnow, on_setattr=frozen)
88+
uuid: UUID = field(init=False, factory=lambda: uuid4().hex, on_setattr=frozen)
8689
name: str | None = field(default=None, validator=[_invalid_if_started])
8790
reference: str | None = field(default=None, on_setattr=_on_change)
8891
progress: float = field(
@@ -94,7 +97,6 @@ class SupervisorJob:
9497
stage: str | None = field(
9598
default=None, validator=[_invalid_if_done], on_setattr=_on_change
9699
)
97-
uuid: UUID = field(init=False, factory=lambda: uuid4().hex, on_setattr=frozen)
98100
parent_id: UUID | None = field(
99101
factory=lambda: _CURRENT_JOB.get(None), on_setattr=frozen
100102
)
@@ -119,6 +121,7 @@ def as_dict(self) -> dict[str, Any]:
119121
"done": self.done,
120122
"parent_id": self.parent_id,
121123
"errors": [err.as_dict() for err in self.errors],
124+
"created": self.created.isoformat(),
122125
}
123126

124127
def capture_error(self, err: HassioError | None = None) -> None:

tests/api/test_jobs.py

Lines changed: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,18 @@ async def test_jobs_tree_internal(self):
102102
result = await resp.json()
103103
assert result["data"]["jobs"] == [
104104
{
105+
"created": ANY,
106+
"name": "test_jobs_tree_alt",
107+
"reference": None,
108+
"uuid": ANY,
109+
"progress": 0,
110+
"stage": "init",
111+
"done": False,
112+
"child_jobs": [],
113+
"errors": [],
114+
},
115+
{
116+
"created": ANY,
105117
"name": "test_jobs_tree_outer",
106118
"reference": None,
107119
"uuid": ANY,
@@ -111,6 +123,7 @@ async def test_jobs_tree_internal(self):
111123
"errors": [],
112124
"child_jobs": [
113125
{
126+
"created": ANY,
114127
"name": "test_jobs_tree_inner",
115128
"reference": None,
116129
"uuid": ANY,
@@ -122,16 +135,6 @@ async def test_jobs_tree_internal(self):
122135
},
123136
],
124137
},
125-
{
126-
"name": "test_jobs_tree_alt",
127-
"reference": None,
128-
"uuid": ANY,
129-
"progress": 0,
130-
"stage": "init",
131-
"done": False,
132-
"child_jobs": [],
133-
"errors": [],
134-
},
135138
]
136139

137140
test.event.set()
@@ -141,6 +144,7 @@ async def test_jobs_tree_internal(self):
141144
result = await resp.json()
142145
assert result["data"]["jobs"] == [
143146
{
147+
"created": ANY,
144148
"name": "test_jobs_tree_alt",
145149
"reference": None,
146150
"uuid": ANY,
@@ -182,6 +186,7 @@ async def test_job_manual_cleanup(self) -> None:
182186
assert resp.status == 200
183187
result = await resp.json()
184188
assert result["data"] == {
189+
"created": ANY,
185190
"name": "test_job_manual_cleanup",
186191
"reference": None,
187192
"uuid": test.job_id,
@@ -229,3 +234,86 @@ async def test_job_not_found(api_client: TestClient, method: str, url: str):
229234
assert resp.status == 404
230235
body = await resp.json()
231236
assert body["message"] == "Job does not exist"
237+
238+
239+
async def test_jobs_sorted(api_client: TestClient, coresys: CoreSys):
240+
"""Test jobs are sorted by datetime in results."""
241+
242+
class TestClass:
243+
"""Test class."""
244+
245+
def __init__(self, coresys: CoreSys):
246+
"""Initialize the test class."""
247+
self.coresys = coresys
248+
249+
@Job(name="test_jobs_sorted_1", cleanup=False)
250+
async def test_jobs_sorted_1(self):
251+
"""Sorted test method 1."""
252+
await self.test_jobs_sorted_inner_1()
253+
await self.test_jobs_sorted_inner_2()
254+
255+
@Job(name="test_jobs_sorted_inner_1", cleanup=False)
256+
async def test_jobs_sorted_inner_1(self):
257+
"""Sorted test inner method 1."""
258+
259+
@Job(name="test_jobs_sorted_inner_2", cleanup=False)
260+
async def test_jobs_sorted_inner_2(self):
261+
"""Sorted test inner method 2."""
262+
263+
@Job(name="test_jobs_sorted_2", cleanup=False)
264+
async def test_jobs_sorted_2(self):
265+
"""Sorted test method 2."""
266+
267+
test = TestClass(coresys)
268+
await test.test_jobs_sorted_1()
269+
await test.test_jobs_sorted_2()
270+
271+
resp = await api_client.get("/jobs/info")
272+
result = await resp.json()
273+
assert result["data"]["jobs"] == [
274+
{
275+
"created": ANY,
276+
"name": "test_jobs_sorted_2",
277+
"reference": None,
278+
"uuid": ANY,
279+
"progress": 0,
280+
"stage": None,
281+
"done": True,
282+
"errors": [],
283+
"child_jobs": [],
284+
},
285+
{
286+
"created": ANY,
287+
"name": "test_jobs_sorted_1",
288+
"reference": None,
289+
"uuid": ANY,
290+
"progress": 0,
291+
"stage": None,
292+
"done": True,
293+
"errors": [],
294+
"child_jobs": [
295+
{
296+
"created": ANY,
297+
"name": "test_jobs_sorted_inner_1",
298+
"reference": None,
299+
"uuid": ANY,
300+
"progress": 0,
301+
"stage": None,
302+
"done": True,
303+
"errors": [],
304+
"child_jobs": [],
305+
},
306+
{
307+
"created": ANY,
308+
"name": "test_jobs_sorted_inner_2",
309+
"reference": None,
310+
"uuid": ANY,
311+
"progress": 0,
312+
"stage": None,
313+
"done": True,
314+
"errors": [],
315+
"child_jobs": [],
316+
},
317+
],
318+
},
319+
]

tests/backups/test_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,7 @@ def _make_backup_message_for_assert(
10661066
"done": done,
10671067
"parent_id": None,
10681068
"errors": [],
1069+
"created": ANY,
10691070
},
10701071
},
10711072
}

tests/jobs/test_job_decorator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,7 @@ async def execute_default(self) -> bool:
997997
"done": True,
998998
"parent_id": None,
999999
"errors": [],
1000+
"created": ANY,
10001001
},
10011002
},
10021003
}

tests/jobs/test_job_manager.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ async def test_notify_on_change(coresys: CoreSys):
104104
"done": None,
105105
"parent_id": None,
106106
"errors": [],
107+
"created": ANY,
107108
},
108109
},
109110
}
@@ -125,6 +126,7 @@ async def test_notify_on_change(coresys: CoreSys):
125126
"done": None,
126127
"parent_id": None,
127128
"errors": [],
129+
"created": ANY,
128130
},
129131
},
130132
}
@@ -146,6 +148,7 @@ async def test_notify_on_change(coresys: CoreSys):
146148
"done": None,
147149
"parent_id": None,
148150
"errors": [],
151+
"created": ANY,
149152
},
150153
},
151154
}
@@ -167,6 +170,7 @@ async def test_notify_on_change(coresys: CoreSys):
167170
"done": False,
168171
"parent_id": None,
169172
"errors": [],
173+
"created": ANY,
170174
},
171175
},
172176
}
@@ -193,6 +197,7 @@ async def test_notify_on_change(coresys: CoreSys):
193197
"message": "Unknown error, see supervisor logs",
194198
}
195199
],
200+
"created": ANY,
196201
},
197202
},
198203
}
@@ -218,6 +223,7 @@ async def test_notify_on_change(coresys: CoreSys):
218223
"message": "Unknown error, see supervisor logs",
219224
}
220225
],
226+
"created": ANY,
221227
},
222228
},
223229
}

0 commit comments

Comments
 (0)