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