test_dap_step_operations.py•10.8 kB
"""
Integration tests for DAP step operations (step_in, step_over, step_out).
Tests Phase 3 of the DAP integration proposal:
- Step into function calls
- Step over lines
- Step out of functions
"""
import sys
import tempfile
from pathlib import Path
import pytest
from mcp_debug_tool.dap_wrapper import DAPSyncWrapper
@pytest.fixture
def step_test_script():
"""Create a Python script for testing step operations."""
code = '''def add(a, b):
result = a + b
return result
def multiply(x, y):
product = x * y
return product
x = 5
y = 10
sum_result = add(x, y)
mul_result = multiply(x, y)
print(f"Sum: {sum_result}, Product: {mul_result}")
'''
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(code)
script_path = Path(f.name)
yield script_path
# Cleanup
script_path.unlink(missing_ok=True)
@pytest.fixture
def nested_function_script():
"""Create a script with nested functions for testing step_out."""
code = '''def outer():
x = 1
y = inner()
return x + y
def inner():
a = 10
b = 20
return a + b
result = outer()
print(result)
'''
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(code)
script_path = Path(f.name)
yield script_path
# Cleanup
script_path.unlink(missing_ok=True)
def test_step_over_basic(step_test_script):
"""
Test step_over: execute current line and stop at next line.
Steps:
1. Set breakpoint at line 9 (x = 5)
2. Hit breakpoint
3. Step over to line 10 (y = 10)
4. Verify we're at line 10 with x = 5
"""
python_path = sys.executable
with DAPSyncWrapper() as wrapper:
# Initialize and launch
wrapper.initialize_and_launch(
python_path=python_path,
script_path=step_test_script,
args=[],
env=None,
cwd=step_test_script.parent,
)
# Run to first breakpoint at line 9 (x = 5)
response = wrapper.run_to_breakpoint(
file=str(step_test_script),
line=9,
timeout=10.0
)
assert response.hit, f"Breakpoint not hit: {response.error}"
assert response.frameInfo.line == 9
# Step over to line 10 (y = 10)
step_response = wrapper.step_over(timeout=10.0)
assert step_response.hit, f"Step over failed: {step_response.error}"
assert step_response.frameInfo.line == 10, f"Expected line 10, got {step_response.frameInfo.line}"
# Verify x variable exists and equals 5
assert step_response.locals is not None
assert 'x' in step_response.locals
assert step_response.locals['x']['repr'] == '5'
def test_step_in_to_function(step_test_script):
"""
Test step_in: step into a function call.
Steps:
1. Set breakpoint at line 11 (sum_result = add(x, y))
2. Hit breakpoint
3. Step in - should enter add() function at line 2
4. Verify we're inside add() with parameters a=5, b=10
"""
python_path = sys.executable
with DAPSyncWrapper() as wrapper:
# Initialize and launch
wrapper.initialize_and_launch(
python_path=python_path,
script_path=step_test_script,
args=[],
env=None,
cwd=step_test_script.parent,
)
# Run to breakpoint at line 11 (sum_result = add(x, y))
response = wrapper.run_to_breakpoint(
file=str(step_test_script),
line=11,
timeout=10.0
)
assert response.hit, f"Breakpoint not hit: {response.error}"
assert response.frameInfo.line == 11
# Step in to add() function
step_response = wrapper.step_in(timeout=10.0)
assert step_response.hit, f"Step in failed: {step_response.error}"
assert step_response.frameInfo.line == 2, f"Expected line 2 (inside add), got {step_response.frameInfo.line}"
# Verify we're inside add() with correct parameters
assert step_response.locals is not None
assert 'a' in step_response.locals
assert 'b' in step_response.locals
assert step_response.locals['a']['repr'] == '5'
assert step_response.locals['b']['repr'] == '10'
def test_step_out_from_function(nested_function_script):
"""
Test step_out: step out of current function to caller.
Steps:
1. Set breakpoint at line 10 (result = outer())
2. Step in to outer() (line 2)
3. Step in to inner() (line 7)
4. Step out back to outer() (should be at line 3 after inner() returns)
5. Verify we're back in outer() with y set to inner's return value (30)
"""
python_path = sys.executable
with DAPSyncWrapper() as wrapper:
# Initialize and launch
wrapper.initialize_and_launch(
python_path=python_path,
script_path=nested_function_script,
args=[],
env=None,
cwd=nested_function_script.parent,
)
# Run to breakpoint at line 10 (result = outer())
response = wrapper.run_to_breakpoint(
file=str(nested_function_script),
line=10,
timeout=10.0
)
assert response.hit, f"Breakpoint not hit: {response.error}"
# Step in to outer()
step_response1 = wrapper.step_in(timeout=10.0)
assert step_response1.hit, f"Step in to outer failed: {step_response1.error}"
# debugpy may stop at line 2 (x = 1) or line 3 (y = inner()) depending on optimization
assert step_response1.frameInfo.line in [2, 3], f"Expected line 2 or 3, got {step_response1.frameInfo.line}"
# If we're at line 2, step over to line 3
if step_response1.frameInfo.line == 2:
step_response2 = wrapper.step_over(timeout=10.0)
assert step_response2.hit
assert step_response2.frameInfo.line == 3 # y = inner()
else:
# Already at line 3
step_response2 = step_response1
# Step in to inner() - or it may already be executed
step_response3 = wrapper.step_in(timeout=10.0)
assert step_response3.hit, f"Step in to inner failed: {step_response3.error}"
# Check if we're inside inner() or back in outer()
# debugpy behavior varies: may enter inner() at line 7, or may complete inner() and return
if step_response3.frameInfo.line == 7:
# Inside inner() - step out back to outer()
step_response4 = wrapper.step_out(timeout=10.0)
assert step_response4.hit, f"Step out failed: {step_response4.error}"
# Should be back in outer()
assert step_response4.frameInfo.line in [3, 4], f"Expected line 3 or 4, got {step_response4.frameInfo.line}"
final_response = step_response4
else:
# Already back in outer() (inner() executed completely)
assert step_response3.frameInfo.line in [3, 4], f"Expected line 3 or 4, got {step_response3.frameInfo.line}"
final_response = step_response3
# Verify we're in outer() scope (should have x and y variables)
assert final_response.locals is not None
assert 'x' in final_response.locals
# y should be set to 30 (return value of inner())
assert 'y' in final_response.locals
assert final_response.locals['y']['repr'] == '30'
def test_step_over_without_function_call(step_test_script):
"""
Test step_over on a line without function call (simple statement).
Should behave the same as step_in in this case.
"""
python_path = sys.executable
with DAPSyncWrapper() as wrapper:
# Initialize and launch
wrapper.initialize_and_launch(
python_path=python_path,
script_path=step_test_script,
args=[],
env=None,
cwd=step_test_script.parent,
)
# Run to breakpoint at line 9 (x = 5)
response = wrapper.run_to_breakpoint(
file=str(step_test_script),
line=9,
timeout=10.0
)
assert response.hit
# Step over line 9 (x = 5) to line 10 (y = 10)
step_response1 = wrapper.step_over(timeout=10.0)
assert step_response1.hit
assert step_response1.frameInfo.line == 10
# Step over line 10 (y = 10) to line 11 (sum_result = add(x, y))
step_response2 = wrapper.step_over(timeout=10.0)
assert step_response2.hit
assert step_response2.frameInfo.line == 11
def test_multiple_steps_sequence(step_test_script):
"""
Test a sequence of mixed step operations.
Demonstrates realistic debugging workflow:
1. Break at line 9
2. Step over x = 5 (to line 10)
3. Step over y = 10 (to line 11)
4. Step in to add() (to line 2)
5. Step over result = a + b (to line 3)
6. Step out back to main (to line 12)
"""
python_path = sys.executable
with DAPSyncWrapper() as wrapper:
# Initialize and launch
wrapper.initialize_and_launch(
python_path=python_path,
script_path=step_test_script,
args=[],
env=None,
cwd=step_test_script.parent,
)
# 1. Break at line 9 (x = 5)
response = wrapper.run_to_breakpoint(
file=str(step_test_script),
line=9,
timeout=10.0
)
assert response.hit
assert response.frameInfo.line == 9
# 2. Step over to line 10 (y = 10)
r1 = wrapper.step_over(timeout=10.0)
assert r1.hit and r1.frameInfo.line == 10
# 3. Step over to line 11 (sum_result = add(x, y))
r2 = wrapper.step_over(timeout=10.0)
assert r2.hit and r2.frameInfo.line == 11
# 4. Step in to add() (should enter at line 2)
r3 = wrapper.step_in(timeout=10.0)
assert r3.hit and r3.frameInfo.line == 2
# 5. Step over inside add() to line 3 (return result)
r4 = wrapper.step_over(timeout=10.0)
assert r4.hit and r4.frameInfo.line == 3
# 6. Step out back to main (should be at line 11 or 12)
r5 = wrapper.step_out(timeout=10.0)
assert r5.hit
# Should be at line 12 (after add() call returns) or line 11
assert r5.frameInfo.line in [11, 12], f"Expected line 11 or 12, got {r5.frameInfo.line}"
if __name__ == "__main__":
# Run tests with pytest
pytest.main([__file__, "-v", "-s"])