diff options
Diffstat (limited to '')
7 files changed, 1058 insertions, 0 deletions
diff --git a/toolkit/modules/subprocess/test/xpcshell/data_test_script.py b/toolkit/modules/subprocess/test/xpcshell/data_test_script.py new file mode 100644 index 0000000000..e1f5f5de93 --- /dev/null +++ b/toolkit/modules/subprocess/test/xpcshell/data_test_script.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +import os +import signal +import struct +import sys + + +def output(line, stream=sys.stdout, print_only=False): + if isinstance(line, str): + line = line.encode("utf-8", "surrogateescape") + if not print_only: + stream.buffer.write(struct.pack("@I", len(line))) + stream.buffer.write(line) + stream.flush() + + +def echo_loop(): + while True: + line = sys.stdin.buffer.readline() + if not line: + break + + output(line) + + +if sys.platform == "win32": + import msvcrt + + msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY) + + +cmd = sys.argv[1] +if cmd == "echo": + echo_loop() +elif cmd == "exit": + sys.exit(int(sys.argv[2])) +elif cmd == "env": + for var in sys.argv[2:]: + output(os.environ.get(var, "!")) +elif cmd == "pwd": + output(os.path.abspath(os.curdir)) +elif cmd == "print_args": + for arg in sys.argv[2:]: + output(arg) +elif cmd == "ignore_sigterm": + signal.signal(signal.SIGTERM, signal.SIG_IGN) + + output("Ready") + while True: + try: + signal.pause() + except AttributeError: + import time + + time.sleep(3600) +elif cmd == "print": + output(sys.argv[2], stream=sys.stdout, print_only=True) + output(sys.argv[3], stream=sys.stderr, print_only=True) diff --git a/toolkit/modules/subprocess/test/xpcshell/data_text_file.txt b/toolkit/modules/subprocess/test/xpcshell/data_text_file.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/toolkit/modules/subprocess/test/xpcshell/data_text_file.txt diff --git a/toolkit/modules/subprocess/test/xpcshell/head.js b/toolkit/modules/subprocess/test/xpcshell/head.js new file mode 100644 index 0000000000..a2b85047d3 --- /dev/null +++ b/toolkit/modules/subprocess/test/xpcshell/head.js @@ -0,0 +1,13 @@ +"use strict"; + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +// eslint-disable-next-line no-unused-vars +ChromeUtils.defineESModuleGetters(this, { + Subprocess: "resource://gre/modules/Subprocess.sys.mjs", +}); 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 0000000000..77d09f41ae --- /dev/null +++ b/toolkit/modules/subprocess/test/xpcshell/test_subprocess.js @@ -0,0 +1,868 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +"use strict"; + +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +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 = async function (pipe) { + let result = []; + let string; + while ((string = await pipe.readString())) { + result.push(string); + } + + return result.join(""); +}; + +add_task(async function setup() { + PYTHON = await Subprocess.pathSearch(Services.env.get("PYTHON")); + + PYTHON_BIN = PathUtils.filename(PYTHON); + PYTHON_DIR = PathUtils.parent(PYTHON); +}); + +add_task(async function test_subprocess_io() { + let proc = await 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); + + await new Promise(resolve => setTimeout(resolve, 100)); + + let [output] = await 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); + + await new Promise(resolve => setTimeout(resolve, 100)); + + [output] = await 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 = await proc.stdout.readUint32().then(count => { + return proc.stdout.readJSON(count); + }); + + Assert.deepEqual(output, JSON_BLOB, "Got expected JSON output"); + + await proc.stdin.close(); + + let { exitCode } = await proc.wait(); + + equal(exitCode, 0, "Got expected exit code"); +}); + +add_task(async function test_subprocess_large_io() { + let proc = await 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 * 0.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 = await read(proc.stdout); + equal(output, msg, "Got the expected output"); + + output = await read(proc.stdout); + equal(output, msg, "Got the expected output"); + + output = await read(proc.stdout); + equal(output, LINE, "Got the expected output"); + + proc.stdin.close(); + + let { exitCode } = await proc.wait(); + + equal(exitCode, 0, "Got expected exit code"); +}); + +add_task(async function test_subprocess_huge() { + let proc = await 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 = await read(proc.stdout); + equal(output, msg, "Got the expected output"); + + proc.stdin.close(); + + let { exitCode } = await proc.wait(); + + equal(exitCode, 0, "Got expected exit code"); +}); + +add_task( + { skip_if: () => mozinfo.ccov }, + async 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 = await 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] = await 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; + + await proc.stdin.close(); + + let { exitCode } = await 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(async function test_subprocess_stderr_default() { + const LINE1 = "I'm a leaf on the wind.\n"; + const LINE2 = "Watch how I soar.\n"; + + let proc = await 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 = await readAll(proc.stdout); + + equal(stdout, LINE1, "Got the expected stdout output"); + + let { exitCode } = await proc.wait(); + + equal(exitCode, 0, "Got expected exit code"); +}); + +add_task(async function test_subprocess_stderr_pipe() { + const LINE1 = "I'm a leaf on the wind.\n"; + const LINE2 = "Watch how I soar.\n"; + + let proc = await Subprocess.call({ + command: PYTHON, + arguments: ["-u", TEST_SCRIPT, "print", LINE1, LINE2], + stderr: "pipe", + }); + + let [stdout, stderr] = await 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 } = await proc.wait(); + + equal(exitCode, 0, "Got expected exit code"); +}); + +add_task(async function test_subprocess_stderr_merged() { + const LINE1 = "I'm a leaf on the wind.\n"; + const LINE2 = "Watch how I soar.\n"; + + let proc = await 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 = await readAll(proc.stdout); + + equal(stdout, LINE1 + LINE2, "Got the expected merged stdout output"); + + let { exitCode } = await proc.wait(); + + equal(exitCode, 0, "Got expected exit code"); +}); + +add_task(async 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 = await Subprocess.call({ + command: PYTHON, + arguments: ["-u", TEST_SCRIPT, "print", LINE1, LINE2], + stderr: "pipe", + }); + + let { exitCode } = await proc.wait(); + equal(exitCode, 0, "Process exited with expected code"); + + let [stdout, stderr] = await 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(async function test_subprocess_lazy_close_output() { + let proc = await 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 = await read(proc.stdout); + let output2 = await read(proc.stdout); + + await Promise.all([...writePromises, closedPromise]); + + equal(output1, LINE1, "Got expected output"); + equal(output2, LINE2, "Got expected output"); + + let { exitCode } = await proc.wait(); + + equal(exitCode, 0, "Got expected exit code"); +}); + +add_task(async function test_subprocess_lazy_close_input() { + let proc = await 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 = await readPromise; + equal(len, LINE.length); + + await 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. + await proc.wait(); +}); + +add_task(async function test_subprocess_force_close() { + let proc = await Subprocess.call({ + command: PYTHON, + arguments: ["-u", TEST_SCRIPT, "echo"], + }); + + let readPromise = proc.stdout.readUint32(); + let closedPromise = proc.stdout.close(true); + + await 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" + ); + + await closedPromise; + await proc.stdin.close(); + + let { exitCode } = await proc.wait(); + + equal(exitCode, 0, "Got expected exit code"); +}); + +add_task(async function test_subprocess_eof() { + let proc = await Subprocess.call({ + command: PYTHON, + arguments: ["-u", TEST_SCRIPT, "echo"], + }); + + let readPromise = proc.stdout.readUint32(); + + await proc.stdin.close(); + + await 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 } = await proc.wait(); + + equal(exitCode, 0, "Got expected exit code"); +}); + +add_task(async function test_subprocess_invalid_json() { + let proc = await 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 = await proc.stdout.readUint32(); + let readPromise = proc.stdout.readJSON(count); + + await 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 } = await proc.wait(); + + equal(exitCode, 0, "Got expected exit code"); +}); + +if (AppConstants.isPlatformAndVersionAtLeast("win", "6")) { + add_task(async function test_subprocess_inherited_descriptors() { + let { libc, win32 } = ChromeUtils.importESModule( + "resource://gre/modules/subprocess/subprocess_win.sys.mjs" + ); + const { ctypes } = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" + ); + + let secAttr = new win32.SECURITY_ATTRIBUTES(); + secAttr.nLength = win32.SECURITY_ATTRIBUTES.size; + secAttr.bInheritHandle = true; + + let handles = win32.createPipe(secAttr, 0); + + let proc = await 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 } = await proc.wait(); + + equal(exitCode, 0, "Got expected exit code"); + }); +} + +add_task(async function test_subprocess_wait() { + let proc = await Subprocess.call({ + command: PYTHON, + arguments: ["-u", TEST_SCRIPT, "exit", "42"], + }); + + let { exitCode } = await proc.wait(); + + equal(exitCode, 42, "Got expected exit code"); +}); + +add_task(async function test_subprocess_pathSearch() { + let promise = Subprocess.call({ + command: PYTHON_BIN, + arguments: ["-u", TEST_SCRIPT, "exit", "13"], + environment: { + PATH: PYTHON_DIR, + }, + }); + + await Assert.rejects( + promise, + function (error) { + return error.errorCode == Subprocess.ERROR_BAD_EXECUTABLE; + }, + "Subprocess.call should fail for a bad executable" + ); +}); + +add_task(async function test_subprocess_workdir() { + let procDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile).path; + let tmpDir = PathUtils.normalize(PathUtils.tempDir); + + notEqual( + procDir, + tmpDir, + "Current process directory must not be the current temp directory" + ); + + async function pwd(options) { + let proc = await Subprocess.call( + Object.assign( + { + command: PYTHON, + arguments: ["-u", TEST_SCRIPT, "pwd"], + }, + options + ) + ); + + let pwdOutput = read(proc.stdout); + + let { exitCode } = await proc.wait(); + equal(exitCode, 0, "Got expected exit code"); + + return pwdOutput; + } + + let dir = await pwd({}); + equal( + dir, + procDir, + "Process should normally launch in current process directory" + ); + + dir = await pwd({ workdir: tmpDir }); + equal( + dir, + tmpDir, + "Process should launch in the directory specified in `workdir`" + ); +}); + +add_task(async function test_subprocess_term() { + let proc = await 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 } = await proc.kill(); + + equal(exitCode, retVal, "Got expected exit code"); + + ({ exitCode } = await proc.wait()); + + equal(exitCode, retVal, "Got expected exit code"); +}); + +add_task(async function test_subprocess_kill() { + let proc = await Subprocess.call({ + command: PYTHON, + arguments: ["-u", TEST_SCRIPT, "echo"], + }); + + // Force kill with no gracefull termination timeout. + let { exitCode } = await proc.kill(0); + + equal(exitCode, -9, "Got expected exit code"); + + ({ exitCode } = await proc.wait()); + + equal(exitCode, -9, "Got expected exit code"); +}); + +add_task(async function test_subprocess_kill_timeout() { + let proc = await 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 = await 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 } = await 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 } = await proc.wait()); + + equal(exitCode, -9, "Got expected exit code"); +}); + +add_task(async 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 = await Subprocess.call({ + command: PYTHON, + arguments: ["-u", TEST_SCRIPT, "print_args", ...args], + }); + + for (let [i, arg] of args.entries()) { + let val = await read(proc.stdout); + equal(val, arg, `Got correct value for args[${i}]`); + } + + let { exitCode } = await proc.wait(); + + equal(exitCode, 0, "Got expected exit code"); +}); + +add_task(async function test_subprocess_environment() { + let environment = { + FOO: "BAR", + EMPTY: "", + IGNORED: null, + }; + + // Our Windows environment can't handle launching python without + // PATH variables. + if (AppConstants.platform == "win") { + Object.assign(environment, { + PATH: Services.env.get("PATH"), + PATHEXT: Services.env.get("PATHEXT"), + SYSTEMROOT: Services.env.get("SYSTEMROOT"), + }); + } + + Services.env.set("BAR", "BAZ"); + + let proc = await Subprocess.call({ + command: PYTHON, + arguments: ["-u", TEST_SCRIPT, "env", "FOO", "BAR", "EMPTY", "IGNORED"], + environment, + }); + + let foo = await read(proc.stdout); + let bar = await read(proc.stdout); + let empty = await read(proc.stdout); + let ignored = await read(proc.stdout); + + equal(foo, "BAR", "Got expected $FOO value"); + equal(bar, "!", "Got expected $BAR value"); + equal(empty, "", "Got expected $EMPTY value"); + equal(ignored, "!", "Got expected $IGNORED value"); + + let { exitCode } = await proc.wait(); + + equal(exitCode, 0, "Got expected exit code"); +}); + +add_task(async function test_subprocess_environmentAppend() { + Services.env.set("VALUE_FROM_BASE_ENV", "untouched"); + Services.env.set("VALUE_FROM_BASE_ENV_EMPTY", "untouched"); + Services.env.set("VALUE_FROM_BASE_ENV_REMOVED", "untouched"); + + let proc = await Subprocess.call({ + command: PYTHON, + arguments: [ + "-u", + TEST_SCRIPT, + "env", + "VALUE_FROM_BASE_ENV", + "VALUE_FROM_BASE_ENV_EMPTY", + "VALUE_FROM_BASE_ENV_REMOVED", + "VALUE_APPENDED_ONCE", + ], + environmentAppend: true, + environment: { + VALUE_FROM_BASE_ENV_EMPTY: "", + VALUE_FROM_BASE_ENV_REMOVED: null, + VALUE_APPENDED_ONCE: "soon empty", + }, + }); + + let valueFromBaseEnv = await read(proc.stdout); + let valueFromBaseEnvEmpty = await read(proc.stdout); + let valueFromBaseEnvRemoved = await read(proc.stdout); + let valueAppendedOnce = await read(proc.stdout); + + equal( + valueFromBaseEnv, + "untouched", + "Got expected $VALUE_FROM_BASE_ENV value" + ); + equal( + valueFromBaseEnvEmpty, + "", + "Got expected $VALUE_FROM_BASE_ENV_EMPTY value" + ); + equal( + valueFromBaseEnvRemoved, + "!", + "Got expected $VALUE_FROM_BASE_ENV_REMOVED value" + ); + equal( + valueAppendedOnce, + "soon empty", + "Got expected $VALUE_APPENDED_ONCE value" + ); + + let { exitCode } = await proc.wait(); + + equal(exitCode, 0, "Got expected exit code"); + + proc = await Subprocess.call({ + command: PYTHON, + arguments: [ + "-u", + TEST_SCRIPT, + "env", + "VALUE_FROM_BASE_ENV", + "VALUE_APPENDED_ONCE", + ], + environmentAppend: true, + }); + + valueFromBaseEnv = await read(proc.stdout); + valueAppendedOnce = await read(proc.stdout); + + equal( + valueFromBaseEnv, + "untouched", + "Got expected $VALUE_FROM_BASE_ENV value" + ); + equal(valueAppendedOnce, "!", "Got expected $VALUE_APPENDED_ONCE value"); + + ({ exitCode } = await proc.wait()); + + equal(exitCode, 0, "Got expected exit code"); +}); + +if (AppConstants.platform !== "win") { + add_task(async function test_subprocess_nonASCII() { + const { libc } = ChromeUtils.importESModule( + "resource://gre/modules/subprocess/subprocess_unix.sys.mjs" + ); + + // Use TextDecoder rather than a string with a \xff escape, since + // the latter will automatically be normalized to valid UTF-8. + let val = new TextDecoder().decode(Uint8Array.of(1, 255)); + + libc.setenv( + "FOO", + Uint8Array.from(val + "\0", c => c.charCodeAt(0)), + 1 + ); + + let proc = await Subprocess.call({ + command: PYTHON, + arguments: ["-u", TEST_SCRIPT, "env", "FOO"], + }); + + let foo = await read(proc.stdout); + + equal(foo, val, "Got expected $FOO value"); + + Services.env.set("FOO", ""); + + let { exitCode } = await proc.wait(); + + equal(exitCode, 0, "Got expected exit code"); + }); +} + +add_task(async 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: [], + }); + + await 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: [], + }); + + await Assert.rejects( + promise, + function (error) { + return error.errorCode == Subprocess.ERROR_BAD_EXECUTABLE; + }, + "Subprocess.call should fail for a bad executable" + ); +}); + +add_task(async function test_cleanup() { + let { getSubprocessImplForTest } = ChromeUtils.importESModule( + "resource://gre/modules/Subprocess.sys.mjs" + ); + + let worker = getSubprocessImplForTest().Process.getWorker(); + + let openFiles = await worker.call("getOpenFiles", []); + let processes = await worker.call("getProcesses", []); + + equal(openFiles.size, 0, "No remaining open files"); + equal(processes.size, 0, "No remaining processes"); +}); diff --git a/toolkit/modules/subprocess/test/xpcshell/test_subprocess_getEnvironment.js b/toolkit/modules/subprocess/test/xpcshell/test_subprocess_getEnvironment.js new file mode 100644 index 0000000000..cb4ea7247d --- /dev/null +++ b/toolkit/modules/subprocess/test/xpcshell/test_subprocess_getEnvironment.js @@ -0,0 +1,15 @@ +"use strict"; + +add_task(async function test_getEnvironment() { + Services.env.set("FOO", "BAR"); + + let environment = Subprocess.getEnvironment(); + + equal(environment.FOO, "BAR"); + equal(environment.PATH, Services.env.get("PATH")); + + Services.env.set("FOO", null); + + environment = Subprocess.getEnvironment(); + equal(environment.FOO || "", ""); +}); diff --git a/toolkit/modules/subprocess/test/xpcshell/test_subprocess_pathSearch.js b/toolkit/modules/subprocess/test/xpcshell/test_subprocess_pathSearch.js new file mode 100644 index 0000000000..9d9374f536 --- /dev/null +++ b/toolkit/modules/subprocess/test/xpcshell/test_subprocess_pathSearch.js @@ -0,0 +1,87 @@ +"use strict"; + +const PYTHON = Services.env.get("PYTHON"); + +const PYTHON_BIN = PathUtils.filename(PYTHON); +const PYTHON_DIR = PathUtils.parent(PYTHON); + +const DOES_NOT_EXIST = PathUtils.join( + PathUtils.tempDir, + "ThisPathDoesNotExist" +); + +const PATH_SEP = AppConstants.platform == "win" ? ";" : ":"; + +add_task(async function test_pathSearchAbsolute() { + let env = {}; + + let path = await Subprocess.pathSearch(PYTHON, env); + equal(path, PYTHON, "Full path resolves even with no PATH."); + + env.PATH = ""; + path = await Subprocess.pathSearch(PYTHON, env); + equal(path, PYTHON, "Full path resolves even with empty PATH."); + + await Assert.rejects( + Subprocess.pathSearch(DOES_NOT_EXIST, env), + function (e) { + equal( + e.errorCode, + Subprocess.ERROR_BAD_EXECUTABLE, + "Got the expected error code" + ); + return /File at path .* does not exist, or is not (executable|a normal file)/.test( + e.message + ); + }, + "Absolute path should throw for a nonexistent execuable" + ); +}); + +add_task(async function test_pathSearchRelative() { + let env = {}; + + await Assert.rejects( + Subprocess.pathSearch(PYTHON_BIN, env), + function (e) { + equal( + e.errorCode, + Subprocess.ERROR_BAD_EXECUTABLE, + "Got the expected error code" + ); + return /Executable not found:/.test(e.message); + }, + "Relative path should not be found when PATH is missing" + ); + + env.PATH = [DOES_NOT_EXIST, PYTHON_DIR].join(PATH_SEP); + + let path = await Subprocess.pathSearch(PYTHON_BIN, env); + equal(path, PYTHON, "Correct executable should be found in the path"); +}); + +add_task( + { + skip_if: () => AppConstants.platform != "win", + }, + async function test_pathSearch_PATHEXT() { + ok(PYTHON_BIN.endsWith(".exe"), "Python executable must end with .exe"); + + const python_bin = PYTHON_BIN.slice(0, -4); + + let env = { + PATH: PYTHON_DIR, + PATHEXT: [".com", ".exe", ".foobar"].join(";"), + }; + + let path = await Subprocess.pathSearch(python_bin, env); + equal( + path, + PYTHON, + "Correct executable should be found in the path, with guessed extension" + ); + } +); +// IMPORTANT: Do not add any tests beyond this point without removing +// the `skip_if` condition from the previous task, or it will prevent +// all succeeding tasks from running when it does not match. diff --git a/toolkit/modules/subprocess/test/xpcshell/xpcshell.ini b/toolkit/modules/subprocess/test/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..ffed6b6de0 --- /dev/null +++ b/toolkit/modules/subprocess/test/xpcshell/xpcshell.ini @@ -0,0 +1,16 @@ +[DEFAULT] +head = head.js +firefox-appdir = browser +skip-if = os == 'android' +subprocess = true +support-files = + data_text_file.txt + data_test_script.py + +[test_subprocess.js] +skip-if = + verify + apple_silicon # bug 1729546 +run-sequentially = very high failure rate in parallel +[test_subprocess_getEnvironment.js] +[test_subprocess_pathSearch.js] |