Coverage for app/backend/src/tests/test_app.py: 100%
74 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-19 15:47 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-19 15:47 +0000
1import signal
2from multiprocessing import Process
3from typing import cast
5import grpc
7from couchers import supervisor
8from couchers.constants import GRACEFUL_SHUTDOWN_TIMEOUT
9from couchers.server import create_main_server, create_media_server
12def test_create_servers():
13 server = create_main_server(port=0)
14 media_server = create_media_server(port=0)
15 server.start()
16 media_server.start()
17 server.stop(None).wait()
18 media_server.stop(None).wait()
21class FakeProcess:
22 """Stands in for multiprocessing.Process so the supervisor can be tested without spawning."""
24 def __init__(self, name="fake", alive=True):
25 self.name = name
26 self.pid = 123
27 self.exitcode = 0
28 self._alive = alive
29 self.terminated = False
30 self.joined = False
32 def is_alive(self):
33 return self._alive
35 def terminate(self):
36 self.terminated = True
37 self._alive = False
39 def join(self, timeout=None):
40 self.joined = True
43class FakeServer:
44 """Stands in for a grpc.Server so parent-server draining can be tested without a real server."""
46 def __init__(self):
47 self.stop_grace = None
48 self.waited = False
50 def stop(self, grace):
51 self.stop_grace = grace
52 return self
54 def wait(self, timeout=None):
55 self.waited = True
58def _as_children(*procs: FakeProcess) -> list[Process]:
59 return [cast(Process, p) for p in procs]
62def test_supervise_returns_crashed_child_and_drains_the_rest(monkeypatch):
63 monkeypatch.setattr(signal, "signal", lambda *a: None)
65 alive = FakeProcess("api-1761", alive=True)
66 dead = FakeProcess("api-1762", alive=False)
67 children = _as_children(alive, dead)
69 crashed = supervisor.supervise(children)
71 assert crashed is children[1]
72 assert alive.terminated
73 assert alive.joined
76def test_supervise_returns_none_on_graceful_shutdown(monkeypatch):
77 # fire the handler as soon as it's registered, so the loop exits without any child dying
78 def fire_on_register(sig, handler):
79 if sig == signal.SIGTERM:
80 handler(sig, None)
82 monkeypatch.setattr(signal, "signal", fire_on_register)
84 a = FakeProcess("api-1761")
85 b = FakeProcess("api-1762")
87 crashed = supervisor.supervise(_as_children(a, b))
89 assert crashed is None
90 assert a.terminated and b.terminated
91 assert a.joined and b.joined
94def test_supervise_only_terminates_live_children(monkeypatch):
95 monkeypatch.setattr(signal, "signal", lambda *a: None)
97 already_dead = FakeProcess("api-1761", alive=False)
98 live = FakeProcess("api-1762", alive=True)
100 supervisor.supervise(_as_children(already_dead, live))
102 assert not already_dead.terminated
103 assert live.terminated
106def test_supervise_drains_parent_servers_within_the_shutdown_window(monkeypatch):
107 monkeypatch.setattr(signal, "signal", lambda *a: None)
109 dead = FakeProcess("api-1761", alive=False)
110 media = FakeServer()
112 supervisor.supervise(_as_children(dead), parent_servers=[cast(grpc.Server, media)])
114 assert media.stop_grace == GRACEFUL_SHUTDOWN_TIMEOUT
115 assert media.waited