Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions lib/debug/server_dap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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'
Expand Down
49 changes: 49 additions & 0 deletions test/protocol/catch_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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