Skip to content

Commit 7dbba78

Browse files
committed
Add BackgroundService tests
Add some tests for the `BackgroundService` class. The tests were written looking at the coverage to avoid duplicating a lot of tests that are already performed in the `Actor` tests. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent a25578e commit 7dbba78

File tree

1 file changed

+152
-0
lines changed

1 file changed

+152
-0
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# License: MIT
2+
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Simple test for the BaseActor."""
5+
import asyncio
6+
from collections.abc import Iterator
7+
from typing import Literal, assert_never
8+
9+
import async_solipsism
10+
import pytest
11+
12+
from frequenz.sdk.actor import BackgroundService
13+
14+
15+
# Setting 'autouse' has no effect as this method replaces the event loop for all tests in the file.
16+
@pytest.fixture()
17+
def event_loop() -> Iterator[async_solipsism.EventLoop]:
18+
"""Replace the loop with one that doesn't interact with the outside world."""
19+
loop = async_solipsism.EventLoop()
20+
yield loop
21+
loop.close()
22+
23+
24+
class FakeService(BackgroundService):
25+
"""A background service that does nothing."""
26+
27+
def __init__(
28+
self,
29+
*,
30+
name: str | None = None,
31+
sleep: float | None = None,
32+
exc: BaseException | None = None,
33+
) -> None:
34+
"""Initialize a new FakeService."""
35+
super().__init__(name=name)
36+
self._sleep = sleep
37+
self._exc = exc
38+
39+
async def start(self) -> None:
40+
"""Start this service."""
41+
42+
async def nop() -> None:
43+
if self._sleep is not None:
44+
await asyncio.sleep(self._sleep)
45+
if self._exc is not None:
46+
raise self._exc
47+
48+
self._tasks.add(asyncio.create_task(nop(), name="nop"))
49+
50+
51+
async def test_construction_defaults() -> None:
52+
"""Test the construction of a background service with default arguments."""
53+
fake_service = FakeService()
54+
assert fake_service.name == str(id(fake_service))
55+
assert fake_service.tasks == set()
56+
assert fake_service.is_running is False
57+
assert str(fake_service) == f"FakeService[{fake_service.name}]"
58+
assert repr(fake_service) == f"FakeService(name={fake_service.name!r}, tasks=set())"
59+
60+
61+
async def test_construction_custom() -> None:
62+
"""Test the construction of a background service with a custom name."""
63+
fake_service = FakeService(name="test")
64+
assert fake_service.name == "test"
65+
assert fake_service.tasks == set()
66+
assert fake_service.is_running is False
67+
68+
69+
async def test_start_await() -> None:
70+
"""Test a background service starts and can be awaited."""
71+
fake_service = FakeService(name="test")
72+
assert fake_service.name == "test"
73+
assert fake_service.is_running is False
74+
75+
# Is a no-op if the service is not running
76+
await fake_service.stop()
77+
assert fake_service.is_running is False
78+
79+
await fake_service.start()
80+
assert fake_service.is_running is True
81+
82+
# Should stop immediately
83+
async with asyncio.timeout(1.0):
84+
await fake_service
85+
86+
assert fake_service.is_running is False
87+
88+
89+
async def test_start_stop() -> None:
90+
"""Test a background service starts and stops correctly."""
91+
fake_service = FakeService(name="test", sleep=2.0)
92+
assert fake_service.name == "test"
93+
assert fake_service.is_running is False
94+
95+
# Is a no-op if the service is not running
96+
await fake_service.stop()
97+
assert fake_service.is_running is False
98+
99+
await fake_service.start()
100+
assert fake_service.is_running is True
101+
102+
await asyncio.sleep(1.0)
103+
assert fake_service.is_running is True
104+
105+
await fake_service.stop()
106+
assert fake_service.is_running is False
107+
108+
await fake_service.stop()
109+
assert fake_service.is_running is False
110+
111+
112+
@pytest.mark.parametrize("method", ["await", "wait", "stop"])
113+
async def test_start_and_crash(
114+
method: Literal["await"] | Literal["wait"] | Literal["stop"],
115+
) -> None:
116+
"""Test a background service reports when crashing."""
117+
exc = RuntimeError("error")
118+
fake_service = FakeService(name="test", exc=exc)
119+
assert fake_service.name == "test"
120+
assert fake_service.is_running is False
121+
122+
await fake_service.start()
123+
with pytest.raises(BaseExceptionGroup) as exc_info:
124+
match method:
125+
case "await":
126+
await fake_service
127+
case "wait":
128+
await fake_service.wait()
129+
case "stop":
130+
# Give the service some time to run and crash, otherwise stop() will
131+
# cancel it before it has a chance to crash
132+
await asyncio.sleep(1.0)
133+
await fake_service.stop()
134+
case _:
135+
assert_never(method)
136+
137+
rt_errors, rest_errors = exc_info.value.split(RuntimeError)
138+
assert rt_errors is not None
139+
assert rest_errors is None
140+
assert len(rt_errors.exceptions) == 1
141+
assert rt_errors.exceptions[0] is exc
142+
143+
144+
async def test_async_context_manager() -> None:
145+
"""Test a background service works as an async context manager."""
146+
async with FakeService(name="test", sleep=1.0) as fake_service:
147+
assert fake_service.is_running is True
148+
# Is a no-op if the service is running
149+
await fake_service.start()
150+
assert fake_service.is_running is True
151+
152+
assert fake_service.is_running is False

0 commit comments

Comments
 (0)