blob: b5cc61812d5e41e8578626f0cc08050e0c680166 [file] [log] [blame]
// 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<String, Source>();
// Takes a JSON Debugger response and increments the count for
// the source position.
static void recordBp(Map<String,dynamic> 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<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, columnNumber).
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;
}
});
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<String> 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<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);
// 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<String,dynamic> 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<String,dynamic> 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<String> 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);
});
}