// 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:convert"; import "dart:io"; // 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; var debugger = null; class Program { static int numBps = 0; // Maps source code url to source. static var sources = new Map(); // Takes a JSON Debugger response and increments the count for // the source position. static void recordBp(Map msg) { // Progress indicator. if (++numBps % 1000 == 0) print(numBps); var location = msg["params"]["location"]; if (location == null) return; String url = location["url"]; assert(url != null); int libId = location["libraryId"]; assert(libId != null); int tokenPos = location["tokenOffset"];; Source s = sources[url]; if (s == null) { debugger.getLineNumberTable(url, libId); 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(); // Maps token position to line number. final tokenPosToLine = new Map(); 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, columnNumber). lineInfoTable.forEach((List 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(); // 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; } }); String srcPath = Uri.parse(url).toFilePath(); List lines = new File(srcPath).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 GetLibrariesCmd { Map msg; GetLibrariesCmd(int isolateId) { msg = { "id": 0, "command": "getLibraries", "params": { "isolateId" : isolateId } }; } void handleResponse(Map response) { List libs = response["result"]["libraries"]; for (var lib in libs) { String url = lib["url"]; int libraryId = lib["id"]; bool enable = !url.startsWith("dart:") && !url.startsWith("package:"); if (enable) { print("Enable stepping for '$url'"); debugger.enableDebugging(libraryId, true); } } } } class SetLibraryPropertiesCmd { Map msg; SetLibraryPropertiesCmd(int isolateId, int libraryId, bool enableDebugging) { // Note that in the debugger protocol, boolean values true and false // must be sent as string literals. msg = { "id": 0, "command": "setLibraryProperties", "params": { "isolateId" : isolateId, "libraryId": libraryId, "debuggingEnabled": "$enableDebugging" } }; } void handleResponse(Map response) { // Nothing to do. } } class Debugger { // Debug target process properties. Process targetProcess; Socket socket; bool cleanupDone = false; JsonBuffer responses = new JsonBuffer(); List errors = new List(); // Data collected from debug target. Map currentMessage = null; // Currently handled message sent by target. var outstandingCommand = null; var queuedCommands = new List(); 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(UTF8.decoder) .transform(new LineSplitter()); stdoutStringStream.listen((line) { if (showDebuggeeOutput) { print("TARG: $line"); } if (line.startsWith("Debugger listening")) { RegExp portExpr = new RegExp(r"\d+"); var port = portExpr.stringMatch(line); var pid = targetProcess.pid; print("Coverage target process (pid $pid) found " "listening on port $port."); openConnection(int.parse(port)); } }); var stderrStringStream = targetProcess.stderr .transform(UTF8.decoder) .transform(new LineSplitter()); stderrStringStream.listen((line) { if (showDebuggeeOutput) { print("TARG: $line"); } }); } // Handle debugger events, updating the debugger state. void handleEvent(Map 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); // This is the first paused event we got. Get all libraries from // the debugger so we can turn on debugging events for them. getLibraries(); } if (msg["params"]["reason"] == "breakpoint") { Program.recordBp(msg); } } else { error("Error: unknown debugger event received"); } } // Handle one JSON message object. void handleMessage(Map receivedMsg) { currentMessage = receivedMsg; if (receivedMsg["event"] != null) { handleEvent(receivedMsg); if (errorsDetected) { error("Error while handling event message"); error("Event received from coverage 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 coverage 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.decode(msg); handleMessage(msgObj); if (errorsDetected) { error("Error while handling message from coverage target"); error("Message received from coverage target: $msg"); cleanup(); return; } if (shutdownEventSeen) { if (outstandingCommand != null) { error("Error: outstanding command when shutdown received"); } cleanup(); return; } if (isPaused && (outstandingCommand == null)) { var cmd = queuedCommands.length > 0 ? queuedCommands.removeAt(0) : 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 msg) { assert(msg["id"] != null); msg["id"] = nextMessageId++; String jsonMsg = JSON.encode(msg); if (verboseWire) print("SEND: $jsonMsg"); socket.write(jsonMsg); } void getLineNumberTable(String url, int libId) { queuedCommands.add(new GetLineTableCmd(isolateId, libId, url)); } void getLibraries() { queuedCommands.add(new GetLibrariesCmd(isolateId)); } void enableDebugging(libraryId, enable) { queuedCommands.add(new SetLibraryPropertiesCmd(isolateId, libraryId, enable)); } 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; socket.setOption(SocketOption.TCP_NODELAY, true); var stringStream = socket.transform(UTF8.decoder); stringStream.listen( (str) { try { responses.append(str); handleMessages(); } catch(e, trace) { print("Unexpected exception:\n$e\n$trace"); cleanup(); } }, onDone: () { print("Connection closed by coverage target"); cleanup(); }, onError: (e) { print("Error '$e' detected in input stream from coverage target"); cleanup(); }); }, onError: (e, trace) { String msg = "Error while connecting to coverage target: $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 coverage 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 + 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 next json message in the // 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.decode() 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(List arguments) { var targetOpts = [ "--debug:0" ]; for (String str in arguments) { switch (str) { case "--verbose": showDebuggeeOutput = true; break; case "--wire": verboseWire = true; break; default: targetOpts.add(str); break; } } Process.start(Platform.executable, targetOpts).then((Process process) { process.stdin.close(); debugger = new Debugger(process); }); }