Bug 1035512: Add a new common JS testing harness based on the mochitest test runner. r=gps, ted, erikvold, jmaher
authorDave Townsend <dtownsend@oxymoronical.com>
Mon, 22 Sep 2014 11:08:06 -0700
changeset 206459 1401f98a790e4c02254f0ce61c027acc41930548
parent 206458 fac90451603fbf142fdafec8cebce4da986a1461
child 206460 846759ed5d7c6c81215b1503c0b4cc53343d14ab
push id8900
push userdtownsend@mozilla.com
push dateMon, 22 Sep 2014 18:08:33 +0000
treeherderfx-team@1401f98a790e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps, ted, erikvold, jmaher
bugs1035512
milestone35.0a1
Bug 1035512: Add a new common JS testing harness based on the mochitest test runner. r=gps, ted, erikvold, jmaher
addon-sdk/Makefile.in
addon-sdk/moz.build
addon-sdk/mozbuild.template
addon-sdk/source/test/addons/jetpack-addon.ini
addon-sdk/source/test/jetpack-package.ini
python/mozbuild/mozbuild/frontend/context.py
python/mozbuild/mozbuild/frontend/emitter.py
python/mozbuild/mozbuild/testing.py
testing/mochitest/jar.mn
testing/mochitest/jetpack-addon-harness.js
testing/mochitest/jetpack-addon-overlay.xul
testing/mochitest/jetpack-package-harness.js
testing/mochitest/jetpack-package-overlay.xul
testing/mochitest/mach_commands.py
testing/mochitest/mochitest_options.py
testing/mochitest/moz.build
testing/mochitest/runtests.py
--- a/addon-sdk/Makefile.in
+++ b/addon-sdk/Makefile.in
@@ -1,20 +1,39 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+TESTADDONS = source/test/addons
+ADDONSRC = $(srcdir)/$(TESTADDONS)
+TESTROOT = $(CURDIR)/$(DEPTH)/_tests/testing/mochitest/jetpack-addon/$(relativesrcdir)/$(TESTADDONS)
+
+# Build a list of the test add-ons
+ADDONS = $(patsubst $(ADDONSRC)/%/package.json,$(TESTADDONS)/%.xpi,$(wildcard $(ADDONSRC)/*/package.json))
+
+INSTALL_TARGETS += test_addons
+test_addons_FILES = $(ADDONS)
+test_addons_DEST = $(TESTROOT)
+
+sinclude $(topsrcdir)/config/rules.mk
+
+# This can switch to just zipping the files when native jetpacks land
+$(TESTADDONS)/%.xpi: FORCE $(call mkdir_deps,$(CURDIR)/$(TESTADDONS)) $(ADDONSRC)/%
+	$(PYTHON) $(srcdir)/source/bin/cfx xpi --pkgdir=$(lastword $^) --output-file=$@
+
+#libs:: $(ADDONS)
+
 TEST_FILES = \
-  source/app-extension \
-  source/bin \
-  source/python-lib \
-  source/test \
-  source/package.json \
-  source/mapping.json \
+  $(srcdir)/source/app-extension \
+  $(srcdir)/source/bin \
+  $(srcdir)/source/python-lib \
+  $(srcdir)/source/test \
+  $(srcdir)/source/package.json \
+  $(srcdir)/source/mapping.json \
   $(NULL)
 
 # Remove this once the test harness uses the APIs built into Firefox
-TEST_FILES += source/lib
+TEST_FILES += $(srcdir)/source/lib
 
 PKG_STAGE = $(DIST)/test-stage
 
 stage-tests-package:: $(TEST_FILES)
 	$(INSTALL) $^ $(PKG_STAGE)/jetpack
--- a/addon-sdk/moz.build
+++ b/addon-sdk/moz.build
@@ -5,16 +5,18 @@
 
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+JETPACK_PACKAGE_MANIFESTS += ['source/test/jetpack-package.ini']
+JETPACK_ADDON_MANIFESTS += ['source/test/addons/jetpack-addon.ini']
 
 DIRS += ["source/modules/system"]
 
 EXTRA_JS_MODULES.sdk += [
     'source/app-extension/bootstrap.js',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] != "gonk":
--- a/addon-sdk/mozbuild.template
+++ b/addon-sdk/mozbuild.template
@@ -1,13 +1,15 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+JETPACK_PACKAGE_MANIFESTS += ['source/test/jetpack-package.ini']
+JETPACK_ADDON_MANIFESTS += ['source/test/addons/jetpack-addon.ini']
 
 DIRS += ["source/modules/system"]
 
 EXTRA_JS_MODULES.sdk += [
     'source/app-extension/bootstrap.js',
 ]
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/addons/jetpack-addon.ini
@@ -0,0 +1,36 @@
+[addon-page.xpi]
+[author-email.xpi]
+[child_process.xpi]
+[chrome.xpi]
+[content-permissions.xpi]
+[contributors.xpi]
+[curly-id.xpi]
+[developers.xpi]
+[e10s.xpi]
+skip-if = true
+[e10s-tabs.xpi]
+skip-if = true
+[l10n.xpi]
+[l10n-properties.xpi]
+[layout-change.xpi]
+[main.xpi]
+[packaging.xpi]
+[packed.xpi]
+[page-mod-debugger-post.xpi]
+[page-mod-debugger-pre.xpi]
+[places.xpi]
+[predefined-id-with-at.xpi]
+[preferences-branch.xpi]
+[private-browsing-supported.xpi]
+skip-if = true
+[require.xpi]
+[self.xpi]
+[simple-prefs.xpi]
+[simple-prefs-l10n.xpi]
+[simple-prefs-regression.xpi]
+[standard-id.xpi]
+[symbiont.xpi]
+[tab-close-on-startup.xpi]
+[translators.xpi]
+[unpacked.xpi]
+[unsafe-content-script.xpi]
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/jetpack-package.ini
@@ -0,0 +1,164 @@
+[DEFAULT]
+support-files =
+  buffers/**
+  commonjs-test-adapter/**
+  event/**
+  fixtures/**
+  loader/**
+  modules/**
+  private-browsing/**
+  sidebar/**
+  tabs/**
+  traits/**
+  windows/**
+  zip/**
+  fixtures.js
+  pagemod-test-helpers.js
+  test-context-menu.html
+  test-tmp-file.txt
+
+[test-addon-installer.js]
+[test-addon-window.js]
+[test-api-utils.js]
+[test-array.js]
+[test-base64.js]
+[test-bootstrap.js]
+[test-browser-events.js]
+[test-buffer.js]
+[test-byte-streams.js]
+[test-child_process.js]
+[test-chrome.js]
+[test-clipboard.js]
+[test-collection.js]
+[test-commonjs-test-adapter.js]
+[test-content-events.js]
+[test-content-loader.js]
+[test-content-script.js]
+[test-content-symbiont.js]
+[test-content-worker.js]
+[test-context-menu.js]
+[test-cortex.js]
+[test-cuddlefish.js]
+# Cuddlefish loader is unsupported
+skip-if = true
+[test-deprecate.js]
+[test-deprecated-list.js]
+[test-dev-panel.js]
+[test-diffpatcher.js]
+[test-dispatcher.js]
+[test-disposable.js]
+[test-dom.js]
+[test-environment.js]
+[test-errors.js]
+[test-event-core.js]
+[test-event-target.js]
+[test-event-utils.js]
+[test-events.js]
+[test-file.js]
+[test-frame-utils.js]
+[test-fs.js]
+[test-functional.js]
+[test-globals.js]
+[test-heritage.js]
+[test-hidden-frame.js]
+[test-host-events.js]
+[test-hotkeys.js]
+[test-httpd.js]
+[test-indexed-db.js]
+[test-keyboard-observer.js]
+[test-keyboard-utils.js]
+[test-l10n-locale.js]
+[test-l10n-plural-rules.js]
+[test-libxul.js]
+[test-light-traits.js]
+[test-list.js]
+[test-loader.js]
+[test-match-pattern.js]
+[test-memory.js]
+[test-method.js]
+[test-module.js]
+[test-modules.js]
+[test-namespace.js]
+[test-native-loader.js]
+[test-native-options.js]
+[test-net-url.js]
+[test-node-os.js]
+[test-notifications.js]
+[test-object.js]
+[test-observers.js]
+[test-page-mod.js]
+[test-page-worker.js]
+[test-panel.js]
+[test-passwords-utils.js]
+[test-passwords.js]
+[test-path.js]
+[test-plain-text-console.js]
+[test-preferences-service.js]
+[test-preferences-target.js]
+[test-private-browsing.js]
+[test-promise.js]
+[test-querystring.js]
+[test-reference.js]
+[test-registry.js]
+[test-request.js]
+[test-require.js]
+[test-rules.js]
+[test-sandbox.js]
+[test-selection.js]
+[test-self.js]
+[test-sequence.js]
+[test-set-exports.js]
+[test-simple-prefs.js]
+[test-simple-storage.js]
+[test-system-events.js]
+[test-system-input-output.js]
+[test-system-runtime.js]
+[test-system-startup.js]
+[test-system.js]
+[test-tab-events.js]
+[test-tab-observer.js]
+[test-tab-utils.js]
+[test-tab.js]
+[test-tabs-common.js]
+[test-tabs.js]
+[test-test-loader.js]
+[test-test-memory.js]
+[test-test-utils-async.js]
+[test-test-utils-sync.js]
+[test-test-utils.js]
+[test-text-streams.js]
+[test-timer.js]
+[test-tmp-file.js]
+[test-traceback.js]
+[test-traits-core.js]
+[test-traits.js]
+[test-type.js]
+[test-ui-action-button.js]
+[test-ui-frame.js]
+[test-ui-id.js]
+[test-ui-sidebar-private-browsing.js]
+[test-ui-sidebar.js]
+[test-ui-toggle-button.js]
+[test-ui-toolbar.js]
+[test-unit-test-finder.js]
+[test-unit-test.js]
+[test-unload.js]
+[test-unsupported-skip.js]
+# Bug 1037235
+skip-if = true
+[test-url.js]
+[test-uuid.js]
+[test-weak-set.js]
+[test-widget.js]
+[test-window-events.js]
+[test-window-loader.js]
+[test-window-observer.js]
+[test-window-utils-global-private-browsing.js]
+[test-window-utils-private-browsing.js]
+[test-window-utils.js]
+[test-window-utils2.js]
+[test-windows-common.js]
+[test-windows.js]
+[test-xhr.js]
+[test-xpcom.js]
+[test-xul-app.js]
--- a/python/mozbuild/mozbuild/frontend/context.py
+++ b/python/mozbuild/mozbuild/frontend/context.py
@@ -802,16 +802,24 @@ VARIABLES = {
     'A11Y_MANIFESTS': (StrictOrderingOnAppendList, list,
         """List of manifest files defining a11y tests.
         """, None),
 
     'BROWSER_CHROME_MANIFESTS': (StrictOrderingOnAppendList, list,
         """List of manifest files defining browser chrome tests.
         """, None),
 
+    'JETPACK_PACKAGE_MANIFESTS': (StrictOrderingOnAppendList, list,
+        """List of manifest files defining jetpack package tests.
+        """, None),
+
+    'JETPACK_ADDON_MANIFESTS': (StrictOrderingOnAppendList, list,
+        """List of manifest files defining jetpack addon tests.
+        """, None),
+
     'CRASHTEST_MANIFESTS': (StrictOrderingOnAppendList, list,
         """List of manifest files defining crashtests.
 
         These are commonly named crashtests.list.
         """, None),
 
     'METRO_CHROME_MANIFESTS': (StrictOrderingOnAppendList, list,
         """List of manifest files defining metro browser chrome tests.
--- a/python/mozbuild/mozbuild/frontend/emitter.py
+++ b/python/mozbuild/mozbuild/frontend/emitter.py
@@ -759,16 +759,18 @@ class TreeMetadataEmitter(LoggingMixin):
         #     manifest.
         #
         # We ideally don't filter out inactive tests. However, not every test
         # harness can yet deal with test filtering. Once all harnesses can do
         # this, this feature can be dropped.
         test_manifests = dict(
             A11Y=('a11y', 'testing/mochitest', 'a11y', True),
             BROWSER_CHROME=('browser-chrome', 'testing/mochitest', 'browser', True),
+            JETPACK_PACKAGE=('jetpack-package', 'testing/mochitest', 'jetpack-package', True),
+            JETPACK_ADDON=('jetpack-addon', 'testing/mochitest', 'jetpack-addon', True),
             METRO_CHROME=('metro-chrome', 'testing/mochitest', 'metro', True),
             MOCHITEST=('mochitest', 'testing/mochitest', 'tests', True),
             MOCHITEST_CHROME=('chrome', 'testing/mochitest', 'chrome', True),
             MOCHITEST_WEBAPPRT_CHROME=('webapprt-chrome', 'testing/mochitest', 'webapprtChrome', True),
             WEBRTC_SIGNALLING_TEST=('steeplechase', 'steeplechase', '.', True),
             XPCSHELL_TESTS=('xpcshell', 'xpcshell', '.', False),
         )
 
@@ -845,21 +847,24 @@ class TreeMetadataEmitter(LoggingMixin):
             filtered = m.tests
 
             if filter_inactive:
                 # We return tests that don't exist because we want manifests
                 # defining tests that don't exist to result in error.
                 filtered = m.active_tests(exists=False, disabled=True,
                     **self.info)
 
-            missing = [t['name'] for t in filtered if not os.path.exists(t['path'])]
-            if missing:
-                raise SandboxValidationError('Test manifest (%s) lists '
-                    'test that does not exist: %s' % (
-                    path, ', '.join(missing)), context)
+            # Jetpack add-on tests are expected to be generated during the
+            # build process so they won't exist here.
+            if flavor != 'jetpack-addon':
+                missing = [t['name'] for t in filtered if not os.path.exists(t['path'])]
+                if missing:
+                    raise SandboxValidationError('Test manifest (%s) lists '
+                        'test that does not exist: %s' % (
+                        path, ', '.join(missing)), context)
 
             out_dir = mozpath.join(install_prefix, manifest_reldir)
             if 'install-to-subdir' in defaults:
                 # This is terrible, but what are you going to do?
                 out_dir = mozpath.join(out_dir, defaults['install-to-subdir'])
                 obj.manifest_obj_relpath = mozpath.join(manifest_reldir,
                                                         defaults['install-to-subdir'],
                                                         mozpath.basename(path))
@@ -920,18 +925,21 @@ class TreeMetadataEmitter(LoggingMixin):
                                     continue
 
                             obj.installs[full] = (mozpath.normpath(dest_path),
                                 False)
 
             for test in filtered:
                 obj.tests.append(test)
 
-                obj.installs[mozpath.normpath(test['path'])] = \
-                    (mozpath.join(out_dir, test['relpath']), True)
+                # Jetpack add-on tests are generated directly in the test
+                # directory
+                if flavor != 'jetpack-addon':
+                    obj.installs[mozpath.normpath(test['path'])] = \
+                        (mozpath.join(out_dir, test['relpath']), True)
 
                 process_support_files(test)
 
             if not filtered:
                 # If there are no tests, look for support-files under DEFAULT.
                 process_support_files(defaults)
 
             # We also copy manifests into the output directory,
--- a/python/mozbuild/mozbuild/testing.py
+++ b/python/mozbuild/mozbuild/testing.py
@@ -139,16 +139,20 @@ class TestResolver(MozbuildObject):
 
         self._tests = TestMetadata(filename=os.path.join(self.topobjdir,
             'all-tests.json'))
         self._test_rewrites = {
             'a11y': os.path.join(self.topobjdir, '_tests', 'testing',
                 'mochitest', 'a11y'),
             'browser-chrome': os.path.join(self.topobjdir, '_tests', 'testing',
                 'mochitest', 'browser'),
+            'jetpack-package': os.path.join(self.topobjdir, '_tests', 'testing',
+                'mochitest', 'jetpack-package'),
+            'jetpack-addon': os.path.join(self.topobjdir, '_tests', 'testing',
+                'mochitest', 'jetpack-addon'),
             'chrome': os.path.join(self.topobjdir, '_tests', 'testing',
                 'mochitest', 'chrome'),
             'mochitest': os.path.join(self.topobjdir, '_tests', 'testing',
                 'mochitest', 'tests'),
             'xpcshell': os.path.join(self.topobjdir, '_tests', 'xpcshell'),
         }
 
     def resolve_tests(self, cwd=None, **kwargs):
--- a/testing/mochitest/jar.mn
+++ b/testing/mochitest/jar.mn
@@ -1,13 +1,17 @@
 mochikit.jar:
 % content mochikit %content/
   content/browser-harness.xul (browser-harness.xul)
   content/browser-test.js (browser-test.js)
   content/browser-test-overlay.xul (browser-test-overlay.xul)
+  content/jetpack-package-harness.js (jetpack-package-harness.js)
+  content/jetpack-package-overlay.xul (jetpack-package-overlay.xul)
+  content/jetpack-addon-harness.js (jetpack-addon-harness.js)
+  content/jetpack-addon-overlay.xul (jetpack-addon-overlay.xul)
   content/cc-analyzer.js (cc-analyzer.js)
   content/chrome-harness.js (chrome-harness.js)
   content/mochitest-e10s-utils.js (mochitest-e10s-utils.js)
   content/harness.xul (harness.xul)
   content/redirect.html (redirect.html)
   content/server.js (server.js)
   content/chunkifyTests.js (chunkifyTests.js)
   content/manifestLibrary.js (manifestLibrary.js)
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/jetpack-addon-harness.js
@@ -0,0 +1,211 @@
+/* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */
+var gConfig;
+
+if (Cc === undefined) {
+  var Cc = Components.classes;
+  var Ci = Components.interfaces;
+  var Cu = Components.utils;
+}
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+  "resource://gre/modules/AddonManager.jsm");
+
+// Start the tests after the window has been displayed
+window.addEventListener("load", function testOnLoad() {
+  window.removeEventListener("load", testOnLoad);
+  window.addEventListener("MozAfterPaint", function testOnMozAfterPaint() {
+    window.removeEventListener("MozAfterPaint", testOnMozAfterPaint);
+    setTimeout(testInit, 0);
+  });
+});
+
+let sdkpath = null;
+
+// Installs a single add-on returning a promise for when install is completed
+function installAddon(url) {
+  return new Promise(function(resolve, reject) {
+    AddonManager.getInstallForURL(url, function(install) {
+      install.addListener({
+        onDownloadEnded: function(install) {
+          // Set add-on's test options
+          const options = {
+            test: {
+              iterations: 1,
+              stop: false,
+              keepOpen: true,
+            },
+            profile: {
+              memory: false,
+              leaks: false,
+            },
+            output: {
+              logLevel: "verbose",
+              format: "tbpl",
+            },
+          }
+          setPrefs("extensions." + install.addon.id + ".sdk", options);
+
+          // If necessary override the add-ons module paths to point somewhere
+          // else
+          if (sdkpath) {
+            let paths = {}
+            for (let path of ["dev", "diffpatcher", "framescript", "method", "node", "sdk", "toolkit"]) {
+              paths[path] = sdkpath + path;
+            }
+            setPrefs("extensions.modules." + install.addon.id + ".path", paths);
+          }
+        },
+
+        onInstallEnded: function(install, addon) {
+          resolve(addon);
+        },
+
+        onInstallFailed: function(install) {
+          reject();
+        }
+      });
+
+      install.install();
+    }, "application/x-xpinstall");
+  });
+}
+
+// Uninstalls an add-on returning a promise for when it is gone
+function uninstallAddon(oldAddon) {
+  return new Promise(function(resolve, reject) {
+    AddonManager.addAddonListener({
+      onUninstalled: function(addon) {
+        if (addon.id != oldAddon.id)
+          return;
+
+        // Some add-ons do async work on uninstall, we must wait for that to
+        // complete
+        let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+        timer.init(resolve, 500, timer.TYPE_ONE_SHOT);
+      }
+    });
+
+    oldAddon.uninstall();
+  });
+}
+
+// Waits for a test add-on to signal it has completed its tests
+function waitForResults() {
+  return new Promise(function(resolve, reject) {
+    Services.obs.addObserver(function(subject, topic, data) {
+      Services.obs.removeObserver(arguments.callee, "sdk:test:results");
+
+      resolve(JSON.parse(data));
+    }, "sdk:test:results", false);
+  });
+}
+
+// Runs tests for the add-on available at URL.
+let testAddon = Task.async(function*(url) {
+  let addon = yield installAddon(url);
+  let results = yield waitForResults();
+  yield uninstallAddon(addon);
+
+  return results;
+});
+
+// Sets a set of prefs for test add-ons
+function setPrefs(root, options) {
+  Object.keys(options).forEach(id => {
+    const key = root + "." + id;
+    const value = options[id]
+    const type = typeof(value);
+
+    value === null ? void(0) :
+    value === undefined ? void(0) :
+    type === "boolean" ? Services.prefs.setBoolPref(key, value) :
+    type === "string" ? Services.prefs.setCharPref(key, value) :
+    type === "number" ? Services.prefs.setIntPref(key, parseInt(value)) :
+    type === "object" ? setPrefs(key, value) :
+    void(0);
+  });
+}
+
+function testInit() {
+  // Make sure to run the test harness for the first opened window only
+  if (Services.prefs.prefHasUserValue("testing.jetpackTestHarness.running"))
+    return;
+
+  Services.prefs.setBoolPref("testing.jetpackTestHarness.running", true);
+
+  // Get the list of tests to run
+  let config = readConfig();
+  getTestList(config, function(links) {
+    try {
+      let fileNames = [];
+      let fileNameRegexp = /.+\.xpi$/;
+      arrayOfTestFiles(links, fileNames, fileNameRegexp);
+
+      if (config.startAt || config.endAt) {
+        fileNames = skipTests(fileNames, config.startAt, config.endAt);
+      }
+
+      if (config.totalChunks && config.thisChunk) {
+        fileNames = chunkifyTests(fileNames, config.totalChunks,
+                                  config.thisChunk, config.chunkByDir);
+      }
+
+      // Override the SDK modules if necessary
+      try {
+        let sdklibs = Services.prefs.getCharPref("extensions.sdk.path");
+        // sdkpath is a file path, make it a URI
+        let sdkfile = Cc["@mozilla.org/file/local;1"].
+                      createInstance(Ci.nsIFile);
+        sdkfile.initWithPath(sdklibs);
+        sdkpath = Services.io.newFileURI(sdkfile).spec;
+      }
+      catch (e) {
+        // Stick with the built-in modules
+      }
+
+      let passed = 0;
+      let failed = 0;
+
+      function finish() {
+        if (passed + failed == 0) {
+          dump("TEST-UNEXPECTED-FAIL | jetpack-addon-harness.js | " +
+               "No tests to run. Did you pass an invalid --test-path?\n");
+        }
+        else {
+          dump("Jetpack Addon Test Summary\n");
+          dump("\tPassed: " + passed + "\n" +
+               "\tFailed: " + failed + "\n" +
+               "\tTodo: 0\n");
+        }
+
+        if (config.closeWhenDone) {
+          const appStartup = Cc['@mozilla.org/toolkit/app-startup;1'].
+                             getService(Ci.nsIAppStartup);
+          appStartup.quit(appStartup.eAttemptQuit);
+        }
+      }
+
+      function testNextAddon() {
+        if (fileNames.length == 0)
+          return finish();
+
+        let filename = fileNames.shift();
+        testAddon(filename).then(results => {
+          passed += results.passed;
+          failed += results.failed;
+        }).then(testNextAddon);
+      }
+
+      testNextAddon();
+    }
+    catch (e) {
+      dump("TEST-UNEXPECTED-FAIL: jetpack-addon-harness.js | error starting test harness (" + e + ")\n");
+      dump(e.stack);
+    }
+  });
+}
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/jetpack-addon-overlay.xul
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<overlay id="jetpackTestOverlay"
+         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"/>
+  <script type="application/javascript" src="chrome://mochikit/content/manifestLibrary.js"/>
+  <script type="application/javascript" src="chrome://mochikit/content/chunkifyTests.js"/>
+  <script type="application/javascript" src="chrome://mochikit/content/server.js"/>
+  <script type="application/javascript" src="chrome://mochikit/content/jetpack-addon-harness.js"/>
+</overlay>
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/jetpack-package-harness.js
@@ -0,0 +1,250 @@
+/* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */
+const TEST_PACKAGE = "chrome://mochitests/content/";
+const TEST_ID = "jetpack-tests@mozilla.org";
+
+var gConfig;
+
+if (Cc === undefined) {
+  var Cc = Components.classes;
+  var Ci = Components.interfaces;
+  var Cu = Components.utils;
+}
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
+
+// Start the tests after the window has been displayed
+window.addEventListener("load", function testOnLoad() {
+  window.removeEventListener("load", testOnLoad);
+  window.addEventListener("MozAfterPaint", function testOnMozAfterPaint() {
+    window.removeEventListener("MozAfterPaint", testOnMozAfterPaint);
+    setTimeout(testInit, 0);
+  });
+});
+
+// Tests a single module
+function testModule(require, url) {
+  return new Promise(resolve => {
+    let path = url.substring(TEST_PACKAGE.length);
+
+    const { stdout } = require("sdk/system");
+
+    const { runTests } = require("sdk/test/harness");
+    const loaderModule = require("toolkit/loader");
+    const options = require("sdk/test/options");
+
+    function findAndRunTests(loader, nextIteration) {
+      const { TestRunner } = loaderModule.main(loader, "sdk/deprecated/unit-test");
+
+      const NOT_TESTS = ['setup', 'teardown'];
+      var runner = new TestRunner();
+
+      let tests = [];
+
+      let suiteModule;
+      try {
+        dump("TEST-INFO: " + path + " | Loading test module\n");
+        suiteModule = loaderModule.main(loader, "tests/" + path.substring(0, path.length - 3));
+      }
+      catch (e) {
+        // If `Unsupported Application` error thrown during test,
+        // skip the test suite
+        suiteModule = {
+          'test suite skipped': assert => assert.pass(e.message)
+        };
+      }
+
+      for (let name of Object.keys(suiteModule).sort()) {
+        if (NOT_TESTS.indexOf(name) != -1)
+          continue;
+
+        tests.push({
+          setup: suiteModule.setup,
+          teardown: suiteModule.teardown,
+          testFunction: suiteModule[name],
+          name: path + "." + name
+        });
+      }
+
+      runner.startMany({
+        tests: {
+          getNext: () => Promise.resolve(tests.shift())
+        },
+        stopOnError: options.stopOnError,
+        onDone: nextIteration
+      });
+    }
+
+    runTests({
+      findAndRunTests: findAndRunTests,
+      iterations: options.iterations,
+      filter: options.filter,
+      profileMemory: options.profileMemory,
+      stopOnError: options.stopOnError,
+      verbose: options.verbose,
+      parseable: options.parseable,
+      print: stdout.write,
+      onDone: resolve
+    });
+  });
+}
+
+// Sets the test prefs
+function setPrefs(root, options) {
+  Object.keys(options).forEach(id => {
+    const key = root + "." + id;
+    const value = options[id]
+    const type = typeof(value);
+
+    value === null ? void(0) :
+    value === undefined ? void(0) :
+    type === "boolean" ? Services.prefs.setBoolPref(key, value) :
+    type === "string" ? Services.prefs.setCharPref(key, value) :
+    type === "number" ? Services.prefs.setIntPref(key, parseInt(value)) :
+    type === "object" ? setPrefs(key, value) :
+    void(0);
+  });
+}
+
+function testInit() {
+  // Make sure to run the test harness for the first opened window only
+  if (Services.prefs.prefHasUserValue("testing.jetpackTestHarness.running"))
+    return;
+
+  Services.prefs.setBoolPref("testing.jetpackTestHarness.running", true);
+
+  // Get the list of tests to run
+  let config = readConfig();
+  getTestList(config, function(links) {
+    try {
+      let fileNames = [];
+      let fileNameRegexp = /test-.+\.js$/;
+      arrayOfTestFiles(links, fileNames, fileNameRegexp);
+
+      if (config.startAt || config.endAt) {
+        fileNames = skipTests(fileNames, config.startAt, config.endAt);
+      }
+
+      if (config.totalChunks && config.thisChunk) {
+        fileNames = chunkifyTests(fileNames, config.totalChunks,
+                                  config.thisChunk, config.chunkByDir);
+      }
+
+      // The SDK assumes it is being run from resource URIs
+      let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIChromeRegistry);
+      let realPath = chromeReg.convertChromeURL(Services.io.newURI(TEST_PACKAGE, null, null));
+      let resProtocol = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsIResProtocolHandler);
+      resProtocol.setSubstitution("jetpack-package-tests", realPath);
+
+      // Set the test options
+      const options = {
+        test: {
+          iterations: config.runUntilFailure ? config.repeat : 1,
+          stop: false,
+          keepOpen: true,
+        },
+        profile: {
+          memory: false,
+          leaks: false,
+        },
+        output: {
+          logLevel: "verbose",
+          format: "tbpl",
+        },
+      }
+      setPrefs("extensions." + TEST_ID + ".sdk", options);
+
+      // Override the SDK modules if necessary
+      let sdkpath = "resource://gre/modules/commonjs/";
+      try {
+        let sdklibs = Services.prefs.getCharPref("extensions.sdk.path");
+        // sdkpath is a file path, make it a URI and map a resource URI to it
+        let sdkfile = Cc["@mozilla.org/file/local;1"].
+                      createInstance(Ci.nsIFile);
+        sdkfile.initWithPath(sdklibs);
+        let sdkuri = Services.io.newFileURI(sdkfile);
+        resProtocol.setSubstitution("jetpack-modules", sdkuri);
+        sdkpath = "resource://jetpack-modules/";
+      }
+      catch (e) {
+        // Stick with the built-in modules
+      }
+
+      const paths = {
+        "": sdkpath,
+        "tests/": "resource://jetpack-package-tests/",
+      };
+
+      // Create the base module loader to load the test harness
+      const loaderID = "toolkit/loader";
+      const loaderURI = paths[""] + loaderID + ".js";
+      const loaderModule = Cu.import(loaderURI, {}).Loader;
+
+      const modules = {};
+
+      // Manually set the loader's module cache to include itself;
+      // which otherwise fails due to lack of `Components`.
+      modules[loaderID] = loaderModule;
+      modules["@test/options"] = {};
+
+      let loader = loaderModule.Loader({
+        id: TEST_ID,
+        name: "addon-sdk",
+        version: "1.0",
+        loadReason: "install",
+        paths: paths,
+        modules: modules,
+        isNative: true,
+        rootURI: paths["tests/"],
+        prefixURI: paths["tests/"],
+        metadata: {},
+      });
+
+      const module = loaderModule.Module(loaderID, loaderURI);
+      const require = loaderModule.Require(loader, module);
+
+      // Wait until the add-on window is ready
+      require("sdk/addon/window").ready.then(() => {
+        let passed = 0;
+        let failed = 0;
+
+        function finish() {
+          if (passed + failed == 0) {
+            dump("TEST-UNEXPECTED-FAIL | jetpack-package-harness.js | " +
+                 "No tests to run. Did you pass an invalid --test-path?\n");
+          }
+          else {
+            dump("Jetpack Package Test Summary\n");
+            dump("\tPassed: " + passed + "\n" +
+                 "\tFailed: " + failed + "\n" +
+                 "\tTodo: 0\n");
+          }
+
+          if (config.closeWhenDone) {
+            require("sdk/system").exit(failed == 0 ? 0 : 1);
+          }
+        }
+
+        function testNextModule() {
+          if (fileNames.length == 0)
+            return finish();
+
+          let filename = fileNames.shift();
+          testModule(require, filename).then(tests => {
+            passed += tests.passed;
+            failed += tests.failed;
+          }).then(testNextModule);
+        }
+
+        testNextModule();
+      });
+    }
+    catch (e) {
+      dump("TEST-UNEXPECTED-FAIL: jetpack-package-harness.js | error starting test harness (" + e + ")\n");
+      dump(e.stack);
+    }
+  });
+}
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/jetpack-package-overlay.xul
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<overlay id="jetpackTestOverlay"
+         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"/>
+  <script type="application/javascript" src="chrome://mochikit/content/manifestLibrary.js"/>
+  <script type="application/javascript" src="chrome://mochikit/content/chunkifyTests.js"/>
+  <script type="application/javascript" src="chrome://mochikit/content/server.js"/>
+  <script type="application/javascript" src="chrome://mochikit/content/jetpack-package-harness.js"/>
+</overlay>
--- a/testing/mochitest/mach_commands.py
+++ b/testing/mochitest/mach_commands.py
@@ -68,16 +68,18 @@ There should be an app called 'test-cont
 %s.
 '''.lstrip()
 
 # Maps test flavors to mochitest suite type.
 FLAVORS = {
     'mochitest': 'plain',
     'chrome': 'chrome',
     'browser-chrome': 'browser',
+    'jetpack-package': 'jetpack-package',
+    'jetpack-addon': 'jetpack-addon',
     'a11y': 'a11y',
     'webapprt-chrome': 'webapprt-chrome',
 }
 
 
 class MochitestRunner(MozbuildObject):
     """Easily run mochitests.
 
@@ -195,17 +197,17 @@ class MochitestRunner(MozbuildObject):
         useTestMediaDevices=False, **kwargs):
         """Runs a mochitest.
 
         test_paths are path to tests. They can be a relative path from the
         top source directory, an absolute filename, or a directory containing
         test files.
 
         suite is the type of mochitest to run. It can be one of ('plain',
-        'chrome', 'browser', 'metro', 'a11y').
+        'chrome', 'browser', 'metro', 'a11y', 'jetpack-package', 'jetpack-addon').
 
         debugger is a program name or path to a binary (presumably a debugger)
         to run the test in. e.g. 'gdb'
 
         debugger_args are the arguments passed to the debugger.
 
         slowscript is true if the user has requested the SIGSEGV mechanism of
         invoking the slow script dialog.
@@ -264,16 +266,20 @@ class MochitestRunner(MozbuildObject):
         elif suite == 'chrome':
             options.chrome = True
         elif suite == 'browser':
             options.browserChrome = True
             flavor = 'browser-chrome'
         elif suite == 'devtools':
             options.browserChrome = True
             options.subsuite = 'devtools'
+        elif suite == 'jetpack-package':
+            options.jetpackPackage = True
+        elif suite == 'jetpack-addon':
+            options.jetpackAddon = True
         elif suite == 'metro':
             options.immersiveMode = True
             options.browserChrome = True
         elif suite == 'a11y':
             options.a11y = True
         elif suite == 'webapprt-content':
             options.webapprtContent = True
             options.app = self.get_webapp_runtime_path()
@@ -643,16 +649,30 @@ class MachCommands(MachCommandBase):
     @Command('mochitest-devtools', category='testing',
         conditions=[conditions.is_firefox],
         description='Run a devtools mochitest with browser chrome (integration test with a standard browser with the devtools frame).',
         parser=_st_parser)
     @MochitestCommand
     def run_mochitest_devtools(self, test_paths, **kwargs):
         return self.run_mochitest(test_paths, 'devtools', **kwargs)
 
+    @Command('jetpack-package', category='testing',
+        conditions=[conditions.is_firefox],
+        description='Run a jetpack package test.')
+    @MochitestCommand
+    def run_mochitest_jetpack_package(self, test_paths, **kwargs):
+        return self.run_mochitest(test_paths, 'jetpack-package', **kwargs)
+
+    @Command('jetpack-addon', category='testing',
+        conditions=[conditions.is_firefox],
+        description='Run a jetpack addon test.')
+    @MochitestCommand
+    def run_mochitest_jetpack_addon(self, test_paths, **kwargs):
+        return self.run_mochitest(test_paths, 'jetpack-addon', **kwargs)
+
     @Command('mochitest-metro', category='testing',
         conditions=[conditions.is_firefox],
         description='Run a mochitest with metro browser chrome (tests for Windows touch interface).',
         parser=_st_parser)
     @MochitestCommand
     def run_mochitest_metro(self, test_paths, **kwargs):
         return self.run_mochitest(test_paths, 'metro', **kwargs)
 
--- a/testing/mochitest/mochitest_options.py
+++ b/testing/mochitest/mochitest_options.py
@@ -160,16 +160,28 @@ class MochitestOptions(optparse.OptionPa
           "default": False,
         }],
         [["--subsuite"],
         { "action": "store",
           "dest": "subsuite",
           "help": "subsuite of tests to run",
           "default": "",
         }],
+        [["--jetpack-package"],
+        { "action": "store_true",
+          "dest": "jetpackPackage",
+          "help": "run jetpack package tests",
+          "default": False,
+        }],
+        [["--jetpack-addon"],
+        { "action": "store_true",
+          "dest": "jetpackAddon",
+          "help": "run jetpack addon tests",
+          "default": False,
+        }],
         [["--webapprt-content"],
         { "action": "store_true",
           "dest": "webapprtContent",
           "help": "run WebappRT content tests",
           "default": False,
         }],
         [["--webapprt-chrome"],
         { "action": "store_true",
--- a/testing/mochitest/moz.build
+++ b/testing/mochitest/moz.build
@@ -49,16 +49,20 @@ TEST_HARNESS_FILES.testing.mochitest += 
     'browser-test-overlay.xul',
     'browser-test.js',
     'cc-analyzer.js',
     'chrome-harness.js',
     'chunkifyTests.js',
     'gen_template.pl',
     'gl.json',
     'harness.xul',
+    'jetpack-addon-harness.js',
+    'jetpack-addon-overlay.xul',
+    'jetpack-package-harness.js',
+    'jetpack-package-overlay.xul',
     'manifest.webapp',
     'manifestLibrary.js',
     'mochitest_options.py',
     'pywebsocket_wrapper.py',
     'redirect.html',
     'runtests.py',
     'runtestsb2g.py',
     'runtestsremote.py',
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -499,17 +499,18 @@ class MochitestUtilsMixin(object):
         options.fileLevel = 'INFO'
 
     # allow relative paths for logFile
     if options.logFile:
       options.logFile = self.getLogFilePath(options.logFile)
 
     # Note that all tests under options.subsuite need to be browser chrome tests.
     if options.browserChrome or options.chrome or options.subsuite or \
-       options.a11y or options.webapprtChrome:
+       options.a11y or options.webapprtChrome or options.jetpackPackage or \
+       options.jetpackAddon:
       self.makeTestConfig(options)
     else:
       if options.autorun:
         self.urlOpts.append("autorun=1")
       if options.timeout:
         self.urlOpts.append("timeout=%d" % options.timeout)
       if options.closeWhenDone:
         self.urlOpts.append("closeWhenDone=1")
@@ -558,31 +559,40 @@ class MochitestUtilsMixin(object):
       if options.dumpDMDAfterTest:
         self.urlOpts.append("dumpDMDAfterTest=true")
       if options.debugger:
         self.urlOpts.append("interactiveDebugger=true")
 
   def getTestFlavor(self, options):
     if options.browserChrome:
       return "browser-chrome"
+    elif options.jetpackPackage:
+      return "jetpack-package"
+    elif options.jetpackAddon:
+      return "jetpack-addon"
     elif options.chrome:
       return "chrome"
     elif options.a11y:
       return "a11y"
     elif options.webapprtChrome:
       return "webapprt-chrome"
     else:
       return "mochitest"
 
   # This check can be removed when bug 983867 is fixed.
   def isTest(self, options, filename):
     allow_js_css = False
     if options.browserChrome:
       allow_js_css = True
       testPattern = re.compile(r"browser_.+\.js")
+    elif options.jetpackPackage:
+      allow_js_css = True
+      testPattern = re.compile(r"test-.+\.js")
+    elif options.jetpackAddon:
+      testPattern = re.compile(r".+\.xpi")
     elif options.chrome or options.a11y:
       testPattern = re.compile(r"(browser|test)_.+\.(xul|html|js|xhtml)")
     elif options.webapprtContent:
       testPattern = re.compile(r"webapprt_")
     elif options.webapprtChrome:
       allow_js_css = True
       testPattern = re.compile(r"browser_")
     else:
@@ -606,16 +616,20 @@ class MochitestUtilsMixin(object):
     if hasattr(self, "testRoot"):
       return self.testRoot, self.testRootAbs
     else:
       if options.browserChrome:
         if options.immersiveMode:
           self.testRoot = 'metro'
         else:
           self.testRoot = 'browser'
+      elif options.jetpackPackage:
+        self.testRoot = 'jetpack-package'
+      elif options.jetpackAddon:
+        self.testRoot = 'jetpack-addon'
       elif options.a11y:
         self.testRoot = 'a11y'
       elif options.webapprtChrome:
         self.testRoot = 'webapprtChrome'
       elif options.chrome:
         self.testRoot = 'chrome'
       else:
         self.testRoot = self.TEST_PATH
@@ -624,17 +638,17 @@ class MochitestUtilsMixin(object):
   def buildTestURL(self, options):
     testHost = "http://mochi.test:8888"
     testPath = self.getTestPath(options)
     testURL = "/".join([testHost, self.TEST_PATH, testPath])
     if os.path.isfile(os.path.join(self.oldcwd, os.path.dirname(__file__), self.TEST_PATH, testPath)) and options.repeat > 0:
       testURL = "/".join([testHost, self.TEST_PATH, os.path.dirname(testPath)])
     if options.chrome or options.a11y:
       testURL = "/".join([testHost, self.CHROME_PATH])
-    elif options.browserChrome:
+    elif options.browserChrome or options.jetpackPackage or options.jetpackAddon:
       testURL = "about:blank"
     return testURL
 
   def buildTestPath(self, options, testsToFilter=None, disabled=True):
     """ Build the url path to the specific test harness and test file or directory
         Build a manifest of tests to run and write out a json file for the harness to read
         testsToFilter option is used to filter/keep the tests provided in the list
 
@@ -793,16 +807,26 @@ toolbar#nav-bar {
     if options.browserChrome or options.chrome or options.a11y or options.webapprtChrome:
       chrome += """
 overlay chrome://browser/content/browser.xul chrome://mochikit/content/browser-test-overlay.xul
 overlay chrome://browser/content/shell.xhtml chrome://mochikit/content/browser-test-overlay.xul
 overlay chrome://navigator/content/navigator.xul chrome://mochikit/content/browser-test-overlay.xul
 overlay chrome://webapprt/content/webapp.xul chrome://mochikit/content/browser-test-overlay.xul
 """
 
+    if options.jetpackPackage:
+      chrome += """
+overlay chrome://browser/content/browser.xul chrome://mochikit/content/jetpack-package-overlay.xul
+"""
+
+    if options.jetpackAddon:
+      chrome += """
+overlay chrome://browser/content/browser.xul chrome://mochikit/content/jetpack-addon-overlay.xul
+"""
+
     self.installChromeJar(chrome, options)
     return manifest
 
   def getExtensionsToInstall(self, options):
     "Return a list of extensions to install in the profile"
     extensions = options.extensionsToInstall or []
     appDir = options.app[:options.app.rfind(os.sep)] if options.app else options.utilityPath