diff --git a/lib/debug/server_dap.rb b/lib/debug/server_dap.rb index 8dbefda8a..87c22ca45 100644 --- a/lib/debug/server_dap.rb +++ b/lib/debug/server_dap.rb @@ -788,7 +788,9 @@ def value_inspect obj, short: true def dap_eval b, expr, _context, prompt: '(repl_eval)' begin - b.eval(expr.to_s, prompt) + tp_allow_reentry do + b.eval(expr.to_s, prompt) + end rescue Exception => e e end diff --git a/test/protocol/break_test.rb b/test/protocol/break_test.rb index 30ad2025d..a78f70ebd 100644 --- a/test/protocol/break_test.rb +++ b/test/protocol/break_test.rb @@ -83,4 +83,56 @@ def test_break_stops_at_the_extra_file end end end + + class NestedBreakTest < ProtocolTestCase + PROGRAM = <<~RUBY + 1| def foo(x) + 2| x + 3| end + 4| + 5| foo("foo") + RUBY + + def test_breakpoint_can_be_triggered_inside_suspenssion + run_protocol_scenario PROGRAM, cdp: false do + req_add_breakpoint 2 + req_continue + assert_line_num 2 + + assert_locals_result( + [ + { name: "%self", value: "main", type: "Object" }, + { name: "x", value: "foo", type: "String" }, + ] + ) + + # Only if TracePoint.allow_reentry is available, we can trigger TracePoint events + # inside another TracePoint event, which is essential for nested breakpoints. + if TracePoint.respond_to? :allow_reentry + evaluate("foo('bar')") + + assert_line_num 2 + assert_locals_result( + [ + { name: "%self", value: "main", type: "Object" }, + { name: "x", value: "bar", type: "String" }, + ] + ) + end + + req_terminate_debuggee + end + end + + private + + def evaluate(expression) + res = send_dap_request 'stackTrace', + threadId: 1, + startFrame: 0, + levels: 20 + f_id = res.dig(:body, :stackFrames, 0, :id) + send_request 'evaluate', expression: expression, frameId: f_id, context: "repl" + end + end end