11import asyncio
22import irc .client_aio
33import signal
4+ import os
45
56import unittest
67import tempfile
@@ -94,14 +95,12 @@ def test_reconnect_loop(self, mock_settings, mock_sleep):
9495 @patch ("bot.PluginInterface" )
9596 @patch ("irc.client_aio.AioReactor" )
9697 @patch ("os.spawnvpe" , return_value = 1234 )
97- @patch ("os.kill" ) # temporary until we have graceful shutdowns of plugins
9898 @patch ("bot.irc.connection.AioFactory" )
9999 @patch ("bot.CommandLine" )
100100 def test_run (
101101 self ,
102102 mock_command_line ,
103103 mock_factory ,
104- mock_kill ,
105104 mock_spawnvpe ,
106105 mock_reactor ,
107106 mock_plugin_interface ,
@@ -116,6 +115,8 @@ def test_run(
116115
117116 plugin_interface_instance = MagicMock ()
118117 plugin_interface_instance .pid = 666
118+ plugin_interface_instance .name = "test_plugin"
119+ plugin_interface_instance .shutdown_plugin = AsyncMock ()
119120 mock_plugin_interface .return_value = plugin_interface_instance
120121
121122 bot = self .create_bot (mock_settings )
@@ -124,7 +125,7 @@ def test_run(
124125 bot .run ()
125126
126127 mock_plugin_interface .assert_called_once () # Make sure we loaded one pluign
127- mock_kill . assert_called_with ( 666 , signal . SIGTERM ) # Make sure the plugin was destroyed
128+ plugin_interface_instance . shutdown_plugin . assert_called_once ( ) # Make sure the plugin was gracefully shut down
128129 reactor_instance .server .assert_called_once () # Make sure we create the server
129130 server_mock .connect .assert_called_once () # Make sure we try to connect to the server
130131 reactor_instance .process_forever .assert_called_once () # Make sure the reactor runs forever
@@ -144,3 +145,52 @@ def test_plugin_init(self, mock_settings, mock_context):
144145 plugin = PluginInterface ("testplugin" , bot , 123 )
145146 mock_socket .bind .assert_called ()
146147 bot .plugin_started .assert_called_with (plugin )
148+
149+ @patch ("bot.zmq.asyncio.Context" )
150+ @patch ("bot.settings" )
151+ @patch ("bot.os.waitpid" )
152+ @patch ("bot.os.kill" )
153+ @patch ("bot.asyncio.sleep" , new_callable = AsyncMock )
154+ async def test_shutdown_plugin_clean (
155+ self , mock_sleep , mock_kill , mock_waitpid , mock_settings , mock_context
156+ ):
157+ bot = MagicMock ()
158+ mock_socket = MagicMock ()
159+ mock_context .return_value .socket .return_value = mock_socket
160+
161+ plugin = PluginInterface ("testplugin" , bot , 123 )
162+ # Mocking the _call method to avoid sending real zmq messages
163+ plugin ._call = MagicMock ()
164+
165+ # Simulate process exiting cleanly
166+ mock_waitpid .return_value = (123 , 0 )
167+
168+ await plugin .shutdown_plugin ()
169+
170+ plugin ._call .assert_called_with ("shutdown" )
171+ mock_waitpid .assert_called_with (123 , os .WNOHANG )
172+ mock_kill .assert_not_called ()
173+
174+ @patch ("bot.zmq.asyncio.Context" )
175+ @patch ("bot.settings" )
176+ @patch ("bot.os.waitpid" )
177+ @patch ("bot.os.kill" )
178+ @patch ("bot.asyncio.sleep" , new_callable = AsyncMock )
179+ async def test_shutdown_plugin_timeout (
180+ self , mock_sleep , mock_kill , mock_waitpid , mock_settings , mock_context
181+ ):
182+ bot = MagicMock ()
183+ mock_socket = MagicMock ()
184+ mock_context .return_value .socket .return_value = mock_socket
185+
186+ plugin = PluginInterface ("testplugin" , bot , 123 )
187+ plugin ._call = MagicMock ()
188+
189+ # Simulate process not exiting
190+ mock_waitpid .return_value = (0 , 0 )
191+
192+ await plugin .shutdown_plugin ()
193+
194+ plugin ._call .assert_called_with ("shutdown" )
195+ self .assertEqual (mock_waitpid .call_count , 5 )
196+ mock_kill .assert_called_with (123 , signal .SIGTERM )
0 commit comments