OLD | NEW |
| (Empty) |
1 // Copyright (c) 2013, 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 // This test forks a second vm process that runs a dart script as | |
6 // a debug target, single stepping through the entire program, and | |
7 // recording each breakpoint. At the end, a coverage map of the source | |
8 // is printed. | |
9 // | |
10 // Usage: dart coverage.dart [--wire] [--verbose] target_script.dart | |
11 // | |
12 // --wire see json messages sent between the processes. | |
13 // --verbose see the stdout and stderr output of the debug | |
14 // target process. | |
15 | |
16 import "dart:convert"; | |
17 import "dart:io"; | |
18 | |
19 | |
20 // Whether or not to print debug target process on the console. | |
21 var showDebuggeeOutput = false; | |
22 | |
23 // Whether or not to print the debugger wire messages on the console. | |
24 var verboseWire = false; | |
25 | |
26 var debugger = null; | |
27 | |
28 class Program { | |
29 static int numBps = 0; | |
30 | |
31 // Maps source code url to source. | |
32 static var sources = new Map<String, Source>(); | |
33 | |
34 // Takes a JSON Debugger response and increments the count for | |
35 // the source position. | |
36 static void recordBp(Map<String,dynamic> msg) { | |
37 // Progress indicator. | |
38 if (++numBps % 1000 == 0) print(numBps); | |
39 var location = msg["params"]["location"]; | |
40 if (location == null) return; | |
41 String url = location["url"]; | |
42 assert(url != null); | |
43 int libId = location["libraryId"]; | |
44 assert(libId != null); | |
45 int tokenPos = location["tokenOffset"];; | |
46 Source s = sources[url]; | |
47 if (s == null) { | |
48 debugger.getLineNumberTable(url, libId); | |
49 s = new Source(url); | |
50 sources[url] = s; | |
51 } | |
52 s.recordBp(tokenPos); | |
53 } | |
54 | |
55 // Prints the annotated source code. | |
56 static void printCoverage() { | |
57 print("Coverage info collected from $numBps breakpoints:"); | |
58 for(Source s in sources.values) s.printCoverage(); | |
59 } | |
60 } | |
61 | |
62 | |
63 class Source { | |
64 final String url; | |
65 | |
66 // Maps token position to breakpoint count. | |
67 final tokenCounts = new Map<int,int>(); | |
68 | |
69 // Maps token position to line number. | |
70 final tokenPosToLine = new Map<int,int>(); | |
71 | |
72 Source(this.url); | |
73 | |
74 void recordBp(int tokenPos) { | |
75 var count = tokenCounts[tokenPos]; | |
76 tokenCounts[tokenPos] = count == null ? 1 : count + 1; | |
77 } | |
78 | |
79 void SetLineInfo(List lineInfoTable) { | |
80 // Each line is encoded as an array with first element being the line | |
81 // number, followed by pairs of (tokenPosition, columnNumber). | |
82 lineInfoTable.forEach((List<int> line) { | |
83 int lineNumber = line[0]; | |
84 for (int t = 1; t < line.length; t += 2) { | |
85 assert(tokenPosToLine[line[t]] == null); | |
86 tokenPosToLine[line[t]] = lineNumber; | |
87 } | |
88 }); | |
89 } | |
90 | |
91 // Print out the annotated source code. For each line that has seen | |
92 // a breakpoint, print out the maximum breakpoint count for all | |
93 // tokens in the line. | |
94 void printCoverage() { | |
95 var lineCounts = new Map<int,int>(); // BP counts for each line. | |
96 print(url); | |
97 tokenCounts.forEach((tp, bpCount) { | |
98 int lineNumber = tokenPosToLine[tp]; | |
99 var lineCount = lineCounts[lineNumber]; | |
100 // Remember maximum breakpoint count of all tokens in this line. | |
101 if (lineCount == null || lineCount < bpCount) { | |
102 lineCounts[lineNumber] = bpCount; | |
103 } | |
104 }); | |
105 | |
106 String srcPath = Uri.parse(url).toFilePath(); | |
107 List lines = new File(srcPath).readAsLinesSync(); | |
108 for (int line = 1; line <= lines.length; line++) { | |
109 String prefix = " "; | |
110 if (lineCounts.containsKey(line)) { | |
111 prefix = lineCounts[line].toString(); | |
112 StringBuffer b = new StringBuffer(); | |
113 for (int i = prefix.length; i < 6; i++) b.write(" "); | |
114 b.write(prefix); | |
115 prefix = b.toString(); | |
116 } | |
117 print("${prefix}|${lines[line-1]}"); | |
118 } | |
119 } | |
120 } | |
121 | |
122 | |
123 class StepCmd { | |
124 Map msg; | |
125 StepCmd(int isolateId) { | |
126 msg = {"id": 0, "command": "stepInto", "params": {"isolateId": isolateId}}; | |
127 } | |
128 void handleResponse(Map response) {} | |
129 } | |
130 | |
131 | |
132 class GetLineTableCmd { | |
133 Map msg; | |
134 GetLineTableCmd(int isolateId, int libraryId, String url) { | |
135 msg = { "id": 0, | |
136 "command": "getLineNumberTable", | |
137 "params": { "isolateId" : isolateId, | |
138 "libraryId": libraryId, | |
139 "url": url } }; | |
140 } | |
141 | |
142 void handleResponse(Map response) { | |
143 var url = msg["params"]["url"]; | |
144 Source s = Program.sources[url]; | |
145 assert(s != null); | |
146 s.SetLineInfo(response["result"]["lines"]); | |
147 } | |
148 } | |
149 | |
150 | |
151 class GetLibrariesCmd { | |
152 Map msg; | |
153 GetLibrariesCmd(int isolateId) { | |
154 msg = { "id": 0, | |
155 "command": "getLibraries", | |
156 "params": { "isolateId" : isolateId } }; | |
157 } | |
158 | |
159 void handleResponse(Map response) { | |
160 List libs = response["result"]["libraries"]; | |
161 for (var lib in libs) { | |
162 String url = lib["url"]; | |
163 int libraryId = lib["id"]; | |
164 bool enable = !url.startsWith("dart:") && !url.startsWith("package:"); | |
165 if (enable) { | |
166 print("Enable stepping for '$url'"); | |
167 debugger.enableDebugging(libraryId, true); | |
168 } | |
169 } | |
170 } | |
171 } | |
172 | |
173 | |
174 class SetLibraryPropertiesCmd { | |
175 Map msg; | |
176 SetLibraryPropertiesCmd(int isolateId, int libraryId, bool enableDebugging) { | |
177 // Note that in the debugger protocol, boolean values true and false | |
178 // must be sent as string literals. | |
179 msg = { "id": 0, | |
180 "command": "setLibraryProperties", | |
181 "params": { "isolateId" : isolateId, | |
182 "libraryId": libraryId, | |
183 "debuggingEnabled": "$enableDebugging" } }; | |
184 } | |
185 | |
186 void handleResponse(Map response) { | |
187 // Nothing to do. | |
188 } | |
189 } | |
190 | |
191 | |
192 class Debugger { | |
193 // Debug target process properties. | |
194 Process targetProcess; | |
195 Socket socket; | |
196 bool cleanupDone = false; | |
197 JsonBuffer responses = new JsonBuffer(); | |
198 List<String> errors = new List(); | |
199 | |
200 // Data collected from debug target. | |
201 Map currentMessage = null; // Currently handled message sent by target. | |
202 var outstandingCommand = null; | |
203 var queuedCommands = new List(); | |
204 String scriptUrl = null; | |
205 bool shutdownEventSeen = false; | |
206 int isolateId = 0; | |
207 int libraryId = null; | |
208 | |
209 int nextMessageId = 0; | |
210 bool isPaused = false; | |
211 bool pendingAck = false; | |
212 | |
213 Debugger(this.targetProcess) { | |
214 var stdoutStringStream = targetProcess.stdout | |
215 .transform(UTF8.decoder) | |
216 .transform(new LineSplitter()); | |
217 stdoutStringStream.listen((line) { | |
218 if (showDebuggeeOutput) { | |
219 print("TARG: $line"); | |
220 } | |
221 if (line.startsWith("Debugger listening")) { | |
222 RegExp portExpr = new RegExp(r"\d+"); | |
223 var port = portExpr.stringMatch(line); | |
224 var pid = targetProcess.pid; | |
225 print("Coverage target process (pid $pid) found " | |
226 "listening on port $port."); | |
227 openConnection(int.parse(port)); | |
228 } | |
229 }); | |
230 | |
231 var stderrStringStream = targetProcess.stderr | |
232 .transform(UTF8.decoder) | |
233 .transform(new LineSplitter()); | |
234 stderrStringStream.listen((line) { | |
235 if (showDebuggeeOutput) { | |
236 print("TARG: $line"); | |
237 } | |
238 }); | |
239 } | |
240 | |
241 // Handle debugger events, updating the debugger state. | |
242 void handleEvent(Map<String,dynamic> msg) { | |
243 if (msg["event"] == "isolate") { | |
244 if (msg["params"]["reason"] == "created") { | |
245 isolateId = msg["params"]["id"]; | |
246 assert(isolateId != null); | |
247 print("Debuggee isolate id $isolateId created."); | |
248 } else if (msg["params"]["reason"] == "shutdown") { | |
249 print("Debuggee isolate id ${msg["params"]["id"]} shut down."); | |
250 shutdownEventSeen = true; | |
251 } | |
252 } else if (msg["event"] == "breakpointResolved") { | |
253 var bpId = msg["params"]["breakpointId"]; | |
254 assert(bpId != null); | |
255 var isolateId = msg["params"]["isolateId"]; | |
256 assert(isolateId != null); | |
257 var location = msg["params"]["location"]; | |
258 assert(location != null); | |
259 print("Isolate $isolateId: breakpoint $bpId resolved" | |
260 " at location $location"); | |
261 // We may want to maintain a table of breakpoints in the future. | |
262 } else if (msg["event"] == "paused") { | |
263 isPaused = true; | |
264 if (libraryId == null) { | |
265 libraryId = msg["params"]["location"]["libraryId"]; | |
266 assert(libraryId != null); | |
267 // This is the first paused event we got. Get all libraries from | |
268 // the debugger so we can turn on debugging events for them. | |
269 getLibraries(); | |
270 } | |
271 if (msg["params"]["reason"] == "breakpoint") { | |
272 Program.recordBp(msg); | |
273 } | |
274 } else { | |
275 error("Error: unknown debugger event received"); | |
276 } | |
277 } | |
278 | |
279 // Handle one JSON message object. | |
280 void handleMessage(Map<String,dynamic> receivedMsg) { | |
281 currentMessage = receivedMsg; | |
282 if (receivedMsg["event"] != null) { | |
283 handleEvent(receivedMsg); | |
284 if (errorsDetected) { | |
285 error("Error while handling event message"); | |
286 error("Event received from coverage target: $receivedMsg"); | |
287 } | |
288 } else if (receivedMsg["id"] != null) { | |
289 // This is a response to the last command we sent. | |
290 int id = receivedMsg["id"]; | |
291 assert(outstandingCommand != null); | |
292 assert(outstandingCommand.msg["id"] == id); | |
293 outstandingCommand.handleResponse(receivedMsg); | |
294 outstandingCommand = null; | |
295 } else { | |
296 error("Unexpected message from target"); | |
297 } | |
298 } | |
299 | |
300 // Handle data received over the wire from the coverage target | |
301 // process. Split input from JSON wire format into individual | |
302 // message objects (maps). | |
303 void handleMessages() { | |
304 var msg = responses.getNextMessage(); | |
305 while (msg != null) { | |
306 if (verboseWire) print("RECV: $msg"); | |
307 if (responses.haveGarbage()) { | |
308 error("Error: leftover text after message: '${responses.buffer}'"); | |
309 error("Previous message may be malformed, was: '$msg'"); | |
310 cleanup(); | |
311 return; | |
312 } | |
313 var msgObj = JSON.decode(msg); | |
314 handleMessage(msgObj); | |
315 if (errorsDetected) { | |
316 error("Error while handling message from coverage target"); | |
317 error("Message received from coverage target: $msg"); | |
318 cleanup(); | |
319 return; | |
320 } | |
321 if (shutdownEventSeen) { | |
322 if (outstandingCommand != null) { | |
323 error("Error: outstanding command when shutdown received"); | |
324 } | |
325 cleanup(); | |
326 return; | |
327 } | |
328 if (isPaused && (outstandingCommand == null)) { | |
329 var cmd = queuedCommands.length > 0 ? queuedCommands.removeAt(0) : null; | |
330 if (cmd == null) { | |
331 cmd = new StepCmd(isolateId); | |
332 isPaused = false; | |
333 } | |
334 sendMessage(cmd.msg); | |
335 outstandingCommand = cmd; | |
336 } | |
337 msg = responses.getNextMessage(); | |
338 } | |
339 } | |
340 | |
341 // Send a debugger command to the target VM. | |
342 void sendMessage(Map<String,dynamic> msg) { | |
343 assert(msg["id"] != null); | |
344 msg["id"] = nextMessageId++; | |
345 String jsonMsg = JSON.encode(msg); | |
346 if (verboseWire) print("SEND: $jsonMsg"); | |
347 socket.write(jsonMsg); | |
348 } | |
349 | |
350 void getLineNumberTable(String url, int libId) { | |
351 queuedCommands.add(new GetLineTableCmd(isolateId, libId, url)); | |
352 } | |
353 | |
354 void getLibraries() { | |
355 queuedCommands.add(new GetLibrariesCmd(isolateId)); | |
356 } | |
357 | |
358 void enableDebugging(libraryId, enable) { | |
359 queuedCommands.add(new SetLibraryPropertiesCmd(isolateId, libraryId, enable)
); | |
360 } | |
361 | |
362 bool get errorsDetected => errors.length > 0; | |
363 | |
364 // Record error message. | |
365 void error(String s) { | |
366 errors.add(s); | |
367 } | |
368 | |
369 void openConnection(int portNumber) { | |
370 Socket.connect("127.0.0.1", portNumber).then((s) { | |
371 socket = s; | |
372 socket.setOption(SocketOption.TCP_NODELAY, true); | |
373 var stringStream = socket.transform(UTF8.decoder); | |
374 stringStream.listen( | |
375 (str) { | |
376 try { | |
377 responses.append(str); | |
378 handleMessages(); | |
379 } catch(e, trace) { | |
380 print("Unexpected exception:\n$e\n$trace"); | |
381 cleanup(); | |
382 } | |
383 }, | |
384 onDone: () { | |
385 print("Connection closed by coverage target"); | |
386 cleanup(); | |
387 }, | |
388 onError: (e) { | |
389 print("Error '$e' detected in input stream from coverage target"); | |
390 cleanup(); | |
391 }); | |
392 }, | |
393 onError: (e, trace) { | |
394 String msg = "Error while connecting to coverage target: $e"; | |
395 if (trace != null) msg += "\nStackTrace: $trace"; | |
396 error(msg); | |
397 cleanup(); | |
398 }); | |
399 } | |
400 | |
401 void cleanup() { | |
402 if (cleanupDone) return; | |
403 if (socket != null) { | |
404 socket.close().catchError((error) { | |
405 // Print this directly in addition to adding it to the | |
406 // error message queue, in case the error message queue | |
407 // gets printed before this error handler is called. | |
408 print("Error occurred while closing socket: $error"); | |
409 error("Error while closing socket: $error"); | |
410 }); | |
411 } | |
412 var targetPid = targetProcess.pid; | |
413 print("Sending kill signal to process $targetPid."); | |
414 targetProcess.kill(); | |
415 // If the process was already dead exitCode is already | |
416 // available and we call exit() in the next event loop cycle. | |
417 // Otherwise this will wait for the process to exit. | |
418 | |
419 targetProcess.exitCode.then((exitCode) { | |
420 print("Process $targetPid terminated with exit code $exitCode."); | |
421 if (errorsDetected) { | |
422 print("\n===== Errors detected: ====="); | |
423 for (int i = 0; i < errors.length; i++) print(errors[i]); | |
424 print("============================\n"); | |
425 } | |
426 Program.printCoverage(); | |
427 exit(errors.length); | |
428 }); | |
429 cleanupDone = true; | |
430 } | |
431 } | |
432 | |
433 | |
434 // Class to buffer wire protocol data from coverage target and | |
435 // break it down to individual json messages. | |
436 class JsonBuffer { | |
437 String buffer = null; | |
438 | |
439 append(String s) { | |
440 if (buffer == null || buffer.length == 0) { | |
441 buffer = s; | |
442 } else { | |
443 buffer = buffer + s; | |
444 } | |
445 } | |
446 | |
447 String getNextMessage() { | |
448 if (buffer == null) return null; | |
449 int msgLen = objectLength(); | |
450 if (msgLen == 0) return null; | |
451 String msg = null; | |
452 if (msgLen == buffer.length) { | |
453 msg = buffer; | |
454 buffer = null; | |
455 } else { | |
456 assert(msgLen < buffer.length); | |
457 msg = buffer.substring(0, msgLen); | |
458 buffer = buffer.substring(msgLen); | |
459 } | |
460 return msg; | |
461 } | |
462 | |
463 bool haveGarbage() { | |
464 if (buffer == null || buffer.length == 0) return false; | |
465 var i = 0, char = " "; | |
466 while (i < buffer.length) { | |
467 char = buffer[i]; | |
468 if (char != " " && char != "\n" && char != "\r" && char != "\t") break; | |
469 i++; | |
470 } | |
471 if (i >= buffer.length) { | |
472 return false; | |
473 } else { | |
474 return char != "{"; | |
475 } | |
476 } | |
477 | |
478 // Returns the character length of the next json message in the | |
479 // buffer, or 0 if there is only a partial message in the buffer. | |
480 // The object value must start with '{' and continues to the | |
481 // matching '}'. No attempt is made to otherwise validate the contents | |
482 // as JSON. If it is invalid, a later JSON.decode() will fail. | |
483 int objectLength() { | |
484 int skipWhitespace(int index) { | |
485 while (index < buffer.length) { | |
486 String char = buffer[index]; | |
487 if (char != " " && char != "\n" && char != "\r" && char != "\t") break; | |
488 index++; | |
489 } | |
490 return index; | |
491 } | |
492 int skipString(int index) { | |
493 assert(buffer[index - 1] == '"'); | |
494 while (index < buffer.length) { | |
495 String char = buffer[index]; | |
496 if (char == '"') return index + 1; | |
497 if (char == r'\') index++; | |
498 if (index == buffer.length) return index; | |
499 index++; | |
500 } | |
501 return index; | |
502 } | |
503 int index = 0; | |
504 index = skipWhitespace(index); | |
505 // Bail out if the first non-whitespace character isn't '{'. | |
506 if (index == buffer.length || buffer[index] != '{') return 0; | |
507 int nesting = 0; | |
508 while (index < buffer.length) { | |
509 String char = buffer[index++]; | |
510 if (char == '{') { | |
511 nesting++; | |
512 } else if (char == '}') { | |
513 nesting--; | |
514 if (nesting == 0) return index; | |
515 } else if (char == '"') { | |
516 // Strings can contain braces. Skip their content. | |
517 index = skipString(index); | |
518 } | |
519 } | |
520 return 0; | |
521 } | |
522 } | |
523 | |
524 | |
525 void main(List<String> arguments) { | |
526 var targetOpts = [ "--debug:0" ]; | |
527 for (String str in arguments) { | |
528 switch (str) { | |
529 case "--verbose": | |
530 showDebuggeeOutput = true; | |
531 break; | |
532 case "--wire": | |
533 verboseWire = true; | |
534 break; | |
535 default: | |
536 targetOpts.add(str); | |
537 break; | |
538 } | |
539 } | |
540 | |
541 Process.start(Platform.executable, targetOpts).then((Process process) { | |
542 process.stdin.close(); | |
543 debugger = new Debugger(process); | |
544 }); | |
545 } | |
OLD | NEW |