summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/subprocess/test/xpcshell/test_subprocess.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/subprocess/test/xpcshell/test_subprocess.js')
-rw-r--r--toolkit/modules/subprocess/test/xpcshell/test_subprocess.js769
1 files changed, 769 insertions, 0 deletions
diff --git a/toolkit/modules/subprocess/test/xpcshell/test_subprocess.js b/toolkit/modules/subprocess/test/xpcshell/test_subprocess.js
new file mode 100644
index 000000000..1b8e02820
--- /dev/null
+++ b/toolkit/modules/subprocess/test/xpcshell/test_subprocess.js
@@ -0,0 +1,769 @@
+"use strict";
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+
+
+const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+
+const MAX_ROUND_TRIP_TIME_MS = AppConstants.DEBUG || AppConstants.ASAN ? 18 : 9;
+const MAX_RETRIES = 5;
+
+let PYTHON;
+let PYTHON_BIN;
+let PYTHON_DIR;
+
+const TEST_SCRIPT = do_get_file("data_test_script.py").path;
+
+let read = pipe => {
+ return pipe.readUint32().then(count => {
+ return pipe.readString(count);
+ });
+};
+
+
+let readAll = Task.async(function* (pipe) {
+ let result = [];
+ let string;
+ while ((string = yield pipe.readString())) {
+ result.push(string);
+ }
+
+ return result.join("");
+});
+
+
+add_task(function* setup() {
+ PYTHON = yield Subprocess.pathSearch(env.get("PYTHON"));
+
+ PYTHON_BIN = OS.Path.basename(PYTHON);
+ PYTHON_DIR = OS.Path.dirname(PYTHON);
+});
+
+
+add_task(function* test_subprocess_io() {
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "echo"],
+ });
+
+ Assert.throws(() => { proc.stdout.read(-1); },
+ /non-negative integer/);
+ Assert.throws(() => { proc.stdout.read(1.1); },
+ /non-negative integer/);
+
+ Assert.throws(() => { proc.stdout.read(Infinity); },
+ /non-negative integer/);
+ Assert.throws(() => { proc.stdout.read(NaN); },
+ /non-negative integer/);
+
+ Assert.throws(() => { proc.stdout.readString(-1); },
+ /non-negative integer/);
+ Assert.throws(() => { proc.stdout.readString(1.1); },
+ /non-negative integer/);
+
+ Assert.throws(() => { proc.stdout.readJSON(-1); },
+ /positive integer/);
+ Assert.throws(() => { proc.stdout.readJSON(0); },
+ /positive integer/);
+ Assert.throws(() => { proc.stdout.readJSON(1.1); },
+ /positive integer/);
+
+
+ const LINE1 = "I'm a leaf on the wind.\n";
+ const LINE2 = "Watch how I soar.\n";
+
+
+ let outputPromise = read(proc.stdout);
+
+ yield new Promise(resolve => setTimeout(resolve, 100));
+
+ let [output] = yield Promise.all([
+ outputPromise,
+ proc.stdin.write(LINE1),
+ ]);
+
+ equal(output, LINE1, "Got expected output");
+
+
+ // Make sure it succeeds whether the write comes before or after the
+ // read.
+ let inputPromise = proc.stdin.write(LINE2);
+
+ yield new Promise(resolve => setTimeout(resolve, 100));
+
+ [output] = yield Promise.all([
+ read(proc.stdout),
+ inputPromise,
+ ]);
+
+ equal(output, LINE2, "Got expected output");
+
+
+ let JSON_BLOB = {foo: {bar: "baz"}};
+
+ inputPromise = proc.stdin.write(JSON.stringify(JSON_BLOB) + "\n");
+
+ output = yield proc.stdout.readUint32().then(count => {
+ return proc.stdout.readJSON(count);
+ });
+
+ Assert.deepEqual(output, JSON_BLOB, "Got expected JSON output");
+
+
+ yield proc.stdin.close();
+
+ let {exitCode} = yield proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+
+add_task(function* test_subprocess_large_io() {
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "echo"],
+ });
+
+ const LINE = "I'm a leaf on the wind.\n";
+ const BUFFER_SIZE = 4096;
+
+ // Create a message that's ~3/4 the input buffer size.
+ let msg = Array(BUFFER_SIZE * .75 / 16 | 0).fill("0123456789abcdef").join("") + "\n";
+
+ // This sequence of writes and reads crosses several buffer size
+ // boundaries, and causes some branches of the read buffer code to be
+ // exercised which are not exercised by other tests.
+ proc.stdin.write(msg);
+ proc.stdin.write(msg);
+ proc.stdin.write(LINE);
+
+ let output = yield read(proc.stdout);
+ equal(output, msg, "Got the expected output");
+
+ output = yield read(proc.stdout);
+ equal(output, msg, "Got the expected output");
+
+ output = yield read(proc.stdout);
+ equal(output, LINE, "Got the expected output");
+
+ proc.stdin.close();
+
+
+ let {exitCode} = yield proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+
+add_task(function* test_subprocess_huge() {
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "echo"],
+ });
+
+ // This should be large enough to fill most pipe input/output buffers.
+ const MESSAGE_SIZE = 1024 * 16;
+
+ let msg = Array(MESSAGE_SIZE).fill("0123456789abcdef").join("") + "\n";
+
+ proc.stdin.write(msg);
+
+ let output = yield read(proc.stdout);
+ equal(output, msg, "Got the expected output");
+
+ proc.stdin.close();
+
+
+ let {exitCode} = yield proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+
+add_task(function* test_subprocess_round_trip_perf() {
+ let roundTripTime = Infinity;
+ for (let i = 0; i < MAX_RETRIES && roundTripTime > MAX_ROUND_TRIP_TIME_MS; i++) {
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "echo"],
+ });
+
+
+ const LINE = "I'm a leaf on the wind.\n";
+
+ let now = Date.now();
+ const COUNT = 1000;
+ for (let j = 0; j < COUNT; j++) {
+ let [output] = yield Promise.all([
+ read(proc.stdout),
+ proc.stdin.write(LINE),
+ ]);
+
+ // We don't want to log this for every iteration, but we still need
+ // to fail if it goes wrong.
+ if (output !== LINE) {
+ equal(output, LINE, "Got expected output");
+ }
+ }
+
+ roundTripTime = (Date.now() - now) / COUNT;
+
+ yield proc.stdin.close();
+
+ let {exitCode} = yield proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+ }
+
+ ok(roundTripTime <= MAX_ROUND_TRIP_TIME_MS,
+ `Expected round trip time (${roundTripTime}ms) to be less than ${MAX_ROUND_TRIP_TIME_MS}ms`);
+});
+
+
+add_task(function* test_subprocess_stderr_default() {
+ const LINE1 = "I'm a leaf on the wind.\n";
+ const LINE2 = "Watch how I soar.\n";
+
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "print", LINE1, LINE2],
+ });
+
+ equal(proc.stderr, undefined, "There should be no stderr pipe by default");
+
+ let stdout = yield readAll(proc.stdout);
+
+ equal(stdout, LINE1, "Got the expected stdout output");
+
+
+ let {exitCode} = yield proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+
+add_task(function* test_subprocess_stderr_pipe() {
+ const LINE1 = "I'm a leaf on the wind.\n";
+ const LINE2 = "Watch how I soar.\n";
+
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "print", LINE1, LINE2],
+ stderr: "pipe",
+ });
+
+ let [stdout, stderr] = yield Promise.all([
+ readAll(proc.stdout),
+ readAll(proc.stderr),
+ ]);
+
+ equal(stdout, LINE1, "Got the expected stdout output");
+ equal(stderr, LINE2, "Got the expected stderr output");
+
+
+ let {exitCode} = yield proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+
+add_task(function* test_subprocess_stderr_merged() {
+ const LINE1 = "I'm a leaf on the wind.\n";
+ const LINE2 = "Watch how I soar.\n";
+
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "print", LINE1, LINE2],
+ stderr: "stdout",
+ });
+
+ equal(proc.stderr, undefined, "There should be no stderr pipe by default");
+
+ let stdout = yield readAll(proc.stdout);
+
+ equal(stdout, LINE1 + LINE2, "Got the expected merged stdout output");
+
+
+ let {exitCode} = yield proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+
+add_task(function* test_subprocess_read_after_exit() {
+ const LINE1 = "I'm a leaf on the wind.\n";
+ const LINE2 = "Watch how I soar.\n";
+
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "print", LINE1, LINE2],
+ stderr: "pipe",
+ });
+
+
+ let {exitCode} = yield proc.wait();
+ equal(exitCode, 0, "Process exited with expected code");
+
+
+ let [stdout, stderr] = yield Promise.all([
+ readAll(proc.stdout),
+ readAll(proc.stderr),
+ ]);
+
+ equal(stdout, LINE1, "Got the expected stdout output");
+ equal(stderr, LINE2, "Got the expected stderr output");
+});
+
+
+add_task(function* test_subprocess_lazy_close_output() {
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "echo"],
+ });
+
+ const LINE1 = "I'm a leaf on the wind.\n";
+ const LINE2 = "Watch how I soar.\n";
+
+ let writePromises = [
+ proc.stdin.write(LINE1),
+ proc.stdin.write(LINE2),
+ ];
+ let closedPromise = proc.stdin.close();
+
+
+ let output1 = yield read(proc.stdout);
+ let output2 = yield read(proc.stdout);
+
+ yield Promise.all([...writePromises, closedPromise]);
+
+ equal(output1, LINE1, "Got expected output");
+ equal(output2, LINE2, "Got expected output");
+
+
+ let {exitCode} = yield proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+
+add_task(function* test_subprocess_lazy_close_input() {
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "echo"],
+ });
+
+ let readPromise = proc.stdout.readUint32();
+ let closedPromise = proc.stdout.close();
+
+
+ const LINE = "I'm a leaf on the wind.\n";
+
+ proc.stdin.write(LINE);
+ proc.stdin.close();
+
+ let len = yield readPromise;
+ equal(len, LINE.length);
+
+ yield closedPromise;
+
+
+ // Don't test for a successful exit here. The process may exit with a
+ // write error if we close the pipe after it's written the message
+ // size but before it's written the message.
+ yield proc.wait();
+});
+
+
+add_task(function* test_subprocess_force_close() {
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "echo"],
+ });
+
+ let readPromise = proc.stdout.readUint32();
+ let closedPromise = proc.stdout.close(true);
+
+ yield Assert.rejects(
+ readPromise,
+ function(e) {
+ equal(e.errorCode, Subprocess.ERROR_END_OF_FILE,
+ "Got the expected error code");
+ return /File closed/.test(e.message);
+ },
+ "Promise should be rejected when file is closed");
+
+ yield closedPromise;
+ yield proc.stdin.close();
+
+
+ let {exitCode} = yield proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+
+add_task(function* test_subprocess_eof() {
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "echo"],
+ });
+
+ let readPromise = proc.stdout.readUint32();
+
+ yield proc.stdin.close();
+
+ yield Assert.rejects(
+ readPromise,
+ function(e) {
+ equal(e.errorCode, Subprocess.ERROR_END_OF_FILE,
+ "Got the expected error code");
+ return /File closed/.test(e.message);
+ },
+ "Promise should be rejected on EOF");
+
+ let {exitCode} = yield proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+
+add_task(function* test_subprocess_invalid_json() {
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "echo"],
+ });
+
+ const LINE = "I'm a leaf on the wind.\n";
+
+ proc.stdin.write(LINE);
+ proc.stdin.close();
+
+ let count = yield proc.stdout.readUint32();
+ let readPromise = proc.stdout.readJSON(count);
+
+ yield Assert.rejects(
+ readPromise,
+ function(e) {
+ equal(e.errorCode, Subprocess.ERROR_INVALID_JSON,
+ "Got the expected error code");
+ return /SyntaxError/.test(e);
+ },
+ "Promise should be rejected on EOF");
+
+ let {exitCode} = yield proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+
+if (AppConstants.isPlatformAndVersionAtLeast("win", "6")) {
+ add_task(function* test_subprocess_inherited_descriptors() {
+ let {ctypes, libc, win32} = Cu.import("resource://gre/modules/subprocess/subprocess_win.jsm");
+
+ let secAttr = new win32.SECURITY_ATTRIBUTES();
+ secAttr.nLength = win32.SECURITY_ATTRIBUTES.size;
+ secAttr.bInheritHandle = true;
+
+ let handles = win32.createPipe(secAttr, 0);
+
+
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "echo"],
+ });
+
+
+ // Close the output end of the pipe.
+ // Ours should be the only copy, so reads should fail after this.
+ handles[1].dispose();
+
+ let buffer = new ArrayBuffer(1);
+ let succeeded = libc.ReadFile(handles[0], buffer, buffer.byteLength,
+ null, null);
+
+ ok(!succeeded, "ReadFile should fail on broken pipe");
+ equal(ctypes.winLastError, win32.ERROR_BROKEN_PIPE, "Read should fail with ERROR_BROKEN_PIPE");
+
+
+ proc.stdin.close();
+
+ let {exitCode} = yield proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+ });
+}
+
+
+add_task(function* test_subprocess_wait() {
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "exit", "42"],
+ });
+
+ let {exitCode} = yield proc.wait();
+
+ equal(exitCode, 42, "Got expected exit code");
+});
+
+
+add_task(function* test_subprocess_pathSearch() {
+ let promise = Subprocess.call({
+ command: PYTHON_BIN,
+ arguments: ["-u", TEST_SCRIPT, "exit", "13"],
+ environment: {
+ PATH: PYTHON_DIR,
+ },
+ });
+
+ yield Assert.rejects(
+ promise,
+ function(error) {
+ return error.errorCode == Subprocess.ERROR_BAD_EXECUTABLE;
+ },
+ "Subprocess.call should fail for a bad executable");
+});
+
+
+add_task(function* test_subprocess_workdir() {
+ let procDir = yield OS.File.getCurrentDirectory();
+ let tmpDirFile = Components.classes["@mozilla.org/file/local;1"]
+ .createInstance(Components.interfaces.nsILocalFile);
+ tmpDirFile.initWithPath(OS.Constants.Path.tmpDir);
+ tmpDirFile.normalize();
+ let tmpDir = tmpDirFile.path;
+
+ notEqual(procDir, tmpDir,
+ "Current process directory must not be the current temp directory");
+
+ function* pwd(options) {
+ let proc = yield Subprocess.call(Object.assign({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "pwd"],
+ }, options));
+
+ let pwdOutput = read(proc.stdout);
+
+ let {exitCode} = yield proc.wait();
+ equal(exitCode, 0, "Got expected exit code");
+
+ return pwdOutput;
+ }
+
+ let dir = yield pwd({});
+ equal(dir, procDir, "Process should normally launch in current process directory");
+
+ dir = yield pwd({workdir: tmpDir});
+ equal(dir, tmpDir, "Process should launch in the directory specified in `workdir`");
+
+ dir = yield OS.File.getCurrentDirectory();
+ equal(dir, procDir, "`workdir` should not change the working directory of the current process");
+});
+
+
+add_task(function* test_subprocess_term() {
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "echo"],
+ });
+
+ // Windows does not support killing processes gracefully, so they will
+ // always exit with -9 there.
+ let retVal = AppConstants.platform == "win" ? -9 : -15;
+
+ // Kill gracefully with the default timeout of 300ms.
+ let {exitCode} = yield proc.kill();
+
+ equal(exitCode, retVal, "Got expected exit code");
+
+ ({exitCode} = yield proc.wait());
+
+ equal(exitCode, retVal, "Got expected exit code");
+});
+
+
+add_task(function* test_subprocess_kill() {
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "echo"],
+ });
+
+ // Force kill with no gracefull termination timeout.
+ let {exitCode} = yield proc.kill(0);
+
+ equal(exitCode, -9, "Got expected exit code");
+
+ ({exitCode} = yield proc.wait());
+
+ equal(exitCode, -9, "Got expected exit code");
+});
+
+
+add_task(function* test_subprocess_kill_timeout() {
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "ignore_sigterm"],
+ });
+
+ // Wait for the process to set up its signal handler and tell us it's
+ // ready.
+ let msg = yield read(proc.stdout);
+ equal(msg, "Ready", "Process is ready");
+
+ // Kill gracefully with the default timeout of 300ms.
+ // Expect a force kill after 300ms, since the process traps SIGTERM.
+ const TIMEOUT = 300;
+ let startTime = Date.now();
+
+ let {exitCode} = yield proc.kill(TIMEOUT);
+
+ // Graceful termination is not supported on Windows, so don't bother
+ // testing the timeout there.
+ if (AppConstants.platform != "win") {
+ let diff = Date.now() - startTime;
+ ok(diff >= TIMEOUT, `Process was killed after ${diff}ms (expected ~${TIMEOUT}ms)`);
+ }
+
+ equal(exitCode, -9, "Got expected exit code");
+
+ ({exitCode} = yield proc.wait());
+
+ equal(exitCode, -9, "Got expected exit code");
+});
+
+
+add_task(function* test_subprocess_arguments() {
+ let args = [
+ String.raw`C:\Program Files\Company\Program.exe`,
+ String.raw`\\NETWORK SHARE\Foo Directory${"\\"}`,
+ String.raw`foo bar baz`,
+ String.raw`"foo bar baz"`,
+ String.raw`foo " bar`,
+ String.raw`Thing \" with "" "\" \\\" \\\\" quotes\\" \\`,
+ ];
+
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "print_args", ...args],
+ });
+
+ for (let [i, arg] of args.entries()) {
+ let val = yield read(proc.stdout);
+ equal(val, arg, `Got correct value for args[${i}]`);
+ }
+
+ let {exitCode} = yield proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+
+// Windows XP can't handle launching Python with a partial environment.
+if (!AppConstants.isPlatformAndVersionAtMost("win", "5.2")) {
+ add_task(function* test_subprocess_environment() {
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "env", "PATH", "FOO"],
+ environment: {
+ FOO: "BAR",
+ },
+ });
+
+ let path = yield read(proc.stdout);
+ let foo = yield read(proc.stdout);
+
+ equal(path, "", "Got expected $PATH value");
+ equal(foo, "BAR", "Got expected $FOO value");
+
+ let {exitCode} = yield proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+ });
+}
+
+
+add_task(function* test_subprocess_environmentAppend() {
+ let proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "env", "PATH", "FOO"],
+ environmentAppend: true,
+ environment: {
+ FOO: "BAR",
+ },
+ });
+
+ let path = yield read(proc.stdout);
+ let foo = yield read(proc.stdout);
+
+ equal(path, env.get("PATH"), "Got expected $PATH value");
+ equal(foo, "BAR", "Got expected $FOO value");
+
+ let {exitCode} = yield proc.wait();
+
+ equal(exitCode, 0, "Got expected exit code");
+
+ proc = yield Subprocess.call({
+ command: PYTHON,
+ arguments: ["-u", TEST_SCRIPT, "env", "PATH", "FOO"],
+ environmentAppend: true,
+ });
+
+ path = yield read(proc.stdout);
+ foo = yield read(proc.stdout);
+
+ equal(path, env.get("PATH"), "Got expected $PATH value");
+ equal(foo, "", "Got expected $FOO value");
+
+ ({exitCode} = yield proc.wait());
+
+ equal(exitCode, 0, "Got expected exit code");
+});
+
+
+add_task(function* test_bad_executable() {
+ // Test with a non-executable file.
+
+ let textFile = do_get_file("data_text_file.txt").path;
+
+ let promise = Subprocess.call({
+ command: textFile,
+ arguments: [],
+ });
+
+ yield Assert.rejects(
+ promise,
+ function(error) {
+ if (AppConstants.platform == "win") {
+ return /Failed to create process/.test(error.message);
+ }
+ return error.errorCode == Subprocess.ERROR_BAD_EXECUTABLE;
+ },
+ "Subprocess.call should fail for a bad executable");
+
+ // Test with a nonexistent file.
+ promise = Subprocess.call({
+ command: textFile + ".doesNotExist",
+ arguments: [],
+ });
+
+ yield Assert.rejects(
+ promise,
+ function(error) {
+ return error.errorCode == Subprocess.ERROR_BAD_EXECUTABLE;
+ },
+ "Subprocess.call should fail for a bad executable");
+});
+
+
+add_task(function* test_cleanup() {
+ let {SubprocessImpl} = Cu.import("resource://gre/modules/Subprocess.jsm");
+
+ let worker = SubprocessImpl.Process.getWorker();
+
+ let openFiles = yield worker.call("getOpenFiles", []);
+ let processes = yield worker.call("getProcesses", []);
+
+ equal(openFiles.size, 0, "No remaining open files");
+ equal(processes.size, 0, "No remaining processes");
+});