summaryrefslogtreecommitdiff
path: root/gdb/testsuite/lib/dap-support.exp
blob: 6bb9b6e6377d2dab6bebe97409e29d15217584b1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# Copyright 2022-2023 Free Software Foundation, Inc.

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# The JSON parser.
load_lib ton.tcl

# The sequence number for the next DAP request.  This is used by the
# automatic sequence-counting code below.  It is reset each time GDB
# is restarted.
set dap_seq 1

# Start gdb using the DAP interpreter.
proc dap_gdb_start {} {
    # Keep track of the number of times GDB has been launched.
    global gdb_instances
    incr gdb_instances

    gdb_stdin_log_init

    global GDBFLAGS stty_init
    save_vars { GDBFLAGS stty_init } {
	set stty_init "-echo raw"
	set logfile [standard_output_file "dap.log.$gdb_instances"]
	append GDBFLAGS " -iex \"set debug dap-log-file $logfile\" -q -i=dap"
	set res [gdb_spawn]
	if {$res != 0} {
	    return $res
	}
    }

    # Reset the counter.
    set ::dap_seq 1

    return 0
}

# A helper for dap_to_ton that decides if the list L is a JSON object
# or if it is an array.
proc _dap_is_obj {l} {
    if {[llength $l] % 2 != 0} {
	return 0
    }
    foreach {key value} $l {
	if {![string is alpha $key]} {
	    return 0
	}
    }
    return 1
}

# The "TON" format is a bit of a pain to write by hand, so this proc
# can be used to convert an ordinary Tcl list into TON by guessing at
# the correct forms to use.  This can't be used in all cases, because
# Tcl can't really differentiate between literal forms.  For example,
# there's no way to decide if "true" should be a string or the literal
# true.
#
# JSON objects must be passed in a particular form here -- as a list
# with an even number of elements, alternating keys and values.  Each
# key must consist only of letters, no digits or other non-letter
# characters.  Note that this is compatible with the Tcl 'dict'
# representation.
proc dap_to_ton {obj} {
    if {[string is list $obj] && [llength $obj] > 1} {
	if {[_dap_is_obj $obj]} {
	    set result o
	    foreach {key value} $obj {
		lappend result $key \[[dap_to_ton $value]\]
	    }
	} else {
	    set result a
	    foreach val $obj {
		lappend result \[[dap_to_ton $val]\]
	    }
	}
    } elseif {[string is entier $obj]} {
	set result [list i $obj]
    } elseif {[string is double $obj]} {
	set result [list d $obj]
    } elseif {$obj == "true" || $obj == "false" || $obj == "null"} {
	set result [list l $obj]
    } else {
	set result [list s $obj]
    }
    return $result
}

# Format the object OBJ, in TON format, as JSON and send it to gdb.
proc _dap_send_ton {obj} {
    set json [namespace eval ton::2json $obj]
    # FIXME this is wrong for non-ASCII characters.
    set len [string length $json]
    verbose -log ">>> $json"
    send_gdb "Content-Length: $len\r\n\r\n$json"
}

# Send a DAP request to gdb.  COMMAND is the request's "command"
# field, and OBJ is the "arguments" field.  If OBJ is empty, it is
# omitted.  The sequence number of the request is automatically added,
# and this is also the return value.  OBJ is assumed to already be in
# TON form.
proc _dap_send_request {command {obj {}}} {
    # We can construct this directly as a TON object.
    set result $::dap_seq
    incr ::dap_seq
    set req [format {o seq [i %d] type [s request] command [%s]} \
		 $result [list s $command]]
    if {$obj != ""} {
	append req " arguments \[$obj\]"
    }
    _dap_send_ton $req
    return $result
}

# Read a JSON response from gdb.  This will return a dict on
# success, or throw an exception on error.
proc _dap_read_json {} {
    set length ""
    gdb_expect {
	-re "^Content-Length: (\[0-9\]+)\r\n" {
	    set length $expect_out(1,string)
	    exp_continue
	}
	-re "^(\[^\r\n\]+)\r\n" {
	    # Any other header field.
	    exp_continue
	}
	-re "^\r\n" {
	    # Done.
	}
	timeout {
	    error "timeout reading json header"
	}
	eof {
	    error "eof reading json header"
	}
    }

    if {$length == ""} {
	error "didn't find content-length"
    }

    set json ""
    while {$length > 0} {
	# Tcl only allows up to 255 characters in a {} expression in a
	# regexp, so we may need to read in chunks.
	set this_len [expr {min ($length, 255)}]
	gdb_expect {
	    -re "^.{$this_len}" {
		append json $expect_out(0,string)
	    }
	    timeout {
		error "timeout reading json body"
	    }
	    eof {
		error "eof reading json body"
	    }
	}
	incr length -$this_len
    }

    set ton [ton::json2ton $json]
    return [namespace eval ton::2dict $ton]
}

# Read a sequence of JSON objects from gdb, until a response object is
# seen.  If the response object has the request sequence number NUM,
# and is for command CMD, return a list of two elements: the response
# object and a list of any preceding events, in the order they were
# emitted.  The objects are dicts.  If a response object is seen but has
# the wrong sequence number or command, throw an exception

proc _dap_read_response {cmd num} {
    set result {}
    while 1 {
	set d [_dap_read_json]
	if {[dict get $d type] == "response"} {
	    if {[dict get $d request_seq] != $num} {
		error "saw wrong request_seq in $obj"
	    } elseif {[dict get $d command] != $cmd} {
		error "saw wrong command in $obj"
	    } else {
		return [list $d $result]
	    }
	} else {
	    lappend result $d
	}
    }
}

# A wrapper for _dap_send_request and _dap_read_response.  This sends a
# request to gdb and returns the response as a dict.
proc dap_request_and_response {command {obj {}}} {
    set seq [_dap_send_request $command $obj]
    return [_dap_read_response $command $seq]
}

# Like dap_request_and_response, but also checks that the response
# indicates success.  NAME is used to issue a test result.
proc dap_check_request_and_response {name command {obj {}}} {
    set response_and_events [dap_request_and_response $command $obj]
    set response [lindex $response_and_events 0]
    if {[dict get $response success] != "true"} {
	verbose "request failure: $response"
	fail "$name success"
	return ""
    }
    pass "$name success"
    return $response_and_events
}

# Start gdb, send a DAP initialization request and return the
# response.  This approach lets the caller check the feature list, if
# desired.  Callers not caring about this should probably use
# dap_launch.  Returns the empty string on failure.  NAME is used as
# the test name.
proc _dap_initialize {name} {
    if {[dap_gdb_start]} {
	return ""
    }
    return [dap_check_request_and_response $name initialize]
}

# Start gdb, send a DAP initialize request, and then a launch request
# specifying FILE as the program to use for the inferior.  Returns the
# empty string on failure, or the response object from the launch
# request.  After this is called, gdb will be ready to accept
# breakpoint requests.  NAME is used as the test name.  It has a
# reasonable default but can be overridden in case a test needs to
# launch gdb more than once.
proc dap_launch {file {name startup}} {
    if {[_dap_initialize "$name - initialize"] == ""} {
	return ""
    }
    return [dap_check_request_and_response "$name - launch" launch \
		[format {o program [%s]} \
		     [list s [standard_output_file $file]]]]
}

# Cleanly shut down gdb.  NAME is used as the test name.
proc dap_shutdown {{name shutdown}} {
    dap_check_request_and_response $name disconnect
}

# Search the event list EVENTS for an output event matching the regexp
# RX.  Pass the test NAME if found, fail if not.
proc dap_search_output {name rx events} {
    foreach d $events {
	if {[dict get $d type] != "event"
	    || [dict get $d event] != "output"} {
	    continue
	}
	if {[regexp $rx [dict get $d body output]]} {
	    pass $name
	    return
	}
    }
    fail $name
}

# Check that D (a dict object) has values that match the
# key/value pairs given in ARGS.  NAME is used as the test name.
proc dap_match_values {name d args} {
    foreach {key value} $args {
	if {[eval dict get [list $d] $key] != $value} {
	    fail "$name (checking $key)"
	    return ""
	}
    }
    pass $name
}

# A helper for dap_wait_for_event_and_check that reads events, looking for one
# matching TYPE.
#
# Return a list of two items:
#
#  - the matched event
#  - a list of any JSON objects (events or others) seen before the matched
#    event.
proc _dap_wait_for_event { {type ""} } {
    set preceding [list]

    while 1 {
	# We don't do any extra error checking here for the time
	# being; we'll just get a timeout thrown instead.
	set d [_dap_read_json]
	if {[dict get $d type] == "event"
	    && ($type == "" || [dict get $d event] == $type)} {
	    return [list $d $preceding]
	}

	lappend preceding $d
    }
}

# Read JSON objects looking for an event whose "event" field is TYPE.
#
# NAME is used as the test name; it defaults to TYPE.  Extra arguments
# are used to check fields of the event; the arguments alternate
# between a field name (in "dict get" form) and its expected value.
#
# Return a list of two items:
#
#  - the matched event (regardless of whether it passed the field validation or
#    not)
#  - a list of any JSON objects (events or others) seen before the matched
#    event.
proc dap_wait_for_event_and_check {name type args} {
    if {$name == ""} {
	set name $type
    }

    set result [_dap_wait_for_event $type]
    set event [lindex $result 0]
    eval dap_match_values [list $name $event] $args

    return $result
}

# A convenience function to extract the breakpoint number when a new
# breakpoint is created.  OBJ is an object as returned by
# dap_check_request_and_response.
proc dap_get_breakpoint_number {obj} {
    set d [lindex $obj 0]
    set bplist [dict get $d body breakpoints]
    return [dict get [lindex $bplist 0] id]
}