Skip to content

Commit e61cc31

Browse files
committed
Add some basic tests
1 parent baaf28a commit e61cc31

File tree

2 files changed

+348
-1
lines changed

2 files changed

+348
-1
lines changed

Lib/test/test_pyclbr.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,8 @@ def test_others(self):
253253
cm(
254254
'pdb',
255255
# pyclbr does not handle elegantly `typing` or properties
256-
ignore=('Union', '_ModuleTarget', '_ScriptTarget', '_ZipTarget', 'curframe_locals'),
256+
ignore=('Union', '_ModuleTarget', '_ScriptTarget', '_ZipTarget', 'curframe_locals',
257+
'_InteractState'),
257258
)
258259
cm('pydoc', ignore=('input', 'output',)) # properties
259260

Lib/test/test_remote_pdb.py

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
import io
2+
import json
3+
import os
4+
import signal
5+
import socket
6+
import subprocess
7+
import sys
8+
import tempfile
9+
import threading
10+
import unittest
11+
import unittest.mock
12+
from contextlib import contextmanager
13+
from pathlib import Path
14+
from test.support import os_helper
15+
from test.support.os_helper import temp_dir, TESTFN, unlink
16+
from typing import Dict, List, Optional, Tuple, Union, Any
17+
18+
import pdb
19+
from pdb import _RemotePdb, _PdbClient, _InteractState
20+
21+
22+
class MockSocketFile:
23+
"""Mock socket file for testing _RemotePdb without actual socket connections."""
24+
25+
def __init__(self):
26+
self.input_queue = []
27+
self.output_buffer = []
28+
29+
def write(self, data: bytes) -> None:
30+
"""Simulate write to socket."""
31+
self.output_buffer.append(data)
32+
33+
def flush(self) -> None:
34+
"""No-op flush implementation."""
35+
pass
36+
37+
def readline(self) -> bytes:
38+
"""Read a line from the prepared input queue."""
39+
if not self.input_queue:
40+
return b""
41+
return self.input_queue.pop(0)
42+
43+
def close(self) -> None:
44+
"""Close the mock socket file."""
45+
pass
46+
47+
def add_input(self, data: dict) -> None:
48+
"""Add input that will be returned by readline."""
49+
self.input_queue.append(json.dumps(data).encode() + b"\n")
50+
51+
def get_output(self) -> List[dict]:
52+
"""Get the output that was written by the object being tested."""
53+
results = []
54+
for data in self.output_buffer:
55+
if isinstance(data, bytes) and data.endswith(b"\n"):
56+
try:
57+
results.append(json.loads(data.decode().strip()))
58+
except json.JSONDecodeError:
59+
pass # Ignore non-JSON output
60+
self.output_buffer = []
61+
return results
62+
63+
64+
class RemotePdbTestCase(unittest.TestCase):
65+
"""Tests for the _RemotePdb class."""
66+
67+
def setUp(self):
68+
self.sockfile = MockSocketFile()
69+
self.pdb = _RemotePdb(self.sockfile)
70+
71+
# Create a frame for testing
72+
self.test_globals = {'a': 1, 'b': 2, '__pdb_convenience_variables': {'x': 100}}
73+
self.test_locals = {'c': 3, 'd': 4}
74+
75+
# Create a simple test frame
76+
frame_info = unittest.mock.Mock()
77+
frame_info.f_globals = self.test_globals
78+
frame_info.f_locals = self.test_locals
79+
frame_info.f_lineno = 42
80+
frame_info.f_code = unittest.mock.Mock()
81+
frame_info.f_code.co_filename = "test_file.py"
82+
frame_info.f_code.co_name = "test_function"
83+
84+
self.pdb.curframe = frame_info
85+
86+
def test_message_and_error(self):
87+
"""Test message and error methods send correct JSON."""
88+
self.pdb.message("Test message")
89+
self.pdb.error("Test error")
90+
91+
outputs = self.sockfile.get_output()
92+
self.assertEqual(len(outputs), 2)
93+
self.assertEqual(outputs[0], {"message": "Test message\n"})
94+
self.assertEqual(outputs[1], {"error": "Test error"})
95+
96+
def test_read_command(self):
97+
"""Test reading commands from the socket."""
98+
# Add test input
99+
self.sockfile.add_input({"command": "help"})
100+
101+
# Read the command
102+
cmd = self.pdb._read_command()
103+
self.assertEqual(cmd, "help")
104+
105+
def test_read_command_EOF(self):
106+
"""Test reading EOF command."""
107+
# Simulate socket closure
108+
self.pdb._write_failed = True
109+
cmd = self.pdb._read_command()
110+
self.assertEqual(cmd, "EOF")
111+
112+
def test_completion(self):
113+
"""Test handling completion requests."""
114+
# Mock completenames to return specific values
115+
with unittest.mock.patch.object(self.pdb, 'completenames',
116+
return_value=["continue", "clear"]):
117+
118+
# Add a completion request
119+
self.sockfile.add_input({
120+
"completion": {
121+
"text": "c",
122+
"line": "c",
123+
"begidx": 0,
124+
"endidx": 1
125+
}
126+
})
127+
128+
# Add a regular command to break the loop
129+
self.sockfile.add_input({"command": "help"})
130+
131+
# Read command - this should process the completion request first
132+
cmd = self.pdb._read_command()
133+
134+
# Verify completion response was sent
135+
outputs = self.sockfile.get_output()
136+
self.assertEqual(len(outputs), 1)
137+
self.assertEqual(outputs[0], {"completion": ["continue", "clear"]})
138+
139+
# The actual command should be returned
140+
self.assertEqual(cmd, "help")
141+
142+
def test_do_help(self):
143+
"""Test that do_help sends the help message."""
144+
self.pdb.do_help("break")
145+
146+
outputs = self.sockfile.get_output()
147+
self.assertEqual(len(outputs), 1)
148+
self.assertEqual(outputs[0], {"help": "break"})
149+
150+
def test_interact_mode(self):
151+
"""Test interaction mode setup and execution."""
152+
# First set up interact mode
153+
self.pdb.do_interact("")
154+
155+
# Verify _interact_state is properly initialized
156+
self.assertIsNotNone(self.pdb._interact_state)
157+
self.assertIsInstance(self.pdb._interact_state, _InteractState)
158+
159+
# Test running code in interact mode
160+
with unittest.mock.patch.object(self.pdb, '_error_exc') as mock_error:
161+
self.pdb._run_in_python_repl("print('test')")
162+
mock_error.assert_not_called()
163+
164+
# Test with syntax error
165+
self.pdb._run_in_python_repl("if:")
166+
mock_error.assert_called_once()
167+
168+
def test_do_commands(self):
169+
"""Test handling breakpoint commands."""
170+
# Mock get_bpbynumber
171+
with unittest.mock.patch.object(self.pdb, 'get_bpbynumber'):
172+
# Test command entry mode initiation
173+
self.pdb.do_commands("1")
174+
175+
outputs = self.sockfile.get_output()
176+
self.assertEqual(len(outputs), 1)
177+
self.assertIn("commands_entry", outputs[0])
178+
self.assertEqual(outputs[0]["commands_entry"]["bpnum"], 1)
179+
180+
# Test with commands
181+
self.pdb.do_commands("1\nsilent\nprint('hi')\nend")
182+
183+
# Should have set up the commands for bpnum 1
184+
self.assertEqual(self.pdb.commands_bnum, 1)
185+
self.assertIn(1, self.pdb.commands)
186+
self.assertEqual(len(self.pdb.commands[1]), 2) # silent and print
187+
188+
def test_detach(self):
189+
"""Test the detach method."""
190+
with unittest.mock.patch.object(self.sockfile, 'close') as mock_close:
191+
self.pdb.detach()
192+
mock_close.assert_called_once()
193+
self.assertFalse(self.pdb.quitting)
194+
195+
def test_cmdloop(self):
196+
"""Test the command loop with various commands."""
197+
# Mock onecmd to track command execution
198+
with unittest.mock.patch.object(self.pdb, 'onecmd', return_value=False) as mock_onecmd:
199+
# Add commands to the queue
200+
self.pdb.cmdqueue = ['help', 'list']
201+
202+
# Add a command from the socket for when cmdqueue is empty
203+
self.sockfile.add_input({"command": "next"})
204+
205+
# Add a second command to break the loop
206+
self.sockfile.add_input({"command": "quit"})
207+
208+
# Configure onecmd to exit the loop on "quit"
209+
def side_effect(line):
210+
return line == 'quit'
211+
mock_onecmd.side_effect = side_effect
212+
213+
# Run the command loop
214+
self.pdb.quitting = False # Set this by hand because we don't want to really call set_trace()
215+
self.pdb.cmdloop()
216+
217+
# Should have processed 4 commands: 2 from cmdqueue, 2 from socket
218+
self.assertEqual(mock_onecmd.call_count, 4)
219+
mock_onecmd.assert_any_call('help')
220+
mock_onecmd.assert_any_call('list')
221+
mock_onecmd.assert_any_call('next')
222+
mock_onecmd.assert_any_call('quit')
223+
224+
# Check if prompt was sent to client
225+
outputs = self.sockfile.get_output()
226+
prompts = [o for o in outputs if 'prompt' in o]
227+
self.assertEqual(len(prompts), 2) # Should have sent 2 prompts
228+
229+
230+
class PdbConnectTestCase(unittest.TestCase):
231+
"""Tests for the _connect mechanism using direct socket communication."""
232+
233+
def setUp(self):
234+
# Create a server socket that will wait for the debugger to connect
235+
self.server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
236+
self.server_sock.bind(('127.0.0.1', 0)) # Let OS assign port
237+
self.server_sock.listen(1)
238+
self.port = self.server_sock.getsockname()[1]
239+
240+
# Create a file for subprocess script
241+
self.script_path = TESTFN + "_connect_test.py"
242+
with open(self.script_path, 'w') as f:
243+
f.write(f"""
244+
import pdb
245+
import sys
246+
import time
247+
248+
def connect_to_debugger():
249+
# Create a frame to debug
250+
def dummy_function():
251+
x = 42
252+
# Call connect to establish connection with the test server
253+
frame = sys._getframe() # Get the current frame
254+
pdb._connect('127.0.0.1', {self.port}, frame)
255+
return x # This line should not be reached in debugging
256+
257+
return dummy_function()
258+
259+
result = connect_to_debugger()
260+
print(f"Function returned: {{result}}")
261+
""")
262+
263+
def tearDown(self):
264+
self.server_sock.close()
265+
try:
266+
unlink(self.script_path)
267+
except OSError:
268+
pass
269+
270+
def test_connect_and_basic_commands(self):
271+
"""Test connecting to a remote debugger and sending basic commands."""
272+
# Start the subprocess that will connect to our socket
273+
with subprocess.Popen(
274+
[sys.executable, self.script_path],
275+
stdout=subprocess.PIPE,
276+
stderr=subprocess.PIPE,
277+
text=True
278+
) as process:
279+
# Accept the connection from the subprocess
280+
client_sock, _ = self.server_sock.accept()
281+
client_file = client_sock.makefile('rwb')
282+
self.addCleanup(client_file.close)
283+
self.addCleanup(client_sock.close)
284+
285+
# We should receive initial data from the debugger
286+
data = client_file.readline()
287+
initial_data = json.loads(data.decode())
288+
self.assertIn('message', initial_data)
289+
self.assertIn('pdb._connect', initial_data['message'])
290+
291+
# First, look for command_list message
292+
data = client_file.readline()
293+
command_list = json.loads(data.decode())
294+
self.assertIn('command_list', command_list)
295+
296+
# Then, look for the first prompt
297+
data = client_file.readline()
298+
prompt_data = json.loads(data.decode())
299+
self.assertIn('prompt', prompt_data)
300+
self.assertEqual(prompt_data['mode'], 'pdb')
301+
302+
# Send 'bt' (backtrace) command
303+
client_file.write(json.dumps({"command": "bt"}).encode() + b"\n")
304+
client_file.flush()
305+
306+
# Check for response - we should get some stack frames
307+
# We may get multiple messages so we need to read until we get a new prompt
308+
got_stack_info = False
309+
text_msg = []
310+
while True:
311+
data = client_file.readline()
312+
if not data:
313+
break
314+
315+
msg = json.loads(data.decode())
316+
if 'message' in msg and 'connect_to_debugger' in msg['message']:
317+
got_stack_info = True
318+
text_msg.append(msg['message'])
319+
320+
if 'prompt' in msg:
321+
break
322+
323+
expected_stacks = [
324+
"<module>",
325+
"connect_to_debugger",
326+
]
327+
328+
for stack, msg in zip(expected_stacks, text_msg, strict=True):
329+
self.assertIn(stack, msg)
330+
331+
self.assertTrue(got_stack_info, "Should have received stack trace information")
332+
333+
# Send 'c' (continue) command to let the program finish
334+
client_file.write(json.dumps({"command": "c"}).encode() + b"\n")
335+
client_file.flush()
336+
337+
# Wait for process to finish
338+
stdout, _ = process.communicate(timeout=5)
339+
340+
# Check if we got the expected output
341+
self.assertIn("Function returned: 42", stdout)
342+
self.assertEqual(process.returncode, 0)
343+
344+
345+
if __name__ == "__main__":
346+
unittest.main()

0 commit comments

Comments
 (0)