Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(63)

Side by Side Diff: tests/standalone/debugger/debug_lib.dart

Issue 11576053: Add basic standalone debugger test (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart/
Patch Set: Created 8 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 // Library used by debugger wire protocol tests (standalone VM debugging).
6
7 library DartDebugger;
8
9 import "dart:io";
10 import "dart:utf";
11 import "dart:json";
12
13 // TODO(hausner): need to select a different port number for each
14 // test that runs in parallel.
15 var debugPort = 5860;
16
17 // Whether or not to print debug target process on the console.
18 var showDebuggeeOutput = true;
19
20 // Whether or not to print the debugger wire messages on the console.
21 var verboseWire = false;
22
23 // Class to buffer wire protocol data from debug target and
24 // break it down to individual json messages.
25 class JsonBuffer {
26 String buffer = null;
27
28 append(String s) {
29 if (buffer == null || buffer.length == 0) {
30 buffer = s;
31 } else {
32 buffer = buffer.concat(s);
33 }
34 }
35
36 String getNextMessage() {
37 if (buffer == null) return null;
38 int msgLen = objectLength();
39 if (msgLen == 0) return null;
40 String msg = null;
41 if (msgLen == buffer.length) {
42 msg = buffer;
43 buffer = null;
44 } else {
siva 2012/12/19 18:28:58 assert(msgLen < buffer.length);
hausner 2012/12/19 22:01:51 Done.
45 msg = buffer.substring(0, msgLen);
46 buffer = buffer.substring(msgLen);
47 }
48 return msg;
49 }
50
51 // Skip past a JSON object value.
52 // The object value must start with '{' and continues to the
53 // matching '}'. No attempt is made to otherwise validate the contents
54 // as JSON. If it is invalid, a later JSON.parse() will fail.
siva 2012/12/19 18:28:58 Maybe you should add to the comment that this func
hausner 2012/12/19 22:01:51 Done.
hausner 2012/12/19 22:01:51 Done.
55 int objectLength() {
56 int skipWhitespace(int index) {
57 while (index < buffer.length) {
58 String char = buffer[index];
59 if (char != " " && char != "\n" && char != "\r" && char != "\t") break;
60 index++;
61 }
62 return index;
63 }
64 int skipString(int index) {
65 assert(buffer[index - 1] == '"');
66 while (index < buffer.length) {
67 String char = buffer[index];
68 if (char == '"') return index + 1;
69 if (char == r'\') index++;
70 if (index == buffer.length) return index;
71 index++;
72 }
73 return index;
74 }
75 int index = 0;
76 index = skipWhitespace(index);
77 // Bail out if the first non-whitespace character isn't '{'.
78 if (index == buffer.length || buffer[index] != '{') return 0;
79 int nexting = 0;
Mads Ager (google) 2012/12/18 06:38:50 nesting
hausner 2012/12/19 22:01:51 Done.
80 while (index < buffer.length) {
81 String char = buffer[index++];
82 if (char == '{') {
83 nexting++;
84 } else if (char == '}') {
85 nexting--;
86 if (nexting == 0) return index;
87 } else if (char == '"') {
88 // Strings can contain braces. Skip their content.
89 index = skipString(index);
90 }
91 }
92 return 0;
93 }
94 }
95
96
97 getJsonValue(Map jsonMsg, String path) {
98 List properties = path.split(new RegExp(":"));
99 assert(properties.length >= 1);
100 var node = jsonMsg;
101 for (int i = 0; i < properties.length; i++) {
102 if (node == null) return null;
103 String property = properties[i];
104 var index = null;
105 if (property.endsWith("]")) {
106 var bracketPos = property.lastIndexOf("[");
107 if (bracketPos <= 0) return null;
108 var indexStr = property.substring(bracketPos + 1, property.length - 1);
109 try {
110 index = int.parse(indexStr);
111 } on FormatException {
112 print("$indexStr is not a valid array index");
113 return null;
114 }
115 property = property.substring(0, bracketPos);
116 }
117 if (node is Map) {
Mads Ager (google) 2012/12/18 06:38:50 Identation a bit off here.
hausner 2012/12/19 22:01:51 Done.
118 node = node[property];
119 } else {
120 return null;
121 }
122 if (index != null) {
123 if (node is List && node.length > index) {
124 node = node[index];
125 } else {
126 return null;
127 }
128 }
129 }
130 return node;
131 }
132
133
134 // Returns true if [template] is a subset of [map].
135 bool matchMaps(Map template, Map msg) {
136 bool isMatch = true;
137 template.forEach((k, v) {
138 if (msg.containsKey(k)) {
139 var receivedValue = msg[k];
140 if ((v is Map) && (receivedValue is Map)) {
141 if (!matchMaps(v, receivedValue)) isMatch = false;
142 } else if (v == null) {
143 // null in the template matches everything.
144 } else if (v != receivedValue) {
145 isMatch = false;
146 }
147 } else {
148 isMatch = false;
149 }
150 });
151 return isMatch;
152 }
153
154
155 class BreakpointEvent {
156 String functionName;
157 var template = { "event": "paused", "params": { "reason": "breakpoint" }};
158
159 BreakpointEvent({String function: null}) {
160 functionName = function;
161 }
162
163 void match(Debugger debugger) {
164 var msg = debugger.currentMessage;
165 if (!matchMaps(template, msg)) debugger.error("message does not match $templ ate");
Mads Ager (google) 2012/12/18 06:38:50 There are a few long lines in this file.
hausner 2012/12/19 22:01:51 Done.
166 var name = getJsonValue(msg, "params:callFrames[0]:functionName");
167 if (name == "main") {
168 // Extract script url of debugged script.
169 var scriptUrl = getJsonValue(msg, "params:callFrames[0]:location:url");
170 assert(scriptUrl != null);
171 debugger.scriptUrl = scriptUrl;
172 }
173 if (functionName != null) {
174 var name = getJsonValue(msg, "params:callFrames[0]:functionName");
175 if (functionName != name) {
176 debugger.error("expected function name $functionName but got $name");
177 }
178 }
179 }
180 }
181
182 Breakpoint({String function}) {
183 return new BreakpointEvent(function: function);
184 }
185
186 class Matcher {
187 void match(Debugger debugger);
188 }
189
190 class FrameMatcher extends Matcher {
191 int frameIndex;
192 List<String> functionNames;
193
194 FrameMatcher(this.frameIndex, this.functionNames);
195
196 void match(Debugger debugger) {
197 var msg = debugger.currentMessage;
198 List frames = getJsonValue(msg, "params:callFrames");
199 assert(frames != null);
200 if (frames.length < functionNames.length) {
201 debugger.error("stack trace not long enough to match ${functionNames.lengt h} frames");
202 return;
203 }
204 for (int i = 0; i < functionNames.length; i++) {
205 var idx = i + frameIndex;
206 var property = "params:callFrames[$idx]:functionName";
207 var name = getJsonValue(msg, property);
208 if (name == null) {
209 debugger.error("property '$property' not found");
210 return;
211 }
212 if (name != functionNames[i]) {
213 debugger.error("call frame $idx: "
214 "expected function name '${functionNames[i]}' but found '$name'");
215 return;
216 }
217 }
218 }
219 }
220
221
222 MatchFrame(int frameIndex, String functionName) {
223 return new FrameMatcher(frameIndex, [ functionName ]);
224 }
225
226 MatchFrames(List<String> functionNames) {
227 return new FrameMatcher(0, functionNames);
228 }
229
230
231 class Command {
232 var template;
233 Command();
234 Command.resume() {
235 template = {"id": 0, "command": "resume", "params": {"isolateId": 0}};
236 }
237 Command.step() {
238 template = {"id": 0, "command": "stepOver", "params": {"isolateId": 0}};
239 }
240 Map makeMsg(int cmdId, int isolateId) {
241 template["id"] = cmdId;
242 if ((template["params"] != null) && (template["params"]["isolateId"] != null )) {
243 template["params"]["isolateId"] = isolateId;
244 }
245 return template;
246 }
247
248 void send(Debugger debugger) {
249 template["id"] = debugger.seqNr;
250 template["params"]["isolateId"] = debugger.isolateId;
251 debugger.sendMessage(template);
252 }
253
254 void matchResponse(Debugger debugger) {
255 Map response = debugger.currentMessage;
256 var id = template["id"];
257 assert(id != null && id >= 0);
258 if (response["id"] != id) {
259 debugger.error("Expected messaged id $id but got ${response["id"]}.");
260 }
261 }
262 }
263
264 Resume() => new Command.resume();
265 Step() => new Command.step();
266
267 class SetBreakpointCommand extends Command {
268 int line;
269 SetBreakpointCommand(int this.line) {
270 template = {"id": 0,
271 "command": "setBreakpoint",
272 "params": { "isolateId": 0,
273 "url": null,
274 "line": null }};
275 }
276 void send(Debugger debugger) {
277 assert(debugger.scriptUrl != null);
278 template["params"]["url"] = debugger.scriptUrl;
279 template["params"]["line"] = line;
280 super.send(debugger);
281 }
282 }
283
284 SetBreakpoint(int line) => new SetBreakpointCommand(line);
285
286
287 // A debug script is a list of Event, Matcher and Command objects.
288 class DebugScript {
289 List entries;
290 int currentIndex;
291 DebugScript(List this.entries) : currentIndex = 0;
292 get currentEntry {
293 if (currentIndex < entries.length) return entries[currentIndex];
294 return null;
295 }
296 advance() {
297 currentIndex++;
298 }
299 }
300
301
302 class Debugger {
303 // Debug target process properties.
304 Process targetProcess;
305 int portNumber;
306 Socket socket;
307 OutputStream to;
308 StringInputStream from;
309 JsonBuffer responses = new JsonBuffer();
310
311 DebugScript script;
312 int seqNr = 0; // Sequence number of next debugger command message.
313 Command lastCommand = null; // Most recent command sent to target.
314 List<String> errors = new List();
315
316 // Data collected from debug target.
317 Map currentMessage = null; // Currently handled message sent by target.
318 String scriptUrl = null;
319 bool shutdownEventSeen = false;
320 int isolateId = 0;
321
322 Debugger(this.targetProcess, this.portNumber) {
323 var targetStdout = new StringInputStream(targetProcess.stdout);
324 targetStdout.onLine = () {
325 var s = targetStdout.readLine();
326 if (showDebuggeeOutput) {
327 print("TARG: $s");
328 }
329 };
330 var targetStderr = new StringInputStream(targetProcess.stderr);
331 targetStderr.onLine = () {
332 var s = targetStderr.readLine();
333 if (showDebuggeeOutput) {
334 print("TARG: $s");
335 }
336 };
337 }
338
339 // Handle debugger events for which there is no explicit
340 // entry in the debug script, for example isolate create and
341 // shutdown events, breakpoint resolution events, etc.
342 bool handleImplicitEvents(Map<String,dynamic> msg) {
343 if (msg["event"] == "isolate") {
344 if (msg["params"]["reason"] == "created") {
345 isolateId = msg["params"]["id"];
346 assert(isolateId != null);
347 print("Debuggee isolate id $isolateId created.");
348 } else if (msg["params"]["reason"] == "shutdown") {
349 print("Debuggee isolate id ${msg["params"]["id"]} shut down.");
350 shutdownEventSeen = true;
351 }
352 return true;
353 } else if (msg["event"] == "breakpointResolved") {
354 // Ignore the event. We may want to maintain a table of
355 // breakpoints in the future.
356 return true;
357 }
358 return false;
359 }
360
361 // Handle one JSON message object and match it to the
362 // expected events and responses in the debugging script.
363 void handleMessage(Map<String,dynamic> receivedMsg) {
364 currentMessage = receivedMsg;
365 var isHandled = handleImplicitEvents(receivedMsg);
366 if (isHandled) return;
367
368 if (receivedMsg["id"] != null) {
369 // This is a response to the last command we sent.
370 assert(lastCommand != null);
371 lastCommand.matchResponse(this);
372 lastCommand = null;
373 if (errorsDetected) {
374 error("Error while matching response to debugger command");
375 error("Response received from debug target: $receivedMsg");
376 }
377 return;
378 }
379
380 // This message must be an event that is expected by the script.
381 assert(receivedMsg["event"] != null);
382 if ((script.currentEntry == null) || (script.currentEntry is Command)) {
383 // Error: unexpected event received.
384 error("unexpected event received: $receivedMsg");
385 return;
386 } else {
387 // Match received message with expected event.
388 script.currentEntry.match(this);
389 if (errorsDetected) return;
390 script.advance();
391 while (script.currentEntry is Matcher) {
392 script.currentEntry.match(this);
393 if (errorsDetected) return;
394 script.advance();
395 }
396 }
397 }
398
399 // Send next debugger command in the script, if a response
400 // form the last command has been received and processed.
401 void sendNextCommand() {
402 if (lastCommand == null) {
403 if (script.currentEntry is Command) {
404 script.currentEntry.send(this);
405 lastCommand = script.currentEntry;
406 seqNr++;
407 script.advance();
408 }
409 }
410 }
411
412 // Handle data received over the wire from the debug target
413 // process. Split input from JSON wire format into individual
414 // message objects (maps).
415 void handleMessages() {
416 var msg = responses.getNextMessage();
417 while (msg != null) {
418 if (verboseWire) print("RECV: $msg");
419 var msgObj = JSON.parse(msg);
420 handleMessage(msgObj);
421 if (errorsDetected) {
422 error("Error while handling script entry ${script.currentIndex}");
423 error("Message received from debug target: $msg");
424 close();
425 return;
426 }
427 if (shutdownEventSeen) {
428 close();
429 return;
430 }
431 sendNextCommand();
432 msg = responses.getNextMessage();
433 }
434 }
435
436 runScript(List entries) {
437 script = new DebugScript(entries);
438 openConnection();
439 }
440
441 // Send a debugger command to the target VM.
442 void sendMessage(Map<String,dynamic> msg) {
443 String jsonMsg = JSON.stringify(msg);
444 if (verboseWire) print("SEND: $jsonMsg");
445 to.writeString(jsonMsg, Encoding.UTF_8);
446 }
447
448 bool get errorsDetected => errors.length > 0;
449
450 // Record error message.
451 void error(String s) {
452 errors.add(s);
453 }
454
455 void openConnection() {
456 socket = new Socket("127.0.0.1", portNumber);
457 to = socket.outputStream;
458 from = new StringInputStream(socket.inputStream, Encoding.UTF_8);
459 from.onData = () {
460 try {
461 responses.append(from.read());
462 handleMessages();
463 } catch(e, trace) {
464 print("Unexpected exception:\n$e\n$trace");
465 close();
466 }
467 };
468 from.onClosed = () {
469 print("Connection closed by debug target");
470 close();
471 };
472 from.onError = (e) {
473 print("Error '$e' detected in input stream from debug target");
474 close();
475 };
476 }
477
478 void close() {
479 if (errorsDetected) {
480 for (int i = 0; i < errors.length; i++) print(errors[i]);
481 }
482 to.close();
483 socket.close();
484 targetProcess.kill();
485 print("Target process killed");
486 Expect.isTrue(!errorsDetected);
487 stdin.close();
488 stdout.close();
489 stderr.close();
490 }
491 }
492
493
494 bool RunScript(List script) {
495 var options = new Options();
496 if (options.arguments.contains("--debuggee")) {
497 return false;
498 }
499 showDebuggeeOutput = options.arguments.contains("--verbose");
500 verboseWire = options.arguments.contains("--wire");
501
502 var targetOpts = [ "--debug:$debugPort" ];
503 if (showDebuggeeOutput) targetOpts.add("--verbose_debug");
504 targetOpts.add(options.script);
505 targetOpts.add("--debuggee");
506
507 Process.start(options.executable, targetOpts).then((Process process) {
508 print("Debug target process started");
509 process.stdin.close();
Mads Ager (google) 2012/12/18 06:38:50 You should drain stdout and stderr here as well to
hausner 2012/12/19 22:01:51 Done. But is this really the right thing to do sin
Mads Ager (google) 2012/12/20 07:40:52 Urgh, I missed that. No, in that case you shouldn'
510 process.onExit = (int exitCode) {
511 print("Debug target process exited with exit code $exitCode");
512 };
513 var debugger = new Debugger(process, debugPort);
514 stdin.onClosed = () => debugger.close();
515 stdin.onError = (error) => debugger.close();
516 debugger.runScript(script);
517 });
518 return true;
519 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698