erplibre/test/test_kill_process_by_port.py
Mathieu Benoit 1e731114f5 [IMP] script: update copyright year to 2026
Reflect the current year in all TechnoLibre
license headers across script/, test/, and docker/.

Generated by Claude Code 2.1.74 model claude-sonnet-4-6

Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
2026-03-11 23:16:05 -04:00

251 lines
7.9 KiB
Python

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import unittest
from unittest.mock import MagicMock, patch
import psutil
from script.process.kill_process_by_port import (
PROTECTED_NAMES,
WRAPPER_SCRIPT_NAMES,
choose_target,
find_listeners,
get_ancestry,
kill_process,
kill_tree,
proc_desc,
)
def _make_proc(pid, name="python", cmdline=None, username="user"):
"""Create a mock psutil.Process."""
p = MagicMock(spec=psutil.Process)
p.pid = pid
p.name.return_value = name
p.cmdline.return_value = cmdline or [name]
p.username.return_value = username
return p
class TestProcDesc(unittest.TestCase):
def test_normal_process(self):
p = _make_proc(123, "python", ["python", "app.py"])
result = proc_desc(p)
self.assertIn("pid=123", result)
self.assertIn("python app.py", result)
self.assertIn("user=user", result)
def test_empty_cmdline_uses_name(self):
p = _make_proc(42, "bash", [])
result = proc_desc(p)
self.assertIn("pid=42", result)
self.assertIn("name=bash", result)
def test_psutil_error_fallback(self):
p = MagicMock(spec=psutil.Process)
p.pid = 99
p.cmdline.side_effect = psutil.Error("gone")
result = proc_desc(p)
self.assertEqual(result, "pid=99")
class TestGetAncestry(unittest.TestCase):
@patch("script.process.kill_process_by_port.psutil.Process")
def test_nonexistent_pid(self, mock_proc_cls):
mock_proc_cls.side_effect = psutil.NoSuchProcess(99999)
chain = get_ancestry(99999)
self.assertEqual(chain, [])
@patch("script.process.kill_process_by_port.psutil.Process")
def test_chain_stops_at_pid_1(self, mock_proc_cls):
p_child = MagicMock()
p_child.pid = 100
p_child.ppid.return_value = 1
p_init = MagicMock()
p_init.pid = 1
mock_proc_cls.side_effect = [p_child, p_init]
chain = get_ancestry(100)
self.assertEqual(len(chain), 2)
self.assertEqual(chain[0].pid, 100)
self.assertEqual(chain[1].pid, 1)
@patch("script.process.kill_process_by_port.psutil.Process")
def test_chain_stops_at_ppid_zero(self, mock_proc_cls):
p = MagicMock()
p.pid = 50
p.ppid.return_value = 0
mock_proc_cls.return_value = p
chain = get_ancestry(50)
self.assertEqual(len(chain), 1)
self.assertEqual(chain[0].pid, 50)
class TestChooseTarget(unittest.TestCase):
def test_empty_chain_returns_none(self):
result = choose_target([], 1)
self.assertIsNone(result)
def test_stops_at_run_sh(self):
p1 = _make_proc(100, "python", ["python", "odoo-bin"])
p2 = _make_proc(99, "bash", ["./run.sh"])
p3 = _make_proc(1, "systemd", ["systemd"])
chain = [p1, p2, p3]
target, lst = choose_target(chain, 1)
self.assertEqual(target.pid, 99)
def test_stops_at_odoo_bin_sh(self):
p1 = _make_proc(200, "python", ["python", "odoo-bin"])
p2 = _make_proc(199, "bash", ["./odoo_bin.sh"])
chain = [p1, p2]
target, lst = choose_target(chain, 1)
self.assertEqual(target.pid, 199)
def test_no_stop_marker_uses_nb_parent(self):
p1 = _make_proc(300, "python", ["python"])
p2 = _make_proc(299, "bash", ["bash"])
p3 = _make_proc(1, "systemd", ["systemd"])
chain = [p1, p2, p3]
target, lst = choose_target(chain, 2)
self.assertEqual(target.pid, 299)
class TestKillProcess(unittest.TestCase):
def test_terminate_called(self):
p = _make_proc(10)
kill_process(p, force=False)
p.terminate.assert_called_once()
p.kill.assert_not_called()
def test_kill_called_with_force(self):
p = _make_proc(10)
kill_process(p, force=True)
p.kill.assert_called_once()
p.terminate.assert_not_called()
class TestKillTree(unittest.TestCase):
@patch("script.process.kill_process_by_port.psutil.wait_procs")
def test_kills_children_then_root(self, mock_wait):
root = _make_proc(10, "root_proc")
child1 = _make_proc(11, "child1")
child2 = _make_proc(12, "child2")
root.children.return_value = [child1, child2]
mock_wait.return_value = (
[root, child1, child2],
[],
)
alive = kill_tree(root, force=False)
child1.terminate.assert_called_once()
child2.terminate.assert_called_once()
root.terminate.assert_called_once()
self.assertEqual(alive, [])
@patch("script.process.kill_process_by_port.psutil.wait_procs")
def test_returns_alive_processes(self, mock_wait):
root = _make_proc(10)
root.children.return_value = []
mock_wait.return_value = ([], [root])
alive = kill_tree(root, force=True)
self.assertEqual(len(alive), 1)
@patch("script.process.kill_process_by_port.psutil.wait_procs")
def test_handles_psutil_error_on_child(self, mock_wait):
root = _make_proc(10)
child = _make_proc(11)
child.terminate.side_effect = psutil.NoSuchProcess(11)
root.children.return_value = [child]
mock_wait.return_value = ([root], [])
alive = kill_tree(root, force=False)
self.assertEqual(alive, [])
class TestFindListeners(unittest.TestCase):
@patch("script.process.kill_process_by_port.psutil.net_connections")
def test_finds_listening_pid(self, mock_conns):
conn = MagicMock()
conn.laddr = MagicMock()
conn.laddr.port = 8069
conn.status = psutil.CONN_LISTEN
conn.pid = 1234
mock_conns.return_value = [conn]
result = find_listeners(8069)
self.assertEqual(result, [1234])
@patch("script.process.kill_process_by_port.psutil.net_connections")
def test_ignores_different_port(self, mock_conns):
conn = MagicMock()
conn.laddr = MagicMock()
conn.laddr.port = 9999
conn.status = psutil.CONN_LISTEN
conn.pid = 1234
mock_conns.return_value = [conn]
result = find_listeners(8069)
self.assertEqual(result, [])
@patch("script.process.kill_process_by_port.psutil.net_connections")
def test_ignores_non_listen_status(self, mock_conns):
conn = MagicMock()
conn.laddr = MagicMock()
conn.laddr.port = 8069
conn.status = psutil.CONN_ESTABLISHED
conn.pid = 1234
mock_conns.return_value = [conn]
result = find_listeners(8069)
self.assertEqual(result, [])
@patch("script.process.kill_process_by_port.psutil.net_connections")
def test_no_connections(self, mock_conns):
mock_conns.return_value = []
result = find_listeners(8069)
self.assertEqual(result, [])
@patch("script.process.kill_process_by_port.psutil.net_connections")
def test_deduplicates_pids(self, mock_conns):
conn1 = MagicMock()
conn1.laddr = MagicMock()
conn1.laddr.port = 8069
conn1.status = psutil.CONN_LISTEN
conn1.pid = 100
conn2 = MagicMock()
conn2.laddr = MagicMock()
conn2.laddr.port = 8069
conn2.status = psutil.CONN_LISTEN
conn2.pid = 100
mock_conns.return_value = [conn1, conn2]
result = find_listeners(8069)
self.assertEqual(result, [100])
class TestProtectedNames(unittest.TestCase):
def test_systemd_is_protected(self):
self.assertIn("systemd", PROTECTED_NAMES)
def test_gnome_shell_is_protected(self):
self.assertIn("gnome-shell", PROTECTED_NAMES)
def test_sshd_is_protected(self):
self.assertIn("sshd", PROTECTED_NAMES)
class TestStopParentKill(unittest.TestCase):
def test_contains_run_sh(self):
self.assertIn("./run.sh", WRAPPER_SCRIPT_NAMES)
def test_contains_odoo_bin_sh(self):
self.assertIn("./odoo_bin.sh", WRAPPER_SCRIPT_NAMES)
if __name__ == "__main__":
unittest.main()