Chromium Code Reviews| Index: tools/coverage.dart |
| =================================================================== |
| --- tools/coverage.dart (revision 0) |
| +++ tools/coverage.dart (revision 0) |
| @@ -0,0 +1,490 @@ |
| +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| +// for details. All rights reserved. Use of this source code is governed by a |
| +// BSD-style license that can be found in the LICENSE file. |
| + |
| +// This test forks a second vm process that runs a dart script as |
| +// a debug target, single stepping through the entire program, and |
| +// recording each breakpoint. At the end, a coverage map of the source |
| +// is printed. |
| +// |
| +// Usage: dart coverage.dart [--wire] [--verbose] target_script.dart |
| +// |
| +// --wire see json messages sent between the processes. |
| +// --verbose see the stdout and stderr output of the debug |
| +// target process. |
| + |
| +import "dart:io"; |
| +import "dart:utf"; |
| +import "dart:uri"; |
| +import "dart:json" as JSON; |
| + |
| + |
| +// Whether or not to print debug target process on the console. |
| +var showDebuggeeOutput = false; |
| + |
| +// Whether or not to print the debugger wire messages on the console. |
| +var verboseWire = false; |
| + |
| + |
| +class Program { |
| + static int numBps = 0; |
| + |
| + // Maps source code url to source. |
| + static var sources = new Map<String, Source>(); |
| + |
| + // Takes a JSON Debugger response and increments the count for the source position |
|
Ivan Posva
2013/05/29 21:42:44
long liner
hausner
2013/05/29 21:55:34
Done.
|
| + static void recordBp(Debugger debugger, Map<String,dynamic> msg) { |
| + // Progress indicator. |
| + if (++numBps % 100 == 0) print(numBps); |
| + String url = msg["params"]["location"]["url"]; |
| + assert(url != null); |
| + int tokenPos = msg["params"]["location"]["tokenOffset"];; |
|
Ivan Posva
2013/05/29 21:42:44
What if the breakpoint message does not have a loc
hausner
2013/05/29 21:55:34
True. Changed to ignore the breakpoint if it has n
|
| + Source s = sources[url]; |
| + if (s == null) { |
| + debugger.GetLineNumberTable(url); |
| + s = new Source(url); |
| + sources[url] = s; |
| + } |
| + s.recordBp(tokenPos); |
| + } |
| + |
| + // Prints the annotated source code. |
| + static void printCoverage() { |
| + print("Coverage info collected from $numBps breakpoints:"); |
| + for(Source s in sources.values) s.printCoverage(); |
| + } |
| +} |
| + |
| + |
| +class Source { |
| + final String url; |
| + |
| + // Maps token position to breakpoint count. |
| + final tokenCounts = new Map<int,int>(); |
| + |
| + // Maps token position to line number. |
| + final tokenPosToLine = new Map<int,int>(); |
| + |
| + Source(this.url); |
| + |
| + void recordBp(int tokenPos) { |
| + var count = tokenCounts[tokenPos]; |
| + tokenCounts[tokenPos] = count == null ? 1 : count + 1; |
| + } |
| + |
| + void SetLineInfo(List lineInfoTable) { |
| + // Each line is encoded as an array with first element being the line |
| + // number, followed by pairs of (tokenPosition, textOffset). |
| + lineInfoTable.forEach((List<int> line) { |
| + int lineNumber = line[0]; |
| + for (int t = 1; t < line.length; t += 2) { |
| + assert(tokenPosToLine[line[t]] == null); |
| + tokenPosToLine[line[t]] = lineNumber; |
| + } |
| + }); |
| + } |
| + |
| + // Print out the annotated source code. For each line that has seen |
| + // a breakpoint, print out the maximum breakpoint count for all |
| + // tokens in the line. |
| + void printCoverage() { |
| + var lineCounts = new Map<int,int>(); // BP counts for each line. |
| + print(url); |
| + tokenCounts.forEach((tp, bpCount) { |
| + int lineNumber = tokenPosToLine[tp]; |
| + var lineCount = lineCounts[lineNumber]; |
| + // Remember maximum breakpoint count of all tokens in this line. |
| + if (lineCount == null || lineCount < bpCount) { |
| + lineCounts[lineNumber] = bpCount; |
| + } |
| + }); |
| + |
| + List lines = new File(Uri.parse(url).path).readAsLinesSync(); |
| + for (int line = 1; line <= lines.length; line++) { |
| + String prefix = " "; |
| + if (lineCounts.containsKey(line)) { |
| + prefix = lineCounts[line].toString(); |
| + StringBuffer b = new StringBuffer(); |
| + for (int i = prefix.length; i < 6; i++) b.write(" "); |
| + b.write(prefix); |
| + prefix = b.toString(); |
| + } |
| + print("${prefix}|${lines[line-1]}"); |
| + } |
| + } |
| +} |
| + |
| + |
| +class StepCmd { |
| + Map msg; |
| + StepCmd(int isolateId) { |
| + msg = {"id": 0, "command": "stepInto", "params": {"isolateId": isolateId}}; |
| + } |
| + void handleResponse(Map response) {} |
| +} |
| + |
| + |
| +class GetLineTableCmd { |
| + Map msg; |
| + GetLineTableCmd(int isolateId, int libraryId, String url) { |
| + msg = { "id": 0, |
| + "command": "getLineNumberTable", |
| + "params": { "isolateId" : isolateId, |
| + "libraryId": libraryId, |
| + "url": url } }; |
| + } |
| + |
| + void handleResponse(Map response) { |
| + var url = msg["params"]["url"]; |
| + Source s = Program.sources[url]; |
| + assert(s != null); |
| + s.SetLineInfo(response["result"]["lines"]); |
| + } |
| +} |
| + |
| + |
| +class Debugger { |
| + // Debug target process properties. |
| + Process targetProcess; |
| + Socket socket; |
| + bool cleanupDone = false; |
| + JsonBuffer responses = new JsonBuffer(); |
| + List<String> errors = new List(); |
| + |
| + // Data collected from debug target. |
| + Map currentMessage = null; // Currently handled message sent by target. |
| + var outstandingCommand = null; |
| + var queuedCommand = null; |
| + String scriptUrl = null; |
| + bool shutdownEventSeen = false; |
| + int isolateId = 0; |
| + int libraryId = null; |
| + |
| + int nextMessageId = 0; |
| + bool isPaused = false; |
| + bool pendingAck = false; |
| + |
| + Debugger(this.targetProcess) { |
| + var stdoutStringStream = targetProcess.stdout |
| + .transform(new StringDecoder()) |
| + .transform(new LineTransformer()); |
| + stdoutStringStream.listen((line) { |
| + if (showDebuggeeOutput) { |
| + print("TARG: $line"); |
| + } |
| + if (line.startsWith("Debugger listening")) { |
| + RegExp portExpr = new RegExp(r"\d+"); |
| + var port = portExpr.stringMatch(line); |
| + print("Debug target found listening at port '$port'"); |
| + openConnection(int.parse(port)); |
| + } |
| + }); |
| + |
| + var stderrStringStream = targetProcess.stderr |
| + .transform(new StringDecoder()) |
| + .transform(new LineTransformer()); |
| + stderrStringStream.listen((line) { |
| + if (showDebuggeeOutput) { |
| + print("TARG: $line"); |
| + } |
| + }); |
| + } |
| + |
| + // Handle debugger events, updating the debugger state. |
| + void handleEvent(Map<String,dynamic> msg) { |
| + if (msg["event"] == "isolate") { |
| + if (msg["params"]["reason"] == "created") { |
| + isolateId = msg["params"]["id"]; |
| + assert(isolateId != null); |
| + print("Debuggee isolate id $isolateId created."); |
| + } else if (msg["params"]["reason"] == "shutdown") { |
| + print("Debuggee isolate id ${msg["params"]["id"]} shut down."); |
| + shutdownEventSeen = true; |
| + } |
| + } else if (msg["event"] == "breakpointResolved") { |
| + var bpId = msg["params"]["breakpointId"]; |
| + assert(bpId != null); |
| + var isolateId = msg["params"]["isolateId"]; |
| + assert(isolateId != null); |
| + var location = msg["params"]["location"]; |
| + assert(location != null); |
| + print("Isolate $isolateId: breakpoint $bpId resolved" |
| + " at location $location"); |
| + // We may want to maintain a table of breakpoints in the future. |
| + } else if (msg["event"] == "paused") { |
| + isPaused = true; |
| + if (libraryId == null) { |
| + libraryId = msg["params"]["location"]["libraryId"]; |
| + assert(libraryId != null); |
| + } |
| + if (msg["params"]["reason"] == "breakpoint") { |
| + Program.recordBp(this, msg); |
| + } |
| + } else { |
| + error("Error: unknown debugger event received"); |
| + } |
| + } |
| + |
| + // Handle one JSON message object and match it to the |
| + // expected events and responses in the debugging script. |
| + void handleMessage(Map<String,dynamic> receivedMsg) { |
| + currentMessage = receivedMsg; |
| + if (receivedMsg["event"] != null) { |
| + handleEvent(receivedMsg); |
| + if (errorsDetected) { |
| + error("Error while handling debugger event"); |
| + error("Event received from debug target: $receivedMsg"); |
| + } |
| + } else if (receivedMsg["id"] != null) { |
| + // This is a response to the last command we sent. |
| + int id = receivedMsg["id"]; |
| + assert(outstandingCommand != null); |
| + assert(outstandingCommand.msg["id"] == id); |
| + outstandingCommand.handleResponse(receivedMsg); |
| + outstandingCommand = null; |
| + } else { |
| + error("Unexpected message from target"); |
| + } |
| + } |
| + |
| + // Handle data received over the wire from the debug target |
| + // process. Split input from JSON wire format into individual |
| + // message objects (maps). |
| + void handleMessages() { |
| + var msg = responses.getNextMessage(); |
| + while (msg != null) { |
| + if (verboseWire) print("RECV: $msg"); |
| + if (responses.haveGarbage()) { |
| + error("Error: leftover text after message: '${responses.buffer}'"); |
| + error("Previous message may be malformed, was: '$msg'"); |
| + cleanup(); |
| + return; |
| + } |
| + var msgObj = JSON.parse(msg); |
| + handleMessage(msgObj); |
| + if (errorsDetected) { |
| + error("Error while handling message from debug target"); |
| + error("Message received from debug target: $msg"); |
| + cleanup(); |
| + return; |
| + } |
| + if (shutdownEventSeen) { |
| + if (outstandingCommand != null) { |
| + error("Error: outstanding command when shutdown received"); |
| + } |
| + cleanup(); |
| + return; |
| + } |
| + if (isPaused && (outstandingCommand == null)) { |
| + var cmd = queuedCommand; |
| + queuedCommand = null; |
| + if (cmd == null) { |
| + cmd = new StepCmd(isolateId); |
| + isPaused = false; |
| + } |
| + sendMessage(cmd.msg); |
| + outstandingCommand = cmd; |
| + } |
| + msg = responses.getNextMessage(); |
| + } |
| + } |
| + |
| + // Send a debugger command to the target VM. |
| + void sendMessage(Map<String,dynamic> msg) { |
| + assert(msg["id"] != null); |
| + msg["id"] = nextMessageId++; |
| + String jsonMsg = JSON.stringify(msg); |
| + if (verboseWire) print("SEND: $jsonMsg"); |
| + socket.write(jsonMsg); |
| + } |
| + |
| + void GetLineNumberTable(String url) { |
| + assert(queuedCommand == null); |
| + queuedCommand = new GetLineTableCmd(isolateId, libraryId, url); |
| + } |
| + |
| + bool get errorsDetected => errors.length > 0; |
| + |
| + // Record error message. |
| + void error(String s) { |
| + errors.add(s); |
| + } |
| + |
| + void openConnection(int portNumber) { |
| + Socket.connect("127.0.0.1", portNumber).then((s) { |
| + socket = s; |
| + var stringStream = socket.transform(new StringDecoder()); |
| + stringStream.listen( |
| + (str) { |
| + try { |
| + responses.append(str); |
| + handleMessages(); |
| + } catch(e, trace) { |
| + print("Unexpected exception:\n$e\n$trace"); |
| + cleanup(); |
| + } |
| + }, |
| + onDone: () { |
| + print("Connection closed by debug target"); |
| + cleanup(); |
| + }, |
| + onError: (e) { |
| + print("Error '$e' detected in input stream from debug target"); |
| + cleanup(); |
| + }); |
| + }, |
| + onError: (e) { |
| + String msg = "Error while connecting to debugee: $e"; |
| + var trace = getAttachedStackTrace(e); |
| + if (trace != null) msg += "\nStackTrace: $trace"; |
| + error(msg); |
| + cleanup(); |
| + }); |
| + } |
| + |
| + void cleanup() { |
| + if (cleanupDone) return; |
| + if (socket != null) { |
| + socket.close().catchError((error) { |
| + // Print this directly in addition to adding it to the |
| + // error message queue, in case the error message queue |
| + // gets printed before this error handler is called. |
| + print("Error occurred while closing socket: $error"); |
| + error("Error while closing socket: $error"); |
| + }); |
| + } |
| + var targetPid = targetProcess.pid; |
| + print("Sending kill signal to process $targetPid..."); |
| + targetProcess.kill(); |
| + // If the process was already dead exitCode is already |
| + // available and we call exit() in the next event loop cycle. |
| + // Otherwise this will wait for the process to exit. |
| + |
| + targetProcess.exitCode.then((exitCode) { |
| + print("process $targetPid terminated with exit code $exitCode."); |
| + if (errorsDetected) { |
| + print("\n===== Errors detected: ====="); |
| + for (int i = 0; i < errors.length; i++) print(errors[i]); |
| + print("============================\n"); |
| + } |
| + Program.printCoverage(); |
| + exit(errors.length); |
| + }); |
| + cleanupDone = true; |
| + } |
| +} |
| + |
| + |
| +// Class to buffer wire protocol data from debug target and |
| +// break it down to individual json messages. |
| +class JsonBuffer { |
| + String buffer = null; |
| + |
| + append(String s) { |
| + if (buffer == null || buffer.length == 0) { |
| + buffer = s; |
| + } else { |
| + buffer = buffer.concat(s); |
| + } |
| + } |
| + |
| + String getNextMessage() { |
| + if (buffer == null) return null; |
| + int msgLen = objectLength(); |
| + if (msgLen == 0) return null; |
| + String msg = null; |
| + if (msgLen == buffer.length) { |
| + msg = buffer; |
| + buffer = null; |
| + } else { |
| + assert(msgLen < buffer.length); |
| + msg = buffer.substring(0, msgLen); |
| + buffer = buffer.substring(msgLen); |
| + } |
| + return msg; |
| + } |
| + |
| + bool haveGarbage() { |
| + if (buffer == null || buffer.length == 0) return false; |
| + var i = 0, char = " "; |
| + while (i < buffer.length) { |
| + char = buffer[i]; |
| + if (char != " " && char != "\n" && char != "\r" && char != "\t") break; |
| + i++; |
| + } |
| + if (i >= buffer.length) { |
| + return false; |
| + } else { |
| + return char != "{"; |
| + } |
| + } |
| + |
| + // Returns the character length of the newxt json message in the |
|
Ivan Posva
2013/05/29 21:42:44
next
hausner
2013/05/29 21:55:34
Downe
|
| + // buffer, or 0 if there is only a partial message in the buffer. |
| + // The object value must start with '{' and continues to the |
| + // matching '}'. No attempt is made to otherwise validate the contents |
| + // as JSON. If it is invalid, a later JSON.parse() will fail. |
| + int objectLength() { |
| + int skipWhitespace(int index) { |
| + while (index < buffer.length) { |
| + String char = buffer[index]; |
| + if (char != " " && char != "\n" && char != "\r" && char != "\t") break; |
| + index++; |
| + } |
| + return index; |
| + } |
| + int skipString(int index) { |
| + assert(buffer[index - 1] == '"'); |
| + while (index < buffer.length) { |
| + String char = buffer[index]; |
| + if (char == '"') return index + 1; |
| + if (char == r'\') index++; |
| + if (index == buffer.length) return index; |
| + index++; |
| + } |
| + return index; |
| + } |
| + int index = 0; |
| + index = skipWhitespace(index); |
| + // Bail out if the first non-whitespace character isn't '{'. |
| + if (index == buffer.length || buffer[index] != '{') return 0; |
| + int nesting = 0; |
| + while (index < buffer.length) { |
| + String char = buffer[index++]; |
| + if (char == '{') { |
| + nesting++; |
| + } else if (char == '}') { |
| + nesting--; |
| + if (nesting == 0) return index; |
| + } else if (char == '"') { |
| + // Strings can contain braces. Skip their content. |
| + index = skipString(index); |
| + } |
| + } |
| + return 0; |
| + } |
| +} |
| + |
| + |
| +void main() { |
| + var options = new Options(); |
| + var targetOpts = [ "--debug:0" ]; |
| + for (String str in options.arguments) { |
| + switch (str) { |
| + case "--verbose": |
| + showDebuggeeOutput = true; |
| + break; |
| + case "--wire": |
| + verboseWire = true; |
| + break; |
| + default: |
| + targetOpts.add(str); |
| + break; |
| + } |
| + } |
| + |
| + Process.start(options.executable, targetOpts).then((Process process) { |
| + process.stdin.close(); |
| + var debugger = new Debugger(process); |
| + }); |
| +} |