gpii-windows
Version:
Components of the GPII personalization infrastructure for use on Microsoft's "Windows" ™
584 lines (495 loc) • 19.6 kB
JavaScript
/* Tests for gpii-process.js
*
* Copyright 2017 Raising the Floor - International
*
* Licensed under the New BSD license. You may not use this file except in
* compliance with this License.
*
* The R&D leading to these results received funding from the
* Department of Education - Grant H421A150005 (GPII-APCP). However,
* these results do not necessarily represent the policy of the
* Department of Education, and you should not assume endorsement by the
* Federal Government.
*
* You may obtain a copy of the License at
* https://github.com/GPII/universal/blob/master/LICENSE.txt
*/
;
var jqUnit = require("node-jqunit"),
path = require("path"),
child_process = require("child_process"),
processHandling = require("../src/processHandling.js"),
windows = require("../src/windows.js"),
winapi = require("../src/winapi.js");
var processHandlingTests = {
testData: {}
};
var teardowns = [];
jqUnit.module("GPII Service processHandling tests", {
teardown: function () {
while (teardowns.length) {
teardowns.pop()();
}
}
});
processHandlingTests.testData.startChildProcess = [
// Shouldn't restart if stopChildProcess is used.
{
input: {
childProcess: {
key: "norestart-stop-test",
command: "test-command",
ipc: null,
autoRestart: false
},
stopChildProcess: false
},
expect: {
restart: false
}
},
{
input: {
childProcess: {
key: "restart-stop-test",
command: "test-command",
ipc: null,
autoRestart: true
},
stopChildProcess: true
},
expect: {
restart: false
}
},
{
input: {
childProcess: {
key: "norestart-ipc-stop-test",
command: "test-command",
ipc: "norestart",
autoRestart: false
},
stopChildProcess: true
},
expect: {
restart: false
}
},
{
input: {
childProcess: {
key: "restart-ipc-stop-test",
command: "test-command",
ipc: "restart",
autoRestart: true
},
stopChildProcess: true
},
expect: {
restart: false
}
},
// Should restart if killed, and autoRestart is set
{
input: {
childProcess: {
key: "norestart-kill-test",
command: "test-command",
ipc: null,
autoRestart: false
},
stopChildProcess: false
},
expect: {
restart: false
}
},
{
input: {
childProcess: {
key: "restart-kill-test",
command: "test-command",
ipc: null,
autoRestart: true
},
stopChildProcess: false
},
expect: {
restart: true
}
},
{
input: {
childProcess: {
key: "norestart-ipc-kill-test",
command: "test-command",
ipc: "norestart",
autoRestart: false
},
stopChildProcess: false
},
expect: {
restart: false
}
},
{
input: {
childProcess: {
key: "restart-ipc-kill-test",
command: "test-command",
ipc: "restart",
autoRestart: true
},
stopChildProcess: false
},
expect: {
restart: true
}
}
];
processHandlingTests.testData.monitorProcessFailures = [
{ input: null },
{ input: 0 },
{ input: -1 }
];
/**
* Start a process that self terminates after 10 seconds.
* @return {ChildProcess} The child process.
*/
processHandlingTests.startProcess = function () {
var id = "processHandlingTest" + Math.random().toString(32).substr(2);
var exe = path.join(process.env.SystemRoot, "/System32/waitfor.exe");
var command = exe + " " + id + " /T 10 ";
return child_process.exec(command);
};
/**
* Waits for a mutex to be create, by polling until OpenMutex succeeds.
*
* @param {String} mutexName The name of the mutex.
* @param {Number} timeout [Optional] How long to wait (ms), default 1000.
* @return {Promise} Resolves when a mutex with the given name has been created, or with value of "timeout".
*/
processHandlingTests.waitForMutex = function (mutexName, timeout) {
return new Promise(function (resolve, reject) {
var nameBuffer = winapi.stringToWideChar(mutexName);
var timedout = false;
setTimeout(function () {
timedout = true;
resolve("timeout");
}, timeout || 1000);
// Poll until OpenMutex succeeds.
var checkMutex = function () {
var mutexHandle = winapi.kernel32.OpenMutexW(winapi.constants.SYNCHRONIZE, false, nameBuffer);
if (mutexHandle) {
winapi.kernel32.ReleaseMutex(mutexHandle);
winapi.kernel32.CloseHandle(mutexHandle);
resolve();
} else if (!timedout) {
var err = winapi.kernel32.GetLastError();
var ERROR_FILE_NOT_FOUND = 2;
if (err === ERROR_FILE_NOT_FOUND) {
setTimeout(checkMutex, 200);
} else {
reject(winapi.error("OpenMutex", 0, err));
}
}
};
checkMutex();
});
};
// Tests getProcessCreationTime
jqUnit.test("Test getProcessCreationTime", function () {
var value;
// This process
var thisProcess = processHandling.getProcessCreationTime(process.pid);
jqUnit.assertNotNull("creation time must not be null", thisProcess);
jqUnit.assertFalse("creation time must be a number", isNaN(thisProcess));
value = processHandling.getProcessCreationTime(process.pid);
jqUnit.assertEquals("two calls must return the same value", thisProcess, value);
// A different process.
var child = processHandlingTests.startProcess();
value = processHandling.getProcessCreationTime(child.pid);
jqUnit.assertNotNull("creation time of child process must not be null", value);
jqUnit.assertNotNull("creation time of child process must be different to this process", thisProcess);
// A process that's not running.
value = processHandling.getProcessCreationTime(1);
jqUnit.assertNull("creation time for non-running process must be null", value);
// A pid that's not a pid
value = processHandling.getProcessCreationTime("not a pid");
jqUnit.assertNull("creation time for non-pid must be null", value);
});
// Tests isProcessRunning
jqUnit.asyncTest("Test isProcessRunning", function () {
jqUnit.expect(10);
var running;
// This process
running = processHandling.isProcessRunning(process.pid);
jqUnit.assertTrue("This process should be running", running);
// A process that's not running.
running = processHandling.isProcessRunning(3);
jqUnit.assertFalse("This process should not be running", running);
// A pid that's not a pid
running = processHandling.isProcessRunning("not a pid");
jqUnit.assertFalse("This invalid process should not be running", running);
// A null pid
running = processHandling.isProcessRunning(null);
jqUnit.assertFalse("This invalid process should not be running", running);
var creationTime, pid;
// This process, with creation time
pid = process.pid;
creationTime = processHandling.getProcessCreationTime(pid);
running = processHandling.isProcessRunning(pid);
jqUnit.assertTrue("This process (with creation time) should be running", running);
// This process, with wrong creation time
pid = process.pid;
creationTime = "12345";
running = processHandling.isProcessRunning(pid, creationTime);
jqUnit.assertFalse("This process (incorrect creation time) should not be running", running);
// Test a child process, with a handle still open - the pid will still be valid, but it's not a running process.
// (first tested without the handle opening, for a sanity check)
var testChild = function (openHandle) {
var assertSuffix = openHandle ? " (open handle)" : "";
var child = processHandlingTests.startProcess();
running = processHandling.isProcessRunning(child.pid);
jqUnit.assertTrue("Child process should be running" + assertSuffix, running);
var handle;
if (openHandle) {
handle = winapi.kernel32.OpenProcess(winapi.constants.PROCESS_QUERY_LIMITED_INFORMATION, 0, child.pid);
if (!handle || handle === winapi.NULL) {
jqUnit.fail(winapi.errorText("OpenProcess failed"));
}
}
child.on("exit", function () {
setTimeout(function () {
running = processHandling.isProcessRunning(child.pid);
jqUnit.assertFalse("Child process should not be running after kill" + assertSuffix, running);
if (openHandle) {
winapi.kernel32.CloseHandle(handle);
jqUnit.start();
} else {
testChild(true);
}
}, 100);
});
child.kill();
};
testChild(false);
});
// Tests startChildProcess, stopChildProcess, and autoRestartProcess (indirectly)
jqUnit.asyncTest("Test startChildProcess", function () {
var testData = processHandlingTests.testData.startChildProcess;
// Don't delay restarting the process.
var throttleRate_orig = processHandling.throttleRate;
processHandling.throttleRate = function () {
return 1;
};
teardowns.push(function () {
processHandling.throttleRate = throttleRate_orig;
});
// For each test a child process is started (using the input data). It's then stopped via either stopChildProcess or
// kill. A check is made if the process has ended, and if the process has been restarted or not.
// The pid of the new child process is unknown, so a mutex is created by the child then checked here if it exists or
// not to determine if the new process is running.
var nextTest = function (testIndex) {
if (testIndex >= testData.length) {
jqUnit.start();
return;
}
var test = testData[testIndex];
var messageSuffix = " - " + test.input.childProcess.key;
var procConfig = Object.assign({}, test.input.childProcess);
var mutexName = procConfig.key + Math.random().toString(32);
procConfig.command = "node.exe " + path.join(__dirname, "gpii-ipc-tests-child.js") + " mutex " + mutexName;
var promise = processHandling.startChildProcess(procConfig);
jqUnit.assertTrue("startProcess must return a promise" + messageSuffix,
promise && typeof(promise.then) === "function");
promise.then(function (pid) {
jqUnit.assertFalse("startProcess must resolve with a numeric pid" + messageSuffix, isNaN(pid));
var pidRunning = processHandling.isProcessRunning(pid);
jqUnit.assertTrue("startProcess must resolve with a running pid" + messageSuffix, pidRunning);
// See if the process gets restarted (or not).
windows.waitForProcessTermination(pid, 1000).then(function (value) {
if (value === "timeout") {
jqUnit.fail("child process didn't terminate" + messageSuffix);
} else {
// See if the new process was restarted, by waiting for the mutex it creates.
processHandlingTests.waitForMutex(mutexName, 2000).then(function (value) {
if (test.expect.restart) {
jqUnit.assertNotEquals("process should restart" + messageSuffix, "timeout", value);
} else {
jqUnit.assertEquals("process should not restart" + messageSuffix, "timeout", value);
}
processHandling.stopChildProcess(procConfig.key, false);
nextTest(testIndex + 1);
}, jqUnit.fail);
}
}, jqUnit.fail);
// Kill the first process.
if (test.input.stopChildProcess) {
processHandling.stopChildProcess(procConfig.key, false);
} else {
process.kill(pid);
}
}, jqUnit.fail);
};
nextTest(0);
});
jqUnit.asyncTest("Test monitorProcess - single process", function () {
jqUnit.expect(3);
var child = processHandlingTests.startProcess();
var promise = processHandling.monitorProcess(child.pid);
jqUnit.assertTrue("monitorProcess must return a promise", promise && typeof(promise.then) === "function");
var killed = false;
promise.then(function (value) {
jqUnit.assertTrue("monitorProcess should not resolve before the process is killed", killed);
jqUnit.assertEquals("monitorProcess should resolve with the process id", child.pid, value);
jqUnit.start();
}, jqUnit.fail);
setTimeout(function () {
killed = true;
child.kill();
}, 100);
});
jqUnit.asyncTest("Test monitorProcess - multiple processes", function () {
var killOrder = [ 4, 0, 2, 1, 3 ];
var procs = [];
for (var n = 0; n < killOrder.length; n++) {
procs.push(processHandlingTests.startProcess());
}
jqUnit.expect(procs.length * 2);
var killed = [];
var killProcess = function () {
if (killOrder.length > 0) {
var proc = procs[killOrder.shift()];
killed.push(proc.pid);
proc.kill();
} else {
jqUnit.start();
}
};
procs.forEach(function (proc) {
processHandling.monitorProcess(proc.pid).then(function (pid) {
jqUnit.assertEquals("monitorProcess must resolve with the same pid", proc.pid, pid);
jqUnit.assertNotEquals("monitorProcess must resolve after the process is killed", -1, killed.indexOf(pid));
killProcess();
}, jqUnit.fail);
});
killProcess();
});
jqUnit.asyncTest("Test monitorProcess failures", function () {
var testData = processHandlingTests.testData.monitorProcessFailures;
jqUnit.expect(testData.length * 4 * 2 + 1);
var child = processHandlingTests.startProcess();
setTimeout(jqUnit.fail, 10000, "timeout");
// Tests are ran twice - the 2nd time, another process is also being monitored. This is to check an innocent process
// doesn't get caught up in the failure.
var pass = 0;
var runTest = function (testIndex) {
if (testIndex >= testData.length) {
pass++;
if (pass > 1) {
child.kill();
return;
} else {
// Monitor a process to check it doesn't also get rejected.
processHandling.monitorProcess(child.pid).then(function () {
jqUnit.assertTrue("Child shouldn't resolve until the end" + messageSuffix, pass > 1);
jqUnit.start();
}, function () {
jqUnit.fail("Child shouldn't fail");
});
testIndex = 0;
}
}
var test = testData[testIndex];
var messageSuffix = " - testIndex=" + testIndex + ", pass=" + pass;
console.log("Running test" + messageSuffix);
var promise = processHandling.monitorProcess(test.input, 100, false);
jqUnit.assertTrue("monitorProcess must return a promise", promise && typeof(promise.then) === "function");
promise.then(function () {
jqUnit.fail("monitorProcess should not have resolved" + messageSuffix);
}, function (e) {
jqUnit.assert("monitorProcess should have rejected" + messageSuffix);
jqUnit.assertTrue("monitorProcess should have rejected with a value" + messageSuffix, !!e);
jqUnit.assertTrue("monitorProcess should have rejected with an error" + messageSuffix,
e instanceof Error || e.isError);
runTest(testIndex + 1);
});
};
runTest(0);
});
jqUnit.asyncTest("Test unmonitorProcess", function () {
jqUnit.expect(4);
var child1 = processHandlingTests.startProcess();
var child2 = processHandlingTests.startProcess();
var promise1 = processHandling.monitorProcess(child1.pid);
var promise2 = processHandling.monitorProcess(child2.pid);
var killed = false;
var removed = false;
promise1.then(function (value) {
jqUnit.assertTrue("promise1 should not resolve before it's removed", removed);
jqUnit.assertEquals("promise1 should resolve with 'removed'", "removed", value);
killed = true;
child2.kill();
}, jqUnit.fail);
promise2.then(function (value) {
jqUnit.assertTrue("promise2 should not resolve before the process is killed", killed);
jqUnit.assertEquals("promise2 should resolve with the process id", child2.pid, value);
jqUnit.start();
}, jqUnit.fail);
setTimeout(function () {
removed = true;
processHandling.unmonitorProcess(child1.pid);
}, 100);
});
// Test starting and stopping the service
jqUnit.asyncTest("Service start+stop", function () {
jqUnit.expect(1);
var service = require("../src/service.js");
var mutexName = "gpii-test-" + Math.random().toString(32);
// Configure a child process
service.config = {
processes: {
testProcess: {
command: "node.exe " + path.join(__dirname, "gpii-ipc-tests-child.js") + " mutex " + mutexName,
autoRestart: false
}
}
};
// Mock process.exit - the call to this is expected, but not desired.
var oldExit = process.exit;
var exitAsserted = false;
process.exit = function () {
console.log("process.exit()");
if (!exitAsserted) {
// In order to stop the service from lingering, process.exit() could be called more than once (because
// during the test it doesn't really exit).
jqUnit.assert("process.exit");
}
exitAsserted = true;
};
teardowns.push(function () {
process.exit = oldExit;
});
// Start the service
service.start();
// Wait for the child process to start.
processHandlingTests.waitForMutex(mutexName, 5000).then(function (value) {
if (value === "timeout") {
jqUnit.fail("Timed out waiting for child process");
} else {
var pid = processHandling.childProcesses.testProcess.pid;
// stop the service, see if the child terminates.
service.stop();
windows.waitForProcessTermination(pid, 15000).then(function (value) {
if (value === "timeout") {
jqUnit.fail("Timed out waiting for child process to terminate");
} else {
console.log("Process died");
jqUnit.start();
}
});
}
}, jqUnit.fail);
});