@@ -35,6 +35,8 @@ SpecialPowers.addPermission("contacts-create", true, document);
var utils = SpecialPowers.getDOMWindowUtils(window);
+function tests() {
+
function getView(size)
{
var buffer = new ArrayBuffer(size);
@@ -130,7 +132,7 @@ function verifyBlob(blob1, blob2, isLast)
verifyBuffers(buffer1, buffer2, isLast);
}
}
-
+
var reader2 = new FileReader();
reader2.readAsArrayBuffer(blob1);
reader2.onload = function(event) {
@@ -155,7 +157,6 @@ function verifyBlobArray(blobs1, blobs2)
verifyBlob(blobs1[i], blobs2[i], i == blobs1.length - 1);
}
-
var req;
var index = 0;
@@ -314,9 +315,12 @@ function permissionTest() {
}
}
+permissionTest();
+}
+
var gContactsEnabled = SpecialPowers.getBoolPref("dom.mozContacts.enabled");
+addLoadEvent(tests);
SimpleTest.waitForExplicitFinish();
-addLoadEvent(permissionTest);
ok(true, "test passed");
diff --git a/dom/contacts/tests/test_contacts_getall.html b/dom/contacts/tests/test_contacts_getall.html
index 5f132e69033..9b15d52cb90 100644
--- a/dom/contacts/tests/test_contacts_getall.html
+++ b/dom/contacts/tests/test_contacts_getall.html
@@ -52,9 +52,7 @@ let properties1 = {
email: [{type: ["work"], value: "x@y.com"}]
};
-function onUnwantedSuccess() {
- ok(false, "onUnwantedSuccess: shouldn't get here");
-}
+function tests() {
function onFailure() {
ok(false, "in on Failure!");
@@ -415,9 +413,12 @@ function permissionTest() {
}
}
+permissionTest();
+}
+
let gContactsEnabled = SpecialPowers.getBoolPref("dom.mozContacts.enabled");
+addLoadEvent(tests);
SimpleTest.waitForExplicitFinish();
-addLoadEvent(permissionTest);
diff --git a/dom/contacts/tests/test_contacts_international.html b/dom/contacts/tests/test_contacts_international.html
index c9edcc58d99..f3ed923d51a 100644
--- a/dom/contacts/tests/test_contacts_international.html
+++ b/dom/contacts/tests/test_contacts_international.html
@@ -1,17 +1,17 @@
- Test for Bug {815833} WebContacts
+ Test for Bug 815833 WebContacts
-Mozilla Bug {815833}
+Mozilla Bug 815833
@@ -28,7 +28,7 @@ if (!SpecialPowers.getBoolPref("dom.mozContacts.enabled")) {
comp.utils.import("resource://gre/modules/PermissionPromptHelper.jsm");
SpecialPowers.setBoolPref("dom.mozContacts.enabled", true);
}
-
+
SpecialPowers.addPermission("contacts-write", true, document);
SpecialPowers.addPermission("contacts-read", true, document);
SpecialPowers.addPermission("contacts-create", true, document);
@@ -58,6 +58,8 @@ var properties2 = {
tel: [{type: ["work"], value: shortNumber, carrier: "testCarrier"}]
};
+function tests() {
+
var req;
var index = 0;
var createResult1;
@@ -254,9 +256,12 @@ function permissionTest() {
}
}
+permissionTest();
+}
+
var gContactsEnabled = SpecialPowers.getBoolPref("dom.mozContacts.enabled");
+addLoadEvent(tests);
SimpleTest.waitForExplicitFinish();
-addLoadEvent(permissionTest);
ok(true, "test passed");
diff --git a/dom/ipc/TabContext.cpp b/dom/ipc/TabContext.cpp
index f0975f5bf87..4f5e44f5aa3 100644
--- a/dom/ipc/TabContext.cpp
+++ b/dom/ipc/TabContext.cpp
@@ -294,6 +294,32 @@ TabContext::GetAppForId(uint32_t aAppId) const
return nullptr;
}
+ // This application caching is needed to avoid numerous unecessary application clones.
+ // See Bug 853632 for details.
+
+ if (aAppId == mOwnAppId) {
+ if (!mOwnApp) {
+ mOwnApp = GetAppForIdNoCache(aAppId);
+ }
+ nsCOMPtr ownApp = mOwnApp;
+ return ownApp.forget();
+ }
+
+ if (aAppId == mContainingAppId) {
+ if (!mContainingApp) {
+ mContainingApp = GetAppForIdNoCache(mContainingAppId);
+ }
+ nsCOMPtr containingApp = mContainingApp;
+ return containingApp.forget();
+ }
+ // We need the fallthrough here because mOwnAppId/mContainingAppId aren't always
+ // set before calling GetAppForId().
+ return GetAppForIdNoCache(aAppId);
+}
+
+already_AddRefed
+TabContext::GetAppForIdNoCache(uint32_t aAppId) const
+{
nsCOMPtr appsService = do_GetService(APPS_SERVICE_CONTRACTID);
NS_ENSURE_TRUE(appsService, nullptr);
diff --git a/dom/ipc/TabContext.h b/dom/ipc/TabContext.h
index b9ecd44865c..cf45018f51e 100644
--- a/dom/ipc/TabContext.h
+++ b/dom/ipc/TabContext.h
@@ -159,10 +159,15 @@ protected:
private:
/**
- * Translate an appId into a mozIApplication.
+ * Translate an appId into a mozIApplication, using lazy caching.
*/
already_AddRefed GetAppForId(uint32_t aAppId) const;
+ /**
+ * Translate an appId into a mozIApplication.
+ */
+ already_AddRefed GetAppForIdNoCache(uint32_t aAppId) const;
+
/**
* Has this TabContext been initialized? If so, mutator methods will fail.
*/
@@ -174,6 +179,12 @@ private:
*/
uint32_t mOwnAppId;
+ /**
+ * Cache of this TabContext's own app. If mOwnAppId is NO_APP_ID, this is
+ * guaranteed to be nullptr. Otherwise, it may or may not be null.
+ */
+ mutable nsCOMPtr mOwnApp;
+
/**
* The id of the app which contains this TabContext's frame. If mIsBrowser,
* this corresponds to the ID of the app which contains the browser frame;
@@ -182,6 +193,13 @@ private:
*/
uint32_t mContainingAppId;
+ /**
+ * Cache of the app that contains this TabContext's frame. If mContainingAppId
+ * is NO_APP_ID, this is guaranteed to be nullptr. Otherwise, it may or may not
+ * be null.
+ */
+ mutable nsCOMPtr mContainingApp;
+
/**
* The requested scrolling behavior for this frame.
*/
diff --git a/dom/mms/src/ril/MmsService.js b/dom/mms/src/ril/MmsService.js
index 277f0a85411..c8efa87b3e3 100644
--- a/dom/mms/src/ril/MmsService.js
+++ b/dom/mms/src/ril/MmsService.js
@@ -159,6 +159,8 @@ XPCOMUtils.defineLazyGetter(this, "gMmsConnection", function () {
"available later.");
this.clearMmsProxySettings();
}
+ this.connected = gRIL.getDataCallStateByType("mms") ==
+ Ci.nsINetworkInterface.NETWORK_STATE_CONNECTED;
},
/**
diff --git a/dom/mobilemessage/tests/marionette/test_message_classes.js b/dom/mobilemessage/tests/marionette/test_message_classes.js
index 374ee3ff21d..b1663b688d6 100644
--- a/dom/mobilemessage/tests/marionette/test_message_classes.js
+++ b/dom/mobilemessage/tests/marionette/test_message_classes.js
@@ -30,7 +30,6 @@ function sendSmsPduToEmulator(pdu) {
});
}
-const TIMESTAMP = Date.UTC(2000, 0, 1);
function checkMessage(message, id, threadId, messageClass) {
ok(message instanceof MozSmsMessage,
"message is instanceof " + message.constructor);
@@ -51,7 +50,6 @@ function checkMessage(message, id, threadId, messageClass) {
is(message.messageClass, messageClass, "message.messageClass");
ok(message.timestamp instanceof Date,
"message.timestamp is instanceof " + message.timestamp.constructor);
- is(message.timestamp.getTime(), TIMESTAMP, "message.timestamp");
is(message.read, false, "message.read");
}
@@ -68,6 +66,10 @@ function test_message_class_0() {
let message = event.message;
checkMessage(message, -1, 0, "class-0");
+ ok(event.message.timestamp.getTime() >= timeBeforeSend,
+ "Message's timestamp should be greater then the timetamp of sending");
+ ok(event.message.timestamp.getTime() <= Date.now(),
+ "Message's timestamp should be lesser than the timestamp of now");
// Make sure the message is not stored.
let cursor = sms.getMessages(null, false);
@@ -97,7 +99,7 @@ function test_message_class_0() {
log(" Testing DCS " + dcs);
let pdu = PDU_SMSC + PDU_FIRST_OCTET + PDU_SENDER + PDU_PID_NORMAL +
dcs + PDU_TIMESTAMP + PDU_UDL + PDU_UD;
-
+ let timeBeforeSend = Date.now();
sendSmsPduToEmulator(pdu);
}
@@ -112,6 +114,10 @@ function doTestMessageClassGeneric(allDCSs, messageClass, next) {
// Make sure we can correctly receive the message
checkMessage(event.message, null, null, messageClass);
+ ok(event.message.timestamp.getTime() >= timeBeforeSend,
+ "Message's timestamp should be greater then the timetamp of sending");
+ ok(event.message.timestamp.getTime() <= Date.now(),
+ "Message's timestamp should be lesser than the timestamp of now");
++dcsIndex;
if (dcsIndex >= allDCSs.length) {
@@ -126,6 +132,7 @@ function doTestMessageClassGeneric(allDCSs, messageClass, next) {
let pdu = PDU_SMSC + PDU_FIRST_OCTET + PDU_SENDER + PDU_PID_NORMAL +
dcs + PDU_TIMESTAMP + PDU_UDL + PDU_UD;
+ let timeBeforeSend = Date.now();
sendSmsPduToEmulator(pdu);
}
@@ -162,6 +169,10 @@ function test_message_class_2() {
if (pidIndex == 0) {
// Make sure we can correctly receive the message
checkMessage(event.message, null, null, "class-2");
+ ok(event.message.timestamp.getTime() >= timeBeforeSend,
+ "Message's timestamp should be greater then the timetamp of sending");
+ ok(event.message.timestamp.getTime() <= Date.now(),
+ "Message's timestamp should be lesser than the timestamp of now");
next();
return;
@@ -203,7 +214,7 @@ function test_message_class_2() {
let pdu = PDU_SMSC + PDU_FIRST_OCTET + PDU_SENDER + pid + dcs +
PDU_TIMESTAMP + PDU_UDL + PDU_UD;
-
+ let timeBeforeSend = Date.now();
sendSmsPduToEmulator(pdu);
}
diff --git a/dom/settings/tests/test_settings_blobs.html b/dom/settings/tests/test_settings_blobs.html
index ffb6a27f71f..9f1b415680d 100644
--- a/dom/settings/tests/test_settings_blobs.html
+++ b/dom/settings/tests/test_settings_blobs.html
@@ -43,6 +43,8 @@ function onFailure() {
}
}
+function tests() {
+
let mozSettings = window.navigator.mozSettings;
let req;
@@ -154,9 +156,12 @@ function permissionTest() {
}
}
+permissionTest();
+}
+
let gSettingsEnabled = SpecialPowers.getBoolPref("dom.mozSettings.enabled");
+addLoadEvent(tests);
SimpleTest.waitForExplicitFinish();
-addLoadEvent(permissionTest);
diff --git a/dom/settings/tests/test_settings_onsettingchange.html b/dom/settings/tests/test_settings_onsettingchange.html
index 909c3697d6b..741264b3fcc 100644
--- a/dom/settings/tests/test_settings_onsettingchange.html
+++ b/dom/settings/tests/test_settings_onsettingchange.html
@@ -1,17 +1,17 @@
- Test for Bug {678695} Settings API
+ Test for Bug 678695 Settings API
-Mozilla Bug {678695}
+Mozilla Bug 678695
@@ -29,6 +29,8 @@ if (!SpecialPowers.getBoolPref("dom.mozSettings.enabled")) {
SpecialPowers.addPermission("settings-write", true, document);
SpecialPowers.addPermission("settings-read", true, document);
+function tests() {
+
var screenBright = {"screen.brightness": 0.7};
function onFailure() {
@@ -280,9 +282,12 @@ function permissionTest() {
}
}
+permissionTest();
+}
+
var gSettingsEnabled = SpecialPowers.getBoolPref("dom.mozSettings.enabled");
+addLoadEvent(tests);
SimpleTest.waitForExplicitFinish();
-addLoadEvent(permissionTest);
ok(true, "test passed");
diff --git a/dom/system/gonk/RadioInterfaceLayer.js b/dom/system/gonk/RadioInterfaceLayer.js
index a00166ad891..f1689ab9fa9 100644
--- a/dom/system/gonk/RadioInterfaceLayer.js
+++ b/dom/system/gonk/RadioInterfaceLayer.js
@@ -1552,6 +1552,7 @@ RadioInterfaceLayer.prototype = {
message.sender = message.sender || null;
message.receiver = message.receiver || null;
message.body = message.fullBody = message.fullBody || null;
+ message.timestamp = Date.now();
// TODO: Bug #768441
// For now we don't store indicators persistently. When the mwi.discard
diff --git a/dom/system/gonk/ril_worker.js b/dom/system/gonk/ril_worker.js
index ff48bbb3aa7..be0b4c1c40e 100644
--- a/dom/system/gonk/ril_worker.js
+++ b/dom/system/gonk/ril_worker.js
@@ -74,6 +74,8 @@ let RILQUIRKS_V5_LEGACY = libcutils.property_get("ro.moz.ril.v5_legacy", "true")
let RILQUIRKS_REQUEST_USE_DIAL_EMERGENCY_CALL = libcutils.property_get("ro.moz.ril.dial_emergency_call", "false") === "true";
let RILQUIRKS_MODEM_DEFAULTS_TO_EMERGENCY_MODE = libcutils.property_get("ro.moz.ril.emergency_by_default", "false") === "true";
let RILQUIRKS_SIM_APP_STATE_EXTRA_FIELDS = libcutils.property_get("ro.moz.ril.simstate_extra_field", "false") === "true";
+// Needed for call-waiting on Peak device
+let RILQUIRKS_EXTRA_UINT32_2ND_CALL = libcutils.property_get("ro.moz.ril.extra_int_2nd_call", "false") == "true";
// Marker object.
let PENDING_NETWORK_TYPE = {};
@@ -4366,6 +4368,13 @@ RIL[REQUEST_GET_CURRENT_CALLS] = function REQUEST_GET_CURRENT_CALLS(length, opti
let calls = {};
for (let i = 0; i < calls_length; i++) {
let call = {};
+
+ // Extra uint32 field to get correct callIndex and rest of call data for
+ // call waiting feature.
+ if (RILQUIRKS_EXTRA_UINT32_2ND_CALL && i > 0) {
+ Buf.readUint32();
+ }
+
call.state = Buf.readUint32(); // CALL_STATE_*
call.callIndex = Buf.readUint32(); // GSM index (1-based)
call.toa = Buf.readUint32();
diff --git a/toolkit/devtools/Console.jsm b/toolkit/devtools/Console.jsm
index 8d78dfabd21..6a90d079ceb 100644
--- a/toolkit/devtools/Console.jsm
+++ b/toolkit/devtools/Console.jsm
@@ -24,6 +24,11 @@ this.EXPORTED_SYMBOLS = [ "console" ];
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+let gTimerRegistry = new Map();
+
/**
* String utility to ensure that strings are a specified length. Strings
* that are too long are truncated to the max length and the last char is
@@ -250,7 +255,7 @@ function logProperty(aProp, aValue) {
/**
* Parse a stack trace, returning an array of stack frame objects, where
- * each has file/line/call members
+ * each has filename/lineNumber/functionName members
*
* @param {string} aStack
* The serialized stack trace
@@ -266,35 +271,42 @@ function parseStack(aStack) {
let at = line.lastIndexOf("@");
let posn = line.substring(at + 1);
trace.push({
- file: posn.split(":")[0],
- line: posn.split(":")[1],
- call: line.substring(0, at)
+ filename: posn.split(":")[0],
+ lineNumber: posn.split(":")[1],
+ functionName: line.substring(0, at)
});
});
return trace;
}
/**
- * parseStack() takes output from an exception from which it creates the an
- * array of stack frame objects, this has the same output but using data from
- * Components.stack
+ * Format a frame coming from Components.stack such that it can be used by the
+ * Browser Console, via console-api-log-event notifications.
*
- * @param {string} aFrame
- * The stack frame from which to begin the walk
+ * @param {object} aFrame
+ * The stack frame from which to begin the walk.
+ * @param {number=0} aMaxDepth
+ * Maximum stack trace depth. Default is 0 - no depth limit.
* @return {object[]}
- * Array of { file: "...", line: NNN, call: "..." } objects
+ * An array of {filename, lineNumber, functionName, language} objects.
+ * These objects follow the same format as other console-api-log-event
+ * messages.
*/
-function getStack(aFrame) {
+function getStack(aFrame, aMaxDepth = 0) {
if (!aFrame) {
aFrame = Components.stack.caller;
}
let trace = [];
while (aFrame) {
trace.push({
- file: aFrame.filename,
- line: aFrame.lineNumber,
- call: aFrame.name
+ filename: aFrame.filename,
+ lineNumber: aFrame.lineNumber,
+ functionName: aFrame.name,
+ language: aFrame.language,
});
+ if (aMaxDepth == trace.length) {
+ break;
+ }
aFrame = aFrame.caller;
}
return trace;
@@ -311,13 +323,52 @@ function getStack(aFrame) {
function formatTrace(aTrace) {
let reply = "";
aTrace.forEach(function(frame) {
- reply += fmt(frame.file, 20, 20, { truncate: "start" }) + " " +
- fmt(frame.line, 5, 5) + " " +
- fmt(frame.call, 75, 75) + "\n";
+ reply += fmt(frame.filename, 20, 20, { truncate: "start" }) + " " +
+ fmt(frame.lineNumber, 5, 5) + " " +
+ fmt(frame.functionName, 75, 75) + "\n";
});
return reply;
}
+/**
+ * Create a new timer by recording the current time under the specified name.
+ *
+ * @param {string} aName
+ * The name of the timer.
+ * @param {number} [aTimestamp=Date.now()]
+ * Optional timestamp that tells when the timer was originally started.
+ * @return {object}
+ * The name property holds the timer name and the started property
+ * holds the time the timer was started. In case of error, it returns
+ * an object with the single property "error" that contains the key
+ * for retrieving the localized error message.
+ */
+function startTimer(aName, aTimestamp) {
+ let key = aName.toString();
+ if (!gTimerRegistry.has(key)) {
+ gTimerRegistry.set(key, aTimestamp || Date.now());
+ }
+ return { name: aName, started: gTimerRegistry.get(key) };
+}
+
+/**
+ * Stop the timer with the specified name and retrieve the elapsed time.
+ *
+ * @param {string} aName
+ * The name of the timer.
+ * @param {number} [aTimestamp=Date.now()]
+ * Optional timestamp that tells when the timer was originally stopped.
+ * @return {object}
+ * The name property holds the timer name and the duration property
+ * holds the number of milliseconds since the timer was started.
+ */
+function stopTimer(aName, aTimestamp) {
+ let key = aName.toString();
+ let duration = (aTimestamp || Date.now()) - gTimerRegistry.get(key);
+ gTimerRegistry.delete(key);
+ return { name: aName, duration: duration };
+}
+
/**
* Create a function which will output a concise level of output when used
* as a logging function
@@ -332,6 +383,8 @@ function formatTrace(aTrace) {
function createDumper(aLevel) {
return function() {
let args = Array.prototype.slice.call(arguments, 0);
+ let frame = getStack(Components.stack.caller, 1)[0];
+ sendConsoleAPIMessage(aLevel, frame, args);
let data = args.map(function(arg) {
return stringify(arg);
});
@@ -354,12 +407,72 @@ function createMultiLineDumper(aLevel) {
return function() {
dump("console." + aLevel + ": \n");
let args = Array.prototype.slice.call(arguments, 0);
+ let frame = getStack(Components.stack.caller, 1)[0];
+ sendConsoleAPIMessage(aLevel, frame, args);
args.forEach(function(arg) {
dump(log(arg));
});
};
}
+/**
+ * Send a Console API message. This function will send a console-api-log-event
+ * notification through the nsIObserverService.
+ *
+ * @param {string} aLevel
+ * Message severity level. This is usually the name of the console method
+ * that was called.
+ * @param {object} aFrame
+ * The youngest stack frame coming from Components.stack, as formatted by
+ * getStack().
+ * @param {array} aArgs
+ * The arguments given to the console method.
+ * @param {object} aOptions
+ * Object properties depend on the console method that was invoked:
+ * - timer: for time() and timeEnd(). Holds the timer information.
+ * - groupName: for group(), groupCollapsed() and groupEnd().
+ * - stacktrace: for trace(). Holds the array of stack frames as given by
+ * getStack().
+ */
+function sendConsoleAPIMessage(aLevel, aFrame, aArgs, aOptions = {})
+{
+ let consoleEvent = {
+ ID: aFrame.filename,
+ level: aLevel,
+ filename: aFrame.filename,
+ lineNumber: aFrame.lineNumber,
+ functionName: aFrame.functionName,
+ timeStamp: Date.now(),
+ arguments: aArgs,
+ };
+
+ consoleEvent.wrappedJSObject = consoleEvent;
+
+ switch (aLevel) {
+ case "trace":
+ consoleEvent.stacktrace = aOptions.stacktrace;
+ break;
+ case "time":
+ case "timeEnd":
+ consoleEvent.timer = aOptions.timer;
+ break;
+ case "group":
+ case "groupCollapsed":
+ case "groupEnd":
+ try {
+ consoleEvent.groupName = Array.prototype.join.call(aArgs, " ");
+ }
+ catch (ex) {
+ Cu.reportError(ex);
+ Cu.reportError(ex.stack);
+ return;
+ }
+ break;
+ }
+
+ Services.obs.notifyObservers(consoleEvent, "console-api-log-event", null);
+}
+
/**
* This creates a console object that somewhat replicates Firebug's console
* object. It currently writes to dump(), but should write to the web
@@ -373,13 +486,32 @@ this.console = {
error: createMultiLineDumper("error"),
trace: function Console_trace() {
+ let args = Array.prototype.slice.call(arguments, 0);
let trace = getStack(Components.stack.caller);
- dump(formatTrace(trace) + "\n");
+ sendConsoleAPIMessage("trace", trace[0], args,
+ { stacktrace: trace });
+ dump("console.trace:\n" + formatTrace(trace) + "\n");
},
clear: function Console_clear() {},
dir: createMultiLineDumper("dir"),
dirxml: createMultiLineDumper("dirxml"),
group: createDumper("group"),
- groupEnd: createDumper("groupEnd")
+ groupEnd: createDumper("groupEnd"),
+
+ time: function Console_time() {
+ let args = Array.prototype.slice.call(arguments, 0);
+ let frame = getStack(Components.stack.caller, 1)[0];
+ let timer = startTimer(args[0]);
+ sendConsoleAPIMessage("time", frame, args, { timer: timer });
+ dump("console.time: '" + timer.name + "' @ " + (new Date()) + "\n");
+ },
+
+ timeEnd: function Console_timeEnd() {
+ let args = Array.prototype.slice.call(arguments, 0);
+ let frame = getStack(Components.stack.caller, 1)[0];
+ let timer = stopTimer(args[0]);
+ sendConsoleAPIMessage("timeEnd", frame, args, { timer: timer });
+ dump("console.timeEnd: '" + timer.name + "' " + timer.duration + "ms\n");
+ },
};
diff --git a/toolkit/devtools/debugger/dbg-client.jsm b/toolkit/devtools/debugger/dbg-client.jsm
index f1c037aa3af..03f47948351 100644
--- a/toolkit/devtools/debugger/dbg-client.jsm
+++ b/toolkit/devtools/debugger/dbg-client.jsm
@@ -245,7 +245,7 @@ DebuggerClient.requester = function DC_requester(aPacketSkeleton, { telemetry,
return function (...args) {
let histogram, startTime;
if (telemetry) {
- let transportType = this._transport instanceof LocalDebuggerTransport
+ let transportType = this._transport.onOutputStreamReady === undefined
? "LOCAL_"
: "REMOTE_";
let histogramId = "DEVTOOLS_DEBUGGER_RDP_"
@@ -253,7 +253,6 @@ DebuggerClient.requester = function DC_requester(aPacketSkeleton, { telemetry,
histogram = Services.telemetry.getHistogramById(histogramId);
startTime = +new Date;
}
-
let outgoingPacket = {
to: aPacketSkeleton.to || this.actor
};
@@ -413,7 +412,10 @@ DebuggerClient.prototype = {
*/
attachTab: function DC_attachTab(aTabActor, aOnResponse) {
let self = this;
- let packet = { to: aTabActor, type: "attach" };
+ let packet = {
+ to: aTabActor,
+ type: "attach"
+ };
this.request(packet, function(aResponse) {
let tabClient;
if (!aResponse.error) {
@@ -463,10 +465,17 @@ DebuggerClient.prototype = {
* @param function aOnResponse
* Called with the response packet and a ThreadClient
* (which will be undefined on error).
+ * @param object aOptions
+ * Configuration options.
+ * - useSourceMaps: whether to use source maps or not.
*/
- attachThread: function DC_attachThread(aThreadActor, aOnResponse) {
+ attachThread: function DC_attachThread(aThreadActor, aOnResponse, aOptions={}) {
let self = this;
- let packet = { to: aThreadActor, type: "attach" };
+ let packet = {
+ to: aThreadActor,
+ type: "attach",
+ options: aOptions
+ };
this.request(packet, function(aResponse) {
if (!aResponse.error) {
var threadClient = new ThreadClient(self, aThreadActor);
@@ -523,7 +532,7 @@ DebuggerClient.prototype = {
*/
_sendRequests: function DC_sendRequests() {
let self = this;
- this._pendingRequests = this._pendingRequests.filter(function(request) {
+ this._pendingRequests = this._pendingRequests.filter(function (request) {
if (request.to in self._activeRequests) {
return true;
}
@@ -550,7 +559,7 @@ DebuggerClient.prototype = {
? aPacket
: this.compat.onPacket(aPacket);
- resolve(packet).then(function (aPacket) {
+ resolve(packet).then((aPacket) => {
if (!this._connected) {
// Hello packet.
this._connected = true;
@@ -560,55 +569,53 @@ DebuggerClient.prototype = {
return;
}
- try {
- if (!aPacket.from) {
- let msg = "Server did not specify an actor, dropping packet: " +
- JSON.stringify(aPacket);
- Cu.reportError(msg);
- dumpn(msg);
- return;
- }
+ if (!aPacket.from) {
+ let msg = "Server did not specify an actor, dropping packet: " +
+ JSON.stringify(aPacket);
+ Cu.reportError(msg);
+ dumpn(msg);
+ return;
+ }
- let onResponse;
- // Don't count unsolicited notifications or pauses as responses.
- if (aPacket.from in this._activeRequests &&
- !(aPacket.type in UnsolicitedNotifications) &&
- !(aPacket.type == ThreadStateTypes.paused &&
- aPacket.why.type in UnsolicitedPauses)) {
- onResponse = this._activeRequests[aPacket.from].onResponse;
- delete this._activeRequests[aPacket.from];
- }
+ let onResponse;
+ // Don't count unsolicited notifications or pauses as responses.
+ if (aPacket.from in this._activeRequests &&
+ !(aPacket.type in UnsolicitedNotifications) &&
+ !(aPacket.type == ThreadStateTypes.paused &&
+ aPacket.why.type in UnsolicitedPauses)) {
+ onResponse = this._activeRequests[aPacket.from].onResponse;
+ delete this._activeRequests[aPacket.from];
+ }
- // Packets that indicate thread state changes get special treatment.
- if (aPacket.type in ThreadStateTypes &&
- aPacket.from in this._threadClients) {
- this._threadClients[aPacket.from]._onThreadState(aPacket);
- }
- // On navigation the server resumes, so the client must resume as well.
- // We achieve that by generating a fake resumption packet that triggers
- // the client's thread state change listeners.
- if (this.activeThread &&
- aPacket.type == UnsolicitedNotifications.tabNavigated &&
- aPacket.from in this._tabClients) {
- let resumption = { from: this.activeThread._actor, type: "resumed" };
- this.activeThread._onThreadState(resumption);
- }
- // Only try to notify listeners on events, not responses to requests
- // that lack a packet type.
- if (aPacket.type) {
- this.notify(aPacket.type, aPacket);
- }
+ // Packets that indicate thread state changes get special treatment.
+ if (aPacket.type in ThreadStateTypes &&
+ aPacket.from in this._threadClients) {
+ this._threadClients[aPacket.from]._onThreadState(aPacket);
+ }
+ // On navigation the server resumes, so the client must resume as well.
+ // We achieve that by generating a fake resumption packet that triggers
+ // the client's thread state change listeners.
+ if (this.activeThread &&
+ aPacket.type == UnsolicitedNotifications.tabNavigated &&
+ aPacket.from in this._tabClients) {
+ let resumption = { from: this.activeThread._actor, type: "resumed" };
+ this.activeThread._onThreadState(resumption);
+ }
+ // Only try to notify listeners on events, not responses to requests
+ // that lack a packet type.
+ if (aPacket.type) {
+ this.notify(aPacket.type, aPacket);
+ }
- if (onResponse) {
- onResponse(aPacket);
- }
- } catch(ex) {
- dumpn("Error handling response: " + ex + " - stack:\n" + ex.stack);
- Cu.reportError(ex + "\n" + ex.stack);
+ if (onResponse) {
+ onResponse(aPacket);
}
this._sendRequests();
- }.bind(this));
+ }, function (ex) {
+ dumpn("Error handling response: " + ex + " - stack:\n" + ex.stack);
+ Cu.reportError(ex.message + "\n" + ex.stack);
+ });
},
/**
@@ -831,6 +838,7 @@ function TabClient(aClient, aActor) {
TabClient.prototype = {
get actor() { return this._actor },
+ get _transport() { return this._client._transport; },
/**
* Detach the client from the tab actor.
@@ -885,6 +893,7 @@ ThreadClient.prototype = {
get actor() { return this._actor; },
get compat() { return this._client.compat; },
+ get _transport() { return this._client._transport; },
_assertPaused: function TC_assertPaused(aCommand) {
if (!this.paused) {
@@ -1410,6 +1419,7 @@ function GripClient(aClient, aGrip)
GripClient.prototype = {
get actor() { return this._grip.actor },
+ get _transport() { return this._client._transport; },
valid: true,
@@ -1500,6 +1510,7 @@ LongStringClient.prototype = {
get actor() { return this._grip.actor; },
get length() { return this._grip.length; },
get initial() { return this._grip.initial; },
+ get _transport() { return this._client._transport; },
valid: true,
@@ -1536,6 +1547,8 @@ function SourceClient(aClient, aForm) {
}
SourceClient.prototype = {
+ get _transport() { return this._client._transport; },
+
/**
* Get a long string grip for this SourceClient's source.
*/
@@ -1593,6 +1606,7 @@ BreakpointClient.prototype = {
_actor: null,
get actor() { return this._actor; },
+ get _transport() { return this._client._transport; },
/**
* Remove the breakpoint from the server.
diff --git a/toolkit/devtools/debugger/server/dbg-script-actors.js b/toolkit/devtools/debugger/server/dbg-script-actors.js
index a5875e1e3a8..efb59d4f1fb 100644
--- a/toolkit/devtools/debugger/server/dbg-script-actors.js
+++ b/toolkit/devtools/debugger/server/dbg-script-actors.js
@@ -30,7 +30,6 @@ function ThreadActor(aHooks, aGlobal)
this._frameActors = [];
this._environmentActors = [];
this._hooks = aHooks;
- this._sources = {};
this.global = aGlobal;
// A cache of prototype chains for objects that have received a
@@ -46,6 +45,11 @@ function ThreadActor(aHooks, aGlobal)
this.findGlobals = this.globalManager.findGlobals.bind(this);
this.onNewGlobal = this.globalManager.onNewGlobal.bind(this);
+ this.onNewSource = this.onNewSource.bind(this);
+
+ this._options = {
+ useSourceMaps: false
+ };
}
/**
@@ -73,13 +77,21 @@ ThreadActor.prototype = {
return this._threadLifetimePool;
},
+ get sources() {
+ if (!this._sources) {
+ this._sources = new ThreadSources(this, this._options.useSourceMaps,
+ this._allowSource, this.onNewSource);
+ }
+ return this._sources;
+ },
+
clearDebuggees: function TA_clearDebuggees() {
if (this.dbg) {
this.dbg.removeAllDebuggees();
}
this.conn.removeActorPool(this._threadLifetimePool || undefined);
this._threadLifetimePool = null;
- this._sources = {};
+ this._sources = null;
},
/**
@@ -202,6 +214,8 @@ ThreadActor.prototype = {
this._state = "attached";
+ update(this._options, aRequest.options || {});
+
if (!this.dbg) {
this._initDebugger();
}
@@ -226,15 +240,17 @@ ThreadActor.prototype = {
// We already sent a response to this request, don't send one
// now.
return null;
- } catch(e) {
- Cu.reportError(e);
+ } catch (e) {
+ reportError(e);
return { error: "notAttached", message: e.toString() };
}
},
onDetach: function TA_onDetach(aRequest) {
this.disconnect();
- return { type: "detached" };
+ return {
+ type: "detached"
+ };
},
/**
@@ -245,15 +261,19 @@ ThreadActor.prototype = {
* The newest debuggee frame in the stack.
* @param object aReason
* An object with a 'type' property containing the reason for the pause.
+ * @param function onPacket
+ * Hook to modify the packet before it is sent. Feel free to return a
+ * promise.
*/
- _pauseAndRespond: function TA__pauseAndRespond(aFrame, aReason) {
+ _pauseAndRespond: function TA__pauseAndRespond(aFrame, aReason,
+ onPacket=function (k) k) {
try {
let packet = this._paused(aFrame);
if (!packet) {
return undefined;
}
packet.why = aReason;
- this.conn.send(packet);
+ resolve(onPacket(packet)).then(this.conn.send.bind(this.conn));
return this._nest();
} catch(e) {
let msg = "Got an exception during TA__pauseAndRespond: " + e +
@@ -268,6 +288,14 @@ ThreadActor.prototype = {
* Handle a protocol request to resume execution of the debuggee.
*/
onResume: function TA_onResume(aRequest) {
+ if (this._state !== "paused") {
+ return {
+ error: "wrongState",
+ message: "Can't resume when debuggee isn't paused. Current state is '"
+ + this._state + "'"
+ };
+ }
+
// In case of multiple nested event loops (due to multiple debuggers open in
// different tabs or multiple debugger clients connected to the same tab)
// only allow resumption in a LIFO order.
@@ -439,14 +467,23 @@ ThreadActor.prototype = {
// Return request.count frames, or all remaining
// frames if count is not defined.
let frames = [];
- for (; frame && (!count || i < (start + count)); i++) {
+ let promises = [];
+ for (; frame && (!count || i < (start + count)); i++, frame=frame.older) {
let form = this._createFrameActor(frame).form();
form.depth = i;
frames.push(form);
- frame = frame.older;
+
+ let promise = this.sources.getOriginalLocation(form.where.url,
+ form.where.line)
+ .then(function (aOrigLocation) {
+ form.where = aOrigLocation;
+ });
+ promises.push(promise);
}
- return { frames: frames };
+ return resolveAll(promises).then(function () {
+ return { frames: frames };
+ });
},
onReleaseMany: function TA_onReleaseMany(aRequest) {
@@ -479,25 +516,60 @@ ThreadActor.prototype = {
message: "Breakpoints can only be set while the debuggee is paused."};
}
- let location = aRequest.location;
- let line = location.line;
- if (this.dbg.findScripts({ url: location.url }).length == 0 || line < 0) {
- return { error: "noScript" };
- }
+ // XXX: `originalColumn` is never used. See bug 827639.
+ let { url: originalSource,
+ line: originalLine,
+ column: originalColumn } = aRequest.location;
- // Add the breakpoint to the store for later reuse, in case it belongs to a
- // script that hasn't appeared yet.
- if (!this._breakpointStore[location.url]) {
- this._breakpointStore[location.url] = [];
- }
- let scriptBreakpoints = this._breakpointStore[location.url];
- scriptBreakpoints[line] = {
- url: location.url,
- line: line,
- column: location.column
- };
+ let locationPromise = this.sources.getGeneratedLocation(originalSource,
+ originalLine)
+ return locationPromise.then((aLocation) => {
+ let line = aLocation.line;
+ if (this.dbg.findScripts({ url: aLocation.url }).length == 0 ||
+ line < 0 ||
+ line == null) {
+ return { error: "noScript" };
+ }
- return this._setBreakpoint(location);
+ // Add the breakpoint to the store for later reuse, in case it belongs to a
+ // script that hasn't appeared yet.
+ if (!this._breakpointStore[aLocation.url]) {
+ this._breakpointStore[aLocation.url] = [];
+ }
+ let scriptBreakpoints = this._breakpointStore[aLocation.url];
+ scriptBreakpoints[line] = {
+ url: aLocation.url,
+ line: line,
+ column: aLocation.column
+ };
+
+ let response = this._setBreakpoint(aLocation);
+ // If the original location of our generated location is different from
+ // the original location we attempted to set the breakpoint on, we will
+ // need to know so that we can set actualLocation on the response.
+ let originalLocation = this.sources.getOriginalLocation(aLocation.url,
+ aLocation.line);
+
+ return resolveAll([response, originalLocation])
+ .then(([aResponse, {url, line}]) => {
+ if (aResponse.actualLocation) {
+ let actualOrigLocation = this.sources.getOriginalLocation(
+ aResponse.actualLocation.url, aResponse.actualLocation.line);
+ return actualOrigLocation.then(function ({ url, line }) {
+ if (url !== originalSource || line !== originalLine) {
+ aResponse.actualLocation = { url: url, line: line };
+ }
+ return aResponse;
+ });
+ }
+
+ if (url !== originalSource || line !== originalLine) {
+ aResponse.actualLocation = { url: url, line: line };
+ }
+
+ return aResponse;
+ });
+ });
},
/**
@@ -608,17 +680,16 @@ ThreadActor.prototype = {
* Get the script and source lists from the debugger.
*/
_discoverScriptsAndSources: function TA__discoverScriptsAndSources() {
- for (let s of this.dbg.findScripts()) {
- this._addScript(s);
- }
+ return resolveAll([this._addScript(s)
+ for (s of this.dbg.findScripts())]);
},
onSources: function TA_onSources(aRequest) {
- this._discoverScriptsAndSources();
- let urls = Object.getOwnPropertyNames(this._sources);
- return {
- sources: [this._getSource(url).form() for (url of urls)]
- };
+ return this._discoverScriptsAndSources().then(() => {
+ return {
+ sources: [s.form() for (s of this.sources.iter())]
+ };
+ });
},
/**
@@ -655,8 +726,8 @@ ThreadActor.prototype = {
// We already sent a response to this request, don't send one
// now.
return null;
- } catch(e) {
- Cu.reportError(e);
+ } catch (e) {
+ reportError(e);
return { error: "notInterrupted", message: e.toString() };
}
},
@@ -1029,26 +1100,6 @@ ThreadActor.prototype = {
return aString.length >= DebuggerServer.LONG_STRING_LENGTH;
},
- /**
- * Create a source grip for the given script.
- */
- sourceGrip: function TA_sourceGrip(aScript) {
- // TODO: Once we have Debugger.Source, this should be replaced with a
- // weakmap mapping Debugger.Source instances to SourceActor instances.
- if (!this.threadLifetimePool.sourceActors) {
- this.threadLifetimePool.sourceActors = {};
- }
-
- if (this.threadLifetimePool.sourceActors[aScript.url]) {
- return this.threadLifetimePool.sourceActors[aScript.url].grip();
- }
-
- let actor = new SourceActor(aScript.url, this);
- this.threadLifetimePool.addActor(actor);
- this.threadLifetimePool.sourceActors[aScript.url] = actor;
- return actor.form();
- },
-
// JS Debugger API hooks.
/**
@@ -1114,6 +1165,14 @@ ThreadActor.prototype = {
this._addScript(aScript);
},
+ onNewSource: function TA_onNewSource(aSource) {
+ this.conn.send({
+ from: this.actorID,
+ type: "newSource",
+ source: aSource.form()
+ });
+ },
+
/**
* Check if scripts from the provided source URL are allowed to be stored in
* the cache.
@@ -1122,7 +1181,7 @@ ThreadActor.prototype = {
* The url of the script's source that will be stored.
* @returns true, if the script can be added, false otherwise.
*/
- _allowSource: function TA__allowScript(aSourceUrl) {
+ _allowSource: function TA__allowSource(aSourceUrl) {
// Ignore anything we don't have a URL for (eval scripts, for example).
if (!aSourceUrl)
return false;
@@ -1146,28 +1205,30 @@ ThreadActor.prototype = {
*/
_addScript: function TA__addScript(aScript) {
if (!this._allowSource(aScript.url)) {
- return false;
+ return resolve(false);
}
// TODO bug 637572: we should be dealing with sources directly, not
// inferring them through scripts.
- this._addSource(aScript.url);
+ return this.sources.sourcesForScript(aScript).then(() => {
- // Set any stored breakpoints.
- let existing = this._breakpointStore[aScript.url];
- if (existing) {
- let endLine = aScript.startLine + aScript.lineCount - 1;
- // Iterate over the lines backwards, so that sliding breakpoints don't
- // affect the loop.
- for (let line = existing.length - 1; line >= 0; line--) {
- let bp = existing[line];
- // Limit search to the line numbers contained in the new script.
- if (bp && line >= aScript.startLine && line <= endLine) {
- this._setBreakpoint(bp);
+ // Set any stored breakpoints.
+ let existing = this._breakpointStore[aScript.url];
+ if (existing) {
+ let endLine = aScript.startLine + aScript.lineCount - 1;
+ // Iterate over the lines backwards, so that sliding breakpoints don't
+ // affect the loop.
+ for (let line = existing.length - 1; line >= 0; line--) {
+ let bp = existing[line];
+ // Limit search to the line numbers contained in the new script.
+ if (bp && line >= aScript.startLine && line <= endLine) {
+ this._setBreakpoint(bp);
+ }
}
}
- }
- return true;
+
+ return true;
+ });
},
/**
@@ -1215,48 +1276,6 @@ ThreadActor.prototype = {
return retval;
},
- /**
- * Add a source to the current set of sources.
- *
- * Right now this takes an url, but in the future it should
- * take a Debugger.Source.
- *
- * @param string the source URL.
- * @returns a SourceActor representing the source.
- */
- _addSource: function TA__addSource(aURL) {
- if (!this._allowSource(aURL)) {
- return false;
- }
-
- if (aURL in this._sources) {
- return true;
- }
-
- let actor = new SourceActor(aURL, this);
- this.threadLifetimePool.addActor(actor);
- this._sources[aURL] = actor;
-
- this.conn.send({
- from: this.actorID,
- type: "newSource",
- source: actor.form()
- });
-
- return true;
- },
-
- /**
- * Get the source actor for the given URL.
- */
- _getSource: function TA__getSource(aUrl) {
- let source = this._sources[aUrl];
- if (!source) {
- throw new Error("No source for '" + aUrl + "'");
- }
- return source;
- },
-
};
ThreadActor.prototype.requestTypes = {
@@ -1340,7 +1359,7 @@ PauseScopedActor.prototype = {
error: "wrongState",
message: this.constructor.name +
" actors can only be accessed while the thread is paused."
- }
+ };
}
};
@@ -1362,7 +1381,8 @@ SourceActor.prototype = {
constructor: SourceActor,
actorPrefix: "source",
- get threadActor() { return this._threadActor; },
+ get threadActor() this._threadActor,
+ get url() this._url,
form: function SA_form() {
return {
@@ -1382,10 +1402,9 @@ SourceActor.prototype = {
* Handler for the "source" packet.
*/
onSource: function SA_onSource(aRequest) {
- return this
- ._loadSource()
+ return fetch(this._url)
.then(function(aSource) {
- return this._threadActor.createValueGrip(
+ return this.threadActor.createValueGrip(
aSource, this.threadActor.threadLifetimePool);
}.bind(this))
.then(function (aSourceGrip) {
@@ -1394,121 +1413,17 @@ SourceActor.prototype = {
source: aSourceGrip
};
}.bind(this), function (aError) {
+ let msg = "Got an exception during SA_onSource: " + aError +
+ "\n" + aError.stack;
+ Cu.reportError(msg);
+ dumpn(msg);
return {
"from": this.actorID,
"error": "loadSourceError",
"message": "Could not load the source for " + this._url + "."
};
}.bind(this));
- },
-
- /**
- * Convert a given string, encoded in a given character set, to unicode.
- * @param string aString
- * A string.
- * @param string aCharset
- * A character set.
- * @return string
- * A unicode string.
- */
- _convertToUnicode: function SS__convertToUnicode(aString, aCharset) {
- // Decoding primitives.
- let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
- .createInstance(Ci.nsIScriptableUnicodeConverter);
-
- try {
- converter.charset = aCharset || "UTF-8";
- return converter.ConvertToUnicode(aString);
- } catch(e) {
- return aString;
- }
- },
-
- /**
- * Performs a request to load the desired URL and returns a promise.
- *
- * @param aURL String
- * The URL we will request.
- * @returns Promise
- *
- * XXX: It may be better to use nsITraceableChannel to get to the sources
- * without relying on caching when we can (not for eval, etc.):
- * http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
- */
- _loadSource: function SA__loadSource() {
- let deferred = defer();
- let scheme;
- let url = this._url.split(" -> ").pop();
-
- try {
- scheme = Services.io.extractScheme(url);
- } catch (e) {
- // In the xpcshell tests, the script url is the absolute path of the test
- // file, which will make a malformed URI error be thrown. Add the file
- // scheme prefix ourselves.
- url = "file://" + url;
- scheme = Services.io.extractScheme(url);
- }
-
- switch (scheme) {
- case "file":
- case "chrome":
- case "resource":
- try {
- NetUtil.asyncFetch(url, function onFetch(aStream, aStatus) {
- if (!Components.isSuccessCode(aStatus)) {
- deferred.reject(new Error("Request failed"));
- return;
- }
-
- let source = NetUtil.readInputStreamToString(aStream, aStream.available());
- deferred.resolve(this._convertToUnicode(source));
- aStream.close();
- }.bind(this));
- } catch (ex) {
- deferred.reject(new Error("Request failed"));
- }
- break;
-
- default:
- let channel;
- try {
- channel = Services.io.newChannel(url, null, null);
- } catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") {
- // On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but
- // newChannel won't be able to handle it.
- url = "file:///" + url;
- channel = Services.io.newChannel(url, null, null);
- }
- let chunks = [];
- let streamListener = {
- onStartRequest: function(aRequest, aContext, aStatusCode) {
- if (!Components.isSuccessCode(aStatusCode)) {
- deferred.reject("Request failed");
- }
- },
- onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
- chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
- },
- onStopRequest: function(aRequest, aContext, aStatusCode) {
- if (!Components.isSuccessCode(aStatusCode)) {
- deferred.reject("Request failed");
- return;
- }
-
- deferred.resolve(this._convertToUnicode(chunks.join(""),
- channel.contentCharset));
- }.bind(this)
- };
-
- channel.loadFlags = channel.LOAD_FROM_CACHE;
- channel.asyncOpen(streamListener, null);
- break;
- }
-
- return deferred.promise;
}
-
};
SourceActor.prototype.requestTypes = {
@@ -2095,7 +2010,14 @@ BreakpointActor.prototype = {
hit: function BA_hit(aFrame) {
// TODO: add the rest of the breakpoints on that line (bug 676602).
let reason = { type: "breakpoint", actors: [ this.actorID ] };
- return this.threadActor._pauseAndRespond(aFrame, reason);
+ return this.threadActor._pauseAndRespond(aFrame, reason, (aPacket) => {
+ let { url, line } = aPacket.frame.where;
+ return this.threadActor.sources.getOriginalLocation(url, line)
+ .then(function (aOrigPosition) {
+ aPacket.frame.where = aOrigPosition;
+ return aPacket;
+ });
+ });
},
/**
@@ -2417,6 +2339,205 @@ update(ChromeDebuggerActor.prototype, {
});
+/**
+ * Manages the sources for a thread. Handles source maps, locations in the
+ * sources, etc for ThreadActors.
+ */
+function ThreadSources(aThreadActor, aUseSourceMaps,
+ aAllowPredicate, aOnNewSource) {
+ this._thread = aThreadActor;
+ this._useSourceMaps = aUseSourceMaps;
+ this._allow = aAllowPredicate;
+ this._onNewSource = aOnNewSource;
+
+ // source map URL --> promise of SourceMapConsumer
+ this._sourceMaps = Object.create(null);
+ // generated source url --> promise of SourceMapConsumer
+ this._sourceMapsByGeneratedSource = Object.create(null);
+ // original source url --> promise of SourceMapConsumer
+ this._sourceMapsByOriginalSource = Object.create(null);
+ // source url --> SourceActor
+ this._sourceActors = Object.create(null);
+ // original url --> generated url
+ this._generatedUrlsByOriginalUrl = Object.create(null);
+}
+
+ThreadSources.prototype = {
+ /**
+ * Add a source to the current set of sources.
+ *
+ * Right now this takes a URL, but in the future it should
+ * take a Debugger.Source. See bug 637572.
+ *
+ * @param string the source URL.
+ * @returns a SourceActor representing the source or null.
+ */
+ source: function TS_source(aURL) {
+ if (!this._allow(aURL)) {
+ return null;
+ }
+
+ if (aURL in this._sourceActors) {
+ return this._sourceActors[aURL];
+ }
+
+ let actor = new SourceActor(aURL, this._thread);
+ this._thread.threadLifetimePool.addActor(actor);
+ this._sourceActors[aURL] = actor;
+ try {
+ this._onNewSource(actor);
+ } catch (e) {
+ reportError(e);
+ }
+ return actor;
+ },
+
+ /**
+ * Add all of the sources associated with the given script.
+ */
+ sourcesForScript: function TS_sourcesForScript(aScript) {
+ if (!this._useSourceMaps || !aScript.sourceMapURL) {
+ return resolve([this.source(aScript.url)].filter(isNotNull));
+ }
+
+ return this.sourceMap(aScript)
+ .then((aSourceMap) => {
+ return [
+ this.source(s) for (s of aSourceMap.sources)
+ ];
+ }, (e) => {
+ reportError(e);
+ delete this._sourceMaps[this._normalize(aScript.sourceMapURL, aScript.url)];
+ delete this._sourceMapsByGeneratedSource[aScript.url];
+ return [this.source(aScript.url)];
+ })
+ .then(function (aSources) {
+ return aSources.filter(isNotNull);
+ });
+ },
+
+ /**
+ * Add the source map for the given script.
+ */
+ sourceMap: function TS_sourceMap(aScript) {
+ if (aScript.url in this._sourceMapsByGeneratedSource) {
+ return this._sourceMapsByGeneratedSource[aScript.url];
+ }
+ dbg_assert(aScript.sourceMapURL);
+ let sourceMapURL = this._normalize(aScript.sourceMapURL,
+ aScript.url);
+ let map = this._fetchSourceMap(sourceMapURL)
+ .then((aSourceMap) => {
+ for (let s of aSourceMap.sources) {
+ this._generatedUrlsByOriginalUrl[s] = aScript.url;
+ this._sourceMapsByOriginalSource[s] = resolve(aSourceMap);
+ }
+ return aSourceMap;
+ });
+ this._sourceMapsByGeneratedSource[aScript.url] = map;
+ return map;
+ },
+
+ /**
+ * Fetch the source map located at the given url.
+ */
+ _fetchSourceMap: function TS__fetchSourceMap(aAbsSourceMapURL) {
+ if (aAbsSourceMapURL in this._sourceMaps) {
+ return this._sourceMaps[aAbsSourceMapURL];
+ } else {
+ let promise = fetch(aAbsSourceMapURL).then((rawSourceMap) => {
+ let map = new SourceMapConsumer(rawSourceMap);
+ let base = aAbsSourceMapURL.replace(/\/[^\/]+$/, '/');
+ if (base.indexOf("data:") !== 0) {
+ map.sourceRoot = map.sourceRoot
+ ? this._normalize(map.sourceRoot, base)
+ : base;
+ }
+ return map;
+ });
+ this._sourceMaps[aAbsSourceMapURL] = promise;
+ return promise;
+ }
+ },
+
+ /**
+ * Returns a promise for the location in the original source if the source is
+ * source mapped, otherwise a promise of the same location.
+ *
+ * TODO bug 637572: take/return a column
+ */
+ getOriginalLocation: function TS_getOriginalLocation(aSourceUrl, aLine) {
+ if (aSourceUrl in this._sourceMapsByGeneratedSource) {
+ return this._sourceMapsByGeneratedSource[aSourceUrl]
+ .then(function (aSourceMap) {
+ let { source, line } = aSourceMap.originalPositionFor({
+ source: aSourceUrl,
+ line: aLine,
+ column: Infinity
+ });
+ return {
+ url: source,
+ line: line
+ };
+ });
+ }
+
+ // No source map
+ return resolve({
+ url: aSourceUrl,
+ line: aLine
+ });
+ },
+
+ /**
+ * Returns a promise of the location in the generated source corresponding to
+ * the original source and line given.
+ *
+ * TODO bug 637572: take/return a column
+ */
+ getGeneratedLocation: function TS_getGeneratedLocation(aSourceUrl, aLine) {
+ if (aSourceUrl in this._sourceMapsByOriginalSource) {
+ return this._sourceMapsByOriginalSource[aSourceUrl]
+ .then((aSourceMap) => {
+ let { line } = aSourceMap.generatedPositionFor({
+ source: aSourceUrl,
+ line: aLine,
+ column: Infinity
+ });
+ return {
+ url: this._generatedUrlsByOriginalUrl[aSourceUrl],
+ line: line
+ };
+ });
+ }
+
+ // No source map
+ return resolve({
+ url: aSourceUrl,
+ line: aLine
+ });
+ },
+
+ /**
+ * Normalize multiple relative paths towards the base paths on the right.
+ */
+ _normalize: function TS__normalize(...aURLs) {
+ dbg_assert(aURLs.length > 1);
+ let base = Services.io.newURI(aURLs.pop(), null, null);
+ let url;
+ while ((url = aURLs.pop())) {
+ base = Services.io.newURI(url, null, base);
+ }
+ return base.spec;
+ },
+
+ iter: function TS_iter() {
+ for (let url in this._sourceActors) {
+ yield this._sourceActors[url];
+ }
+ }
+};
+
// Utility functions.
/**
@@ -2437,3 +2558,126 @@ function update(aTarget, aNewAttrs) {
}
}
}
+
+/**
+ * Returns true if its argument is not null.
+ */
+function isNotNull(aThing) {
+ return aThing !== null;
+}
+
+/**
+ * Performs a request to load the desired URL and returns a promise.
+ *
+ * @param aURL String
+ * The URL we will request.
+ * @returns Promise
+ *
+ * XXX: It may be better to use nsITraceableChannel to get to the sources
+ * without relying on caching when we can (not for eval, etc.):
+ * http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
+ */
+function fetch(aURL) {
+ let deferred = defer();
+ let scheme;
+ let url = aURL.split(" -> ").pop();
+ let charset;
+
+ try {
+ scheme = Services.io.extractScheme(url);
+ } catch (e) {
+ // In the xpcshell tests, the script url is the absolute path of the test
+ // file, which will make a malformed URI error be thrown. Add the file
+ // scheme prefix ourselves.
+ url = "file://" + url;
+ scheme = Services.io.extractScheme(url);
+ }
+
+ switch (scheme) {
+ case "file":
+ case "chrome":
+ case "resource":
+ try {
+ NetUtil.asyncFetch(url, function onFetch(aStream, aStatus) {
+ if (!Components.isSuccessCode(aStatus)) {
+ deferred.reject("Request failed: " + url);
+ return;
+ }
+
+ let source = NetUtil.readInputStreamToString(aStream, aStream.available());
+ deferred.resolve(source);
+ aStream.close();
+ });
+ } catch (ex) {
+ deferred.reject("Request failed: " + url);
+ }
+ break;
+
+ default:
+ let channel;
+ try {
+ channel = Services.io.newChannel(url, null, null);
+ } catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") {
+ // On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but
+ // newChannel won't be able to handle it.
+ url = "file:///" + url;
+ channel = Services.io.newChannel(url, null, null);
+ }
+ let chunks = [];
+ let streamListener = {
+ onStartRequest: function(aRequest, aContext, aStatusCode) {
+ if (!Components.isSuccessCode(aStatusCode)) {
+ deferred.reject("Request failed: " + url);
+ }
+ },
+ onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
+ chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
+ },
+ onStopRequest: function(aRequest, aContext, aStatusCode) {
+ if (!Components.isSuccessCode(aStatusCode)) {
+ deferred.reject("Request failed: " + url);
+ return;
+ }
+
+ charset = channel.contentCharset;
+ deferred.resolve(chunks.join(""));
+ }
+ };
+
+ channel.loadFlags = channel.LOAD_FROM_CACHE;
+ channel.asyncOpen(streamListener, null);
+ break;
+ }
+
+ return deferred.promise.then(function (source) {
+ return convertToUnicode(source, charset);
+ });
+}
+
+/**
+ * Convert a given string, encoded in a given character set, to unicode.
+ *
+ * @param string aString
+ * A string.
+ * @param string aCharset
+ * A character set.
+ */
+function convertToUnicode(aString, aCharset=null) {
+ // Decoding primitives.
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ try {
+ converter.charset = aCharset || "UTF-8";
+ return converter.ConvertToUnicode(aString);
+ } catch(e) {
+ return aString;
+ }
+}
+
+/**
+ * Report the given error in the error console and to stdout.
+ */
+function reportError(aError) {
+ Cu.reportError(aError);
+ dumpn(aError.message + ":\n" + aError.stack);
+}
diff --git a/toolkit/devtools/debugger/server/dbg-server.js b/toolkit/devtools/debugger/server/dbg-server.js
index d7a89b8202a..74c3fd1500e 100644
--- a/toolkit/devtools/debugger/server/dbg-server.js
+++ b/toolkit/devtools/debugger/server/dbg-server.js
@@ -26,6 +26,12 @@ addDebuggerToGlobal(this);
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
const { defer, resolve, reject } = Promise;
+let promisedArray = Promise.promised(Array);
+function resolveAll(aPromises) {
+ return promisedArray.apply(null, aPromises);
+};
+
+Cu.import("resource://gre/modules/devtools/SourceMap.jsm");
function dumpn(str) {
if (wantLogging) {
@@ -600,6 +606,17 @@ DebuggerServerConnection.prototype = {
return null;
},
+ _unknownError: function DSC__unknownError(aPrefix, aError) {
+ let errorString = safeErrorString(aError);
+ errorString += "\n" + aError.stack;
+ Cu.reportError(errorString);
+ dumpn(errorString);
+ return {
+ error: "unknownError",
+ message: (aPrefix + "': " + errorString)
+ };
+ },
+
// Transport hooks.
/**
@@ -622,12 +639,9 @@ DebuggerServerConnection.prototype = {
try {
instance = new actor();
} catch (e) {
- Cu.reportError(e);
- this.transport.send({
- error: "unknownError",
- message: ("error occurred while creating actor '" + actor.name +
- "': " + safeErrorString(e))
- });
+ this.transport.send(this._unknownError(
+ "Error occurred while creating actor '" + actor.name,
+ e));
}
instance.parentID = actor.parentID;
// We want the newly-constructed actor to completely replace the factory
@@ -639,16 +653,14 @@ DebuggerServerConnection.prototype = {
}
var ret = null;
-
// Dispatch the request to the actor.
if (actor.requestTypes && actor.requestTypes[aPacket.type]) {
try {
ret = actor.requestTypes[aPacket.type].bind(actor)(aPacket);
} catch(e) {
- Cu.reportError(e);
- ret = { error: "unknownError",
- message: ("error occurred while processing '" + aPacket.type +
- "' request: " + safeErrorString(e)) };
+ this.transport.send(this._unknownError(
+ "error occurred while processing '" + aPacket.type,
+ e));
}
} else {
ret = { error: "unrecognizedPacketType",
@@ -663,12 +675,19 @@ DebuggerServerConnection.prototype = {
return;
}
- resolve(ret).then(function(returnPacket) {
- if (!returnPacket.from) {
- returnPacket.from = aPacket.to;
- }
- this.transport.send(returnPacket);
- }.bind(this));
+ resolve(ret)
+ .then(null, (e) => {
+ return this._unknownError(
+ "error occurred while processing '" + aPacket.type,
+ e);
+ })
+ .then(function (aResponse) {
+ if (!aResponse.from) {
+ aResponse.from = aPacket.to;
+ }
+ return aResponse;
+ })
+ .then(this.transport.send.bind(this.transport));
},
/**
diff --git a/toolkit/devtools/debugger/tests/unit/head_dbg.js b/toolkit/devtools/debugger/tests/unit/head_dbg.js
index 1e13cf8b816..69207ed9358 100644
--- a/toolkit/devtools/debugger/tests/unit/head_dbg.js
+++ b/toolkit/devtools/debugger/tests/unit/head_dbg.js
@@ -111,7 +111,7 @@ function getTestGlobalContext(aClient, aName, aCallback) {
function attachTestGlobalClient(aClient, aName, aCallback) {
getTestGlobalContext(aClient, aName, function(aContext) {
- aClient.attachThread(aContext.actor, aCallback);
+ aClient.attachThread(aContext.actor, aCallback, { useSourceMaps: true });
});
}
@@ -147,7 +147,7 @@ function attachTestTabAndResume(aClient, aName, aCallback) {
aThreadClient.resume(function (aResponse) {
aCallback(aResponse, aTabClient, aThreadClient);
});
- });
+ }, { useSourceMaps: true });
});
}
@@ -175,6 +175,14 @@ function finishClient(aClient)
});
}
+/**
+ * Takes a relative file path and returns the absolute file url for it.
+ */
+function getFileUrl(aName) {
+ let file = do_get_file(aName);
+ return Services.io.newFileURI(file).spec;
+}
+
/**
* Returns the full path of the file with the specified name in a
* platform-independent and URL-like form.
@@ -190,3 +198,20 @@ function getFilePath(aName)
}
return path.slice(filePrePath.length);
}
+
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+/**
+ * Returns the full text contents of the given file.
+ */
+function readFile(aFileName) {
+ let f = do_get_file(aFileName);
+ let s = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ s.init(f, -1, -1, false);
+ try {
+ return NetUtil.readInputStreamToString(s, s.available());
+ } finally {
+ s.close();
+ }
+}
diff --git a/toolkit/devtools/debugger/tests/unit/source-map-data/sourcemapped.coffee b/toolkit/devtools/debugger/tests/unit/source-map-data/sourcemapped.coffee
new file mode 100644
index 00000000000..73a400a219d
--- /dev/null
+++ b/toolkit/devtools/debugger/tests/unit/source-map-data/sourcemapped.coffee
@@ -0,0 +1,6 @@
+foo = (n) ->
+ return "foo" + i for i in [0...n]
+
+[first, second, third] = foo(3)
+
+debugger
\ No newline at end of file
diff --git a/toolkit/devtools/debugger/tests/unit/source-map-data/sourcemapped.map b/toolkit/devtools/debugger/tests/unit/source-map-data/sourcemapped.map
new file mode 100644
index 00000000000..dcee3c33c39
--- /dev/null
+++ b/toolkit/devtools/debugger/tests/unit/source-map-data/sourcemapped.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "file": "sourcemapped.js",
+ "sourceRoot": "",
+ "sources": [
+ "sourcemapped.coffee"
+ ],
+ "names": [],
+ "mappings": ";AAAA;CAAA,KAAA,yBAAA;CAAA;CAAA,CAAA,CAAA,MAAO;CACL,IAAA,GAAA;AAAA,CAAA,EAAA,MAA0B,qDAA1B;CAAA,EAAe,EAAR,QAAA;CAAP,IADI;CAAN,EAAM;;CAAN,CAGA,CAAyB,IAAA;;CAEzB,UALA;CAAA"
+}
\ No newline at end of file
diff --git a/toolkit/devtools/debugger/tests/unit/sourcemapped.js b/toolkit/devtools/debugger/tests/unit/sourcemapped.js
new file mode 100644
index 00000000000..7dbcb2d1493
--- /dev/null
+++ b/toolkit/devtools/debugger/tests/unit/sourcemapped.js
@@ -0,0 +1,16 @@
+// Generated by CoffeeScript 1.6.1
+(function() {
+ var first, foo, second, third, _ref;
+
+ foo = function(n) {
+ var i, _i;
+ for (i = _i = 0; 0 <= n ? _i < n : _i > n; i = 0 <= n ? ++_i : --_i) {
+ return "foo" + i;
+ }
+ };
+
+ _ref = foo(3), first = _ref[0], second = _ref[1], third = _ref[2];
+
+ debugger;
+
+}).call(this);
diff --git a/toolkit/devtools/debugger/tests/unit/test_breakpoint-04.js b/toolkit/devtools/debugger/tests/unit/test_breakpoint-04.js
index 6c91aa956ef..c83c26c445a 100644
--- a/toolkit/devtools/debugger/tests/unit/test_breakpoint-04.js
+++ b/toolkit/devtools/debugger/tests/unit/test_breakpoint-04.js
@@ -61,8 +61,8 @@ function test_child_breakpoint()
gDebuggee.eval("var line0 = Error().lineNumber;\n" +
"function foo() {\n" + // line0 + 1
- " this.a = 1;\n" + // line0 + 2
- " this.b = 2;\n" + // line0 + 3
+ " this.a = 1;\n" + // line0 + 2
+ " this.b = 2;\n" + // line0 + 3
"}\n" + // line0 + 4
"debugger;\n" + // line0 + 5
"foo();\n"); // line0 + 6
diff --git a/toolkit/devtools/debugger/tests/unit/test_dbgactor.js b/toolkit/devtools/debugger/tests/unit/test_dbgactor.js
index 9802638048e..131acda9fb2 100644
--- a/toolkit/devtools/debugger/tests/unit/test_dbgactor.js
+++ b/toolkit/devtools/debugger/tests/unit/test_dbgactor.js
@@ -38,6 +38,7 @@ function run_test()
function test_attach(aContext)
{
gClient.request({ to: aContext.actor, type: "attach" }, function(aResponse) {
+ do_check_true(!aResponse.error);
do_check_eq(aResponse.type, "paused");
// Resume the thread and test the debugger statement.
diff --git a/toolkit/devtools/debugger/tests/unit/test_listsources-02.js b/toolkit/devtools/debugger/tests/unit/test_listsources-02.js
index 044f5eaba26..91eabf3df6b 100644
--- a/toolkit/devtools/debugger/tests/unit/test_listsources-02.js
+++ b/toolkit/devtools/debugger/tests/unit/test_listsources-02.js
@@ -47,6 +47,6 @@ function test_listing_zero_sources()
"Should only send one sources request at most, even though we"
+ " might have had to send one to determine feature support.");
- finishClient(gClient);
+ finishClient(gClient);
});
}
diff --git a/toolkit/devtools/debugger/tests/unit/test_listsources-03.js b/toolkit/devtools/debugger/tests/unit/test_listsources-03.js
new file mode 100644
index 00000000000..7325f4a887e
--- /dev/null
+++ b/toolkit/devtools/debugger/tests/unit/test_listsources-03.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check getSources functionality when there are lots of sources.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+function run_test()
+{
+ initTestDebuggerServer();
+ gDebuggee = addTestGlobal("test-sources");
+ gClient = new DebuggerClient(DebuggerServer.connectPipe());
+ gClient.connect(function () {
+ attachTestGlobalClientAndResume(gClient, "test-sources", function (aResponse, aThreadClient) {
+ gThreadClient = aThreadClient;
+ test_simple_listsources();
+ });
+ });
+ do_test_pending();
+}
+
+function test_simple_listsources()
+{
+ gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+ gThreadClient.getSources(function (aResponse) {
+ do_check_true(
+ !aResponse.error,
+ "There shouldn't be an error fetching large amounts of sources.");
+
+ do_check_true(aResponse.sources.some(function (s) {
+ return s.url.match(/foo-999.js$/);
+ }));
+
+ gThreadClient.resume(function () {
+ finishClient(gClient);
+ });
+ });
+ });
+
+ for (let i = 0; i < 1000; i++) {
+ Cu.evalInSandbox("function foo###() {return ###;}".replace(/###/g, i),
+ gDebuggee,
+ "1.8",
+ "http://example.com/foo-" + i + ".js",
+ 1);
+ }
+ gDebuggee.eval("debugger;");
+}
diff --git a/toolkit/devtools/debugger/tests/unit/test_profiler_actor.js b/toolkit/devtools/debugger/tests/unit/test_profiler_actor.js
index ef38552ccbe..64d11bcea9a 100644
--- a/toolkit/devtools/debugger/tests/unit/test_profiler_actor.js
+++ b/toolkit/devtools/debugger/tests/unit/test_profiler_actor.js
@@ -114,21 +114,15 @@ function test_profile(aClient, aProfiler)
do_check_eq(typeof aResponse.profile.threads[0].samples, "object");
do_check_neq(aResponse.profile.threads[0].samples.length, 0);
- function some(array, cb) {
- for (var i = array.length; i; i--) {
- if (cb(array[i - 1]))
- return true;
- }
- return false;
- }
+ let location = stack.name + " (" + stack.filename + ":" + funcLine + ")";
// At least one sample is expected to have been in the busy wait above.
- do_check_true(some(aResponse.profile.threads[0].samples, function(sample) {
+ do_check_true(aResponse.profile.threads[0].samples.some(function(sample) {
return sample.name == "(root)" &&
typeof sample.frames == "object" &&
sample.frames.length != 0 &&
sample.frames.some(function(f) {
return (f.line == stack.lineNumber) &&
- (f.location == stack.name + " (" + stack.filename + ":" + funcLine + ")");
+ (f.location == location);
});
}));
@@ -161,9 +155,13 @@ function test_profiler_status()
var profiler = aResponse.profilerActor;
do_check_false(Profiler.IsActive());
- client.request({ to: profiler, type: "startProfiler", features: [] }, (aResponse) => {
+ client.request({
+ to: profiler,
+ type: "startProfiler",
+ features: []
+ }, function (aResponse) {
do_check_true(Profiler.IsActive());
- client.close(function () {});
+ client.close();
});
});
});
diff --git a/toolkit/devtools/debugger/tests/unit/test_source-01.js b/toolkit/devtools/debugger/tests/unit/test_source-01.js
index a1fd94e4c48..684e38f44d8 100644
--- a/toolkit/devtools/debugger/tests/unit/test_source-01.js
+++ b/toolkit/devtools/debugger/tests/unit/test_source-01.js
@@ -10,8 +10,6 @@ var gThreadClient;
// and that they can communicate over the protocol to fetch the source text for
// a given script.
-Cu.import("resource://gre/modules/NetUtil.jsm");
-
function run_test()
{
initTestDebuggerServer();
@@ -60,15 +58,9 @@ function test_source()
do_check_true(!aResponse.error);
do_check_true(!!aResponse.source);
- let f = do_get_file("test_source-01.js", false);
- let s = Cc["@mozilla.org/network/file-input-stream;1"]
- .createInstance(Ci.nsIFileInputStream);
- s.init(f, -1, -1, false);
-
- do_check_eq(NetUtil.readInputStreamToString(s, s.available()),
+ do_check_eq(readFile("test_source-01.js"),
aResponse.source);
- s.close();
gThreadClient.resume(function () {
finishClient(gClient);
});
diff --git a/toolkit/devtools/debugger/tests/unit/test_sourcemaps-01.js b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-01.js
new file mode 100644
index 00000000000..3c00183ccd6
--- /dev/null
+++ b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-01.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check basic source map integration with the "newSource" packet in the RDP.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+Components.utils.import('resource:///modules/devtools/SourceMap.jsm');
+
+function run_test()
+{
+ initTestDebuggerServer();
+ gDebuggee = addTestGlobal("test-source-map");
+ gClient = new DebuggerClient(DebuggerServer.connectPipe());
+ gClient.connect(function() {
+ attachTestGlobalClientAndResume(gClient, "test-source-map", function(aResponse, aThreadClient) {
+ gThreadClient = aThreadClient;
+ test_simple_source_map();
+ });
+ });
+ do_test_pending();
+}
+
+function test_simple_source_map()
+{
+ // Because we are source mapping, we should be notified of a.js, b.js, and
+ // c.js as sources, and shouldn't receive abc.js or test_sourcemaps-01.js.
+ let expectedSources = new Set(["http://example.com/www/js/a.js",
+ "http://example.com/www/js/b.js",
+ "http://example.com/www/js/c.js"]);
+
+ gClient.addListener("newSource", function _onNewSource(aEvent, aPacket) {
+ do_check_eq(aEvent, "newSource");
+ do_check_eq(aPacket.type, "newSource");
+ do_check_true(!!aPacket.source);
+
+ do_check_true(expectedSources.has(aPacket.source.url),
+ "The source url should be one of our original sources.");
+ expectedSources.delete(aPacket.source.url);
+
+ if (expectedSources.size === 0) {
+ gClient.removeListener("newSource", _onNewSource);
+ finishClient(gClient);
+ }
+ });
+
+ let { code, map } = (new SourceNode(null, null, null, [
+ new SourceNode(1, 0, "a.js", "function a() { return 'a'; }\n"),
+ new SourceNode(1, 0, "b.js", "function b() { return 'b'; }\n"),
+ new SourceNode(1, 0, "c.js", "function c() { return 'c'; }\n"),
+ ])).toStringWithSourceMap({
+ file: "abc.js",
+ sourceRoot: "http://example.com/www/js/"
+ });
+
+ code += "//@ sourceMappingURL=data:text/json;base64," + btoa(map.toString());
+
+ Components.utils.evalInSandbox(code, gDebuggee, "1.8",
+ "http://example.com/www/js/abc.js", 1);
+}
diff --git a/toolkit/devtools/debugger/tests/unit/test_sourcemaps-02.js b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-02.js
new file mode 100644
index 00000000000..4310884a08b
--- /dev/null
+++ b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-02.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check basic source map integration with the "sources" packet in the RDP.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+Components.utils.import("resource:///modules/devtools/SourceMap.jsm");
+
+function run_test()
+{
+ initTestDebuggerServer();
+ gDebuggee = addTestGlobal("test-source-map");
+ gClient = new DebuggerClient(DebuggerServer.connectPipe());
+ gClient.connect(function() {
+ attachTestGlobalClientAndResume(gClient, "test-source-map", function(aResponse, aThreadClient) {
+ gThreadClient = aThreadClient;
+ test_simple_source_map();
+ });
+ });
+ do_test_pending();
+}
+
+function test_simple_source_map()
+{
+ // Because we are source mapping, we should be notified of a.js, b.js, and
+ // c.js as sources, and shouldn"t receive abc.js or test_sourcemaps-01.js.
+ let expectedSources = new Set(["http://example.com/www/js/a.js",
+ "http://example.com/www/js/b.js",
+ "http://example.com/www/js/c.js"]);
+
+ let numNewSources = 0;
+
+ gClient.addListener("newSource", function _onNewSource(aEvent, aPacket) {
+ if (++numNewSources !== 3) {
+ return;
+ }
+ gClient.removeListener("newSource", _onNewSource);
+
+ gThreadClient.getSources(function (aResponse) {
+ do_check_true(!aResponse.error, "Should not get an error");
+
+ for (let s of aResponse.sources) {
+ do_check_true(expectedSources.has(s.url),
+ "The source's url should be one of our original sources");
+ expectedSources.delete(s.url);
+ }
+
+ do_check_eq(expectedSources.size, 0,
+ "Shouldn't be expecting any more sources");
+
+ finishClient(gClient);
+ });
+ });
+
+ let { code, map } = (new SourceNode(null, null, null, [
+ new SourceNode(1, 0, "a.js", "function a() { return 'a'; }\n"),
+ new SourceNode(1, 0, "b.js", "function b() { return 'b'; }\n"),
+ new SourceNode(1, 0, "c.js", "function c() { return 'c'; }\n"),
+ ])).toStringWithSourceMap({
+ file: "abc.js",
+ sourceRoot: "http://example.com/www/js/"
+ });
+
+ code += "//@ sourceMappingURL=data:text/json;base64," + btoa(map.toString());
+
+ Components.utils.evalInSandbox(code, gDebuggee, "1.8",
+ "http://example.com/www/js/abc.js", 1);
+}
diff --git a/toolkit/devtools/debugger/tests/unit/test_sourcemaps-03.js b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-03.js
new file mode 100644
index 00000000000..802277861c9
--- /dev/null
+++ b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-03.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check setting breakpoints in source mapped sources.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+Components.utils.import('resource:///modules/devtools/SourceMap.jsm');
+
+function run_test()
+{
+ initTestDebuggerServer();
+ gDebuggee = addTestGlobal("test-source-map");
+ gClient = new DebuggerClient(DebuggerServer.connectPipe());
+ gClient.connect(function() {
+ attachTestGlobalClientAndResume(gClient, "test-source-map", function(aResponse, aThreadClient) {
+ gThreadClient = aThreadClient;
+ test_simple_source_map();
+ });
+ });
+ do_test_pending();
+}
+
+function testBreakpointMapping(aName, aCallback)
+{
+ // Pause so we can set a breakpoint.
+ gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+ do_check_true(!aPacket.error);
+ do_check_eq(aPacket.why.type, "debuggerStatement");
+
+ gThreadClient.setBreakpoint({
+ url: "http://example.com/www/js/" + aName + ".js",
+ // Setting the breakpoint on an empty line so that it is pushed down one
+ // line and we can check the source mapped actualLocation later.
+ line: 3,
+ column: 0
+ }, function (aResponse) {
+ do_check_true(!aResponse.error);
+
+ // Actual location should come back source mapped still so that
+ // breakpoints are displayed in the UI correctly, etc.
+ do_check_eq(aResponse.actualLocation.line, 4);
+ do_check_eq(aResponse.actualLocation.url,
+ "http://example.com/www/js/" + aName + ".js");
+
+ // The eval will cause us to resume, then we get an unsolicited pause
+ // because of our breakpoint, we resume again to finish the eval, and
+ // finally receive our last pause which has the result of the client
+ // evaluation.
+ gThreadClient.eval(null, aName + "()", function (aResponse) {
+ do_check_true(!aResponse.error, "Shouldn't be an error resuming to eval");
+ do_check_eq(aResponse.type, "resumed");
+
+ gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+ do_check_eq(aPacket.why.type, "breakpoint");
+ // Assert that we paused because of the breakpoint at the correct
+ // location in the code by testing that the value of `ret` is still
+ // undefined.
+ do_check_eq(aPacket.frame.environment.bindings.variables.ret.value.type,
+ "undefined");
+
+ gThreadClient.resume(function (aResponse) {
+ do_check_true(!aResponse.error);
+
+ gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
+ do_check_eq(aPacket.why.type, "clientEvaluated");
+ do_check_eq(aPacket.why.frameFinished.return, aName);
+
+ gThreadClient.resume(function (aResponse) {
+ do_check_true(!aResponse.error);
+ aCallback();
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+
+ gDebuggee.eval("(" + function () {
+ debugger;
+ } + "());");
+}
+
+function test_simple_source_map()
+{
+ let expectedSources = new Set([
+ "http://example.com/www/js/a.js",
+ "http://example.com/www/js/b.js",
+ "http://example.com/www/js/c.js"
+ ]);
+
+ gClient.addListener("newSource", function _onNewSource(aEvent, aPacket) {
+ expectedSources.delete(aPacket.source.url);
+ if (expectedSources.size > 0) {
+ return;
+ }
+ gClient.removeListener("newSource", _onNewSource);
+
+ testBreakpointMapping("a", function () {
+ testBreakpointMapping("b", function () {
+ testBreakpointMapping("c", function () {
+ finishClient(gClient);
+ });
+ });
+ });
+ });
+
+ let a = new SourceNode(null, null, null, [
+ new SourceNode(1, 0, "a.js", "function a() {\n"),
+ new SourceNode(2, 0, "a.js", " var ret;\n"),
+ new SourceNode(3, 0, "a.js", " // Empty line\n"),
+ new SourceNode(4, 0, "a.js", " ret = 'a';\n"),
+ new SourceNode(5, 0, "a.js", " return ret;\n"),
+ new SourceNode(6, 0, "a.js", "}\n")
+ ]);
+ let b = new SourceNode(null, null, null, [
+ new SourceNode(1, 0, "b.js", "function b() {\n"),
+ new SourceNode(2, 0, "b.js", " var ret;\n"),
+ new SourceNode(3, 0, "b.js", " // Empty line\n"),
+ new SourceNode(4, 0, "b.js", " ret = 'b';\n"),
+ new SourceNode(5, 0, "b.js", " return ret;\n"),
+ new SourceNode(6, 0, "b.js", "}\n")
+ ]);
+ let c = new SourceNode(null, null, null, [
+ new SourceNode(1, 0, "c.js", "function c() {\n"),
+ new SourceNode(2, 0, "c.js", " var ret;\n"),
+ new SourceNode(3, 0, "c.js", " // Empty line\n"),
+ new SourceNode(4, 0, "c.js", " ret = 'c';\n"),
+ new SourceNode(5, 0, "c.js", " return ret;\n"),
+ new SourceNode(6, 0, "c.js", "}\n")
+ ]);
+
+ let { code, map } = (new SourceNode(null, null, null, [
+ a, b, c
+ ])).toStringWithSourceMap({
+ file: "http://example.com/www/js/abc.js",
+ sourceRoot: "http://example.com/www/js/"
+ });
+
+ code += "//@ sourceMappingURL=data:text/json;base64," + btoa(map.toString());
+
+ Components.utils.evalInSandbox(code, gDebuggee, "1.8",
+ "http://example.com/www/js/abc.js", 1);
+}
diff --git a/toolkit/devtools/debugger/tests/unit/test_sourcemaps-04.js b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-04.js
new file mode 100644
index 00000000000..af61f6c4896
--- /dev/null
+++ b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-04.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check that absolute source map urls work.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+Components.utils.import('resource:///modules/devtools/SourceMap.jsm');
+
+function run_test()
+{
+ initTestDebuggerServer();
+ gDebuggee = addTestGlobal("test-source-map");
+ gClient = new DebuggerClient(DebuggerServer.connectPipe());
+ gClient.connect(function() {
+ attachTestGlobalClientAndResume(gClient, "test-source-map", function(aResponse, aThreadClient) {
+ gThreadClient = aThreadClient;
+ test_absolute_source_map();
+ });
+ });
+ do_test_pending();
+}
+
+function test_absolute_source_map()
+{
+ gClient.addOneTimeListener("newSource", function _onNewSource(aEvent, aPacket) {
+ do_check_eq(aEvent, "newSource");
+ do_check_eq(aPacket.type, "newSource");
+ do_check_true(!!aPacket.source);
+
+ do_check_true(aPacket.source.url.indexOf("sourcemapped.coffee") !== -1,
+ "The new source should be a coffee file.");
+ do_check_eq(aPacket.source.url.indexOf("sourcemapped.js"), -1,
+ "The new source should not be a js file.");
+
+ finishClient(gClient);
+ });
+
+ code = readFile("sourcemapped.js")
+ + "\n//@ sourceMappingURL=" + getFileUrl("source-map-data/sourcemapped.map");
+
+ Components.utils.evalInSandbox(code, gDebuggee, "1.8",
+ getFileUrl("sourcemapped.js"), 1);
+}
diff --git a/toolkit/devtools/debugger/tests/unit/test_sourcemaps-05.js b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-05.js
new file mode 100644
index 00000000000..352fab41543
--- /dev/null
+++ b/toolkit/devtools/debugger/tests/unit/test_sourcemaps-05.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check that relative source map urls work.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+Components.utils.import('resource:///modules/devtools/SourceMap.jsm');
+
+function run_test()
+{
+ initTestDebuggerServer();
+ gDebuggee = addTestGlobal("test-source-map");
+ gClient = new DebuggerClient(DebuggerServer.connectPipe());
+ gClient.connect(function() {
+ attachTestGlobalClientAndResume(gClient, "test-source-map", function(aResponse, aThreadClient) {
+ gThreadClient = aThreadClient;
+ test_relative_source_map();
+ });
+ });
+ do_test_pending();
+}
+
+function test_relative_source_map()
+{
+ gClient.addOneTimeListener("newSource", function _onNewSource(aEvent, aPacket) {
+ do_check_eq(aEvent, "newSource");
+ do_check_eq(aPacket.type, "newSource");
+ do_check_true(!!aPacket.source);
+
+ do_check_true(aPacket.source.url.indexOf("sourcemapped.coffee") !== -1,
+ "The new source should be a coffee file.");
+ do_check_eq(aPacket.source.url.indexOf("sourcemapped.js"), -1,
+ "The new source should not be a js file.");
+
+ finishClient(gClient);
+ });
+
+ code = readFile("sourcemapped.js")
+ + "\n//@ sourceMappingURL=source-map-data/sourcemapped.map";
+
+ Components.utils.evalInSandbox(code, gDebuggee, "1.8",
+ getFileUrl("sourcemapped.js"), 1);
+}
diff --git a/toolkit/devtools/debugger/tests/unit/testcompatactors.js b/toolkit/devtools/debugger/tests/unit/testcompatactors.js
index 7eb44846ff2..0b778e51e1b 100644
--- a/toolkit/devtools/debugger/tests/unit/testcompatactors.js
+++ b/toolkit/devtools/debugger/tests/unit/testcompatactors.js
@@ -30,26 +30,26 @@ function createRootActor()
};
actor.thread.requestTypes["scripts"] = function (aRequest) {
- this._discoverScriptsAndSources();
-
- let scripts = [];
- for (let s of this.dbg.findScripts()) {
- if (!s.url) {
- continue;
+ return this._discoverScriptsAndSources().then(function () {
+ let scripts = [];
+ for (let s of this.dbg.findScripts()) {
+ if (!s.url) {
+ continue;
+ }
+ let script = {
+ url: s.url,
+ startLine: s.startLine,
+ lineCount: s.lineCount,
+ source: this.sources.source(s.url).form()
+ };
+ scripts.push(script);
}
- let script = {
- url: s.url,
- startLine: s.startLine,
- lineCount: s.lineCount,
- source: this._getSource(s.url).form()
- };
- scripts.push(script);
- }
- return {
- from: this.actorID,
- scripts: scripts
- };
+ return {
+ from: this.actorID,
+ scripts: scripts
+ };
+ }.bind(this));
};
// Pretend that we do not know about the "sources" packet to force the
@@ -70,7 +70,7 @@ function createRootActor()
url: aScript.url,
startLine: aScript.startLine,
lineCount: aScript.lineCount,
- source: actor.thread._getSource(aScript.url).form()
+ source: actor.thread.sources.source(aScript.url).form()
});
};
}(actor.thread.onNewScript));
diff --git a/toolkit/devtools/debugger/tests/unit/xpcshell.ini b/toolkit/devtools/debugger/tests/unit/xpcshell.ini
index 21b3aaa6be1..a273cdff594 100644
--- a/toolkit/devtools/debugger/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/debugger/tests/unit/xpcshell.ini
@@ -74,9 +74,19 @@ skip-if = toolkit == "gonk"
reason = bug 820380
[test_listsources-01.js]
[test_listsources-02.js]
+[test_listsources-03.js]
[test_new_source-01.js]
[test_sources_backwards_compat-01.js]
[test_sources_backwards_compat-02.js]
+[test_sourcemaps-01.js]
+[test_sourcemaps-02.js]
+[test_sourcemaps-03.js]
+[test_sourcemaps-04.js]
+skip-if = toolkit == "gonk"
+reason = bug 820380
+[test_sourcemaps-05.js]
+skip-if = toolkit == "gonk"
+reason = bug 820380
[test_objectgrips-01.js]
[test_objectgrips-02.js]
[test_objectgrips-03.js]
diff --git a/toolkit/devtools/sourcemap/SourceMap.jsm b/toolkit/devtools/sourcemap/SourceMap.jsm
index 0dc4063c10c..05139f7969d 100644
--- a/toolkit/devtools/sourcemap/SourceMap.jsm
+++ b/toolkit/devtools/sourcemap/SourceMap.jsm
@@ -44,6 +44,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
* - sources: An array of URLs to the original source files.
* - names: An array of identifiers which can be referrenced by individual mappings.
* - sourceRoot: Optional. The URL root from which all sources are relative.
+ * - sourcesContent: Optional. An array of contents of the original source files.
* - mappings: A string of base64 VLQs which contain the actual mappings.
* - file: The generated file this source map is associated with.
*
@@ -70,6 +71,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
var sources = util.getArg(sourceMap, 'sources');
var names = util.getArg(sourceMap, 'names');
var sourceRoot = util.getArg(sourceMap, 'sourceRoot', null);
+ var sourcesContent = util.getArg(sourceMap, 'sourcesContent', null);
var mappings = util.getArg(sourceMap, 'mappings');
var file = util.getArg(sourceMap, 'file');
@@ -79,7 +81,8 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
this._names = ArraySet.fromArray(names);
this._sources = ArraySet.fromArray(sources);
- this._sourceRoot = sourceRoot;
+ this.sourceRoot = sourceRoot;
+ this.sourcesContent = sourcesContent;
this.file = file;
// `this._generatedMappings` and `this._originalMappings` hold the parsed
@@ -121,7 +124,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
Object.defineProperty(SourceMapConsumer.prototype, 'sources', {
get: function () {
return this._sources.toArray().map(function (s) {
- return this._sourceRoot ? util.join(this._sourceRoot, s) : s;
+ return this.sourceRoot ? util.join(this.sourceRoot, s) : s;
}, this);
}
});
@@ -165,12 +168,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
if (str.length > 0 && !mappingSeparator.test(str.charAt(0))) {
// Original source.
temp = base64VLQ.decode(str);
- if (aSourceRoot) {
- mapping.source = util.join(aSourceRoot, this._sources.at(previousSource + temp.value));
- }
- else {
- mapping.source = this._sources.at(previousSource + temp.value);
- }
+ mapping.source = this._sources.at(previousSource + temp.value);
previousSource += temp.value;
str = temp.rest;
if (str.length === 0 || mappingSeparator.test(str.charAt(0))) {
@@ -204,7 +202,9 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
}
this._generatedMappings.push(mapping);
- this._originalMappings.push(mapping);
+ if (typeof mapping.originalLine === 'number') {
+ this._originalMappings.push(mapping);
+ }
}
}
@@ -291,11 +291,15 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
this._generatedMappings,
"generatedLine",
"generatedColumn",
- this._compareGeneratedPositions)
+ this._compareGeneratedPositions);
if (mapping) {
+ var source = util.getArg(mapping, 'source', null);
+ if (source && this.sourceRoot) {
+ source = util.join(this.sourceRoot, source);
+ }
return {
- source: util.getArg(mapping, 'source', null),
+ source: source,
line: util.getArg(mapping, 'originalLine', null),
column: util.getArg(mapping, 'originalColumn', null),
name: util.getArg(mapping, 'name', null)
@@ -310,6 +314,32 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
};
};
+ /**
+ * Returns the original source content. The only argument is
+ * the url of the original source file. Returns null if no
+ * original source content is availible.
+ */
+ SourceMapConsumer.prototype.sourceContentFor =
+ function SourceMapConsumer_sourceContentFor(aSource) {
+ if (!this.sourcesContent) {
+ return null;
+ }
+
+ if (this.sourceRoot) {
+ // Try to remove the sourceRoot
+ var relativeUrl = util.relative(this.sourceRoot, aSource);
+ if (this._sources.has(relativeUrl)) {
+ return this.sourcesContent[this._sources.indexOf(relativeUrl)];
+ }
+ }
+
+ if (this._sources.has(aSource)) {
+ return this.sourcesContent[this._sources.indexOf(aSource)];
+ }
+
+ throw new Error('"' + aSource + '" is not in the SourceMap.');
+ };
+
/**
* Returns the generated line and column information for the original source,
* line, and column positions provided. The only argument is an object with
@@ -332,11 +362,15 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
originalColumn: util.getArg(aArgs, 'column')
};
+ if (this.sourceRoot) {
+ needle.source = util.relative(this.sourceRoot, needle.source);
+ }
+
var mapping = this._findMapping(needle,
this._originalMappings,
"originalLine",
"originalColumn",
- this._compareOriginalPositions)
+ this._compareOriginalPositions);
if (mapping) {
return {
@@ -359,8 +393,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
* generated line/column in this source map.
*
* @param Function aCallback
- * The function that is called with each mapping. This function should
- * not mutate the mapping.
+ * The function that is called with each mapping.
* @param Object aContext
* Optional. If specified, this object will be the value of `this` every
* time that `aCallback` is called.
@@ -388,7 +421,21 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
throw new Error("Unknown order of iteration.");
}
- mappings.forEach(aCallback, context);
+ var sourceRoot = this.sourceRoot;
+ mappings.map(function (mapping) {
+ var source = mapping.source;
+ if (source && sourceRoot) {
+ source = util.join(sourceRoot, source);
+ }
+ return {
+ source: source,
+ generatedLine: mapping.generatedLine,
+ generatedColumn: mapping.generatedColumn,
+ originalLine: mapping.originalLine,
+ originalColumn: mapping.originalColumn,
+ name: mapping.name
+ };
+ }).forEach(aCallback, context);
};
exports.SourceMapConsumer = SourceMapConsumer;
@@ -423,13 +470,64 @@ define('source-map/util', ['require', 'exports', 'module' , ], function(require,
}
exports.getArg = getArg;
+ var urlRegexp = /([\w+\-.]+):\/\/((\w+:\w+)@)?([\w.]+)?(:(\d+))?(\S+)?/;
+
+ function urlParse(aUrl) {
+ var match = aUrl.match(urlRegexp);
+ if (!match) {
+ return null;
+ }
+ return {
+ scheme: match[1],
+ auth: match[3],
+ host: match[4],
+ port: match[6],
+ path: match[7]
+ };
+ }
+
function join(aRoot, aPath) {
- return aPath.charAt(0) === '/'
- ? aPath
- : aRoot.replace(/\/*$/, '') + '/' + aPath;
+ var url;
+
+ if (aPath.match(urlRegexp)) {
+ return aPath;
+ }
+
+ if (aPath.charAt(0) === '/' && (url = urlParse(aRoot))) {
+ return aRoot.replace(url.path, '') + aPath;
+ }
+
+ return aRoot.replace(/\/$/, '') + '/' + aPath;
}
exports.join = join;
+ /**
+ * Because behavior goes wacky when you set `__proto__` on objects, we
+ * have to prefix all the strings in our set with an arbitrary character.
+ *
+ * See https://github.com/mozilla/source-map/pull/31 and
+ * https://github.com/mozilla/source-map/issues/30
+ *
+ * @param String aStr
+ */
+ function toSetString(aStr) {
+ return '$' + aStr;
+ }
+ exports.toSetString = toSetString;
+
+ function fromSetString(aStr) {
+ return aStr.substr(1);
+ }
+ exports.fromSetString = fromSetString;
+
+ function relative(aRoot, aPath) {
+ aRoot = aRoot.replace(/\/$/, '');
+ return aPath.indexOf(aRoot + '/') === 0
+ ? aPath.substr(aRoot.length + 1)
+ : aPath;
+ }
+ exports.relative = relative;
+
});
/* -*- Mode: js; js-indent-level: 2; -*- */
/*
@@ -515,7 +613,9 @@ define('source-map/binary-search', ['require', 'exports', 'module' , ], function
* Licensed under the New BSD license. See LICENSE or:
* http://opensource.org/licenses/BSD-3-Clause
*/
-define('source-map/array-set', ['require', 'exports', 'module' , ], function(require, exports, module) {
+define('source-map/array-set', ['require', 'exports', 'module' , 'source-map/util'], function(require, exports, module) {
+
+ var util = require('source-map/util');
/**
* A data structure which is a combination of an array and a set. Adding a new
@@ -539,19 +639,6 @@ define('source-map/array-set', ['require', 'exports', 'module' , ], function(req
return set;
};
- /**
- * Because behavior goes wacky when you set `__proto__` on `this._set`, we
- * have to prefix all the strings in our set with an arbitrary character.
- *
- * See https://github.com/mozilla/source-map/pull/31 and
- * https://github.com/mozilla/source-map/issues/30
- *
- * @param String aStr
- */
- ArraySet.prototype._toSetString = function ArraySet__toSetString (aStr) {
- return "$" + aStr;
- };
-
/**
* Add the given string to this set.
*
@@ -564,7 +651,7 @@ define('source-map/array-set', ['require', 'exports', 'module' , ], function(req
}
var idx = this._array.length;
this._array.push(aStr);
- this._set[this._toSetString(aStr)] = idx;
+ this._set[util.toSetString(aStr)] = idx;
};
/**
@@ -574,7 +661,7 @@ define('source-map/array-set', ['require', 'exports', 'module' , ], function(req
*/
ArraySet.prototype.has = function ArraySet_has(aStr) {
return Object.prototype.hasOwnProperty.call(this._set,
- this._toSetString(aStr));
+ util.toSetString(aStr));
};
/**
@@ -584,7 +671,7 @@ define('source-map/array-set', ['require', 'exports', 'module' , ], function(req
*/
ArraySet.prototype.indexOf = function ArraySet_indexOf(aStr) {
if (this.has(aStr)) {
- return this._set[this._toSetString(aStr)];
+ return this._set[util.toSetString(aStr)];
}
throw new Error('"' + aStr + '" is not in the set.');
};
@@ -819,10 +906,58 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so
this._sources = new ArraySet();
this._names = new ArraySet();
this._mappings = [];
+ this._sourcesContents = null;
}
SourceMapGenerator.prototype._version = 3;
+ /**
+ * Creates a new SourceMapGenerator based on a SourceMapConsumer
+ *
+ * @param aSourceMapConsumer The SourceMap.
+ */
+ SourceMapGenerator.fromSourceMap =
+ function SourceMapGenerator_fromSourceMap(aSourceMapConsumer) {
+ var sourceRoot = aSourceMapConsumer.sourceRoot;
+ var generator = new SourceMapGenerator({
+ file: aSourceMapConsumer.file,
+ sourceRoot: sourceRoot
+ });
+ aSourceMapConsumer.eachMapping(function (mapping) {
+ var newMapping = {
+ generated: {
+ line: mapping.generatedLine,
+ column: mapping.generatedColumn
+ }
+ };
+
+ if (mapping.source) {
+ newMapping.source = mapping.source;
+ if (sourceRoot) {
+ newMapping.source = util.relative(sourceRoot, newMapping.source);
+ }
+
+ newMapping.original = {
+ line: mapping.originalLine,
+ column: mapping.originalColumn
+ };
+
+ if (mapping.name) {
+ newMapping.name = mapping.name;
+ }
+ }
+
+ generator.addMapping(newMapping);
+ });
+ aSourceMapConsumer.sources.forEach(function (sourceFile) {
+ var content = aSourceMapConsumer.sourceContentFor(sourceFile);
+ if (content) {
+ generator.setSourceContent(sourceFile, content);
+ }
+ });
+ return generator;
+ };
+
/**
* Add a single mapping from original source line and column to the generated
* source's line and column for this source map being created. The mapping
@@ -858,6 +993,110 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so
});
};
+ /**
+ * Set the source content for a source file.
+ */
+ SourceMapGenerator.prototype.setSourceContent =
+ function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) {
+ var source = aSourceFile;
+ if (this._sourceRoot) {
+ source = util.relative(this._sourceRoot, source);
+ }
+
+ if (aSourceContent !== null) {
+ // Add the source content to the _sourcesContents map.
+ // Create a new _sourcesContents map if the property is null.
+ if (!this._sourcesContents) {
+ this._sourcesContents = {};
+ }
+ this._sourcesContents[util.toSetString(source)] = aSourceContent;
+ } else {
+ // Remove the source file from the _sourcesContents map.
+ // If the _sourcesContents map is empty, set the property to null.
+ delete this._sourcesContents[util.toSetString(source)];
+ if (Object.keys(this._sourcesContents).length === 0) {
+ this._sourcesContents = null;
+ }
+ }
+ };
+
+ /**
+ * Applies the mappings of a sub-source-map for a specific source file to the
+ * source map being generated. Each mapping to the supplied source file is
+ * rewritten using the supplied source map. Note: The resolution for the
+ * resulting mappings is the minimium of this map and the supplied map.
+ *
+ * @param aSourceMapConsumer The source map to be applied.
+ * @param aSourceFile Optional. The filename of the source file.
+ * If omitted, SourceMapConsumer's file property will be used.
+ */
+ SourceMapGenerator.prototype.applySourceMap =
+ function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile) {
+ // If aSourceFile is omitted, we will use the file property of the SourceMap
+ if (!aSourceFile) {
+ aSourceFile = aSourceMapConsumer.file;
+ }
+ var sourceRoot = this._sourceRoot;
+ // Make "aSourceFile" relative if an absolute Url is passed.
+ if (sourceRoot) {
+ aSourceFile = util.relative(sourceRoot, aSourceFile);
+ }
+ // Applying the SourceMap can add and remove items from the sources and
+ // the names array.
+ var newSources = new ArraySet();
+ var newNames = new ArraySet();
+
+ // Find mappings for the "aSourceFile"
+ this._mappings.forEach(function (mapping) {
+ if (mapping.source === aSourceFile && mapping.original) {
+ // Check if it can be mapped by the source map, then update the mapping.
+ var original = aSourceMapConsumer.originalPositionFor({
+ line: mapping.original.line,
+ column: mapping.original.column
+ });
+ if (original.source !== null) {
+ // Copy mapping
+ if (sourceRoot) {
+ mapping.source = util.relative(sourceRoot, original.source);
+ } else {
+ mapping.source = original.source;
+ }
+ mapping.original.line = original.line;
+ mapping.original.column = original.column;
+ if (original.name !== null && mapping.name !== null) {
+ // Only use the identifier name if it's an identifier
+ // in both SourceMaps
+ mapping.name = original.name;
+ }
+ }
+ }
+
+ var source = mapping.source;
+ if (source && !newSources.has(source)) {
+ newSources.add(source);
+ }
+
+ var name = mapping.name;
+ if (name && !newNames.has(name)) {
+ newNames.add(name);
+ }
+
+ }, this);
+ this._sources = newSources;
+ this._names = newNames;
+
+ // Copy sourcesContents of applied map.
+ aSourceMapConsumer.sources.forEach(function (sourceFile) {
+ var content = aSourceMapConsumer.sourceContentFor(sourceFile);
+ if (content) {
+ if (sourceRoot) {
+ sourceFile = util.relative(sourceRoot, sourceFile);
+ }
+ this.setSourceContent(sourceFile, content);
+ }
+ }, this);
+ };
+
/**
* A mapping can have one of the three levels of data:
*
@@ -978,6 +1217,17 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so
if (this._sourceRoot) {
map.sourceRoot = this._sourceRoot;
}
+ if (this._sourcesContents) {
+ map.sourcesContent = map.sources.map(function (source) {
+ if (map.sourceRoot) {
+ source = util.relative(map.sourceRoot, source);
+ }
+ return Object.prototype.hasOwnProperty.call(
+ this._sourcesContents, util.toSetString(source))
+ ? this._sourcesContents[util.toSetString(source)]
+ : null;
+ }, this);
+ }
return map;
};
@@ -998,9 +1248,10 @@ define('source-map/source-map-generator', ['require', 'exports', 'module' , 'so
* Licensed under the New BSD license. See LICENSE or:
* http://opensource.org/licenses/BSD-3-Clause
*/
-define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/source-map-generator'], function(require, exports, module) {
+define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/source-map-generator', 'source-map/util'], function(require, exports, module) {
var SourceMapGenerator = require('source-map/source-map-generator').SourceMapGenerator;
+ var util = require('source-map/util');
/**
* SourceNodes provide a way to abstract over interpolating/concatenating
@@ -1012,15 +1263,121 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/
* @param aSource The original source's filename.
* @param aChunks Optional. An array of strings which are snippets of
* generated JS, or other SourceNodes.
+ * @param aName The original identifier.
*/
- function SourceNode(aLine, aColumn, aSource, aChunks) {
+ function SourceNode(aLine, aColumn, aSource, aChunks, aName) {
this.children = [];
- this.line = aLine;
- this.column = aColumn;
- this.source = aSource;
+ this.sourceContents = {};
+ this.line = aLine === undefined ? null : aLine;
+ this.column = aColumn === undefined ? null : aColumn;
+ this.source = aSource === undefined ? null : aSource;
+ this.name = aName === undefined ? null : aName;
if (aChunks != null) this.add(aChunks);
}
+ /**
+ * Creates a SourceNode from generated code and a SourceMapConsumer.
+ *
+ * @param aGeneratedCode The generated code
+ * @param aSourceMapConsumer The SourceMap for the generated code
+ */
+ SourceNode.fromStringWithSourceMap =
+ function SourceNode_fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer) {
+ // The SourceNode we want to fill with the generated code
+ // and the SourceMap
+ var node = new SourceNode();
+
+ // The generated code
+ // Processed fragments are removed from this array.
+ var remainingLines = aGeneratedCode.split('\n');
+
+ // We need to remember the position of "remainingLines"
+ var lastGeneratedLine = 1, lastGeneratedColumn = 0;
+
+ // The generate SourceNodes we need a code range.
+ // To extract it current and last mapping is used.
+ // Here we store the last mapping.
+ var lastMapping = null;
+
+ aSourceMapConsumer.eachMapping(function (mapping) {
+ if (lastMapping === null) {
+ // We add the generated code until the first mapping
+ // to the SourceNode without any mapping.
+ // Each line is added as separate string.
+ while (lastGeneratedLine < mapping.generatedLine) {
+ node.add(remainingLines.shift() + "\n");
+ lastGeneratedLine++;
+ }
+ if (lastGeneratedColumn < mapping.generatedColumn) {
+ var nextLine = remainingLines[0];
+ node.add(nextLine.substr(0, mapping.generatedColumn));
+ remainingLines[0] = nextLine.substr(mapping.generatedColumn);
+ lastGeneratedColumn = mapping.generatedColumn;
+ }
+ } else {
+ // We add the code from "lastMapping" to "mapping":
+ // First check if there is a new line in between.
+ if (lastGeneratedLine < mapping.generatedLine) {
+ var code = "";
+ // Associate full lines with "lastMapping"
+ do {
+ code += remainingLines.shift() + "\n";
+ lastGeneratedLine++;
+ lastGeneratedColumn = 0;
+ } while (lastGeneratedLine < mapping.generatedLine);
+ // When we reached the correct line, we add code until we
+ // reach the correct column too.
+ if (lastGeneratedColumn < mapping.generatedColumn) {
+ var nextLine = remainingLines[0];
+ code += nextLine.substr(0, mapping.generatedColumn);
+ remainingLines[0] = nextLine.substr(mapping.generatedColumn);
+ lastGeneratedColumn = mapping.generatedColumn;
+ }
+ // Create the SourceNode.
+ addMappingWithCode(lastMapping, code);
+ } else {
+ // There is no new line in between.
+ // Associate the code between "lastGeneratedColumn" and
+ // "mapping.generatedColumn" with "lastMapping"
+ var nextLine = remainingLines[0];
+ var code = nextLine.substr(0, mapping.generatedColumn -
+ lastGeneratedColumn);
+ remainingLines[0] = nextLine.substr(mapping.generatedColumn -
+ lastGeneratedColumn);
+ lastGeneratedColumn = mapping.generatedColumn;
+ addMappingWithCode(lastMapping, code);
+ }
+ }
+ lastMapping = mapping;
+ }, this);
+ // We have processed all mappings.
+ // Associate the remaining code in the current line with "lastMapping"
+ // and add the remaining lines without any mapping
+ addMappingWithCode(lastMapping, remainingLines.join("\n"));
+
+ // Copy sourcesContent into SourceNode
+ aSourceMapConsumer.sources.forEach(function (sourceFile) {
+ var content = aSourceMapConsumer.sourceContentFor(sourceFile);
+ if (content) {
+ node.setSourceContent(sourceFile, content);
+ }
+ });
+
+ return node;
+
+ function addMappingWithCode(mapping, code) {
+ if (mapping.source === undefined) {
+ node.add(code);
+ } else {
+ node.add(new SourceNode(mapping.originalLine,
+ mapping.originalColumn,
+ mapping.source,
+ code,
+ mapping.name));
+ }
+ }
+ };
+
/**
* Add a chunk of generated JS to this source node.
*
@@ -1083,7 +1440,10 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/
}
else {
if (chunk !== '') {
- aFn(chunk, { source: this.source, line: this.line, column: this.column });
+ aFn(chunk, { source: this.source,
+ line: this.line,
+ column: this.column,
+ name: this.name });
}
}
}, this);
@@ -1098,7 +1458,7 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/
SourceNode.prototype.join = function SourceNode_join(aSep) {
var newChildren;
var i;
- var len = this.children.length
+ var len = this.children.length;
if (len > 0) {
newChildren = [];
for (i = 0; i < len-1; i++) {
@@ -1132,6 +1492,36 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/
return this;
};
+ /**
+ * Set the source content for a source file. This will be added to the SourceMapGenerator
+ * in the sourcesContent field.
+ *
+ * @param aSourceFile The filename of the source file
+ * @param aSourceContent The content of the source file
+ */
+ SourceNode.prototype.setSourceContent =
+ function SourceNode_setSourceContent(aSourceFile, aSourceContent) {
+ this.sourceContents[util.toSetString(aSourceFile)] = aSourceContent;
+ };
+
+ /**
+ * Walk over the tree of SourceNodes. The walking function is called for each
+ * source file content and is passed the filename and source content.
+ *
+ * @param aFn The traversal function.
+ */
+ SourceNode.prototype.walkSourceContents =
+ function SourceNode_walkSourceContents(aFn) {
+ this.children.forEach(function (chunk) {
+ if (chunk instanceof SourceNode) {
+ chunk.walkSourceContents(aFn);
+ }
+ }, this);
+ Object.keys(this.sourceContents).forEach(function (sourceFileKey) {
+ aFn(util.fromSetString(sourceFileKey), this.sourceContents[sourceFileKey]);
+ }, this);
+ };
+
/**
* Return the string representation of this source node. Walks over the tree
* and concatenates all the various snippets together to one string.
@@ -1155,25 +1545,36 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/
column: 0
};
var map = new SourceMapGenerator(aArgs);
+ var sourceMappingActive = false;
this.walk(function (chunk, original) {
generated.code += chunk;
- if (original.source != null
- && original.line != null
- && original.column != null) {
+ if (original.source !== null
+ && original.line !== null
+ && original.column !== null) {
map.addMapping({
source: original.source,
original: {
line: original.line,
column: original.column
},
+ generated: {
+ line: generated.line,
+ column: generated.column
+ },
+ name: original.name
+ });
+ sourceMappingActive = true;
+ } else if (sourceMappingActive) {
+ map.addMapping({
generated: {
line: generated.line,
column: generated.column
}
});
+ sourceMappingActive = false;
}
- chunk.split('').forEach(function (char) {
- if (char === '\n') {
+ chunk.split('').forEach(function (ch) {
+ if (ch === '\n') {
generated.line++;
generated.column = 0;
} else {
@@ -1181,6 +1582,9 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/
}
});
});
+ this.walkSourceContents(function (sourceFile, sourceContent) {
+ map.setSourceContent(sourceFile, sourceContent);
+ });
return { code: generated.code, map: map };
};
@@ -1191,6 +1595,6 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/
/* -*- Mode: js; js-indent-level: 2; -*- */
///////////////////////////////////////////////////////////////////////////////
-let SourceMapConsumer = require('source-map/source-map-consumer').SourceMapConsumer;
-let SourceMapGenerator = require('source-map/source-map-generator').SourceMapGenerator;
-let SourceNode = require('source-map/source-node').SourceNode;
+this.SourceMapConsumer = require('source-map/source-map-consumer').SourceMapConsumer;
+this.SourceMapGenerator = require('source-map/source-map-generator').SourceMapGenerator;
+this.SourceNode = require('source-map/source-node').SourceNode;
diff --git a/toolkit/devtools/sourcemap/tests/unit/Utils.jsm b/toolkit/devtools/sourcemap/tests/unit/Utils.jsm
index 5f7c81d529f..d78371f3757 100644
--- a/toolkit/devtools/sourcemap/tests/unit/Utils.jsm
+++ b/toolkit/devtools/sourcemap/tests/unit/Utils.jsm
@@ -15,7 +15,7 @@
Components.utils.import('resource://gre/modules/devtools/Require.jsm');
Components.utils.import('resource://gre/modules/devtools/SourceMap.jsm');
-let EXPORTED_SYMBOLS = [ "define", "runSourceMapTests" ];
+this.EXPORTED_SYMBOLS = [ "define", "runSourceMapTests" ];
/* -*- Mode: js; js-indent-level: 2; -*- */
/*
* Copyright 2011 Mozilla Foundation and contributors
@@ -78,7 +78,9 @@ define('test/source-map/assert', ['exports'], function (exports) {
* Licensed under the New BSD license. See LICENSE or:
* http://opensource.org/licenses/BSD-3-Clause
*/
-define('test/source-map/util', ['require', 'exports', 'module' , ], function(require, exports, module) {
+define('test/source-map/util', ['require', 'exports', 'module' , 'lib/source-map/util'], function(require, exports, module) {
+
+ var util = require('source-map/util');
// This is a test mapping which maps functions from two different files
// (one.js and two.js) to a minified generated source.
@@ -99,6 +101,8 @@ define('test/source-map/util', ['require', 'exports', 'module' , ], function(req
//
// ONE.foo=function(a){return baz(a);};
// TWO.inc=function(a){return a+1;};
+ exports.testGeneratedCode = " ONE.foo=function(a){return baz(a);};\n"+
+ " TWO.inc=function(a){return a+1;};";
exports.testMap = {
version: 3,
file: 'min.js',
@@ -107,6 +111,22 @@ define('test/source-map/util', ['require', 'exports', 'module' , ], function(req
sourceRoot: '/the/root',
mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA'
};
+ exports.testMapWithSourcesContent = {
+ version: 3,
+ file: 'min.js',
+ names: ['bar', 'baz', 'n'],
+ sources: ['one.js', 'two.js'],
+ sourcesContent: [
+ ' ONE.foo = function (bar) {\n' +
+ ' return baz(bar);\n' +
+ ' };',
+ ' TWO.inc = function (n) {\n' +
+ ' return n + 1;\n' +
+ ' };'
+ ],
+ sourceRoot: '/the/root',
+ mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA'
+ };
function assertMapping(generatedLine, generatedColumn, originalSource,
originalLine, originalColumn, name, map, assert,
@@ -125,8 +145,21 @@ define('test/source-map/util', ['require', 'exports', 'module' , ], function(req
assert.equal(origMapping.column, originalColumn,
'Incorrect column, expected ' + JSON.stringify(originalColumn)
+ ', got ' + JSON.stringify(origMapping.column));
- assert.equal(origMapping.source, originalSource,
- 'Incorrect source, expected ' + JSON.stringify(originalSource)
+
+ var expectedSource;
+
+ if (originalSource && map.sourceRoot && originalSource.indexOf(map.sourceRoot) === 0) {
+ expectedSource = originalSource;
+ } else if (originalSource) {
+ expectedSource = map.sourceRoot
+ ? util.join(map.sourceRoot, originalSource)
+ : originalSource;
+ } else {
+ expectedSource = null;
+ }
+
+ assert.equal(origMapping.source, expectedSource,
+ 'Incorrect source, expected ' + JSON.stringify(expectedSource)
+ ', got ' + JSON.stringify(origMapping.source));
}
@@ -146,6 +179,135 @@ define('test/source-map/util', ['require', 'exports', 'module' , ], function(req
}
exports.assertMapping = assertMapping;
+ function assertEqualMaps(assert, actualMap, expectedMap) {
+ assert.equal(actualMap.version, expectedMap.version, "version mismatch");
+ assert.equal(actualMap.file, expectedMap.file, "file mismatch");
+ assert.equal(actualMap.names.length,
+ expectedMap.names.length,
+ "names length mismatch: " +
+ actualMap.names.join(", ") + " != " + expectedMap.names.join(", "));
+ for (var i = 0; i < actualMap.names.length; i++) {
+ assert.equal(actualMap.names[i],
+ expectedMap.names[i],
+ "names[" + i + "] mismatch: " +
+ actualMap.names.join(", ") + " != " + expectedMap.names.join(", "));
+ }
+ assert.equal(actualMap.sources.length,
+ expectedMap.sources.length,
+ "sources length mismatch: " +
+ actualMap.sources.join(", ") + " != " + expectedMap.sources.join(", "));
+ for (var i = 0; i < actualMap.sources.length; i++) {
+ assert.equal(actualMap.sources[i],
+ expectedMap.sources[i],
+ "sources[" + i + "] length mismatch: " +
+ actualMap.sources.join(", ") + " != " + expectedMap.sources.join(", "));
+ }
+ assert.equal(actualMap.sourceRoot,
+ expectedMap.sourceRoot,
+ "sourceRoot mismatch: " +
+ actualMap.sourceRoot + " != " + expectedMap.sourceRoot);
+ assert.equal(actualMap.mappings, expectedMap.mappings, "mappings mismatch");
+ if (actualMap.sourcesContent) {
+ assert.equal(actualMap.sourcesContent.length,
+ expectedMap.sourcesContent.length,
+ "sourcesContent length mismatch");
+ for (var i = 0; i < actualMap.sourcesContent.length; i++) {
+ assert.equal(actualMap.sourcesContent[i],
+ expectedMap.sourcesContent[i],
+ "sourcesContent[" + i + "] mismatch");
+ }
+ }
+ }
+ exports.assertEqualMaps = assertEqualMaps;
+
+});
+/* -*- Mode: js; js-indent-level: 2; -*- */
+/*
+ * Copyright 2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+define('lib/source-map/util', ['require', 'exports', 'module' , ], function(require, exports, module) {
+
+ /**
+ * This is a helper function for getting values from parameter/options
+ * objects.
+ *
+ * @param args The object we are extracting values from
+ * @param name The name of the property we are getting.
+ * @param defaultValue An optional value to return if the property is missing
+ * from the object. If this is not specified and the property is missing, an
+ * error will be thrown.
+ */
+ function getArg(aArgs, aName, aDefaultValue) {
+ if (aName in aArgs) {
+ return aArgs[aName];
+ } else if (arguments.length === 3) {
+ return aDefaultValue;
+ } else {
+ throw new Error('"' + aName + '" is a required argument.');
+ }
+ }
+ exports.getArg = getArg;
+
+ var urlRegexp = /([\w+\-.]+):\/\/((\w+:\w+)@)?([\w.]+)?(:(\d+))?(\S+)?/;
+
+ function urlParse(aUrl) {
+ var match = aUrl.match(urlRegexp);
+ if (!match) {
+ return null;
+ }
+ return {
+ scheme: match[1],
+ auth: match[3],
+ host: match[4],
+ port: match[6],
+ path: match[7]
+ };
+ }
+
+ function join(aRoot, aPath) {
+ var url;
+
+ if (aPath.match(urlRegexp)) {
+ return aPath;
+ }
+
+ if (aPath.charAt(0) === '/' && (url = urlParse(aRoot))) {
+ return aRoot.replace(url.path, '') + aPath;
+ }
+
+ return aRoot.replace(/\/$/, '') + '/' + aPath;
+ }
+ exports.join = join;
+
+ /**
+ * Because behavior goes wacky when you set `__proto__` on objects, we
+ * have to prefix all the strings in our set with an arbitrary character.
+ *
+ * See https://github.com/mozilla/source-map/pull/31 and
+ * https://github.com/mozilla/source-map/issues/30
+ *
+ * @param String aStr
+ */
+ function toSetString(aStr) {
+ return '$' + aStr;
+ }
+ exports.toSetString = toSetString;
+
+ function fromSetString(aStr) {
+ return aStr.substr(1);
+ }
+ exports.fromSetString = fromSetString;
+
+ function relative(aRoot, aPath) {
+ aRoot = aRoot.replace(/\/$/, '');
+ return aPath.indexOf(aRoot + '/') === 0
+ ? aPath.substr(aRoot.length + 1)
+ : aPath;
+ }
+ exports.relative = relative;
+
});
/* -*- Mode: js; js-indent-level: 2; -*- */
/*
@@ -167,3 +329,4 @@ function runSourceMapTests(modName, do_throw) {
}
}
+this.runSourceMapTests = runSourceMapTests;
diff --git a/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js b/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js
index e3438fc87f6..515287004df 100644
--- a/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js
+++ b/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js
@@ -15,6 +15,7 @@ Components.utils.import('resource://test/Utils.jsm');
define("test/source-map/test-source-map-consumer", ["require", "exports", "module"], function (require, exports, module) {
var SourceMapConsumer = require('source-map/source-map-consumer').SourceMapConsumer;
+ var SourceMapGenerator = require('source-map/source-map-generator').SourceMapGenerator;
exports['test that we can instantiate with a string or an objects'] = function (assert, util) {
assert.doesNotThrow(function () {
@@ -97,6 +98,10 @@ define("test/source-map/test-source-map-consumer", ["require", "exports", "modul
map.eachMapping(function (mapping) {
assert.ok(mapping.generatedLine >= previousLine);
+ if (mapping.source) {
+ assert.equal(mapping.source.indexOf(util.testMap.sourceRoot), 0);
+ }
+
if (mapping.generatedLine === previousLine) {
assert.ok(mapping.generatedColumn >= previousColumn);
previousColumn = mapping.generatedColumn;
@@ -144,6 +149,150 @@ define("test/source-map/test-source-map-consumer", ["require", "exports", "modul
}, context);
};
+ exports['test that the `sourcesContent` field has the original sources'] = function (assert, util) {
+ var map = new SourceMapConsumer(util.testMapWithSourcesContent);
+ var sourcesContent = map.sourcesContent;
+
+ assert.equal(sourcesContent[0], ' ONE.foo = function (bar) {\n return baz(bar);\n };');
+ assert.equal(sourcesContent[1], ' TWO.inc = function (n) {\n return n + 1;\n };');
+ assert.equal(sourcesContent.length, 2);
+ };
+
+ exports['test that we can get the original sources for the sources'] = function (assert, util) {
+ var map = new SourceMapConsumer(util.testMapWithSourcesContent);
+ var sources = map.sources;
+
+ assert.equal(map.sourceContentFor(sources[0]), ' ONE.foo = function (bar) {\n return baz(bar);\n };');
+ assert.equal(map.sourceContentFor(sources[1]), ' TWO.inc = function (n) {\n return n + 1;\n };');
+ assert.equal(map.sourceContentFor("one.js"), ' ONE.foo = function (bar) {\n return baz(bar);\n };');
+ assert.equal(map.sourceContentFor("two.js"), ' TWO.inc = function (n) {\n return n + 1;\n };');
+ assert.throws(function () {
+ map.sourceContentFor("");
+ }, Error);
+ assert.throws(function () {
+ map.sourceContentFor("/the/root/three.js");
+ }, Error);
+ assert.throws(function () {
+ map.sourceContentFor("three.js");
+ }, Error);
+ };
+
+ exports['test sourceRoot + generatedPositionFor'] = function (assert, util) {
+ var map = new SourceMapGenerator({
+ sourceRoot: 'foo/bar',
+ file: 'baz.js'
+ });
+ map.addMapping({
+ original: { line: 1, column: 1 },
+ generated: { line: 2, column: 2 },
+ source: 'bang.coffee'
+ });
+ map.addMapping({
+ original: { line: 5, column: 5 },
+ generated: { line: 6, column: 6 },
+ source: 'bang.coffee'
+ });
+ map = new SourceMapConsumer(map.toString());
+
+ // Should handle without sourceRoot.
+ var pos = map.generatedPositionFor({
+ line: 1,
+ column: 1,
+ source: 'bang.coffee'
+ });
+
+ assert.equal(pos.line, 2);
+ assert.equal(pos.column, 2);
+
+ // Should handle with sourceRoot.
+ var pos = map.generatedPositionFor({
+ line: 1,
+ column: 1,
+ source: 'foo/bar/bang.coffee'
+ });
+
+ assert.equal(pos.line, 2);
+ assert.equal(pos.column, 2);
+ };
+
+ exports['test sourceRoot + originalPositionFor'] = function (assert, util) {
+ var map = new SourceMapGenerator({
+ sourceRoot: 'foo/bar',
+ file: 'baz.js'
+ });
+ map.addMapping({
+ original: { line: 1, column: 1 },
+ generated: { line: 2, column: 2 },
+ source: 'bang.coffee'
+ });
+ map = new SourceMapConsumer(map.toString());
+
+ var pos = map.originalPositionFor({
+ line: 2,
+ column: 2,
+ });
+
+ // Should always have the prepended source root
+ assert.equal(pos.source, 'foo/bar/bang.coffee');
+ assert.equal(pos.line, 1);
+ assert.equal(pos.column, 1);
+ };
+
+ exports['test github issue #56'] = function (assert, util) {
+ var map = new SourceMapGenerator({
+ sourceRoot: 'http://',
+ file: 'www.example.com/foo.js'
+ });
+ map.addMapping({
+ original: { line: 1, column: 1 },
+ generated: { line: 2, column: 2 },
+ source: 'www.example.com/original.js'
+ });
+ map = new SourceMapConsumer(map.toString());
+
+ var sources = map.sources;
+ assert.equal(sources.length, 1);
+ assert.equal(sources[0], 'http://www.example.com/original.js');
+ };
+
+ exports['test github issue #43'] = function (assert, util) {
+ var map = new SourceMapGenerator({
+ sourceRoot: 'http://example.com',
+ file: 'foo.js'
+ });
+ map.addMapping({
+ original: { line: 1, column: 1 },
+ generated: { line: 2, column: 2 },
+ source: 'http://cdn.example.com/original.js'
+ });
+ map = new SourceMapConsumer(map.toString());
+
+ var sources = map.sources;
+ assert.equal(sources.length, 1,
+ 'Should only be one source.');
+ assert.equal(sources[0], 'http://cdn.example.com/original.js',
+ 'Should not be joined with the sourceRoot.');
+ };
+
+ exports['test absolute path, but same host sources'] = function (assert, util) {
+ var map = new SourceMapGenerator({
+ sourceRoot: 'http://example.com/foo/bar',
+ file: 'foo.js'
+ });
+ map.addMapping({
+ original: { line: 1, column: 1 },
+ generated: { line: 2, column: 2 },
+ source: '/original.js'
+ });
+ map = new SourceMapConsumer(map.toString());
+
+ var sources = map.sources;
+ assert.equal(sources.length, 1,
+ 'Should only be one source.');
+ assert.equal(sources[0], 'http://example.com/original.js',
+ 'Source should be relative the host of the source root.');
+ };
+
});
function run_test() {
runSourceMapTests('test/source-map/test-source-map-consumer', do_throw);
diff --git a/toolkit/devtools/sourcemap/tests/unit/test_source_map_generator.js b/toolkit/devtools/sourcemap/tests/unit/test_source_map_generator.js
index e585755e22d..4811438935d 100644
--- a/toolkit/devtools/sourcemap/tests/unit/test_source_map_generator.js
+++ b/toolkit/devtools/sourcemap/tests/unit/test_source_map_generator.js
@@ -15,6 +15,9 @@ Components.utils.import('resource://test/Utils.jsm');
define("test/source-map/test-source-map-generator", ["require", "exports", "module"], function (require, exports, module) {
var SourceMapGenerator = require('source-map/source-map-generator').SourceMapGenerator;
+ var SourceMapConsumer = require('source-map/source-map-consumer').SourceMapConsumer;
+ var SourceNode = require('source-map/source-node').SourceNode;
+ var util = require('source-map/util');
exports['test some simple stuff'] = function (assert, util) {
var map = new SourceMapGenerator({
@@ -176,19 +179,100 @@ define("test/source-map/test-source-map-generator", ["require", "exports", "modu
map = JSON.parse(map.toString());
- assert.equal(map.version, 3);
- assert.equal(map.file, 'min.js');
- assert.equal(map.names.length, 3);
- assert.equal(map.names[0], 'bar');
- assert.equal(map.names[1], 'baz');
- assert.equal(map.names[2], 'n');
- assert.equal(map.sources.length, 2);
- assert.equal(map.sources[0], 'one.js');
- assert.equal(map.sources[1], 'two.js');
- assert.equal(map.sourceRoot, '/the/root');
- assert.equal(map.mappings, 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA');
+ util.assertEqualMaps(assert, map, util.testMap);
};
+ exports['test that source content can be set'] = function (assert, util) {
+ var map = new SourceMapGenerator({
+ file: 'min.js',
+ sourceRoot: '/the/root'
+ });
+ map.addMapping({
+ generated: { line: 1, column: 1 },
+ original: { line: 1, column: 1 },
+ source: 'one.js'
+ });
+ map.addMapping({
+ generated: { line: 2, column: 1 },
+ original: { line: 1, column: 1 },
+ source: 'two.js'
+ });
+ map.setSourceContent('one.js', 'one file content');
+
+ map = JSON.parse(map.toString());
+ assert.equal(map.sources[0], 'one.js');
+ assert.equal(map.sources[1], 'two.js');
+ assert.equal(map.sourcesContent[0], 'one file content');
+ assert.equal(map.sourcesContent[1], null);
+ };
+
+ exports['test .fromSourceMap'] = function (assert, util) {
+ var map = SourceMapGenerator.fromSourceMap(new SourceMapConsumer(util.testMap));
+ util.assertEqualMaps(assert, map.toJSON(), util.testMap);
+ };
+
+ exports['test .fromSourceMap with sourcesContent'] = function (assert, util) {
+ var map = SourceMapGenerator.fromSourceMap(
+ new SourceMapConsumer(util.testMapWithSourcesContent));
+ util.assertEqualMaps(assert, map.toJSON(), util.testMapWithSourcesContent);
+ };
+
+ exports['test applySourceMap'] = function (assert, util) {
+ var node = new SourceNode(null, null, null, [
+ new SourceNode(2, 0, 'fileX', 'lineX2\n'),
+ 'genA1\n',
+ new SourceNode(2, 0, 'fileY', 'lineY2\n'),
+ 'genA2\n',
+ new SourceNode(1, 0, 'fileX', 'lineX1\n'),
+ 'genA3\n',
+ new SourceNode(1, 0, 'fileY', 'lineY1\n')
+ ]);
+ var mapStep1 = node.toStringWithSourceMap({
+ file: 'fileA'
+ }).map;
+ mapStep1.setSourceContent('fileX', 'lineX1\nlineX2\n');
+ mapStep1 = mapStep1.toJSON();
+
+ node = new SourceNode(null, null, null, [
+ 'gen1\n',
+ new SourceNode(1, 0, 'fileA', 'lineA1\n'),
+ new SourceNode(2, 0, 'fileA', 'lineA2\n'),
+ new SourceNode(3, 0, 'fileA', 'lineA3\n'),
+ new SourceNode(4, 0, 'fileA', 'lineA4\n'),
+ new SourceNode(1, 0, 'fileB', 'lineB1\n'),
+ new SourceNode(2, 0, 'fileB', 'lineB2\n'),
+ 'gen2\n'
+ ]);
+ var mapStep2 = node.toStringWithSourceMap({
+ file: 'fileGen'
+ }).map;
+ mapStep2.setSourceContent('fileB', 'lineB1\nlineB2\n');
+ mapStep2 = mapStep2.toJSON();
+
+ node = new SourceNode(null, null, null, [
+ 'gen1\n',
+ new SourceNode(2, 0, 'fileX', 'lineA1\n'),
+ new SourceNode(2, 0, 'fileA', 'lineA2\n'),
+ new SourceNode(2, 0, 'fileY', 'lineA3\n'),
+ new SourceNode(4, 0, 'fileA', 'lineA4\n'),
+ new SourceNode(1, 0, 'fileB', 'lineB1\n'),
+ new SourceNode(2, 0, 'fileB', 'lineB2\n'),
+ 'gen2\n'
+ ]);
+ var expectedMap = node.toStringWithSourceMap({
+ file: 'fileGen'
+ }).map;
+ expectedMap.setSourceContent('fileX', 'lineX1\nlineX2\n');
+ expectedMap.setSourceContent('fileB', 'lineB1\nlineB2\n');
+ expectedMap = expectedMap.toJSON();
+
+ // apply source map "mapStep1" to "mapStep2"
+ var generator = SourceMapGenerator.fromSourceMap(new SourceMapConsumer(mapStep2));
+ generator.applySourceMap(new SourceMapConsumer(mapStep1));
+ var actualMap = generator.toJSON();
+
+ util.assertEqualMaps(assert, actualMap, expectedMap);
+ };
});
function run_test() {
runSourceMapTests('test/source-map/test-source-map-generator', do_throw);
diff --git a/toolkit/devtools/sourcemap/tests/unit/test_source_node.js b/toolkit/devtools/sourcemap/tests/unit/test_source_node.js
index afdbc781d9d..5b0163282a0 100644
--- a/toolkit/devtools/sourcemap/tests/unit/test_source_node.js
+++ b/toolkit/devtools/sourcemap/tests/unit/test_source_node.js
@@ -136,7 +136,10 @@ define("test/source-map/test-source-node", ["require", "exports", "module"], fun
exports['test .toStringWithSourceMap()'] = function (assert, util) {
var node = new SourceNode(null, null, null,
['(function () {\n',
- ' ', new SourceNode(1, 0, 'a.js', ['someCall()']), ';\n',
+ ' ',
+ new SourceNode(1, 0, 'a.js', 'someCall', 'originalCall'),
+ new SourceNode(1, 8, 'a.js', '()'),
+ ';\n',
' ', new SourceNode(2, 0, 'b.js', ['if (foo) bar()']), ';\n',
'}());']);
var map = node.toStringWithSourceMap({
@@ -148,6 +151,14 @@ define("test/source-map/test-source-node", ["require", "exports", "module"], fun
var actual;
+ actual = map.originalPositionFor({
+ line: 1,
+ column: 4
+ });
+ assert.equal(actual.source, null);
+ assert.equal(actual.line, null);
+ assert.equal(actual.column, null);
+
actual = map.originalPositionFor({
line: 2,
column: 2
@@ -155,6 +166,7 @@ define("test/source-map/test-source-node", ["require", "exports", "module"], fun
assert.equal(actual.source, 'a.js');
assert.equal(actual.line, 1);
assert.equal(actual.column, 0);
+ assert.equal(actual.name, 'originalCall');
actual = map.originalPositionFor({
line: 3,
@@ -163,8 +175,115 @@ define("test/source-map/test-source-node", ["require", "exports", "module"], fun
assert.equal(actual.source, 'b.js');
assert.equal(actual.line, 2);
assert.equal(actual.column, 0);
+
+ actual = map.originalPositionFor({
+ line: 3,
+ column: 16
+ });
+ assert.equal(actual.source, null);
+ assert.equal(actual.line, null);
+ assert.equal(actual.column, null);
+
+ actual = map.originalPositionFor({
+ line: 4,
+ column: 2
+ });
+ assert.equal(actual.source, null);
+ assert.equal(actual.line, null);
+ assert.equal(actual.column, null);
};
+ exports['test .fromStringWithSourceMap()'] = function (assert, util) {
+ var node = SourceNode.fromStringWithSourceMap(
+ util.testGeneratedCode,
+ new SourceMapConsumer(util.testMap));
+
+ var result = node.toStringWithSourceMap({
+ file: 'min.js'
+ });
+ var map = result.map;
+ var code = result.code;
+
+ assert.equal(code, util.testGeneratedCode);
+ assert.ok(map instanceof SourceMapGenerator, 'map instanceof SourceMapGenerator');
+ map = map.toJSON();
+ assert.equal(map.version, util.testMap.version);
+ assert.equal(map.file, util.testMap.file);
+ assert.equal(map.mappings, util.testMap.mappings);
+ };
+
+ exports['test .fromStringWithSourceMap() complex version'] = function (assert, util) {
+ var input = new SourceNode(null, null, null, [
+ "(function() {\n",
+ " var Test = {};\n",
+ " ", new SourceNode(1, 0, "a.js", "Test.A = { value: 1234 };\n"),
+ " ", new SourceNode(2, 0, "a.js", "Test.A.x = 'xyz';"), "\n",
+ "}());\n",
+ "/* Generated Source */"]);
+ input = input.toStringWithSourceMap({
+ file: 'foo.js'
+ });
+
+ var node = SourceNode.fromStringWithSourceMap(
+ input.code,
+ new SourceMapConsumer(input.map.toString()));
+
+ var result = node.toStringWithSourceMap({
+ file: 'foo.js'
+ });
+ var map = result.map;
+ var code = result.code;
+
+ assert.equal(code, input.code);
+ assert.ok(map instanceof SourceMapGenerator, 'map instanceof SourceMapGenerator');
+ map = map.toJSON();
+ var inputMap = input.map.toJSON();
+ util.assertEqualMaps(assert, map, inputMap);
+ };
+
+ exports['test setSourceContent with toStringWithSourceMap'] = function (assert, util) {
+ var aNode = new SourceNode(1, 1, 'a.js', 'a');
+ aNode.setSourceContent('a.js', 'someContent');
+ var node = new SourceNode(null, null, null,
+ ['(function () {\n',
+ ' ', aNode,
+ ' ', new SourceNode(1, 1, 'b.js', 'b'),
+ '}());']);
+ node.setSourceContent('b.js', 'otherContent');
+ var map = node.toStringWithSourceMap({
+ file: 'foo.js'
+ }).map;
+
+ assert.ok(map instanceof SourceMapGenerator, 'map instanceof SourceMapGenerator');
+ map = new SourceMapConsumer(map.toString());
+
+ assert.equal(map.sources.length, 2);
+ assert.equal(map.sources[0], 'a.js');
+ assert.equal(map.sources[1], 'b.js');
+ assert.equal(map.sourcesContent.length, 2);
+ assert.equal(map.sourcesContent[0], 'someContent');
+ assert.equal(map.sourcesContent[1], 'otherContent');
+ };
+
+ exports['test walkSourceContents'] = function (assert, util) {
+ var aNode = new SourceNode(1, 1, 'a.js', 'a');
+ aNode.setSourceContent('a.js', 'someContent');
+ var node = new SourceNode(null, null, null,
+ ['(function () {\n',
+ ' ', aNode,
+ ' ', new SourceNode(1, 1, 'b.js', 'b'),
+ '}());']);
+ node.setSourceContent('b.js', 'otherContent');
+ var results = [];
+ node.walkSourceContents(function (sourceFile, sourceContent) {
+ results.push([sourceFile, sourceContent]);
+ });
+ assert.equal(results.length, 2);
+ assert.equal(results[0][0], 'a.js');
+ assert.equal(results[0][1], 'someContent');
+ assert.equal(results[1][0], 'b.js');
+ assert.equal(results[1][1], 'otherContent');
+ };
});
function run_test() {
runSourceMapTests('test/source-map/test-source-node', do_throw);