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 229760 1401f98a790e4c02254f0ce61c027acc41930548
parent 229706 fac90451603fbf142fdafec8cebce4da986a1461
child 229761 846759ed5d7c6c81215b1503c0b4cc53343d14ab
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps, ted, erikvold, jmaher
bugs1035512
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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