diff --git a/lib/debug/server_dap.rb b/lib/debug/server_dap.rb index 907db2d1f..9388a3405 100644 --- a/lib/debug/server_dap.rb +++ b/lib/debug/server_dap.rb @@ -162,12 +162,12 @@ def dap_setup bytes }, ], supportsExceptionFilterOptions: true, + supportsExceptionOptions: true, supportsStepBack: true, supportsEvaluateForHovers: true, supportsCompletionsRequest: true, ## Will be supported - # supportsExceptionOptions: true, # supportsHitConditionalBreakpoints: # supportsSetVariable: true, # supportSuspendDebuggee: @@ -381,7 +381,13 @@ def process_request req } } - SESSION.clear_catch_breakpoints 'Exception', 'RuntimeError' + # Catch breakpoints from exceptionOptions are registered under + # arbitrary class names, so previously registered names have to be + # remembered to make setExceptionBreakpoints replace (not accumulate) + # exception breakpoints, as the DAP spec requires. + @exception_option_names ||= [] + SESSION.clear_catch_breakpoints 'Exception', 'RuntimeError', *@exception_option_names + @exception_option_names = [] filters = args.fetch('filters').map {|filter_id| process_filter.call(filter_id) @@ -391,6 +397,30 @@ def process_request req process_filter.call(bp_info['filterId'], bp_info['condition']) } + # DAP standard `exceptionOptions` (capability: supportsExceptionOptions). + # Each ExceptionOptions names specific exception classes via + # ExceptionPathSegment; matching uses the same ancestor class-name + # match as the console `catch` command, so subclasses are caught too. + filters += args.fetch('exceptionOptions', []).map{|opt| + names = opt.fetch('path', []).flat_map{|seg| seg['names'] || []} + + if opt.fetch('path', []).any?{|seg| seg['negate']} + { verified: false, message: 'negated exception path segments are not supported' } + elsif names.empty? + { verified: false, message: 'no exception class name given' } + elsif opt['breakMode'] == 'never' + { verified: true } + else + # Ruby's catch breakpoints fire when the exception is raised, so + # 'always', 'unhandled' and 'userUnhandled' all break at raise. + bps = names.map{|name| + @exception_option_names << name + SESSION.add_catch_breakpoint name + } + { verified: true, message: bps.map(&:inspect).join(', ') } + end + } + send_response req, breakpoints: filters when 'disconnect' diff --git a/test/protocol/catch_test.rb b/test/protocol/catch_test.rb index b1749b59e..40b325ec9 100644 --- a/test/protocol/catch_test.rb +++ b/test/protocol/catch_test.rb @@ -44,4 +44,53 @@ def test_set_exception_breakpoints_accepts_condition end end end + + class CatchExceptionOptionsTest < ProtocolTestCase + PROGRAM = <<~RUBY + 1| class MyError < StandardError; end + 2| class MySubError < MyError; end + 3| + 4| def foo + 5| raise MySubError, "foo" + 6| end + 7| + 8| foo + RUBY + + def test_exception_options_catches_a_specific_exception_class + run_protocol_scenario PROGRAM, cdp: false do + send_dap_request 'setExceptionBreakpoints', filters: [], + exceptionOptions: [{ path: [{ names: ["MyError"] }], breakMode: "always" }] + req_continue + assert_line_num 5 + req_terminate_debuggee + end + end + + def test_exception_options_with_break_mode_never_does_not_register_a_breakpoint + run_protocol_scenario PROGRAM, cdp: false do + send_dap_request 'setExceptionBreakpoints', filters: [], + exceptionOptions: [{ path: [{ names: ["MyError"] }], breakMode: "never" }] + req_terminate_debuggee + end + end + + def test_exception_options_breakpoints_are_replaced_by_the_next_request + run_protocol_scenario PROGRAM, cdp: false do + send_dap_request 'setExceptionBreakpoints', filters: [], + exceptionOptions: [{ path: [{ names: ["MyError"] }], breakMode: "always" }] + send_dap_request 'setExceptionBreakpoints', filters: [] + req_terminate_debuggee + end + end + + def test_exception_options_reports_unsupported_negated_segments + run_protocol_scenario PROGRAM, cdp: false do + res = send_dap_request 'setExceptionBreakpoints', filters: [], + exceptionOptions: [{ path: [{ negate: true, names: ["MyError"] }], breakMode: "always" }] + assert_equal false, res.dig(:body, :breakpoints, 0, :verified) + req_terminate_debuggee + end + end + end end