Skip to content

Commit 2cba6e9

Browse files
committed
Add the new test framework for DAP
1 parent d6de653 commit 2cba6e9

File tree

5 files changed

+356
-1
lines changed

5 files changed

+356
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
/Gemfile.lock
1111
/lib/debug/debug.so
1212
.ruby-version
13+
/debugAdapterProtocol.json

lib/debug/server_dap.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ def dap_setup bytes
145145
def send **kw
146146
kw[:seq] = @seq += 1
147147
str = JSON.dump(kw)
148-
show_protocol '<', str
149148
@sock.write "Content-Length: #{str.bytesize}\r\n\r\n#{str}"
149+
show_protocol '<', str
150150
end
151151

152152
def send_response req, success: true, message: nil, **kw

test/support/assertions.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ def assert_line_num(expected)
1212
expected == test_info.internal_info['line']
1313
end
1414
})
15+
when 'vscode'
16+
send_request 'stackTrace',
17+
threadId: 1,
18+
startFrame: 0,
19+
levels: 20
20+
res = find_crt_response
21+
failure_msg = FailureMessage.new{create_protocol_message "result:\n#{JSON.pretty_generate res}"}
22+
result = res.dig(:body, :stackFrames, 0, :line)
23+
assert_equal expected, result, failure_msg
1524
else
1625
raise 'Invalid environment variable'
1726
end

test/support/protocol_utils.rb

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
# frozen_string_literal: true
2+
3+
require 'json'
4+
require 'net/http'
5+
require 'uri'
6+
7+
module DEBUGGER__
8+
module Protocol_TestUtils
9+
class Detach < StandardError
10+
end
11+
12+
DAP_JSON_PATH = "#{__dir__}/../../debugAdapterProtocol.json"
13+
14+
# API
15+
16+
def run_protocol_scenario program, &scenario
17+
ENV['RUBY_DEBUG_TEST_PROTOCOL_MODE'] = 'true'
18+
ENV['RUBY_DEBUG_TEST_UI'] = 'vscode'
19+
20+
write_temp_file(strip_line_num(program))
21+
22+
@remote_info = setup_unix_domain_socket_remote_debuggee
23+
fetch_dap_json
24+
@protocol = JSON.parse(File.read(DAP_JSON_PATH), symbolize_names: true)
25+
@bps_map = {} # {path: [lineno, condition], ...}
26+
@res_backlog = []
27+
@queue = Queue.new
28+
@backlog = []
29+
30+
attach_to_rdbg
31+
scenario.call
32+
33+
check_line_num!(program)
34+
flunk create_protocol_message "Expected the debuggee program to finish" unless wait_pid @remote_info.pid, TIMEOUT_SEC
35+
ensure
36+
@reader_thread.kill
37+
@sock.close
38+
@remote_info.reader_thread.kill
39+
@remote_info.r.close
40+
@remote_info.w.close
41+
end
42+
43+
def req_set_breakpoints lineno, path: temp_file_path, cond: nil
44+
if @bps_map[path].nil?
45+
@bps_map[path] = []
46+
end
47+
@bps_map[path] << [lineno, cond]
48+
@bps_map.each{|tar_path, bps|
49+
send_request 'setBreakpoints',
50+
source: {
51+
name: tar_path,
52+
path: tar_path,
53+
sourceReference: nil
54+
},
55+
breakpoints: bps.map{|lineno, condition|
56+
{
57+
line: lineno,
58+
condition: condition
59+
}
60+
}
61+
res = find_crt_response
62+
assert_dap_res :SetBreakpointsResponse, res
63+
}
64+
end
65+
66+
def req_continue
67+
send_request 'continue',
68+
threadId: 1
69+
res = find_crt_response
70+
assert_dap_res :ContinueResponse, res
71+
end
72+
73+
def req_step
74+
send_request 'stepIn',
75+
threadId: 1
76+
res = find_crt_response
77+
assert_dap_res :StepInResponse, res
78+
end
79+
80+
def req_next
81+
send_request 'next',
82+
threadId: 1
83+
res = find_crt_response
84+
assert_dap_res :NextResponse, res
85+
end
86+
87+
def req_finish
88+
send_request 'stepOut',
89+
threadId: 1
90+
res = find_crt_response
91+
assert_dap_res :StepOutResponse, res
92+
end
93+
94+
def req_set_exception_breakpoints
95+
send_request 'setExceptionBreakpoints',
96+
filters: [],
97+
filterOptions: [
98+
{
99+
filterId: 'RuntimeError'
100+
}
101+
]
102+
res = find_crt_response
103+
assert_dap_res :SetExceptionBreakpointsResponse, res
104+
end
105+
106+
def req_step_back
107+
send_request 'stepBack',
108+
threadId: 1
109+
# TODO: Return the response for "stepBack"
110+
# res = find_crt_response
111+
# assert_dap_res :StepBackResponse, res
112+
end
113+
114+
def req_terminate_debuggee
115+
send_request 'disconnect',
116+
restart: true,
117+
terminateDebuggee: true
118+
assert_disconnect_result
119+
end
120+
121+
def assert_reattach
122+
req_disconnect
123+
attach_to_rdbg
124+
res = find_crt_response
125+
result_cmd = res.dig(:command)
126+
assert_equal 'configurationDone', result_cmd
127+
end
128+
129+
def assert_hover_result expected, expression: nil, frame_idx: 0
130+
assert_eval_result 'hover', expression, expected, frame_idx
131+
end
132+
133+
def assert_repl_result expected, expression: nil, frame_idx: 0
134+
assert_eval_result 'repl', expression, expected, frame_idx
135+
end
136+
137+
def assert_watch_result expected, expression: nil, frame_idx: 0
138+
assert_eval_result 'watch', expression, expected, frame_idx
139+
end
140+
141+
# Not API
142+
143+
def req_disconnect
144+
send_request 'disconnect',
145+
restart: false,
146+
terminateDebuggee: false
147+
assert_disconnect_result
148+
end
149+
150+
def assert_disconnect_result
151+
res = find_crt_response
152+
assert_dap_res :DisconnectResponse, res
153+
@reader_thread.raise Detach
154+
@sock.close
155+
end
156+
157+
def attach_to_rdbg
158+
@sock = Socket.unix @remote_info.sock_path
159+
@seq = 1
160+
@reader_thread = Thread.new do
161+
while res = recv_response
162+
@queue.push res
163+
end
164+
rescue Detach
165+
end
166+
sleep 0.001 while @reader_thread.status != 'sleep'
167+
@reader_thread.run
168+
INITIALIZE_DAP_MSGS.each{|msg| send **msg}
169+
end
170+
171+
def assert_eval_result context, expression, expected, frame_idx
172+
send_request 'stackTrace',
173+
threadId: 1,
174+
startFrame: 0,
175+
levels: 20
176+
res = find_crt_response
177+
f_id = res.dig(:body, :stackFrames, frame_idx, :id)
178+
send_request 'evaluate',
179+
expression: expression,
180+
frameId: f_id,
181+
context: context
182+
res = find_crt_response
183+
assert_dap_res :EvaluateResponse, res
184+
185+
failure_msg = FailureMessage.new{create_protocol_message "result:\n#{JSON.pretty_generate res}"}
186+
if expected.is_a? String
187+
expected_val = expected
188+
else
189+
expected_val = expected.inspect
190+
end
191+
result_val = res.dig(:body, :result)
192+
assert_equal expected_val, result_val, failure_msg
193+
194+
expected_type = expected.class.inspect
195+
result_type = res.dig(:body, :type)
196+
assert_equal expected_type, result_type, failure_msg
197+
end
198+
199+
def send_request command, **kw
200+
send type: 'request',
201+
command: command,
202+
arguments: kw
203+
end
204+
205+
def send **kw
206+
kw[:seq] = @seq += 1
207+
str = JSON.dump(kw)
208+
@sock.write "Content-Length: #{str.bytesize}\r\n\r\n#{str}"
209+
@backlog << "V>D #{str}"
210+
end
211+
212+
TIMEOUT_SEC = (ENV['RUBY_DEBUG_TIMEOUT_SEC'] || 10).to_i
213+
214+
def assert_dap_res expected_def, result_res
215+
properties = @protocol.dig(:definitions, expected_def, :allOf, 1)
216+
failure_msg = FailureMessage.new{create_protocol_message "expected:\n#{JSON.pretty_generate properties}\n\nresult:\n#{JSON.pretty_generate result_res}"}
217+
patterns = DAP_ResponsePattern.new.get properties
218+
patterns.each{|keys, expected|
219+
result = result_res.dig(*keys)
220+
assert_kind_of expected, result, failure_msg
221+
}
222+
end
223+
224+
def fetch_dap_json
225+
return if File.exist? DAP_JSON_PATH
226+
227+
json = Net::HTTP.get(URI.parse('https://microsoft.github.io/debug-adapter-protocol/debugAdapterProtocol.json'))
228+
File.write DAP_JSON_PATH, json
229+
end
230+
231+
def find_crt_response
232+
Timeout.timeout(TIMEOUT_SEC) do
233+
loop do
234+
res = @queue.pop
235+
str = JSON.dump(res)
236+
@backlog << "V<D #{str}"
237+
if res[:request_seq] == @seq
238+
return res
239+
end
240+
end
241+
end
242+
rescue Timeout::Error
243+
flunk create_protocol_message "TIMEOUT ERROR (#{TIMEOUT_SEC} sec) while waiting seq: #{@seq}"
244+
end
245+
246+
# FIXME: Commonalize this method.
247+
def create_protocol_message fail_msg
248+
all_protocol_msg = <<~DEBUGGER_MSG.chomp
249+
-------------------------
250+
| All Protocol Messages |
251+
-------------------------
252+
253+
#{@backlog.join("\n")}
254+
DEBUGGER_MSG
255+
256+
last_msg = @backlog.reverse[0..3].map{|m|
257+
h = m.sub(/(D|V)(<|>)(D|V)\s/, '')
258+
JSON.pretty_generate(JSON.parse h)
259+
}.reverse.join("\n")
260+
261+
last_protocol_msg = <<~DEBUGGER_MSG.chomp
262+
--------------------------
263+
| Last Protocol Messages |
264+
--------------------------
265+
266+
#{last_msg}
267+
DEBUGGER_MSG
268+
269+
debuggee_msg =
270+
<<~DEBUGGEE_MSG.chomp
271+
--------------------
272+
| Debuggee Session |
273+
--------------------
274+
275+
> #{@remote_info.debuggee_backlog.join('> ')}
276+
DEBUGGEE_MSG
277+
278+
failure_msg = <<~FAILURE_MSG.chomp
279+
-------------------
280+
| Failure Message |
281+
-------------------
282+
283+
#{fail_msg}
284+
FAILURE_MSG
285+
286+
<<~MSG.chomp
287+
#{all_protocol_msg}
288+
289+
#{last_protocol_msg}
290+
291+
#{debuggee_msg}
292+
293+
#{failure_msg}
294+
MSG
295+
end
296+
297+
# FIXME: Commonalize this method.
298+
def recv_response
299+
case header = @sock.gets
300+
when /Content-Length: (\d+)/
301+
b = @sock.read(2)
302+
raise b.inspect unless b == "\r\n"
303+
304+
l = @sock.read $1.to_i
305+
JSON.parse l, symbolize_names: true
306+
when nil
307+
nil
308+
when /out/, /input/
309+
recv_response
310+
else
311+
raise "unrecognized line: #{header}"
312+
end
313+
end
314+
end
315+
316+
class DAP_ResponsePattern
317+
OBJ_MAP = {'object' => Object, 'string' => String, 'array' => Array, 'integer' => Integer, 'boolean' => [TrueClass, FalseClass]}
318+
319+
def initialize
320+
@keys = []
321+
@results = []
322+
end
323+
324+
def get props
325+
fotmat_res props
326+
@results
327+
end
328+
329+
private
330+
331+
def fotmat_res properties
332+
return unless required = properties.dig(:required)
333+
334+
required.each{|r|
335+
@keys << r.to_sym
336+
type = properties.dig(:properties, r.to_sym, :type)
337+
@results << [@keys.dup, OBJ_MAP[type]]
338+
fotmat_res properties[:properties][r.to_sym]
339+
@keys.pop
340+
}
341+
end
342+
end
343+
end

test/support/test_case.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
require_relative 'utils'
88
require_relative 'dap_utils'
9+
require_relative 'protocol_utils'
910
require_relative 'assertions'
1011

1112
module DEBUGGER__
1213
class TestCase < Test::Unit::TestCase
1314
include TestUtils
1415
include DAP_TestUtils
16+
include Protocol_TestUtils
1517
include AssertionHelpers
1618

1719
def setup

0 commit comments

Comments
 (0)