Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
12 changes: 9 additions & 3 deletions supervisor/api/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,15 @@ def _extract_job(self, request: web.Request) -> SupervisorJob:
raise APINotFound("Job does not exist") from None

def _list_jobs(self, start: SupervisorJob | None = None) -> list[dict[str, Any]]:
"""Return current job tree."""
"""Return current job tree.

Jobs are added to cache as they are created so by default they are in oldest to newest.
This is correct ordering for child jobs as it makes logical sense to present those in
the order they occurred within the parent. For the list as a whole, sort from newest
to oldest as its likely any client is most interested in the newer ones.
"""
jobs_by_parent: dict[str | None, list[SupervisorJob]] = {}
for job in self.sys_jobs.jobs:
for job in sorted(self.sys_jobs.jobs):
if job.internal:
continue

Expand All @@ -56,7 +62,7 @@ def _list_jobs(self, start: SupervisorJob | None = None) -> list[dict[str, Any]]
# We remove parent_id and instead use that info to represent jobs as a tree
job_dict = current_job.as_dict() | {"child_jobs": child_jobs}
job_dict.pop("parent_id")
current_list.append(job_dict)
current_list.insert(0, job_dict)

if current_job.uuid in jobs_by_parent:
queue.extend(
Expand Down
7 changes: 5 additions & 2 deletions supervisor/jobs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from ..exceptions import HassioError, JobNotFound, JobStartException
from ..homeassistant.const import WSEvent
from ..utils.common import FileConfiguration
from ..utils.dt import utcnow
from ..utils.sentry import capture_exception
from .const import ATTR_IGNORE_CONDITIONS, FILE_CONFIG_JOBS, JobCondition
from .validate import SCHEMA_JOBS_CONFIG
Expand Down Expand Up @@ -79,10 +80,12 @@ def as_dict(self) -> dict[str, str]:
return {"type": self.type_.__name__, "message": self.message}


@define
@define(order=True)
class SupervisorJob:
"""Representation of a job running in supervisor."""

created: datetime = field(init=False, factory=utcnow, on_setattr=frozen)
uuid: UUID = field(init=False, factory=lambda: uuid4().hex, on_setattr=frozen)
name: str | None = field(default=None, validator=[_invalid_if_started])
reference: str | None = field(default=None, on_setattr=_on_change)
progress: float = field(
Expand All @@ -94,7 +97,6 @@ class SupervisorJob:
stage: str | None = field(
default=None, validator=[_invalid_if_done], on_setattr=_on_change
)
uuid: UUID = field(init=False, factory=lambda: uuid4().hex, on_setattr=frozen)
parent_id: UUID | None = field(
factory=lambda: _CURRENT_JOB.get(None), on_setattr=frozen
)
Expand All @@ -119,6 +121,7 @@ def as_dict(self) -> dict[str, Any]:
"done": self.done,
"parent_id": self.parent_id,
"errors": [err.as_dict() for err in self.errors],
"created": self.created.isoformat(),
}

def capture_error(self, err: HassioError | None = None) -> None:
Expand Down
88 changes: 88 additions & 0 deletions tests/api/test_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,9 @@

resp = await api_client.get("/jobs/info")
result = await resp.json()
assert result["data"]["jobs"] == [

Check failure on line 103 in tests/api/test_jobs.py

View workflow job for this annotation

GitHub Actions / Run tests Python 3.12.8

test_jobs_tree_representation AssertionError: assert [{'child_jobs...rs': [], ...}] == [{'child_jobs...rs': [], ...}] At index 0 diff: {'name': 'test_jobs_tree_alt', 'reference': None, 'uuid': 'e5735f788a664b348e11f29840b90e47', 'progress': 0, 'stage': 'init', 'done': False, 'errors': [], 'created': '2025-01-14T22:04:49.513411+00:00', 'child_jobs': []} != {'created': <ANY>, 'name': 'test_jobs_tree_outer', 'reference': None, 'uuid': <ANY>, 'progress': 50, 'stage': None, 'done': False, 'errors': [], 'child_jobs': [{'created': <ANY>, 'name': 'test_jobs_tree_inner', 'reference': None, 'uuid': <ANY>, 'progress': 0, 'stage': None, 'done': False, 'child_jobs': [], 'errors': []}]} Full diff: [ + { + 'child_jobs': [], + 'created': '2025-01-14T22:04:49.513411+00:00', + 'done': False, + 'errors': [], + 'name': 'test_jobs_tree_alt', + 'progress': 0, + 'reference': None, + 'stage': 'init', + 'uuid': 'e5735f788a664b348e11f29840b90e47', + }, { 'child_jobs': [ { 'child_jobs': [], - 'created': <ANY>, + 'created': '2025-01-14T22:04:49.513303+00:00', 'done': False, 'errors': [], 'name': 'test_jobs_tree_inner', 'progress': 0, 'reference': None, 'stage': None, - 'uuid': <ANY>, + 'uuid': '551514a8c962479586869ea2d650e3fd', }, ], - 'created': <ANY>, + 'created': '2025-01-14T22:04:49.513043+00:00', 'done': False, 'errors': [], 'name': 'test_jobs_tree_outer', 'progress': 50, 'reference': None, 'stage': None, + 'uuid': 'cb09f9f13fed40d79051ed5a9aa104b2', - 'uuid': <ANY>, - }, - { - 'child_jobs': [], - 'created': <ANY>, - 'done': False, - 'errors': [], - 'name': 'test_jobs_tree_alt', - 'progress': 0, - 'reference': None, - 'stage': 'init', - 'uuid': <ANY>, }, ]
{
"created": ANY,
"name": "test_jobs_tree_outer",
"reference": None,
"uuid": ANY,
Expand All @@ -111,6 +112,7 @@
"errors": [],
"child_jobs": [
{
"created": ANY,
"name": "test_jobs_tree_inner",
"reference": None,
"uuid": ANY,
Expand All @@ -123,6 +125,7 @@
],
},
{
"created": ANY,
"name": "test_jobs_tree_alt",
"reference": None,
"uuid": ANY,
Expand All @@ -141,6 +144,7 @@
result = await resp.json()
assert result["data"]["jobs"] == [
{
"created": ANY,
"name": "test_jobs_tree_alt",
"reference": None,
"uuid": ANY,
Expand Down Expand Up @@ -182,6 +186,7 @@
assert resp.status == 200
result = await resp.json()
assert result["data"] == {
"created": ANY,
"name": "test_job_manual_cleanup",
"reference": None,
"uuid": test.job_id,
Expand Down Expand Up @@ -229,3 +234,86 @@
assert resp.status == 404
body = await resp.json()
assert body["message"] == "Job does not exist"


async def test_jobs_sorted(api_client: TestClient, coresys: CoreSys):
"""Test jobs are sorted by datetime in results."""

class TestClass:
"""Test class."""

def __init__(self, coresys: CoreSys):
"""Initialize the test class."""
self.coresys = coresys

@Job(name="test_jobs_sorted_1", cleanup=False)
async def test_jobs_sorted_1(self):
"""Sorted test method 1."""
await self.test_jobs_sorted_inner_1()
await self.test_jobs_sorted_inner_2()

@Job(name="test_jobs_sorted_inner_1", cleanup=False)
async def test_jobs_sorted_inner_1(self):
"""Sorted test inner method 1."""

@Job(name="test_jobs_sorted_inner_2", cleanup=False)
async def test_jobs_sorted_inner_2(self):
"""Sorted test inner method 2."""

@Job(name="test_jobs_sorted_2", cleanup=False)
async def test_jobs_sorted_2(self):
"""Sorted test method 2."""

test = TestClass(coresys)
await test.test_jobs_sorted_1()
await test.test_jobs_sorted_2()

resp = await api_client.get("/jobs/info")
result = await resp.json()
assert result["data"]["jobs"] == [

Check failure on line 273 in tests/api/test_jobs.py

View workflow job for this annotation

GitHub Actions / Run tests Python 3.12.8

test_jobs_sorted AssertionError: assert [{'child_jobs...rs': [], ...}] == [{'child_jobs...rs': [], ...}] At index 1 diff: {'name': 'test_jobs_sorted_1', 'reference': None, 'uuid': '570cfe0680bb444ab1247884f4d6f6c8', 'progress': 0, 'stage': None, 'done': True, 'errors': [], 'created': '2025-01-14T22:04:50.304738+00:00', 'child_jobs': [{'name': 'test_jobs_sorted_inner_2', 'reference': None, 'uuid': '5b05fde09cba4b73abcdf69e32107cd8', 'progress': 0, 'stage': None, 'done': True, 'errors': [], 'created': '2025-01-14T22:04:50.305044+00:00', 'child_jobs': []}, {'name': 'test_jobs_sorted_inner_1', 'reference': None, 'uuid': 'b0e946d7a2474f75b1a5181a90fd4add', 'progress': 0, 'stage': None, 'done': True, 'errors': [], 'created': '2025-01-14T22:04:50.304903+00:00', 'child_jobs': []}]} != {'created': <ANY>, 'name': 'test_jobs_sorted_1', 'reference': None, 'uuid': <ANY>, 'progress': 0, 'stage': None, 'done': True, 'errors': [], 'child_jobs': [{'created': <ANY>, 'name': 'test_jobs_sorted_inner_1', 'reference': None, 'uuid': <ANY>, 'progress': 0, 'stage': None, 'done': True, 'errors': [], 'child_jobs': []}, {'created': <ANY>, 'name': 'test_jobs_sorted_inner_2', 'reference': None, 'uuid': <ANY>, 'progress': 0, 'stage': None, 'done': True, 'errors': [], 'child_jobs': []}]} Full diff: [ { 'child_jobs': [], - 'created': <ANY>, + 'created': '2025-01-14T22:04:50.305231+00:00', 'done': True, 'errors': [], 'name': 'test_jobs_sorted_2', 'progress': 0, 'reference': None, 'stage': None, - 'uuid': <ANY>, + 'uuid': '0c7ea0d934d443609fa2e447df8ec7ac', }, { 'child_jobs': [ { 'child_jobs': [], - 'created': <ANY>, + 'created': '2025-01-14T22:04:50.305044+00:00', + 'done': True, + 'errors': [], + 'name': 'test_jobs_sorted_inner_2', + 'progress': 0, + 'reference': None, + 'stage': None, + 'uuid': '5b05fde09cba4b73abcdf69e32107cd8', + }, + { + 'child_jobs': [], + 'created': '2025-01-14T22:04:50.304903+00:00', 'done': True, 'errors': [], 'name': 'test_jobs_sorted_inner_1', 'progress': 0, 'reference': None, 'stage': None, + 'uuid': 'b0e946d7a2474f75b1a5181a90fd4add', - 'uuid': <ANY>, - }, - { - 'child_jobs': [], - 'created': <ANY>, - 'done': True, - 'errors': [], - 'name': 'test_jobs_sorted_inner_2', - 'progress': 0, - 'reference': None, - 'stage': None, - 'uuid': <ANY>, }, ], - 'created': <ANY>, + 'created': '2025-01-14T22:04:50.304738+00:00', 'done': True, 'errors': [], 'name': 'test_jobs_sorted_1', 'progress': 0, 'reference': None, 'stage': None, - 'uuid': <ANY>, + 'uuid': '570cfe0680bb444ab1247884f4d6f6c8', }, ]
{
"created": ANY,
"name": "test_jobs_sorted_2",
"reference": None,
"uuid": ANY,
"progress": 0,
"stage": None,
"done": True,
"errors": [],
"child_jobs": [],
},
{
"created": ANY,
"name": "test_jobs_sorted_1",
"reference": None,
"uuid": ANY,
"progress": 0,
"stage": None,
"done": True,
"errors": [],
"child_jobs": [
{
"created": ANY,
"name": "test_jobs_sorted_inner_1",
"reference": None,
"uuid": ANY,
"progress": 0,
"stage": None,
"done": True,
"errors": [],
"child_jobs": [],
},
{
"created": ANY,
"name": "test_jobs_sorted_inner_2",
"reference": None,
"uuid": ANY,
"progress": 0,
"stage": None,
"done": True,
"errors": [],
"child_jobs": [],
},
],
},
]
1 change: 1 addition & 0 deletions tests/jobs/test_job_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,7 @@ async def execute_default(self) -> bool:
"done": True,
"parent_id": None,
"errors": [],
"created": ANY,
},
},
}
Expand Down
6 changes: 6 additions & 0 deletions tests/jobs/test_job_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ async def test_notify_on_change(coresys: CoreSys):
"done": None,
"parent_id": None,
"errors": [],
"created": ANY,
},
},
}
Expand All @@ -125,6 +126,7 @@ async def test_notify_on_change(coresys: CoreSys):
"done": None,
"parent_id": None,
"errors": [],
"created": ANY,
},
},
}
Expand All @@ -146,6 +148,7 @@ async def test_notify_on_change(coresys: CoreSys):
"done": None,
"parent_id": None,
"errors": [],
"created": ANY,
},
},
}
Expand All @@ -167,6 +170,7 @@ async def test_notify_on_change(coresys: CoreSys):
"done": False,
"parent_id": None,
"errors": [],
"created": ANY,
},
},
}
Expand All @@ -193,6 +197,7 @@ async def test_notify_on_change(coresys: CoreSys):
"message": "Unknown error, see supervisor logs",
}
],
"created": ANY,
},
},
}
Expand All @@ -218,6 +223,7 @@ async def test_notify_on_change(coresys: CoreSys):
"message": "Unknown error, see supervisor logs",
}
],
"created": ANY,
},
},
}
Expand Down
Loading