Skip to content

Commit d676339

Browse files
committed
Add the new test framework for DAP
1 parent 2875447 commit d676339

File tree

6 files changed

+364
-1
lines changed

6 files changed

+364
-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

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ gem "rake"
66
gem "rake-compiler"
77
gem "test-unit", "~> 3.0"
88
gem "test-unit-rr"
9+
gem "json-schema"

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

0 commit comments

Comments
 (0)