aboutsummaryrefslogtreecommitdiff
path: root/tests/test_backends_servers.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_backends_servers.py')
-rw-r--r--tests/test_backends_servers.py146
1 files changed, 146 insertions, 0 deletions
diff --git a/tests/test_backends_servers.py b/tests/test_backends_servers.py
new file mode 100644
index 0000000..02b65e6
--- /dev/null
+++ b/tests/test_backends_servers.py
@@ -0,0 +1,146 @@
+"""Tests for the server lifecycle module (backends/servers.py)."""
+
+import tempfile
+import unittest
+from pathlib import Path
+from unittest.mock import MagicMock, patch
+
+from backends import ServerSpec, servers
+
+
+class StartTests(unittest.TestCase):
+ def setUp(self):
+ self._tmp = tempfile.TemporaryDirectory()
+ self.dir = Path(self._tmp.name)
+ # A fake executable so Path(argv[0]).exists() passes.
+ self.exe = self.dir / "fake_server"
+ self.exe.write_bytes(b"#!/bin/sh\n")
+ self.spec = ServerSpec("test", "http://127.0.0.1:9999",
+ [str(self.exe), "--port", "9999"])
+
+ def tearDown(self):
+ self._tmp.cleanup()
+
+ def test_returns_false_when_executable_missing(self):
+ spec = ServerSpec("nope", "http://127.0.0.1:1", ["/no/such/binary"])
+ with patch.object(servers, "LOG_DIR", self.dir):
+ self.assertFalse(servers.start(spec))
+
+ def test_noop_when_already_running(self):
+ with patch.object(servers, "LOG_DIR", self.dir), \
+ patch("backends.common.server_running", return_value=True), \
+ patch("subprocess.Popen") as mk:
+ self.assertTrue(servers.start(self.spec))
+ mk.assert_not_called()
+
+ def test_happy_path_spawns_and_polls_until_ready(self):
+ proc = MagicMock()
+ proc.pid = 4242
+ proc.poll.return_value = None # process still running
+ # server_running: False on the pre-check, True once inside the loop.
+ with patch.object(servers, "LOG_DIR", self.dir), \
+ patch("subprocess.Popen", return_value=proc) as mk, \
+ patch("backends.common.server_running",
+ side_effect=[False, True]), \
+ patch("time.sleep"):
+ ok = servers.start(self.spec)
+ self.assertTrue(ok)
+ mk.assert_called_once()
+ # Pid file written.
+ self.assertEqual(
+ (self.dir / "test-server.pid").read_text(encoding="utf-8"),
+ "4242")
+
+ def test_returns_false_when_process_exits_early(self):
+ proc = MagicMock()
+ proc.pid = 99
+ proc.poll.return_value = 1 # exited with code 1
+ with patch.object(servers, "LOG_DIR", self.dir), \
+ patch("subprocess.Popen", return_value=proc), \
+ patch("backends.common.server_running", return_value=False), \
+ patch("time.sleep"):
+ ok = servers.start(self.spec)
+ self.assertFalse(ok)
+ # Pid file cleaned up after early exit.
+ self.assertFalse((self.dir / "test-server.pid").exists())
+
+ def test_returns_false_on_timeout(self):
+ proc = MagicMock()
+ proc.pid = 7
+ proc.poll.return_value = None
+ # time.time: first call < deadline loop entry, then past deadline.
+ times = iter([0.0, float(servers.SERVER_START_TIMEOUT + 1)])
+ with patch.object(servers, "LOG_DIR", self.dir), \
+ patch("subprocess.Popen", return_value=proc), \
+ patch("backends.common.server_running", return_value=False), \
+ patch("time.sleep"), \
+ patch("time.time", side_effect=lambda: next(times)):
+ ok = servers.start(self.spec)
+ self.assertFalse(ok)
+
+
+class StopTests(unittest.TestCase):
+ def setUp(self):
+ self._tmp = tempfile.TemporaryDirectory()
+ self.dir = Path(self._tmp.name)
+
+ def tearDown(self):
+ self._tmp.cleanup()
+
+ def _write_pid(self, name, pid):
+ (self.dir / f"{name}-server.pid").write_text(str(pid),
+ encoding="utf-8")
+
+ def test_returns_false_when_no_pid_file(self):
+ with patch.object(servers, "LOG_DIR", self.dir):
+ self.assertFalse(servers.stop("test"))
+
+ def test_stops_alive_process_and_removes_pid_file(self):
+ self._write_pid("test", 1234)
+ with patch.object(servers, "LOG_DIR", self.dir), \
+ patch.object(servers, "_pid_alive", return_value=True), \
+ patch.object(servers, "_kill_pid", return_value=True) as mk:
+ ok = servers.stop("test")
+ self.assertTrue(ok)
+ mk.assert_called_once_with(1234)
+ self.assertFalse((self.dir / "test-server.pid").exists())
+
+ def test_already_dead_returns_true_and_cleans_pid_file(self):
+ self._write_pid("test", 1234)
+ with patch.object(servers, "LOG_DIR", self.dir), \
+ patch.object(servers, "_pid_alive", return_value=False), \
+ patch.object(servers, "_kill_pid") as mk:
+ ok = servers.stop("test")
+ self.assertTrue(ok)
+ mk.assert_not_called()
+ self.assertFalse((self.dir / "test-server.pid").exists())
+
+ def test_corrupt_pid_file_returns_false_and_cleans(self):
+ (self.dir / "test-server.pid").write_text("not-a-number",
+ encoding="utf-8")
+ with patch.object(servers, "LOG_DIR", self.dir):
+ self.assertFalse(servers.stop("test"))
+ self.assertFalse((self.dir / "test-server.pid").exists())
+
+
+class PidForTests(unittest.TestCase):
+ def setUp(self):
+ self._tmp = tempfile.TemporaryDirectory()
+ self.dir = Path(self._tmp.name)
+
+ def tearDown(self):
+ self._tmp.cleanup()
+
+ def test_none_when_no_pid_file(self):
+ with patch.object(servers, "LOG_DIR", self.dir):
+ self.assertIsNone(servers.pid_for("test"))
+
+ def test_returns_pid_from_file(self):
+ (self.dir / "test-server.pid").write_text("555\n",
+ encoding="utf-8")
+ with patch.object(servers, "LOG_DIR", self.dir):
+ self.assertEqual(servers.pid_for("test"), 555)
+
+
+if __name__ == "__main__":
+ unittest.main()