Merge m-c to inbound.
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 14 Jun 2013 13:04:31 -0400
changeset 135456 8c05aa558b45c6a50a71f57fb0e98109b653cb85
parent 135435 81d4522e23858e4a4ae8e16c0084ddfeaa74b237 (current diff)
parent 135455 5ddb1bf962618e8afdbd467a86546e4d5f392019 (diff)
child 135457 f5c81bdff6108d4cf142da7f1b479880e385fbf6
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
milestone24.0a1
Merge m-c to inbound.
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,4 +1,4 @@
 {
-    "revision": "7bfb4efb6e67c97e1178fa340a17eff63d0c33d4", 
+    "revision": "0eef3cf5ff4407369e0bfb773672a6e4b5687558", 
     "repo_path": "/integration/gaia-central"
 }
--- a/browser/devtools/styleeditor/StyleEditorPanel.jsm
+++ b/browser/devtools/styleeditor/StyleEditorPanel.jsm
@@ -86,26 +86,26 @@ StyleEditorPanel.prototype = {
   },
 
   /**
    * Select a stylesheet.
    *
    * @param {string} href
    *        Url of stylesheet to find and select in editor
    * @param {number} line
-   *        Line number to jump to after selecting
+   *        Line number to jump to after selecting. One-indexed
    * @param {number} col
-   *        Column number to jump to after selecting
+   *        Column number to jump to after selecting. One-indexed
    */
   selectStyleSheet: function(href, line, col) {
     if (!this._debuggee || !this.UI) {
       return;
     }
     let stylesheet = this._debuggee.styleSheetFromHref(href);
-    this.UI.selectStyleSheet(href, line, col);
+    this.UI.selectStyleSheet(href, line - 1, col - 1);
   },
 
   /**
    * Destroy the style editor.
    */
   destroy: function() {
     if (!this._destroyed) {
       this._destroyed = true;
--- a/browser/devtools/styleeditor/StyleEditorUI.jsm
+++ b/browser/devtools/styleeditor/StyleEditorUI.jsm
@@ -43,17 +43,17 @@ function StyleEditorUI(debuggee, panelDo
   EventEmitter.decorate(this);
 
   this._debuggee = debuggee;
   this._panelDoc = panelDoc;
   this._window = this._panelDoc.defaultView;
   this._root = this._panelDoc.getElementById("style-editor-chrome");
 
   this.editors = [];
-  this.selectedStyleSheetIndex = -1;
+  this.selectedEditor = null;
 
   this._onStyleSheetCreated = this._onStyleSheetCreated.bind(this);
   this._onStyleSheetsCleared = this._onStyleSheetsCleared.bind(this);
   this._onDocumentLoad = this._onDocumentLoad.bind(this);
   this._onError = this._onError.bind(this);
 
   debuggee.on("document-load", this._onDocumentLoad);
   debuggee.on("stylesheets-cleared", this._onStyleSheetsCleared);
@@ -79,16 +79,24 @@ StyleEditorUI.prototype = {
 
   /*
    * Mark the style editor as having or not having unsaved changes.
    */
   set isDirty(value) {
     this._markedDirty = value;
   },
 
+  /*
+   * Index of selected stylesheet in document.styleSheets
+   */
+  get selectedStyleSheetIndex() {
+    return this.selectedEditor ?
+           this.selectedEditor.styleSheet.styleSheetIndex : -1;
+  },
+
   /**
    * Build the initial UI and wire buttons with event handlers.
    */
   createUI: function() {
     let viewRoot = this._root.parentNode.querySelector(".splitview-root");
 
     this._view = new SplitView(viewRoot);
 
@@ -135,20 +143,27 @@ StyleEditorUI.prototype = {
 
     showFilePicker(file, false, parentWindow, onFileSelected);
   },
 
   /**
    * Handler for debuggee's 'stylesheets-cleared' event. Remove all editors.
    */
   _onStyleSheetsCleared: function() {
-    this._clearStyleSheetEditors();
+    // remember selected sheet and line number for next load
+    if (this.selectedEditor) {
+      let href = this.selectedEditor.styleSheet.href;
+      let {line, col} = this.selectedEditor.sourceEditor.getCaretPosition();
+      this.selectStyleSheet(href, line, col);
+    }
 
+    this._clearStyleSheetEditors();
     this._view.removeAll();
-    this.selectedStyleSheetIndex = -1;
+
+    this.selectedEditor = null;
 
     this._root.classList.add("loading");
   },
 
   /**
    * When a new or imported stylesheet has been added to the document.
    * Add an editor for it.
    */
@@ -161,21 +176,32 @@ StyleEditorUI.prototype = {
    * for all style sheets in the document
    *
    * @param {string} event
    *        Event name
    * @param {StyleSheet} styleSheet
    *        StyleSheet object for new sheet
    */
   _onDocumentLoad: function(event, styleSheets) {
+    if (this._styleSheetToSelect) {
+      // if selected stylesheet from previous load isn't here,
+      // just set first stylesheet to be selected instead
+      let selectedExists = styleSheets.some((sheet) => {
+        return this._styleSheetToSelect.href == sheet.href;
+      })
+      if (!selectedExists) {
+        this._styleSheetToSelect = null;
+      }
+    }
     for (let sheet of styleSheets) {
       this._addStyleSheetEditor(sheet);
     }
-    // this might be the first stylesheet, so remove loading indicator
+
     this._root.classList.remove("loading");
+
     this.emit("document-load");
   },
 
   /**
    * Forward any error from a stylesheet.
    *
    * @param  {string} event
    *         Event name
@@ -290,78 +316,78 @@ StyleEditorUI.prototype = {
           this._selectEditor(editor);
         }
 
         this.emit("editor-added", editor);
       }.bind(this),
 
       onShow: function(summary, details, data) {
         let editor = data.editor;
+        this.selectedEditor = editor;
+        this._styleSheetToSelect = null;
+
         if (!editor.sourceEditor) {
           // only initialize source editor when we switch to this view
           let inputElement = details.querySelector(".stylesheet-editor-input");
           editor.load(inputElement);
         }
         editor.onShow();
-      }
+
+        this.emit("editor-selected", editor);
+      }.bind(this)
     });
   },
 
   /**
    * Switch to the editor that has been marked to be selected.
    */
   switchToSelectedSheet: function() {
     let sheet = this._styleSheetToSelect;
 
     for each (let editor in this.editors) {
       if (editor.styleSheet.href == sheet.href) {
         this._selectEditor(editor, sheet.line, sheet.col);
-        this._styleSheetToSelect = null;
         break;
       }
     }
   },
 
   /**
    * Select an editor in the UI.
    *
    * @param  {StyleSheetEditor} editor
    *         Editor to switch to.
    * @param  {number} line
    *         Line number to jump to
    * @param  {number} col
    *         Column number to jump to
    */
   _selectEditor: function(editor, line, col) {
-    line = line || 1;
-    col = col || 1;
-
-    this.selectedStyleSheetIndex = editor.styleSheet.styleSheetIndex;
+    line = line || 0;
+    col = col || 0;
 
     editor.getSourceEditor().then(() => {
-      editor.sourceEditor.setCaretPosition(line - 1, col - 1);
+      editor.sourceEditor.setCaretPosition(line, col);
     });
 
     this._view.activeSummary = editor.summary;
-
-    this.emit("editor-selected", editor);
   },
 
   /**
    * selects a stylesheet and optionally moves the cursor to a selected line
    *
    * @param {string} [href]
    *        Href of stylesheet that should be selected. If a stylesheet is not passed
    *        and the editor is not initialized we focus the first stylesheet. If
    *        a stylesheet is not passed and the editor is initialized we ignore
    *        the call.
    * @param {Number} [line]
-   *        Line to which the caret should be moved (one-indexed).
+   *        Line to which the caret should be moved (zero-indexed).
    * @param {Number} [col]
-   *        Column to which the caret should be moved (one-indexed).
+   *        Column to which the caret should be moved (zero-indexed).
    */
   selectStyleSheet: function(href, line, col)
   {
     let alreadyCalled = !!this._styleSheetToSelect;
 
     this._styleSheetToSelect = {
       href: href,
       line: line,
--- a/browser/devtools/styleeditor/test/Makefile.in
+++ b/browser/devtools/styleeditor/test/Makefile.in
@@ -23,16 +23,17 @@ include $(topsrcdir)/config/rules.mk
                  browser_styleeditor_new.js \
                  browser_styleeditor_pretty.js \
                  browser_styleeditor_private_perwindowpb.js \
                  browser_styleeditor_sv_keynav.js \
                  browser_styleeditor_sv_resize.js \
                  browser_styleeditor_bug_740541_iframes.js \
                  browser_styleeditor_bug_851132_middle_click.js \
                  browser_styleeditor_nostyle.js \
+                 browser_styleeditor_reload.js \
                  head.js \
                  four.html \
                  head.js \
                  import.css \
                  import.html \
                  import2.css \
                  longload.html \
                  media.html \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_reload.js
@@ -0,0 +1,99 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+const NEW_URI = TEST_BASE_HTTPS + "media.html";
+
+const LINE_NO = 5;
+const COL_NO = 3;
+
+let gContentWin;
+let gUI;
+
+function test()
+{
+  waitForExplicitFinish();
+
+  addTabAndOpenStyleEditor(function(panel) {
+    gContentWin = gBrowser.selectedTab.linkedBrowser.contentWindow.wrappedJSObject;
+    gUI = panel.UI;
+
+    let count = 0;
+    gUI.on("editor-added", function editorAdded(event, editor) {
+      if (++count == 2) {
+        gUI.off("editor-added", editorAdded);
+        gUI.editors[0].getSourceEditor().then(runTests);
+      }
+    })
+  });
+
+  content.location = TESTCASE_URI;
+}
+
+function runTests()
+{
+  let count = 0;
+  gUI.once("editor-selected", (event, editor) => {
+    editor.getSourceEditor().then(() => {
+      info("selected second editor, about to reload page");
+      reloadPage();
+
+      gUI.on("editor-added", function editorAdded(event, editor) {
+        if (++count == 2) {
+          gUI.off("editor-added", editorAdded);
+          gUI.editors[1].getSourceEditor().then(testRemembered);
+        }
+      })
+    });
+  });
+  gUI.selectStyleSheet(gUI.editors[1].styleSheet.href, LINE_NO, COL_NO);
+}
+
+function testRemembered()
+{
+  is(gUI.selectedEditor, gUI.editors[1], "second editor is selected");
+
+  let {line, col} = gUI.selectedEditor.sourceEditor.getCaretPosition();
+  is(line, LINE_NO, "correct line selected");
+  is(col, COL_NO, "correct column selected");
+
+  testNewPage();
+}
+
+function testNewPage()
+{
+  let count = 0;
+  gUI.on("editor-added", function editorAdded(event, editor) {
+    info("editor added here")
+    if (++count == 2) {
+      gUI.off("editor-added", editorAdded);
+      gUI.editors[0].getSourceEditor().then(testNotRemembered);
+    }
+  })
+
+  info("navigating to a different page");
+  navigatePage();
+}
+
+function testNotRemembered()
+{
+  is(gUI.selectedEditor, gUI.editors[0], "first editor is selected");
+
+  let {line, col} = gUI.selectedEditor.sourceEditor.getCaretPosition();
+  is(line, 0, "first line is selected");
+  is(col, 0, "first column is selected");
+
+  gUI = null;
+  finish();
+}
+
+function reloadPage()
+{
+  gContentWin.location.reload();
+}
+
+function navigatePage()
+{
+  gContentWin.location = NEW_URI;
+}
\ No newline at end of file
--- a/dom/network/src/NetworkStatsDB.jsm
+++ b/dom/network/src/NetworkStatsDB.jsm
@@ -20,20 +20,21 @@ const STORE_NAME = "net_stats";
 
 // Constant defining the maximum values allowed per interface. If more, older
 // will be erased.
 const VALUES_MAX_LENGTH = 6 * 30;
 
 // Constant defining the rate of the samples. Daily.
 const SAMPLE_RATE = 1000 * 60 * 60 * 24;
 
-this.NetworkStatsDB = function NetworkStatsDB(aGlobal) {
+this.NetworkStatsDB = function NetworkStatsDB(aGlobal, aConnectionTypes) {
   if (DEBUG) {
     debug("Constructor");
   }
+  this._connectionTypes = aConnectionTypes;
   this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME], aGlobal);
 }
 
 NetworkStatsDB.prototype = {
   __proto__: IndexedDBHelper.prototype,
 
   dbNewTxn: function dbNewTxn(txn_type, callback, txnCb) {
     function successCb(result) {
@@ -62,30 +63,51 @@ NetworkStatsDB.prototype = {
         objectStore.createIndex("timestamp", "timestamp", { unique: false });
         objectStore.createIndex("rxBytes", "rxBytes", { unique: false });
         objectStore.createIndex("txBytes", "txBytes", { unique: false });
         objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false });
         objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false });
         if (DEBUG) {
           debug("Created object stores and indexes");
         }
+
+        // There could be a time delay between the point when the network
+        // interface comes up and the point when the database is initialized.
+        // In this short interval some traffic data are generated but are not
+        // registered by the first sample. The initialization of the database
+        // should make up the missing sample.
+        let stats = [];
+        for (let connection in this._connectionTypes) {
+          let connectionType = this._connectionTypes[connection].name;
+          let timestamp = this.normalizeDate(new Date());
+          stats.push({ connectionType: connectionType,
+                       timestamp:      timestamp,
+                       rxBytes:        0,
+                       txBytes:        0,
+                       rxTotalBytes:   0,
+                       txTotalBytes:   0 });
+        }
+        this._saveStats(aTransaction, objectStore, stats);
+        if (DEBUG) {
+          debug("Database initialized");
+        }
       }
     }
   },
 
-  convertDate: function convertDate(aDate) {
+   normalizeDate: function normalizeDate(aDate) {
     // Convert to UTC according to timezone and
     // filter timestamp to get SAMPLE_RATE precission
-    let timestamp = aDate.getTime() - aDate.getTimezoneOffset() * 60 * 1000;
+    let timestamp = aDate.getTime() - (new Date()).getTimezoneOffset() * 60 * 1000;
     timestamp = Math.floor(timestamp / SAMPLE_RATE) * SAMPLE_RATE;
     return timestamp;
   },
 
   saveStats: function saveStats(stats, aResultCb) {
-    let timestamp = this.convertDate(stats.date);
+    let timestamp = this.normalizeDate(stats.date);
 
     stats = {connectionType: stats.connectionType,
              timestamp:      timestamp,
              rxBytes:        0,
              txBytes:        0,
              rxTotalBytes:   stats.rxBytes,
              txTotalBytes:   stats.txBytes};
 
@@ -229,18 +251,19 @@ NetworkStatsDB.prototype = {
       if (DEBUG) {
         debug("Going to clear all!");
       }
       store.clear();
     }, aResultCb);
   },
 
   find: function find(aResultCb, aOptions) {
-    let start = this.convertDate(aOptions.start);
-    let end = this.convertDate(aOptions.end);
+    let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+    let start = this.normalizeDate(aOptions.start);
+    let end = this.normalizeDate(aOptions.end);
 
     if (DEBUG) {
       debug("Find: connectionType:" + aOptions.connectionType + " start: " + start + " end: " + end);
       debug("Start time: " + new Date(start));
       debug("End time: " + new Date(end));
     }
 
     this.dbNewTxn("readonly", function(txn, store) {
@@ -254,36 +277,37 @@ NetworkStatsDB.prototype = {
         txn.result = {};
       }
 
       let request = store.openCursor(range).onsuccess = function(event) {
         var cursor = event.target.result;
         if (cursor){
           data.push({ rxBytes: cursor.value.rxBytes,
                       txBytes: cursor.value.txBytes,
-                      date: new Date(cursor.value.timestamp) });
+                      date: new Date(cursor.value.timestamp + offset) });
           cursor.continue();
           return;
         }
 
         // When requested samples (start / end) are not in the range of now and
         // now - VALUES_MAX_LENGTH, fill with empty samples.
-        this.fillResultSamples(start, end, data);
+        this.fillResultSamples(start + offset, end + offset, data);
 
         txn.result.connectionType = aOptions.connectionType;
         txn.result.start = aOptions.start;
         txn.result.end = aOptions.end;
         txn.result.data = data;
       }.bind(this);
     }.bind(this), aResultCb);
   },
 
   findAll: function findAll(aResultCb, aOptions) {
-    let start = this.convertDate(aOptions.start);
-    let end = this.convertDate(aOptions.end);
+    let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+    let start = this.normalizeDate(aOptions.start);
+    let end = this.normalizeDate(aOptions.end);
 
     if (DEBUG) {
       debug("FindAll: start: " + start + " end: " + end + "\n");
     }
 
     let self = this;
     this.dbNewTxn("readonly", function(txn, store) {
       let lowFilter = start;
@@ -294,30 +318,31 @@ NetworkStatsDB.prototype = {
 
       if (!txn.result) {
         txn.result = {};
       }
 
       let request = store.index("timestamp").openCursor(range).onsuccess = function(event) {
         var cursor = event.target.result;
         if (cursor) {
-          if (data.length > 0 && data[data.length - 1].date.getTime() == cursor.value.timestamp) {
+          if (data.length > 0 &&
+              data[data.length - 1].date.getTime() == cursor.value.timestamp + offset) {
             // Time is the same, so add values.
             data[data.length - 1].rxBytes += cursor.value.rxBytes;
             data[data.length - 1].txBytes += cursor.value.txBytes;
           } else {
             data.push({ rxBytes: cursor.value.rxBytes,
                         txBytes: cursor.value.txBytes,
-                        date: new Date(cursor.value.timestamp) });
+                        date: new Date(cursor.value.timestamp + offset) });
           }
           cursor.continue();
           return;
         }
 
-        this.fillResultSamples(start, end, data);
+        this.fillResultSamples(start + offset, end + offset, data);
 
         txn.result.connectionType = aOptions.connectionType;
         txn.result.start = aOptions.start;
         txn.result.end = aOptions.end;
         txn.result.data = data;
       }.bind(this);
     }.bind(this), aResultCb);
   },
--- a/dom/network/src/NetworkStatsService.jsm
+++ b/dom/network/src/NetworkStatsService.jsm
@@ -63,17 +63,17 @@ this.NetworkStatsService = {
                      "NetworkStats:SampleRate",
                      "NetworkStats:MaxStorageSamples"];
 
     this.messages.forEach(function(msgName) {
       ppmm.addMessageListener(msgName, this);
     }, this);
 
     gIDBManager.initWindowless(myGlobal);
-    this._db = new NetworkStatsDB(myGlobal);
+    this._db = new NetworkStatsDB(myGlobal, this._connectionTypes);
 
     // Stats for all interfaces are updated periodically
     this.timer.initWithCallback(this, this._db.sampleRate,
                                 Ci.nsITimer.TYPE_REPEATING_PRECISE);
 
     this.updateQueue = [];
     this.isQueueRunning = false;
   },
--- a/dom/network/tests/test_networkstats_basics.html
+++ b/dom/network/tests/test_networkstats_basics.html
@@ -46,18 +46,19 @@ function test() {
    "maxStorageSamples is greater than 0.");
 
   // Test IDL methods
   next();
   return;
 }
 
 function checkDataDates(data, start, end, sampleRate){
-  start = Math.floor(start.getTime() / sampleRate) * sampleRate;
-  end = Math.floor(end.getTime() / sampleRate) * sampleRate;
+  var offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+  start = Math.floor((start.getTime() - offset) / sampleRate) * sampleRate + offset;
+  end = Math.floor((end.getTime() - offset) / sampleRate) * sampleRate + offset;
 
   var counter = 0;
   var date = start;
   var success = true;
 
   do {
     if(data[counter].date.getTime() !=  date) {
       success = false;
@@ -144,17 +145,19 @@ var steps = [
   function () {
     ok(true, "Get stats for a connectionType and dates adapted to samplerate");
     // Prepare get params
     var type = netStats.connectionTypes[0];
     var diff = 2;
     // Get samplerate in millis
     var sampleRate = netStats.sampleRate * 1000;
     // Get date with samplerate's precision
-    var endDate = new Date(Math.floor(new Date().getTime() / sampleRate) * sampleRate);
+    var offset = new Date().getTimezoneOffset() * 60 * 1000;
+    var endDate = new Date(Math.floor((new Date().getTime() - offset) / sampleRate)
+                           * sampleRate + offset);
     var startDate = new Date(endDate.getTime() - (sampleRate * diff));
     // Calculate the number of samples that should be returned based on the
     // the samplerate and including final and initial samples.
     var samples = (endDate.getTime() - startDate.getTime()) / sampleRate + 1;
 
     // Launch request
     req = netStats.getNetworkStats({start: startDate, end: endDate, connectionType: type});
     req.onsuccess = function () {
@@ -174,17 +177,19 @@ var steps = [
   },
   function () {
     ok(true, "Get stats for all connectionTypes and dates adapted to samplerate");
     // Prepare get params
     var diff = 2;
     // Get samplerate in millis
     var sampleRate = netStats.sampleRate * 1000;
     // Get date with samplerate's precision
-    var endDate = new Date(Math.floor(new Date().getTime() / sampleRate) * sampleRate);
+    var offset = new Date().getTimezoneOffset() * 60 * 1000;
+    var endDate = new Date(Math.floor((new Date().getTime() - offset) / sampleRate)
+                           * sampleRate + offset);
     var startDate = new Date(endDate.getTime() - (sampleRate * diff));
     // Calculate the number of samples that should be returned based on the
     // the samplerate and including final and initial samples.
     var samples = (endDate.getTime() - startDate.getTime()) / sampleRate + 1;
 
     // Launch request
     req = netStats.getNetworkStats({start: startDate, end: endDate});
     req.onsuccess = function () {
--- a/python/mach/mach/dispatcher.py
+++ b/python/mach/mach/dispatcher.py
@@ -172,18 +172,25 @@ class CommandAction(argparse.Action):
         # command arguments, we can't simply put all arguments on the same
         # parser instance because argparse would complain. We can't register an
         # argparse subparser here because it won't properly show help for
         # global arguments. So, we employ a strategy similar to command
         # execution where we construct a 2nd, independent ArgumentParser for
         # just the command data then supplement the main help's output with
         # this 2nd parser's. We use a custom formatter class to ignore some of
         # the help output.
-        c_parser = argparse.ArgumentParser(formatter_class=CommandFormatter,
-            add_help=False)
+        parser_args = {
+            'formatter_class': CommandFormatter,
+            'add_help': False,
+        }
+
+        if handler.allow_all_arguments:
+            parser_args['prefix_chars'] = '+'
+
+        c_parser = argparse.ArgumentParser(**parser_args)
 
         group = c_parser.add_argument_group('Command Arguments')
 
         for arg in handler.arguments:
             group.add_argument(*arg[0], **arg[1])
 
         # This will print the description of the command below the usage.
         description = handler.description
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_sourcemaps-08.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Regression test for bug 882986 regarding sourcesContent and absolute source
+ * URLs.
+ */
+
+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() {
+    attachTestTabAndResume(gClient, "test-source-map", function(aResponse, aTabClient, aThreadClient) {
+      gThreadClient = aThreadClient;
+      test_source_maps();
+    });
+  });
+  do_test_pending();
+}
+
+function test_source_maps()
+{
+  gClient.addOneTimeListener("newSource", function (aEvent, aPacket) {
+    let sourceClient = gThreadClient.source(aPacket.source);
+    sourceClient.source(function ({error, source}) {
+      do_check_true(!error, "should be able to grab the source");
+      do_check_eq(source, "foo",
+                  "Should load the source from the sourcesContent field");
+      finishClient(gClient);
+    });
+  });
+
+  let code = "'nothing here';\n";
+  code += "//# sourceMappingURL=data:text/json," + JSON.stringify({
+    version: 3,
+    file: "foo.js",
+    sources: ["/a"],
+    names: [],
+    mappings: "AACA",
+    sourcesContent: ["foo"]
+  });
+  Components.utils.evalInSandbox(code, gDebuggee, "1.8",
+                                 "http://example.com/foo.js", 1);
+}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -91,16 +91,17 @@ skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_sourcemaps-05.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_sourcemaps-06.js]
 [test_sourcemaps-07.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
+[test_sourcemaps-08.js]
 [test_objectgrips-01.js]
 [test_objectgrips-02.js]
 [test_objectgrips-03.js]
 [test_objectgrips-04.js]
 [test_objectgrips-05.js]
 [test_objectgrips-06.js]
 [test_objectgrips-07.js]
 [test_interrupt.js]
--- a/toolkit/devtools/sourcemap/SourceMap.jsm
+++ b/toolkit/devtools/sourcemap/SourceMap.jsm
@@ -321,27 +321,31 @@ define('source-map/source-map-consumer',
    */
   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)];
-        }
+        aSource = util.relative(this.sourceRoot, aSource);
       }
 
       if (this._sources.has(aSource)) {
         return this.sourcesContent[this._sources.indexOf(aSource)];
       }
 
+      var url;
+      if (this.sourceRoot
+          && (url = util.urlParse(this.sourceRoot))
+          && (!url.path || url.path == "/")
+          && 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
    * the following properties:
    *
@@ -480,26 +484,46 @@ define('source-map/util', ['require', 'e
     return {
       scheme: match[1],
       auth: match[3],
       host: match[4],
       port: match[6],
       path: match[7]
     };
   }
+  exports.urlParse = urlParse;
+
+  function urlGenerate(aParsedUrl) {
+    var url = aParsedUrl.scheme + "://";
+    if (aParsedUrl.auth) {
+      url += aParsedUrl.auth + "@"
+    }
+    if (aParsedUrl.host) {
+      url += aParsedUrl.host;
+    }
+    if (aParsedUrl.port) {
+      url += ":" + aParsedUrl.port
+    }
+    if (aParsedUrl.path) {
+      url += aParsedUrl.path;
+    }
+    return url;
+  }
+  exports.urlGenerate = urlGenerate;
 
   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;
+      url.path = aPath;
+      return urlGenerate(url);
     }
 
     return aRoot.replace(/\/$/, '') + '/' + aPath;
   }
   exports.join = join;
 
   /**
    * Because behavior goes wacky when you set `__proto__` on objects, we
@@ -517,16 +541,22 @@ define('source-map/util', ['require', 'e
 
   function fromSetString(aStr) {
     return aStr.substr(1);
   }
   exports.fromSetString = fromSetString;
 
   function relative(aRoot, aPath) {
     aRoot = aRoot.replace(/\/$/, '');
+
+    var url = urlParse(aRoot);
+    if (aPath.charAt(0) == "/" && url && url.path == "/") {
+      return aPath.slice(1);
+    }
+
     return aPath.indexOf(aRoot + '/') === 0
       ? aPath.substr(aRoot.length + 1)
       : aPath;
   }
   exports.relative = relative;
 
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
@@ -1125,16 +1155,34 @@ define('source-map/source-map-generator'
         // Cases 2 and 3.
         return;
       }
       else {
         throw new Error('Invalid mapping.');
       }
     };
 
+  function cmpLocation(loc1, loc2) {
+    var cmp = (loc1 && loc1.line) - (loc2 && loc2.line);
+    return cmp ? cmp : (loc1 && loc1.column) - (loc2 && loc2.column);
+  }
+
+  function strcmp(str1, str2) {
+    str1 = str1 || '';
+    str2 = str2 || '';
+    return (str1 > str2) - (str1 < str2);
+  }
+
+  function cmpMapping(mappingA, mappingB) {
+    return cmpLocation(mappingA.generated, mappingB.generated) ||
+      cmpLocation(mappingA.original, mappingB.original) ||
+      strcmp(mappingA.source, mappingB.source) ||
+      strcmp(mappingA.name, mappingB.name);
+  }
+
   /**
    * Serialize the accumulated mappings in to the stream of base 64 VLQs
    * specified by the source map format.
    */
   SourceMapGenerator.prototype._serializeMappings =
     function SourceMapGenerator_serializeMappings() {
       var previousGeneratedColumn = 0;
       var previousGeneratedLine = 1;
@@ -1145,35 +1193,33 @@ define('source-map/source-map-generator'
       var result = '';
       var mapping;
 
       // The mappings must be guarenteed to be in sorted order before we start
       // serializing them or else the generated line numbers (which are defined
       // via the ';' separators) will be all messed up. Note: it might be more
       // performant to maintain the sorting as we insert them, rather than as we
       // serialize them, but the big O is the same either way.
-      this._mappings.sort(function (mappingA, mappingB) {
-        var cmp = mappingA.generated.line - mappingB.generated.line;
-        return cmp === 0
-          ? mappingA.generated.column - mappingB.generated.column
-          : cmp;
-      });
+      this._mappings.sort(cmpMapping);
 
       for (var i = 0, len = this._mappings.length; i < len; i++) {
         mapping = this._mappings[i];
 
         if (mapping.generated.line !== previousGeneratedLine) {
           previousGeneratedColumn = 0;
           while (mapping.generated.line !== previousGeneratedLine) {
             result += ';';
             previousGeneratedLine++;
           }
         }
         else {
           if (i > 0) {
+            if (!cmpMapping(mapping, this._mappings[i - 1])) {
+              continue;
+            }
             result += ',';
           }
         }
 
         result += base64VLQ.encode(mapping.generated.column
                                    - previousGeneratedColumn);
         previousGeneratedColumn = mapping.generated.column;
 
--- a/toolkit/devtools/sourcemap/tests/unit/Utils.jsm
+++ b/toolkit/devtools/sourcemap/tests/unit/Utils.jsm
@@ -260,26 +260,46 @@ define('lib/source-map/util', ['require'
     return {
       scheme: match[1],
       auth: match[3],
       host: match[4],
       port: match[6],
       path: match[7]
     };
   }
+  exports.urlParse = urlParse;
+
+  function urlGenerate(aParsedUrl) {
+    var url = aParsedUrl.scheme + "://";
+    if (aParsedUrl.auth) {
+      url += aParsedUrl.auth + "@"
+    }
+    if (aParsedUrl.host) {
+      url += aParsedUrl.host;
+    }
+    if (aParsedUrl.port) {
+      url += ":" + aParsedUrl.port
+    }
+    if (aParsedUrl.path) {
+      url += aParsedUrl.path;
+    }
+    return url;
+  }
+  exports.urlGenerate = urlGenerate;
 
   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;
+      url.path = aPath;
+      return urlGenerate(url);
     }
 
     return aRoot.replace(/\/$/, '') + '/' + aPath;
   }
   exports.join = join;
 
   /**
    * Because behavior goes wacky when you set `__proto__` on objects, we
@@ -297,16 +317,22 @@ define('lib/source-map/util', ['require'
 
   function fromSetString(aStr) {
     return aStr.substr(1);
   }
   exports.fromSetString = fromSetString;
 
   function relative(aRoot, aPath) {
     aRoot = aRoot.replace(/\/$/, '');
+
+    var url = urlParse(aRoot);
+    if (aPath.charAt(0) == "/" && url && url.path == "/") {
+      return aPath.slice(1);
+    }
+
     return aPath.indexOf(aRoot + '/') === 0
       ? aPath.substr(aRoot.length + 1)
       : aPath;
   }
   exports.relative = relative;
 
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
--- a/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js
+++ b/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js
@@ -288,12 +288,27 @@ define("test/source-map/test-source-map-
 
     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.');
   };
 
+  exports['test github issue #64'] = function (assert, util) {
+    var map = new SourceMapConsumer({
+      "version": 3,
+      "file": "foo.js",
+      "sourceRoot": "http://example.com/",
+      "sources": ["/a"],
+      "names": [],
+      "mappings": "AACA",
+      "sourcesContent": ["foo"]
+    });
+
+    assert.equal(map.sourceContentFor("a"), "foo");
+    assert.equal(map.sourceContentFor("/a"), "foo");
+  };
+
 });
 function run_test() {
   runSourceMapTests('test/source-map/test-source-map-consumer', do_throw);
 }
--- a/toolkit/devtools/sourcemap/tests/unit/test_source_map_generator.js
+++ b/toolkit/devtools/sourcemap/tests/unit/test_source_map_generator.js
@@ -268,12 +268,132 @@ define("test/source-map/test-source-map-
 
     // 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);
   };
+
+  exports['test sorting with duplicate generated mappings'] = function (assert, util) {
+    var map = new SourceMapGenerator({
+      file: 'test.js'
+    });
+    map.addMapping({
+      generated: { line: 3, column: 0 },
+      original: { line: 2, column: 0 },
+      source: 'a.js'
+    });
+    map.addMapping({
+      generated: { line: 2, column: 0 }
+    });
+    map.addMapping({
+      generated: { line: 2, column: 0 }
+    });
+    map.addMapping({
+      generated: { line: 1, column: 0 },
+      original: { line: 1, column: 0 },
+      source: 'a.js'
+    });
+
+    util.assertEqualMaps(assert, map.toJSON(), {
+      version: 3,
+      file: 'test.js',
+      sources: ['a.js'],
+      names: [],
+      mappings: 'AAAA;A;AACA'
+    });
+  };
+
+  exports['test ignore duplicate mappings.'] = function (assert, util) {
+    var init = { file: 'min.js', sourceRoot: '/the/root' };
+    var map1, map2;
+
+    // null original source location
+    var nullMapping1 = {
+      generated: { line: 1, column: 0 }
+    };
+    var nullMapping2 = {
+      generated: { line: 2, column: 2 }
+    };
+
+    map1 = new SourceMapGenerator(init);
+    map2 = new SourceMapGenerator(init);
+
+    map1.addMapping(nullMapping1);
+    map1.addMapping(nullMapping1);
+
+    map2.addMapping(nullMapping1);
+
+    util.assertEqualMaps(assert, map1.toJSON(), map2.toJSON());
+
+    map1.addMapping(nullMapping2);
+    map1.addMapping(nullMapping1);
+
+    map2.addMapping(nullMapping2);
+
+    util.assertEqualMaps(assert, map1.toJSON(), map2.toJSON());
+
+    // original source location
+    var srcMapping1 = {
+      generated: { line: 1, column: 0 },
+      original: { line: 11, column: 0 },
+      source: 'srcMapping1.js'
+    };
+    var srcMapping2 = {
+      generated: { line: 2, column: 2 },
+      original: { line: 11, column: 0 },
+      source: 'srcMapping2.js'
+    };
+
+    map1 = new SourceMapGenerator(init);
+    map2 = new SourceMapGenerator(init);
+
+    map1.addMapping(srcMapping1);
+    map1.addMapping(srcMapping1);
+
+    map2.addMapping(srcMapping1);
+
+    util.assertEqualMaps(assert, map1.toJSON(), map2.toJSON());
+
+    map1.addMapping(srcMapping2);
+    map1.addMapping(srcMapping1);
+
+    map2.addMapping(srcMapping2);
+
+    util.assertEqualMaps(assert, map1.toJSON(), map2.toJSON());
+
+    // full original source and name information
+    var fullMapping1 = {
+      generated: { line: 1, column: 0 },
+      original: { line: 11, column: 0 },
+      source: 'fullMapping1.js',
+      name: 'fullMapping1'
+    };
+    var fullMapping2 = {
+      generated: { line: 2, column: 2 },
+      original: { line: 11, column: 0 },
+      source: 'fullMapping2.js',
+      name: 'fullMapping2'
+    };
+
+    map1 = new SourceMapGenerator(init);
+    map2 = new SourceMapGenerator(init);
+
+    map1.addMapping(fullMapping1);
+    map1.addMapping(fullMapping1);
+
+    map2.addMapping(fullMapping1);
+
+    util.assertEqualMaps(assert, map1.toJSON(), map2.toJSON());
+
+    map1.addMapping(fullMapping2);
+    map1.addMapping(fullMapping1);
+
+    map2.addMapping(fullMapping2);
+
+    util.assertEqualMaps(assert, map1.toJSON(), map2.toJSON());
+  };
 });
 function run_test() {
   runSourceMapTests('test/source-map/test-source-map-generator', do_throw);
 }