Skip to content

Commit 8363bf6

Browse files
authored
PYTHON-5774 Increase daemon.py coverage to 63% (mongodb#2759)
1 parent 5406feb commit 8363bf6

3 files changed

Lines changed: 186 additions & 0 deletions

File tree

.evergreen/generated_configs/variants.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,7 @@ buildvariants:
615615
- name: test-win64
616616
tasks:
617617
- name: .test-standard !.pypy
618+
- name: .test-no-orchestration !.pypy
618619
display_name: "* Test Win64"
619620
run_on:
620621
- windows-2022-latest-small

.evergreen/scripts/generate_config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ def create_standard_nonlinux_variants() -> list[BuildVariant]:
9797
tasks = [
9898
f".test-standard !.pypy .server-{version}" for version in get_versions_from("6.0")
9999
]
100+
if host_name == "win64":
101+
tasks.append(".test-no-orchestration !.pypy")
100102
host = HOSTS[host_name]
101103
tags = ["standard-non-linux"]
102104
expansions = dict()

test/test_daemon.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Copyright 2026-present MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Test the pymongo daemon module."""
16+
from __future__ import annotations
17+
18+
import subprocess
19+
import sys
20+
import warnings
21+
from unittest.mock import MagicMock, patch
22+
23+
sys.path[0:0] = [""]
24+
25+
from test import unittest
26+
27+
import pymongo.daemon as daemon_module
28+
from pymongo.daemon import _popen_wait, _silence_resource_warning, _spawn_daemon
29+
30+
31+
class TestPopenWait(unittest.TestCase):
32+
def test_returns_returncode_on_success(self):
33+
mock_popen = MagicMock()
34+
mock_popen.wait.return_value = 0
35+
self.assertEqual(0, _popen_wait(mock_popen, timeout=5))
36+
mock_popen.wait.assert_called_once_with(timeout=5)
37+
38+
def test_returns_none_on_timeout_expired(self):
39+
mock_popen = MagicMock()
40+
mock_popen.wait.side_effect = subprocess.TimeoutExpired(cmd="foo", timeout=5)
41+
self.assertIsNone(_popen_wait(mock_popen, timeout=5))
42+
43+
def test_none_timeout_passes_through(self):
44+
mock_popen = MagicMock()
45+
mock_popen.wait.return_value = 1
46+
self.assertEqual(1, _popen_wait(mock_popen, timeout=None))
47+
mock_popen.wait.assert_called_once_with(timeout=None)
48+
49+
50+
class TestSilenceResourceWarning(unittest.TestCase):
51+
def test_sets_returncode_to_zero(self):
52+
mock_popen = MagicMock()
53+
mock_popen.returncode = None
54+
_silence_resource_warning(mock_popen)
55+
self.assertEqual(0, mock_popen.returncode)
56+
57+
def test_no_op_for_none(self):
58+
# Should not raise when popen is None (mongocryptd spawn failed).
59+
_silence_resource_warning(None)
60+
61+
62+
@unittest.skipIf(sys.platform == "win32", "Unix only")
63+
class TestSpawnUnix(unittest.TestCase):
64+
def setUp(self):
65+
from pymongo.daemon import _spawn
66+
67+
self._spawn = _spawn
68+
69+
def test_returns_popen_on_success(self):
70+
mock_popen = MagicMock()
71+
with patch("subprocess.Popen", return_value=mock_popen):
72+
result = self._spawn(["somecommand"])
73+
self.assertIs(mock_popen, result)
74+
75+
def test_filenotfound_warns_and_returns_none(self):
76+
with patch("subprocess.Popen", side_effect=FileNotFoundError("not found")):
77+
with warnings.catch_warnings(record=True) as w:
78+
warnings.simplefilter("always")
79+
result = self._spawn(["nonexistent_command"])
80+
self.assertIsNone(result)
81+
self.assertEqual(1, len(w))
82+
self.assertIs(RuntimeWarning, w[0].category)
83+
self.assertIn("nonexistent_command", str(w[0].message))
84+
85+
86+
@unittest.skipIf(sys.platform == "win32", "Unix only")
87+
class TestSpawnDaemonDoublePopen(unittest.TestCase):
88+
def setUp(self):
89+
from pymongo.daemon import _spawn_daemon_double_popen
90+
91+
self._spawn_daemon_double_popen = _spawn_daemon_double_popen
92+
93+
def test_spawns_this_file_as_intermediate(self):
94+
mock_popen = MagicMock()
95+
mock_popen.wait.return_value = 0
96+
with patch("subprocess.Popen", return_value=mock_popen) as mock_cls:
97+
self._spawn_daemon_double_popen(["somecommand", "--arg"])
98+
spawner_args = mock_cls.call_args[0][0]
99+
self.assertEqual(sys.executable, spawner_args[0])
100+
self.assertIn("daemon.py", spawner_args[1])
101+
self.assertIn("somecommand", spawner_args)
102+
103+
def test_waits_for_intermediate_process(self):
104+
mock_popen = MagicMock()
105+
with patch("subprocess.Popen", return_value=mock_popen):
106+
self._spawn_daemon_double_popen(["somecommand"])
107+
mock_popen.wait.assert_called_once_with(timeout=daemon_module._WAIT_TIMEOUT)
108+
109+
def test_continues_on_timeout(self):
110+
# _popen_wait swallows TimeoutExpired — double Popen must not raise.
111+
mock_popen = MagicMock()
112+
mock_popen.wait.side_effect = subprocess.TimeoutExpired(cmd="foo", timeout=10)
113+
with patch("subprocess.Popen", return_value=mock_popen):
114+
self._spawn_daemon_double_popen(["somecommand"]) # must not raise
115+
116+
117+
@unittest.skipIf(sys.platform == "win32", "Unix only")
118+
class TestSpawnDaemonUnix(unittest.TestCase):
119+
def test_uses_double_popen_when_executable_set(self):
120+
with patch("pymongo.daemon._spawn_daemon_double_popen") as mock_double:
121+
_spawn_daemon(["somecommand"])
122+
mock_double.assert_called_once_with(["somecommand"])
123+
124+
def test_fallback_to_spawn_when_no_executable(self):
125+
with patch("pymongo.daemon._spawn") as mock_spawn:
126+
with patch.object(sys, "executable", ""):
127+
_spawn_daemon(["somecommand"])
128+
mock_spawn.assert_called_once_with(["somecommand"])
129+
130+
131+
@unittest.skipUnless(sys.platform == "win32", "Windows only")
132+
class TestSpawnDaemonWindows(unittest.TestCase):
133+
def test_silences_resource_warning_on_success(self):
134+
mock_popen = MagicMock()
135+
with patch("subprocess.Popen", return_value=mock_popen):
136+
_spawn_daemon(["somecommand"])
137+
self.assertEqual(0, mock_popen.returncode)
138+
139+
def test_filenotfound_warns(self):
140+
with patch("subprocess.Popen", side_effect=FileNotFoundError("not found")):
141+
with warnings.catch_warnings(record=True) as w:
142+
warnings.simplefilter("always")
143+
_spawn_daemon(["nonexistent_command"])
144+
self.assertEqual(1, len(w))
145+
self.assertIs(RuntimeWarning, w[0].category)
146+
self.assertIn("nonexistent_command", str(w[0].message))
147+
148+
def test_uses_detached_process_flag(self):
149+
# DETACHED_PROCESS must be passed so the child survives parent exit.
150+
mock_popen = MagicMock()
151+
with patch("subprocess.Popen", return_value=mock_popen) as mock_cls:
152+
_spawn_daemon(["somecommand"])
153+
kwargs = mock_cls.call_args[1]
154+
self.assertEqual(daemon_module._DETACHED_PROCESS, kwargs["creationflags"])
155+
156+
def test_uses_devnull_for_stdio(self):
157+
# stdin/stdout/stderr must be redirected to devnull to fully detach.
158+
mock_popen = MagicMock()
159+
with patch("subprocess.Popen", return_value=mock_popen) as mock_cls:
160+
_spawn_daemon(["somecommand"])
161+
kwargs = mock_cls.call_args[1]
162+
self.assertIsNotNone(kwargs.get("stdin"))
163+
self.assertIsNotNone(kwargs.get("stdout"))
164+
self.assertIsNotNone(kwargs.get("stderr"))
165+
166+
def test_detached_process_constant_value(self):
167+
# Value must match the Windows DETACHED_PROCESS process creation flag.
168+
self.assertEqual(0x00000008, daemon_module._DETACHED_PROCESS)
169+
170+
171+
@unittest.skipIf(sys.platform == "win32", "Unix only")
172+
class TestMainBlock(unittest.TestCase):
173+
def test_exits_with_zero(self):
174+
# Run daemon.py as a script with a no-op subprocess; verify it exits cleanly.
175+
result = subprocess.run(
176+
[sys.executable, "-m", "pymongo.daemon", sys.executable, "-c", "pass"],
177+
timeout=15,
178+
)
179+
self.assertEqual(0, result.returncode)
180+
181+
182+
if __name__ == "__main__":
183+
unittest.main()

0 commit comments

Comments
 (0)