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