Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 14 additions & 3 deletions supervisor/api/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,16 @@ 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.
"""
# Initially sort oldest to newest so all child lists end up in correct order
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 @@ -42,11 +49,15 @@ def _list_jobs(self, start: SupervisorJob | None = None) -> list[dict[str, Any]]
else:
jobs_by_parent[job.parent_id].append(job)

# After parent-child organization, sort the root jobs only from newest to oldest
job_list: list[dict[str, Any]] = []
queue: list[tuple[list[dict[str, Any]], SupervisorJob]] = (
[(job_list, start)]
if start
else [(job_list, job) for job in jobs_by_parent.get(None, [])]
else [
(job_list, job)
for job in sorted(jobs_by_parent.get(None, []), reverse=True)
]
)

while queue:
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
108 changes: 98 additions & 10 deletions tests/api/test_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ async def test_jobs_tree_internal(self):
result = await resp.json()
assert result["data"]["jobs"] == [
{
"created": ANY,
"name": "test_jobs_tree_alt",
"reference": None,
"uuid": ANY,
"progress": 0,
"stage": "init",
"done": False,
"child_jobs": [],
"errors": [],
},
{
"created": ANY,
"name": "test_jobs_tree_outer",
"reference": None,
"uuid": ANY,
Expand All @@ -111,6 +123,7 @@ async def test_jobs_tree_internal(self):
"errors": [],
"child_jobs": [
{
"created": ANY,
"name": "test_jobs_tree_inner",
"reference": None,
"uuid": ANY,
Expand All @@ -122,16 +135,6 @@ async def test_jobs_tree_internal(self):
},
],
},
{
"name": "test_jobs_tree_alt",
"reference": None,
"uuid": ANY,
"progress": 0,
"stage": "init",
"done": False,
"child_jobs": [],
"errors": [],
},
]

test.event.set()
Expand All @@ -141,6 +144,7 @@ async def test_jobs_tree_internal(self):
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 @@ async def test_job_manual_cleanup(self) -> None:
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 @@ async def test_job_not_found(api_client: TestClient, method: str, url: str):
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"] == [
{
"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/backups/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,7 @@ def _make_backup_message_for_assert(
"done": done,
"parent_id": None,
"errors": [],
"created": ANY,
},
},
}
Expand Down
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