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

1import signal 

2from multiprocessing import Process 

3from typing import cast 

4 

5import grpc 

6 

7from couchers import supervisor 

8from couchers.constants import GRACEFUL_SHUTDOWN_TIMEOUT 

9from couchers.server import create_main_server, create_media_server 

10 

11 

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() 

19 

20 

21class FakeProcess: 

22 """Stands in for multiprocessing.Process so the supervisor can be tested without spawning.""" 

23 

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 

31 

32 def is_alive(self): 

33 return self._alive 

34 

35 def terminate(self): 

36 self.terminated = True 

37 self._alive = False 

38 

39 def join(self, timeout=None): 

40 self.joined = True 

41 

42 

43class FakeServer: 

44 """Stands in for a grpc.Server so parent-server draining can be tested without a real server.""" 

45 

46 def __init__(self): 

47 self.stop_grace = None 

48 self.waited = False 

49 

50 def stop(self, grace): 

51 self.stop_grace = grace 

52 return self 

53 

54 def wait(self, timeout=None): 

55 self.waited = True 

56 

57 

58def _as_children(*procs: FakeProcess) -> list[Process]: 

59 return [cast(Process, p) for p in procs] 

60 

61 

62def test_supervise_returns_crashed_child_and_drains_the_rest(monkeypatch): 

63 monkeypatch.setattr(signal, "signal", lambda *a: None) 

64 

65 alive = FakeProcess("api-1761", alive=True) 

66 dead = FakeProcess("api-1762", alive=False) 

67 children = _as_children(alive, dead) 

68 

69 crashed = supervisor.supervise(children) 

70 

71 assert crashed is children[1] 

72 assert alive.terminated 

73 assert alive.joined 

74 

75 

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) 

81 

82 monkeypatch.setattr(signal, "signal", fire_on_register) 

83 

84 a = FakeProcess("api-1761") 

85 b = FakeProcess("api-1762") 

86 

87 crashed = supervisor.supervise(_as_children(a, b)) 

88 

89 assert crashed is None 

90 assert a.terminated and b.terminated 

91 assert a.joined and b.joined 

92 

93 

94def test_supervise_only_terminates_live_children(monkeypatch): 

95 monkeypatch.setattr(signal, "signal", lambda *a: None) 

96 

97 already_dead = FakeProcess("api-1761", alive=False) 

98 live = FakeProcess("api-1762", alive=True) 

99 

100 supervisor.supervise(_as_children(already_dead, live)) 

101 

102 assert not already_dead.terminated 

103 assert live.terminated 

104 

105 

106def test_supervise_drains_parent_servers_within_the_shutdown_window(monkeypatch): 

107 monkeypatch.setattr(signal, "signal", lambda *a: None) 

108 

109 dead = FakeProcess("api-1761", alive=False) 

110 media = FakeServer() 

111 

112 supervisor.supervise(_as_children(dead), parent_servers=[cast(grpc.Server, media)]) 

113 

114 assert media.stop_grace == GRACEFUL_SHUTDOWN_TIMEOUT 

115 assert media.waited