Bug 1290598: Migrate native messaging tests to xpcshell. r=aswan
☠☠ backed out by 34de0687c35a ☠ ☠
authorKris Maglione <maglione.k@gmail.com>
Tue, 02 Aug 2016 17:39:51 -0700
changeset 307908 486639a0f3ca05afaf0250b514c3641805f1f497
parent 307907 3242411d3294521508f34d1b5beee52b1ae986ea
child 307909 577158be08e8c1211ffa1198a21e3c059f98f477
push id31034
push usercbook@mozilla.com
push dateWed, 03 Aug 2016 15:09:36 +0000
treeherderautoland@8ceb6981305f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaswan
bugs1290598
milestone51.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 1290598: Migrate native messaging tests to xpcshell. r=aswan MozReview-Commit-ID: 4Uz73l8qGVE
toolkit/components/extensions/NativeMessaging.jsm
toolkit/components/extensions/test/mochitest/chrome.ini
toolkit/components/extensions/test/mochitest/test_chrome_ext_native_messaging.html
toolkit/components/extensions/test/xpcshell/head_native_messaging.js
toolkit/components/extensions/test/xpcshell/native_messaging.ini
toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
--- a/toolkit/components/extensions/NativeMessaging.jsm
+++ b/toolkit/components/extensions/NativeMessaging.jsm
@@ -210,17 +210,17 @@ this.NativeApp = class extends EventEmit
       }).then(proc => {
         this.startupPromise = null;
         this.proc = proc;
         this._startRead();
         this._startWrite();
         this._startStderrRead();
       }).catch(err => {
         this.startupPromise = null;
-        Cu.reportError(err);
+        Cu.reportError(err instanceof Error ? err : err.message);
         this._cleanup(err);
       });
   }
 
   // A port is definitely "alive" if this.proc is non-null.  But we have
   // to provide a live port object immediately when connecting so we also
   // need to consider a port alive if proc is null but the startupPromise
   // is still pending.
@@ -238,17 +238,19 @@ this.NativeApp = class extends EventEmit
           throw new Error(`Native application tried to send a message of ${len} bytes, which exceeds the limit of ${this.maxRead} bytes.`);
         }
         return this.proc.stdout.readJSON(len);
       }).then(msg => {
         this.emit("message", msg);
         this.readPromise = null;
         this._startRead();
       }).catch(err => {
-        Cu.reportError(err.message);
+        if (err.errorCode != Subprocess.ERROR_END_OF_FILE) {
+          Cu.reportError(err instanceof Error ? err : err.message);
+        }
         this._cleanup(err);
       });
   }
 
   _startWrite() {
     if (this.sendQueue.length == 0) {
       return;
     }
@@ -422,14 +424,18 @@ this.NativeApp = class extends EventEmit
     let result = this.startupPromise.then(() => {
       this.send(msg);
       return responsePromise;
     });
 
     result.then(() => {
       this._cleanup();
     }, () => {
+      // Prevent the response promise from being reported as an
+      // unchecked rejection if the startup promise fails.
+      responsePromise.catch(() => {});
+
       this._cleanup();
     });
 
     return result;
   }
 };
--- a/toolkit/components/extensions/test/mochitest/chrome.ini
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -4,25 +4,23 @@ support-files =
   head.js
   file_sample.html
 
 [test_chrome_ext_background_debug_global.html]
 skip-if = (os == 'android') # android doesn't have devtools
 [test_chrome_ext_background_page.html]
 skip-if = (toolkit == 'android') # android doesn't have devtools
 [test_chrome_ext_eventpage_warning.html]
-[test_chrome_ext_native_messaging.html]
-skip-if = os == "android"  # native messaging is not supported on android
 [test_chrome_ext_contentscript_unrecognizedprop_warning.html]
 skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android.
 [test_chrome_ext_trustworthy_origin.html]
 [test_chrome_ext_webnavigation_resolved_urls.html]
 skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android.
 [test_chrome_ext_shutdown_cleanup.html]
 [test_chrome_native_messaging_paths.html]
 skip-if = os != "mac" && os != "linux"
 [test_ext_cookies_expiry.html]
 skip-if = buildapp == 'b2g'
 [test_ext_cookies_permissions.html]
 skip-if = buildapp == 'b2g'
 [test_ext_jsversion.html]
 skip-if = buildapp == 'b2g'
-[test_ext_schema.html]
\ No newline at end of file
+[test_ext_schema.html]
deleted file mode 100644
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_native_messaging.html
+++ /dev/null
@@ -1,700 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <title>WebExtension test</title>
-  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
-  <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
-  <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
-  <script type="text/javascript" src="chrome_head.js"></script>
-  <script type="text/javascript" src="head.js"></script>
-  <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
-</head>
-<body>
-
-<script type="text/javascript">
-"use strict";
-
-/* globals OS */
-
-Cu.import("resource://gre/modules/AppConstants.jsm");
-Cu.import("resource://gre/modules/FileUtils.jsm");
-Cu.import("resource://gre/modules/osfile.jsm");
-let {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm");
-Components.utils.import("resource://gre/modules/Task.jsm");
-
-if (AppConstants.platform == "win") {
-  Cu.import("resource://testing-common/MockRegistry.jsm");
-}
-
-var promiseConsoleOutput = Task.async(function* (task) {
-  const DONE = "=== extension test console listener done ===";
-
-  let listener;
-  let messages = [];
-  let awaitListener = new Promise(resolve => {
-    listener = msg => {
-      if (msg == DONE) {
-        resolve();
-      } else if (msg instanceof Ci.nsIConsoleMessage) {
-        messages.push(msg.message);
-      }
-    };
-  });
-
-  Services.console.registerListener(listener);
-  try {
-    let result = yield task();
-
-    Services.console.logStringMessage(DONE);
-    yield awaitListener;
-
-    return {messages, result};
-  } finally {
-    Services.console.unregisterListener(listener);
-  }
-});
-
-const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes";
-const PREF_MAX_WRITE = "webextensions.native-messaging.max-output-message-bytes";
-
-function getSubprocessCount() {
-  return SubprocessImpl.Process.getWorker().call("getProcesses", [])
-                       .then(result => result.size);
-}
-function waitForSubprocessExit() {
-  return SubprocessImpl.Process.getWorker().call("waitForNoProcesses", []);
-}
-
-let dir = FileUtils.getDir("TmpD", ["NativeMessaging"]);
-dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
-info(`Using local directory ${dir.path}\n`);
-
-function getPath(filename) {
-  return OS.Path.join(dir.path, filename);
-}
-
-// Set up a couple of native applications and their manifests for
-// test to use.
-const ID = "native@tests.mozilla.org";
-
-const ECHO_BODY = String.raw`
-import struct
-import sys
-
-while True:
-    rawlen = sys.stdin.read(4)
-    if len(rawlen) == 0:
-        sys.exit(0)
-    msglen = struct.unpack('@I', rawlen)[0]
-    msg = sys.stdin.read(msglen)
-
-    sys.stdout.write(struct.pack('@I', msglen))
-    sys.stdout.write(msg)
-`;
-
-const INFO_BODY = String.raw`
-import json
-import os
-import struct
-import sys
-
-msg = json.dumps({"args": sys.argv, "cwd": os.getcwd()})
-sys.stdout.write(struct.pack('@I', len(msg)))
-sys.stdout.write(msg)
-sys.exit(0)
-`;
-
-const WONTDIE_BODY = String.raw`
-import signal
-import struct
-import sys
-import time
-
-signal.signal(signal.SIGTERM, signal.SIG_IGN)
-
-def spin():
-    while True:
-        try:
-            signal.pause()
-        except AttributeError:
-            time.sleep(5)
-
-while True:
-    rawlen = sys.stdin.read(4)
-    if len(rawlen) == 0:
-        spin()
-
-    msglen = struct.unpack('@I', rawlen)[0]
-    msg = sys.stdin.read(msglen)
-
-    sys.stdout.write(struct.pack('@I', msglen))
-    sys.stdout.write(msg)
-`;
-
-const STDERR_LINES = ["hello stderr", "this should be a separate line"];
-let STDERR_MSG = STDERR_LINES.join("\\n");
-
-// Python apparently line-buffers stderr even with the -u arg on
-// Windows.  Dealing with that is more hassle than its worth but
-// on other platforms, we want to keep testing partial lines.
-if (AppConstants.platform == "win") {
-  STDERR_MSG += "\\n";
-}
-
-const STDERR_BODY = String.raw`
-import sys
-sys.stderr.write("${STDERR_MSG}")
-`;
-
-const SCRIPTS = [
-  {
-    name: "echo",
-    description: "a native app that echoes back messages it receives",
-    script: ECHO_BODY,
-  },
-  {
-    name: "info",
-    description: "a native app that gives some info about how it was started",
-    script: INFO_BODY,
-  },
-  {
-    name: "wontdie",
-    description: "a native app that does not exit when stdin closes or on SIGTERM",
-    script: WONTDIE_BODY,
-  },
-  {
-    name: "stderr",
-    description: "a native app that writes to stderr and then exits",
-    script: STDERR_BODY,
-  },
-];
-
-add_task(function* setup() {
-  const PERMS = {unixMode: 0o755};
-  let pythonPath = yield Subprocess.pathSearch("python2.7").catch(err => {
-    if (err.errorCode != Subprocess.ERROR_BAD_EXECUTABLE) {
-      throw err;
-    }
-    return Subprocess.pathSearch("python");
-  });
-
-  switch (AppConstants.platform) {
-    case "macosx":
-    case "linux":
-      let dirProvider = {
-        getFile(property) {
-          if (property == "XREUserNativeMessaging") {
-            return dir.clone();
-          }
-          return null;
-        },
-      };
-
-      Services.dirsvc.registerProvider(dirProvider);
-      SimpleTest.registerCleanupFunction(() => {
-        Services.dirsvc.unregisterProvider(dirProvider);
-        dir.remove(true);
-      });
-
-      for (let script of SCRIPTS) {
-        let path = getPath(`${script.name}.py`);
-        let body = `#!${pythonPath} -u\n${script.script}`;
-        yield OS.File.writeAtomic(path, body);
-        yield OS.File.setPermissions(path, PERMS);
-
-        let manifest = {
-          name: script.name,
-          description: script.description,
-          path,
-          type: "stdio",
-          allowed_extensions: [ID],
-        };
-
-        yield OS.File.writeAtomic(getPath(`${script.name}.json`), JSON.stringify(manifest));
-      }
-      break;
-
-    case "win":
-      const REGKEY = "Software\\Mozilla\\NativeMessagingHosts";
-      let registry = new MockRegistry();
-      SimpleTest.registerCleanupFunction(() => {
-        registry.shutdown();
-      });
-
-      for (let script of SCRIPTS) {
-        let pyPath = getPath(`${script.name}.py`);
-        yield OS.File.writeAtomic(pyPath, script.script);
-
-        let batPath = getPath(`${script.name}.bat`);
-        let batBody = `@ECHO OFF\n${pythonPath} -u ${pyPath} %*\n`;
-        yield OS.File.writeAtomic(batPath, batBody);
-        yield OS.File.setPermissions(pyPath, PERMS);
-
-        let manifest = {
-          name: script.name,
-          description: script.description,
-          path: batPath,
-          type: "stdio",
-          allowed_extensions: [ID],
-        };
-        let manifestPath = getPath(`${script.name}.json`);
-        yield OS.File.writeAtomic(manifestPath, JSON.stringify(manifest));
-
-        registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
-                          `${REGKEY}\\${script.name}`, "", manifestPath);
-
-        // Create a version of the manifest with a relative path
-        let relativeName = `relative.${script.name}`;
-        manifest.name = relativeName;
-        manifest.path = `${script.name}.bat`;
-
-        manifestPath = getPath(`${relativeName}.json`);
-        yield OS.File.writeAtomic(manifestPath, JSON.stringify(manifest));
-
-        registry.setValue(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
-                          `${REGKEY}\\${relativeName}`, "", manifestPath);
-      }
-      break;
-
-    default:
-      ok(false, `Native messaging is not supported on ${AppConstants.platform}`);
-  }
-});
-
-// Test the basic operation of native messaging with a simple
-// script that echoes back whatever message is sent to it.
-add_task(function* test_happy_path() {
-  function background() {
-    let port = browser.runtime.connectNative("echo");
-    port.onMessage.addListener(msg => {
-      browser.test.sendMessage("message", msg);
-    });
-    browser.test.onMessage.addListener((what, payload) => {
-      if (what == "send") {
-        if (payload._json) {
-          let json = payload._json;
-          payload.toJSON = () => json;
-          delete payload._json;
-        }
-        port.postMessage(payload);
-      }
-    });
-    browser.test.sendMessage("ready");
-  }
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background: `(${background})()`,
-    manifest: {
-      permissions: ["nativeMessaging"],
-    },
-  }, ID);
-
-  yield extension.startup();
-  yield extension.awaitMessage("ready");
-  const tests = [
-    {
-      data: "this is a string",
-      what: "simple string",
-    },
-    {
-      data: "Это юникода",
-      what: "unicode string",
-    },
-    {
-      data: {test: "hello"},
-      what: "simple object",
-    },
-    {
-      data: {
-        what: "An object with a few properties",
-        number: 123,
-        bool: true,
-        nested: {what: "another object"},
-      },
-      what: "object with several properties",
-    },
-
-    {
-      data: {
-        ignoreme: true,
-        _json: {data: "i have a tojson method"},
-      },
-      expected: {data: "i have a tojson method"},
-      what: "object with toJSON() method",
-    },
-  ];
-  for (let test of tests) {
-    extension.sendMessage("send", test.data);
-    let response = yield extension.awaitMessage("message");
-    let expected = test.expected || test.data;
-    isDeeply(response, expected, `Echoed a message of type ${test.what}`);
-  }
-
-  let procCount = yield getSubprocessCount();
-  is(procCount, 1, "subprocess is still running");
-  let exitPromise = waitForSubprocessExit();
-  yield extension.unload();
-  yield exitPromise;
-});
-
-if (AppConstants.platform == "win") {
-  // "relative.echo" has a relative path in the host manifest.
-  add_task(function* test_relative_path() {
-    function background() {
-      let port = browser.runtime.connectNative("relative.echo");
-      let MSG = "test relative echo path";
-      port.onMessage.addListener(msg => {
-        browser.test.assertEq(MSG, msg, "Got expected message back");
-        browser.test.sendMessage("done");
-      });
-      port.postMessage(MSG);
-    }
-
-    let extension = ExtensionTestUtils.loadExtension({
-      background: `(${background})()`,
-      manifest: {
-        permissions: ["nativeMessaging"],
-      },
-    }, ID);
-
-    yield extension.startup();
-    yield extension.awaitMessage("done");
-
-    let procCount = yield getSubprocessCount();
-    is(procCount, 1, "subprocess is still running");
-    let exitPromise = waitForSubprocessExit();
-    yield extension.unload();
-    yield exitPromise;
-  });
-}
-
-// Test sendNativeMessage()
-add_task(function* test_sendNativeMessage() {
-  function background() {
-    let MSG = {test: "hello world"};
-
-    // Check error handling
-    browser.runtime.sendNativeMessage("nonexistent", MSG).then(() => {
-      browser.test.fail("sendNativeMessage() to a nonexistent app should have failed");
-    }, err => {
-      browser.test.succeed("sendNativeMessage() to a nonexistent app failed");
-    }).then(() => {
-      // Check regular message exchange
-      return browser.runtime.sendNativeMessage("echo", MSG);
-    }).then(reply => {
-      let expected = JSON.stringify(MSG);
-      let received = JSON.stringify(reply);
-      browser.test.assertEq(expected, received, "Received echoed native message");
-      browser.test.sendMessage("finished");
-    });
-  }
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background: `(${background})()`,
-    manifest: {
-      permissions: ["nativeMessaging"],
-    },
-  }, ID);
-
-  yield extension.startup();
-  yield extension.awaitMessage("finished");
-
-  // With sendNativeMessage(), the subprocess should be disconnected
-  // after exchanging a single message.
-  yield waitForSubprocessExit();
-
-  yield extension.unload();
-});
-
-// Test calling Port.disconnect()
-add_task(function* test_disconnect() {
-  function background() {
-    let port = browser.runtime.connectNative("echo");
-    port.onMessage.addListener(msg => {
-      browser.test.sendMessage("message", msg);
-    });
-    browser.test.onMessage.addListener((what, payload) => {
-      if (what == "send") {
-        if (payload._json) {
-          let json = payload._json;
-          payload.toJSON = () => json;
-          delete payload._json;
-        }
-        port.postMessage(payload);
-      } else if (what == "disconnect") {
-        try {
-          port.disconnect();
-          browser.test.sendMessage("disconnect-result", {success: true});
-        } catch (err) {
-          browser.test.sendMessage("disconnect-result", {
-            success: false,
-            errmsg: err.message,
-          });
-        }
-      }
-    });
-    browser.test.sendMessage("ready");
-  }
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background: `(${background})()`,
-    manifest: {
-      permissions: ["nativeMessaging"],
-    },
-  }, ID);
-
-  yield extension.startup();
-  yield extension.awaitMessage("ready");
-
-  extension.sendMessage("send", "test");
-  let response = yield extension.awaitMessage("message");
-  is(response, "test", "Echoed a string");
-
-  let procCount = yield getSubprocessCount();
-  is(procCount, 1, "subprocess is running");
-
-  extension.sendMessage("disconnect");
-  response = yield extension.awaitMessage("disconnect-result");
-  is(response.success, true, "disconnect succeeded");
-
-  info("waiting for subprocess to exit");
-  yield waitForSubprocessExit();
-  procCount = yield getSubprocessCount();
-  is(procCount, 0, "subprocess is no longer running");
-
-  extension.sendMessage("disconnect");
-  response = yield extension.awaitMessage("disconnect-result");
-  is(response.success, false, "second call to disconnect failed");
-  ok(/already disconnected/.test(response.errmsg), "disconnect error message is reasonable");
-
-  yield extension.unload();
-});
-
-// Test the limit on message size for writing
-add_task(function* test_write_limit() {
-  Services.prefs.setIntPref(PREF_MAX_WRITE, 10);
-  function clearPref() {
-    Services.prefs.clearUserPref(PREF_MAX_WRITE);
-  }
-  SimpleTest.registerCleanupFunction(clearPref);
-
-  function background() {
-    const PAYLOAD = "0123456789A";
-    let port = browser.runtime.connectNative("echo");
-    try {
-      port.postMessage(PAYLOAD);
-      browser.test.sendMessage("result", null);
-    } catch (ex) {
-      browser.test.sendMessage("result", ex.message);
-    }
-  }
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background: `(${background})()`,
-    manifest: {
-      permissions: ["nativeMessaging"],
-    },
-  }, ID);
-
-  yield extension.startup();
-
-  let errmsg = yield extension.awaitMessage("result");
-  isnot(errmsg, null, "native postMessage() failed for overly large message");
-
-  yield extension.unload();
-  yield waitForSubprocessExit();
-
-  clearPref();
-});
-
-// Test the limit on message size for reading
-add_task(function* test_read_limit() {
-  Services.prefs.setIntPref(PREF_MAX_READ, 10);
-  function clearPref() {
-    Services.prefs.clearUserPref(PREF_MAX_READ);
-  }
-  SimpleTest.registerCleanupFunction(clearPref);
-
-  function background() {
-    const PAYLOAD = "0123456789A";
-    let port = browser.runtime.connectNative("echo");
-    port.onDisconnect.addListener(() => {
-      browser.test.sendMessage("result", "disconnected");
-    });
-    port.onMessage.addListener(msg => {
-      browser.test.sendMessage("result", "message");
-    });
-    port.postMessage(PAYLOAD);
-  }
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background: `(${background})()`,
-    manifest: {
-      permissions: ["nativeMessaging"],
-    },
-  }, ID);
-
-  yield extension.startup();
-
-  let result = yield extension.awaitMessage("result");
-  is(result, "disconnected", "native port disconnected on receiving large message");
-
-  yield extension.unload();
-  yield waitForSubprocessExit();
-
-  clearPref();
-});
-
-// Test that an extension without the nativeMessaging permission cannot
-// use native messaging.
-add_task(function* test_ext_permission() {
-  function background() {
-    browser.test.assertFalse("connectNative" in chrome.runtime, "chrome.runtime.connectNative does not exist without nativeMessaging permission");
-    browser.test.assertFalse("connectNative" in browser.runtime, "browser.runtime.connectNative does not exist without nativeMessaging permission");
-    browser.test.assertFalse("sendNativeMessage" in chrome.runtime, "chrome.runtime.sendNativeMessage does not exist without nativeMessaging permission");
-    browser.test.assertFalse("sendNativeMessage" in browser.runtime, "browser.runtime.sendNativeMessage does not exist without nativeMessaging permission");
-    browser.test.sendMessage("finished");
-  }
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background: `(${background})()`,
-    manifest: {},
-  });
-
-  yield extension.startup();
-  yield extension.awaitMessage("finished");
-  yield extension.unload();
-});
-
-// Test that an extension that is not listed in allowed_extensions for
-// a native application cannot use that application.
-add_task(function* test_app_permission() {
-  function background() {
-    let port = browser.runtime.connectNative("echo");
-    port.onDisconnect.addListener(() => {
-      browser.test.sendMessage("result", "disconnected");
-    });
-    port.onMessage.addListener(msg => {
-      browser.test.sendMessage("result", "message");
-    });
-    port.postMessage({test: "test"});
-  }
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background: `(${background})()`,
-    manifest: {
-      permissions: ["nativeMessaging"],
-    },
-  }, "somethingelse@tests.mozilla.org");
-
-  yield extension.startup();
-
-  let result = yield extension.awaitMessage("result");
-  is(result, "disconnected", "connectNative() failed without native app permission");
-
-  yield extension.unload();
-
-  let procCount = yield getSubprocessCount();
-  is(procCount, 0, "No child process was started");
-});
-
-// Test that the command-line arguments and working directory for the
-// native application are as expected.
-add_task(function* test_child_process() {
-  function background() {
-    let port = browser.runtime.connectNative("info");
-    port.onMessage.addListener(msg => {
-      browser.test.sendMessage("result", msg);
-    });
-  }
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background: `(${background})()`,
-    manifest: {
-      permissions: ["nativeMessaging"],
-    },
-  }, ID);
-
-  yield extension.startup();
-
-  let msg = yield extension.awaitMessage("result");
-  is(msg.args.length, 2, "Received one command line argument");
-  is(msg.args[1], getPath("info.json"), "Command line argument is the path to the native host manifest");
-  is(msg.cwd, dir.path, "Working directory is the directory containing the native appliation");
-
-  let exitPromise = waitForSubprocessExit();
-  yield extension.unload();
-  yield exitPromise;
-});
-
-// Test that an unresponsive native application still gets killed eventually
-add_task(function* test_unresponsive_native_app() {
-  // XXX expose GRACEFUL_SHUTDOWN_TIME as a pref and reduce it
-  // just for this test?
-
-  function background() {
-    let port = browser.runtime.connectNative("wontdie");
-
-    const MSG = "echo me";
-    // bounce a message to make sure the process actually starts
-    port.onMessage.addListener(msg => {
-      browser.test.assertEq(msg, MSG, "Received echoed message");
-      browser.test.sendMessage("ready");
-    });
-    port.postMessage(MSG);
-  }
-
-  let extension = ExtensionTestUtils.loadExtension({
-    background: `(${background})()`,
-    manifest: {
-      permissions: ["nativeMessaging"],
-    },
-  }, ID);
-
-  yield extension.startup();
-  yield extension.awaitMessage("ready");
-
-  let procCount = yield getSubprocessCount();
-  is(procCount, 1, "subprocess is running");
-
-  let exitPromise = waitForSubprocessExit();
-  yield extension.unload();
-  yield exitPromise;
-
-  procCount = yield getSubprocessCount();
-  is(procCount, 0, "subprocess was succesfully killed");
-});
-
-add_task(function* test_stderr() {
-  function background() {
-    let port = browser.runtime.connectNative("stderr");
-    port.onDisconnect.addListener(() => {
-      browser.test.sendMessage("finished");
-    });
-  }
-
-  let {messages} = yield promiseConsoleOutput(function* () {
-    let extension = ExtensionTestUtils.loadExtension({
-      background: `(${background})()`,
-      manifest: {
-        permissions: ["nativeMessaging"],
-      },
-    }, ID);
-
-    yield extension.startup();
-    yield extension.awaitMessage("finished");
-    yield extension.unload();
-  });
-
-  let lines = STDERR_LINES.map(line => messages.findIndex(msg => msg.includes(line)));
-  isnot(lines[0], -1, "Saw first line of stderr output on the console");
-  isnot(lines[1], -1, "Saw second line of stderr output on the console");
-  isnot(lines[0], lines[1], "Stderr output lines are separated in the console");
-
-  yield waitForSubprocessExit();
-});
-
-</script>
-
-</body>
-</html>
--- a/toolkit/components/extensions/test/xpcshell/head_native_messaging.js
+++ b/toolkit/components/extensions/test/xpcshell/head_native_messaging.js
@@ -1,21 +1,21 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 /* globals AppConstants, FileUtils */
-/* exported setupHosts */
+/* exported getSubprocessCount, setupHosts, waitForSubprocessExit */
 
 XPCOMUtils.defineLazyModuleGetter(this, "MockRegistry",
                                   "resource://testing-common/MockRegistry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 
-Cu.import("resource://gre/modules/Subprocess.jsm");
+let {Subprocess, SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm");
 
 
 let tmpDir = FileUtils.getDir("TmpD", ["NativeMessaging"]);
 tmpDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
 
 do_register_cleanup(() => {
   tmpDir.remove(true);
 });
@@ -105,8 +105,17 @@ function* setupHosts(scripts) {
         }
       }
       break;
 
     default:
       ok(false, `Native messaging is not supported on ${AppConstants.platform}`);
   }
 }
+
+
+function getSubprocessCount() {
+  return SubprocessImpl.Process.getWorker().call("getProcesses", [])
+                       .then(result => result.size);
+}
+function waitForSubprocessExit() {
+  return SubprocessImpl.Process.getWorker().call("waitForNoProcesses", []);
+}
--- a/toolkit/components/extensions/test/xpcshell/native_messaging.ini
+++ b/toolkit/components/extensions/test/xpcshell/native_messaging.ini
@@ -2,9 +2,10 @@
 head = head.js head_native_messaging.js
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'gonk' || appname == "thunderbird" || os == "android"
 subprocess = true
 support-files =
   data/**
 
+[test_ext_native_messaging.js]
 [test_ext_native_messaging_perf.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
@@ -0,0 +1,531 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* globals chrome */
+
+const PREF_MAX_READ = "webextensions.native-messaging.max-input-message-bytes";
+const PREF_MAX_WRITE = "webextensions.native-messaging.max-output-message-bytes";
+
+const ECHO_BODY = String.raw`
+  import struct
+  import sys
+
+  while True:
+      rawlen = sys.stdin.read(4)
+      if len(rawlen) == 0:
+          sys.exit(0)
+      msglen = struct.unpack('@I', rawlen)[0]
+      msg = sys.stdin.read(msglen)
+
+      sys.stdout.write(struct.pack('@I', msglen))
+      sys.stdout.write(msg)
+`;
+
+const INFO_BODY = String.raw`
+  import json
+  import os
+  import struct
+  import sys
+
+  msg = json.dumps({"args": sys.argv, "cwd": os.getcwd()})
+  sys.stdout.write(struct.pack('@I', len(msg)))
+  sys.stdout.write(msg)
+  sys.exit(0)
+`;
+
+const WONTDIE_BODY = String.raw`
+  import signal
+  import struct
+  import sys
+  import time
+
+  signal.signal(signal.SIGTERM, signal.SIG_IGN)
+
+  def spin():
+      while True:
+          try:
+              signal.pause()
+          except AttributeError:
+              time.sleep(5)
+
+  while True:
+      rawlen = sys.stdin.read(4)
+      if len(rawlen) == 0:
+          spin()
+
+      msglen = struct.unpack('@I', rawlen)[0]
+      msg = sys.stdin.read(msglen)
+
+      sys.stdout.write(struct.pack('@I', msglen))
+      sys.stdout.write(msg)
+`;
+
+const STDERR_LINES = ["hello stderr", "this should be a separate line"];
+let STDERR_MSG = STDERR_LINES.join("\\n");
+
+const STDERR_BODY = String.raw`
+  import sys
+  sys.stderr.write("${STDERR_MSG}")
+`;
+
+const SCRIPTS = [
+  {
+    name: "echo",
+    description: "a native app that echoes back messages it receives",
+    script: ECHO_BODY.replace(/^ {2}/gm, ""),
+  },
+  {
+    name: "info",
+    description: "a native app that gives some info about how it was started",
+    script: INFO_BODY.replace(/^ {2}/gm, ""),
+  },
+  {
+    name: "wontdie",
+    description: "a native app that does not exit when stdin closes or on SIGTERM",
+    script: WONTDIE_BODY.replace(/^ {2}/gm, ""),
+  },
+  {
+    name: "stderr",
+    description: "a native app that writes to stderr and then exits",
+    script: STDERR_BODY.replace(/^ {2}/gm, ""),
+  },
+];
+
+add_task(function* setup() {
+  yield setupHosts(SCRIPTS);
+});
+
+// Test the basic operation of native messaging with a simple
+// script that echoes back whatever message is sent to it.
+add_task(function* test_happy_path() {
+  function background() {
+    let port = browser.runtime.connectNative("echo");
+    port.onMessage.addListener(msg => {
+      browser.test.sendMessage("message", msg);
+    });
+    browser.test.onMessage.addListener((what, payload) => {
+      if (what == "send") {
+        if (payload._json) {
+          let json = payload._json;
+          payload.toJSON = () => json;
+          delete payload._json;
+        }
+        port.postMessage(payload);
+      }
+    });
+    browser.test.sendMessage("ready");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["nativeMessaging"],
+    },
+  }, ID);
+
+  yield extension.startup();
+  yield extension.awaitMessage("ready");
+  const tests = [
+    {
+      data: "this is a string",
+      what: "simple string",
+    },
+    {
+      data: "Это юникода",
+      what: "unicode string",
+    },
+    {
+      data: {test: "hello"},
+      what: "simple object",
+    },
+    {
+      data: {
+        what: "An object with a few properties",
+        number: 123,
+        bool: true,
+        nested: {what: "another object"},
+      },
+      what: "object with several properties",
+    },
+
+    {
+      data: {
+        ignoreme: true,
+        _json: {data: "i have a tojson method"},
+      },
+      expected: {data: "i have a tojson method"},
+      what: "object with toJSON() method",
+    },
+  ];
+  for (let test of tests) {
+    extension.sendMessage("send", test.data);
+    let response = yield extension.awaitMessage("message");
+    let expected = test.expected || test.data;
+    deepEqual(response, expected, `Echoed a message of type ${test.what}`);
+  }
+
+  let procCount = yield getSubprocessCount();
+  equal(procCount, 1, "subprocess is still running");
+  let exitPromise = waitForSubprocessExit();
+  yield extension.unload();
+  yield exitPromise;
+});
+
+if (AppConstants.platform == "win") {
+  // "relative.echo" has a relative path in the host manifest.
+  add_task(function* test_relative_path() {
+    function background() {
+      let port = browser.runtime.connectNative("relative.echo");
+      let MSG = "test relative echo path";
+      port.onMessage.addListener(msg => {
+        browser.test.assertEq(MSG, msg, "Got expected message back");
+        browser.test.sendMessage("done");
+      });
+      port.postMessage(MSG);
+    }
+
+    let extension = ExtensionTestUtils.loadExtension({
+      background,
+      manifest: {
+        permissions: ["nativeMessaging"],
+      },
+    }, ID);
+
+    yield extension.startup();
+    yield extension.awaitMessage("done");
+
+    let procCount = yield getSubprocessCount();
+    equal(procCount, 1, "subprocess is still running");
+    let exitPromise = waitForSubprocessExit();
+    yield extension.unload();
+    yield exitPromise;
+  });
+}
+
+// Test sendNativeMessage()
+add_task(function* test_sendNativeMessage() {
+  function background() {
+    let MSG = {test: "hello world"};
+
+    // Check error handling
+    browser.runtime.sendNativeMessage("nonexistent", MSG).then(() => {
+      browser.test.fail("sendNativeMessage() to a nonexistent app should have failed");
+    }, err => {
+      browser.test.succeed("sendNativeMessage() to a nonexistent app failed");
+    }).then(() => {
+      // Check regular message exchange
+      return browser.runtime.sendNativeMessage("echo", MSG);
+    }).then(reply => {
+      let expected = JSON.stringify(MSG);
+      let received = JSON.stringify(reply);
+      browser.test.assertEq(expected, received, "Received echoed native message");
+      browser.test.sendMessage("finished");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["nativeMessaging"],
+    },
+  }, ID);
+
+  yield extension.startup();
+  yield extension.awaitMessage("finished");
+
+  // With sendNativeMessage(), the subprocess should be disconnected
+  // after exchanging a single message.
+  yield waitForSubprocessExit();
+
+  yield extension.unload();
+});
+
+// Test calling Port.disconnect()
+add_task(function* test_disconnect() {
+  function background() {
+    let port = browser.runtime.connectNative("echo");
+    port.onMessage.addListener(msg => {
+      browser.test.sendMessage("message", msg);
+    });
+    browser.test.onMessage.addListener((what, payload) => {
+      if (what == "send") {
+        if (payload._json) {
+          let json = payload._json;
+          payload.toJSON = () => json;
+          delete payload._json;
+        }
+        port.postMessage(payload);
+      } else if (what == "disconnect") {
+        try {
+          port.disconnect();
+          browser.test.sendMessage("disconnect-result", {success: true});
+        } catch (err) {
+          browser.test.sendMessage("disconnect-result", {
+            success: false,
+            errmsg: err.message,
+          });
+        }
+      }
+    });
+    browser.test.sendMessage("ready");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["nativeMessaging"],
+    },
+  }, ID);
+
+  yield extension.startup();
+  yield extension.awaitMessage("ready");
+
+  extension.sendMessage("send", "test");
+  let response = yield extension.awaitMessage("message");
+  equal(response, "test", "Echoed a string");
+
+  let procCount = yield getSubprocessCount();
+  equal(procCount, 1, "subprocess is running");
+
+  extension.sendMessage("disconnect");
+  response = yield extension.awaitMessage("disconnect-result");
+  equal(response.success, true, "disconnect succeeded");
+
+  do_print("waiting for subprocess to exit");
+  yield waitForSubprocessExit();
+  procCount = yield getSubprocessCount();
+  equal(procCount, 0, "subprocess is no longer running");
+
+  extension.sendMessage("disconnect");
+  response = yield extension.awaitMessage("disconnect-result");
+  equal(response.success, false, "second call to disconnect failed");
+  ok(/already disconnected/.test(response.errmsg), "disconnect error message is reasonable");
+
+  yield extension.unload();
+});
+
+// Test the limit on message size for writing
+add_task(function* test_write_limit() {
+  Services.prefs.setIntPref(PREF_MAX_WRITE, 10);
+  function clearPref() {
+    Services.prefs.clearUserPref(PREF_MAX_WRITE);
+  }
+  do_register_cleanup(clearPref);
+
+  function background() {
+    const PAYLOAD = "0123456789A";
+    let port = browser.runtime.connectNative("echo");
+    try {
+      port.postMessage(PAYLOAD);
+      browser.test.sendMessage("result", null);
+    } catch (ex) {
+      browser.test.sendMessage("result", ex.message);
+    }
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["nativeMessaging"],
+    },
+  }, ID);
+
+  yield extension.startup();
+
+  let errmsg = yield extension.awaitMessage("result");
+  notEqual(errmsg, null, "native postMessage() failed for overly large message");
+
+  yield extension.unload();
+  yield waitForSubprocessExit();
+
+  clearPref();
+});
+
+// Test the limit on message size for reading
+add_task(function* test_read_limit() {
+  Services.prefs.setIntPref(PREF_MAX_READ, 10);
+  function clearPref() {
+    Services.prefs.clearUserPref(PREF_MAX_READ);
+  }
+  do_register_cleanup(clearPref);
+
+  function background() {
+    const PAYLOAD = "0123456789A";
+    let port = browser.runtime.connectNative("echo");
+    port.onDisconnect.addListener(() => {
+      browser.test.sendMessage("result", "disconnected");
+    });
+    port.onMessage.addListener(msg => {
+      browser.test.sendMessage("result", "message");
+    });
+    port.postMessage(PAYLOAD);
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["nativeMessaging"],
+    },
+  }, ID);
+
+  yield extension.startup();
+
+  let result = yield extension.awaitMessage("result");
+  equal(result, "disconnected", "native port disconnected on receiving large message");
+
+  yield extension.unload();
+  yield waitForSubprocessExit();
+
+  clearPref();
+});
+
+// Test that an extension without the nativeMessaging permission cannot
+// use native messaging.
+add_task(function* test_ext_permission() {
+  function background() {
+    browser.test.assertFalse("connectNative" in chrome.runtime, "chrome.runtime.connectNative does not exist without nativeMessaging permission");
+    browser.test.assertFalse("connectNative" in browser.runtime, "browser.runtime.connectNative does not exist without nativeMessaging permission");
+    browser.test.assertFalse("sendNativeMessage" in chrome.runtime, "chrome.runtime.sendNativeMessage does not exist without nativeMessaging permission");
+    browser.test.assertFalse("sendNativeMessage" in browser.runtime, "browser.runtime.sendNativeMessage does not exist without nativeMessaging permission");
+    browser.test.sendMessage("finished");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {},
+  });
+
+  yield extension.startup();
+  yield extension.awaitMessage("finished");
+  yield extension.unload();
+});
+
+// Test that an extension that is not listed in allowed_extensions for
+// a native application cannot use that application.
+add_task(function* test_app_permission() {
+  function background() {
+    let port = browser.runtime.connectNative("echo");
+    port.onDisconnect.addListener(() => {
+      browser.test.sendMessage("result", "disconnected");
+    });
+    port.onMessage.addListener(msg => {
+      browser.test.sendMessage("result", "message");
+    });
+    port.postMessage({test: "test"});
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["nativeMessaging"],
+    },
+  }, "somethingelse@tests.mozilla.org");
+
+  yield extension.startup();
+
+  let result = yield extension.awaitMessage("result");
+  equal(result, "disconnected", "connectNative() failed without native app permission");
+
+  yield extension.unload();
+
+  let procCount = yield getSubprocessCount();
+  equal(procCount, 0, "No child process was started");
+});
+
+// Test that the command-line arguments and working directory for the
+// native application are as expected.
+add_task(function* test_child_process() {
+  function background() {
+    let port = browser.runtime.connectNative("info");
+    port.onMessage.addListener(msg => {
+      browser.test.sendMessage("result", msg);
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["nativeMessaging"],
+    },
+  }, ID);
+
+  yield extension.startup();
+
+  let msg = yield extension.awaitMessage("result");
+  equal(msg.args.length, 2, "Received one command line argument");
+  equal(msg.args[1], getPath("info.json"), "Command line argument is the path to the native host manifest");
+  equal(msg.cwd.replace(/^\/private\//, "/"), tmpDir.path,
+        "Working directory is the directory containing the native appliation");
+
+  let exitPromise = waitForSubprocessExit();
+  yield extension.unload();
+  yield exitPromise;
+});
+
+// Test that an unresponsive native application still gets killed eventually
+add_task(function* test_unresponsive_native_app() {
+  // XXX expose GRACEFUL_SHUTDOWN_TIME as a pref and reduce it
+  // just for this test?
+
+  function background() {
+    let port = browser.runtime.connectNative("wontdie");
+
+    const MSG = "echo me";
+    // bounce a message to make sure the process actually starts
+    port.onMessage.addListener(msg => {
+      browser.test.assertEq(msg, MSG, "Received echoed message");
+      browser.test.sendMessage("ready");
+    });
+    port.postMessage(MSG);
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      permissions: ["nativeMessaging"],
+    },
+  }, ID);
+
+  yield extension.startup();
+  yield extension.awaitMessage("ready");
+
+  let procCount = yield getSubprocessCount();
+  equal(procCount, 1, "subprocess is running");
+
+  let exitPromise = waitForSubprocessExit();
+  yield extension.unload();
+  yield exitPromise;
+
+  procCount = yield getSubprocessCount();
+  equal(procCount, 0, "subprocess was succesfully killed");
+});
+
+add_task(function* test_stderr() {
+  function background() {
+    let port = browser.runtime.connectNative("stderr");
+    port.onDisconnect.addListener(() => {
+      browser.test.sendMessage("finished");
+    });
+  }
+
+  let {messages} = yield promiseConsoleOutput(function* () {
+    let extension = ExtensionTestUtils.loadExtension({
+      background,
+      manifest: {
+        permissions: ["nativeMessaging"],
+      },
+    }, ID);
+
+    yield extension.startup();
+    yield extension.awaitMessage("finished");
+    yield extension.unload();
+
+    yield waitForSubprocessExit();
+  });
+
+  let lines = STDERR_LINES.map(line => messages.findIndex(msg => msg.message.includes(line)));
+  notEqual(lines[0], -1, "Saw first line of stderr output on the console");
+  notEqual(lines[1], -1, "Saw second line of stderr output on the console");
+  notEqual(lines[0], lines[1], "Stderr output lines are separated in the console");
+});