gpii-universal
Version:
Cross platform, core components of the GPII personalization infrastructure.
866 lines (803 loc) • 23.6 kB
JavaScript
/*
* eventLog Tests
*
* 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
*/
"use strict";
var fluid = require("infusion"),
fs = require("fs"),
os = require("os"),
net = require("net"),
readline = require("readline");
var jqUnit = fluid.require("node-jqunit");
var gpii = fluid.registerNamespace("gpii");
fluid.registerNamespace("gpii.tests.eventLog");
require("../index.js");
require("../../lifecycleManager/");
var teardowns = [];
jqUnit.module("gpii.tests.eventLog", {
teardown: function () {
while (teardowns.length) {
teardowns.pop()();
}
}
});
fluid.defaults("gpii.installID.test", {
invokers: {
getMachineID: "gpii.tests.eventLog.getMachineID"
}
});
gpii.tests.eventLog.getMachineID = function () {
return "TEST-MACHINE-ID";
};
/**
* Creates a testable gpii.eventLog component.
*
* @param {String} logFile - The log file.
* @return {Component} An instance of gpii.eventLog component.
*/
gpii.tests.eventLog.createEventLogComponent = function (logFile) {
var eventLog = gpii.eventLog({
gradeNames: ["gpii.lifecycleManager", "gpii.metrics"],
distributeOptions: {
installID: {
record: "gpii.installID.test",
target: "{that installID}.options.gradeNames"
}
},
durationEvents: {
"test-start1": "test-end1",
"test-start2": "test-end2",
"test-start3": "test-end3",
"test-start4": "test-end4"
},
members: {
eventData: {
installID: gpii.tests.eventLog.installID,
sessionID: gpii.tests.eventLog.sessionID
}
},
logDestination: logFile
});
teardowns.push(eventLog.destroy);
return eventLog;
};
gpii.tests.eventLog.installID = "installation-id";
gpii.tests.eventLog.timestamp = "2000-00-00T00:00:00.000Z";
gpii.tests.eventLog.version = require("../package.json").version;
gpii.tests.eventLog.sessionID = "session-id";
gpii.tests.eventLog.testDefs = fluid.freezeRecursive(fluid.transform([
{
// generated by the component
testData: null,
expected: {
"level": "INFO",
"module": "gpii",
"event": "start"
}
},
{
// data object
testData: {
func: "log",
args: ["{that}", "module0", "event0", {"data0": "value0"}]
},
expected: {
"level": "INFO",
"module": "module0",
"event": "event0",
"data": {"data0": "value0"}
}
},
{
// no data
testData: {
func: "log",
args: ["{that}", "module1", "event1"]
},
expected: {
"level": "INFO",
"module": "module1",
"event": "event1"
}
},
{
// data string
testData: {
func: "log",
args: ["{that}", "module2", "event2", "value2"]
},
expected: {
"level": "INFO",
"module": "module2",
"event": "event2",
"data": "value2"
}
},
{
// data number
testData: {
func: "log",
args: ["{that}", "module3", "event3", 1234]
},
expected: {
"level": "INFO",
"module": "module3",
"event": "event3",
"data": 1234
}
},
{
// empty data
testData: {
func: "log",
args: ["{that}", "module4", "event4", {}]
},
expected: {
"level": "INFO",
"module": "module4",
"event": "event4"
}
},
{
// null data
testData: {
func: "log",
args: ["{that}", "module5", "event5", null]
},
expected: {
"level": "INFO",
"module": "module5",
"event": "event5"
}
},
{
// test log level
testData: {
func: "log",
args: ["{that}", "module14", "event14", {"data0": "value0"}, fluid.logLevel.WARN]
},
expected: {
"level": "WARN",
"module": "module14",
"event": "event14",
"data": {"data0": "value0"}
}
},
{
// test log level (dropped)
testData: {
func: "log",
args: ["{that}", "module14b", "event14b", {"data0": "value0"}, fluid.logLevel.TRACE]
},
expected: null
},
{
// logError
testData: {
func: "logError",
args: ["{that}", "module15", "event15", {"error15": "value15"}]
},
expected: {
"level": "FAIL",
"module": "module15",
"event": "Error.event15",
"data": {
"error": {"error15": "value15", "stack": ""}
}
}
},
// Duration events
{
// start
testData: {
func: "log",
args: ["{that}", "module", "test-start1", {"value": "yes"}]
},
expected: {
"module": "module",
"event": "test-start1",
"level": "INFO",
"data": {
"value": "yes"
}
}
},
{
// end with data
testData: {
func: "log",
args: ["{that}", "module", "test-end1", {"value": "yes"}]
},
expected: {
"module": "module",
"event": "test-end1",
"level": "INFO",
"data": {
"value": "yes",
"duration": 2
}
}
},
{
// start with no data
testData: {
func: "log",
args: ["{that}", "module", "test-start1" ]
},
expected: {
"module": "module",
"event": "test-start1",
"level": "INFO"
}
},
{
// end with no data
testData: {
func: "log",
args: ["{that}", "module", "test-end1" ]
},
expected: {
"module": "module",
"event": "test-end1",
"level": "INFO",
"data": {
"duration": 2
}
}
},
// multiple pairs stacked
{
// start 1
testData: {
func: "log",
args: ["{that}", "module", "test-start1" ]
},
expected: {
"module": "module",
"event": "test-start1",
"level": "INFO"
}
},
{
// start 2
testData: {
func: "log",
args: ["{that}", "module", "test-start2" ]
},
expected: {
"module": "module",
"event": "test-start2",
"level": "INFO"
}
},
{
// end 2
testData: {
func: "log",
args: ["{that}", "module", "test-end2" ]
},
expected: {
"module": "module",
"event": "test-end2",
"level": "INFO",
"data": {
"duration": 2
}
}
},
{
// end 1
testData: {
func: "log",
args: ["{that}", "module", "test-end1" ]
},
expected: {
"module": "module",
"event": "test-end1",
"level": "INFO",
"data": {
"duration": 2
}
}
},
// multiple pairs intersected
{
// start 1
testData: {
func: "log",
args: ["{that}", "module", "test-start1" ]
},
expected: {
"module": "module",
"event": "test-start1",
"level": "INFO"
}
},
{
// start 2
testData: {
func: "log",
args: ["{that}", "module", "test-start2" ]
},
expected: {
"module": "module",
"event": "test-start2",
"level": "INFO"
}
},
{
// end 1
testData: {
func: "log",
args: ["{that}", "module", "test-end1" ]
},
expected: {
"module": "module",
"event": "test-end1",
"level": "INFO",
"data": {
"duration": 2
}
}
},
{
// end 2
testData: {
func: "log",
args: ["{that}", "module", "test-end2" ]
},
expected: {
"module": "module",
"event": "test-end2",
"level": "INFO",
"data": {
"duration": 2
}
}
},
// same pairs stacked
{
// start 1
testData: {
func: "log",
args: ["{that}", "module", "test-start1" ]
},
expected: {
"module": "module",
"event": "test-start1",
"level": "INFO"
}
},
{
// start 1 (again)
testData: {
func: "log",
args: ["{that}", "module", "test-start1" ]
},
expected: {
"module": "module",
"event": "test-start1",
"level": "INFO"
}
},
{
// end 1
testData: {
func: "log",
args: ["{that}", "module", "test-end1" ]
},
expected: {
"module": "module",
"event": "test-end1",
"level": "INFO",
"data": {
"duration": 2
}
}
},
{
// end 1 (again)
testData: {
func: "log",
args: ["{that}", "module", "test-end1" ]
},
expected: {
"module": "module",
"event": "test-end1",
"level": "INFO",
"data": {
"duration": 2
}
}
},
// End without start
{
// end 3 (no start)
testData: {
func: "log",
args: ["{that}", "module", "test-end3" ]
},
expected: {
"module": "module",
"event": "test-end3",
"level": "INFO"
}
},
{
// start 3 (prove the test previous is correct)
testData: {
func: "log",
args: ["{that}", "module", "test-start3" ]
},
expected: {
"module": "module",
"event": "test-start3",
"level": "INFO"
}
},
{
// end 3
testData: {
func: "log",
args: ["{that}", "module", "test-end3" ]
},
expected: {
"module": "module",
"event": "test-end3",
"level": "INFO",
"data": {
"duration": 2
}
}
},
// Already has duration data
{
// start 4
testData: {
func: "log",
args: ["{that}", "module", "test-start3" ]
},
expected: {
"module": "module",
"event": "test-start3",
"level": "INFO"
}
},
{
// end 3
testData: {
func: "log",
args: ["{that}", "module", "test-end3", {"duration": "hello"} ]
},
expected: {
"module": "module",
"event": "test-end3",
"level": "INFO",
"data": {
"duration": "hello",
"duration_auto": 2
}
}
}
], function (item) {
fluid.each(fluid.makeArray(item.expected), function (expected) {
expected.timestamp = gpii.tests.eventLog.timestamp;
expected.installID = gpii.tests.eventLog.installID;
expected.version = gpii.tests.eventLog.version;
expected.sessionID = gpii.tests.eventLog.sessionID;
});
return item;
}));
/**
* Sets up the logging so it's predictable and can be examined easily.
*
* @return {String} The log file.
*/
gpii.tests.eventLog.prepareLogFile = function () {
var logFile = os.tmpdir() + "/gpii-test-eventLog-" + Math.random().toString(36);
teardowns.push(function () {
fs.unlinkSync(logFile);
});
// Mock getTimestamp function
var oldGetTimestamp = gpii.eventLog.getTimestamp;
teardowns.push(function () {
// Restore getTimestamp.
gpii.eventLog.getTimestamp = oldGetTimestamp;
});
gpii.eventLog.getTimestamp = function () {
return gpii.tests.eventLog.timestamp;
};
return logFile;
};
/**
* Checks a log line.
*
* @param {String} line - The log lone.
* @param {Object} expected - What it should parse into.
*/
gpii.tests.eventLog.checkLogLine = function (line, expected) {
jqUnit.expect(5);
jqUnit.assertTrue("Line should look like a JSON object", !!line.match(/^{.*}$/));
var obj = JSON.parse(line);
jqUnit.assertEquals("JSON should be an object", "object", typeof(obj));
if (obj.data) {
// The stack property of an error is unpredictable. If it's found, just make sure it's a string.
if (obj.data.error && obj.data.error.stack) {
jqUnit.expect(1);
jqUnit.assertEquals("data.error.stack property should a string", "string", typeof(obj.data.error.stack));
obj.data.error.stack = "";
}
}
// The sequence is unpredictable - just make sure it exists, and is larger than the last one.
var seq = obj.sequence || null;
jqUnit.assertFalse("Log line must have a numeric sequence field.", isNaN(seq));
var prev = gpii.tests.eventLog.checkLogLine.lastSequence || 0;
jqUnit.assertTrue("log sequence should be greater than the previous", seq > prev);
gpii.tests.eventLog.checkLogLine.lastSequence = seq;
delete obj.sequence;
jqUnit.assertDeepEq("Parsed log should match expected", expected, obj);
};
/**
* Checks an entire log file.
*
* @param {String} logFile - The log file.
* @param {Object[]} expected - Array of objects expected for each line.
* @return {Promise} A promise that resolves when the file is read.
*/
gpii.tests.eventLog.checkLogFile = function (logFile, expected) {
var promise = fluid.promise();
var reader = readline.createInterface({
input: fs.createReadStream(logFile)
});
// Inspect each line of the log, making sure each one parses into the expected object.
var lineNumber = 0;
reader.on("line", function (line) {
console.log("Log line", lineNumber, line);
if (lineNumber >= expected.length) {
jqUnit.fail("Log file should not contain more data.");
promise.reject();
reader.close();
return;
}
gpii.tests.eventLog.checkLogLine(line, expected[lineNumber]);
lineNumber++;
});
reader.on("close", function () {
if (!promise.disposition) {
promise.resolve();
}
});
return promise;
};
jqUnit.asyncTest("Uncaught exception test", function () {
jqUnit.expect(1);
var logFile = gpii.tests.eventLog.prepareLogFile();
gpii.tests.eventLog.createEventLogComponent(logFile);
// Clear the log file
try {
fs.unlinkSync(logFile);
} catch (e) {
// ignore
}
jqUnit.assertNotUndefined("Uncaught exception listener added",
fluid.onUncaughtException.listeners["gpii-eventLog"]);
var errorText = "eventLog uncaught exception logging test";
var expected = [
{
"installID": gpii.tests.eventLog.installID,
"timestamp": gpii.tests.eventLog.timestamp,
"version": gpii.tests.eventLog.version,
"sessionID": gpii.tests.eventLog.sessionID,
"level": "FAIL",
"module": "GPII",
"event": "Error.Exception",
"data": {
"error": {
"testError": true,
"message": errorText,
"stack": ""
}
}
}
];
// Disable jqUnit and fluid's handlers, to allow the test to continue when the exception is thrown.
fluid.failureEvent.addListener(fluid.identity, "jqUnit", "before:fail");
fluid.onUncaughtException.addListener(fluid.identity, "fail", "first");
var promise = fluid.promise();
process.nextTick(function () {
try {
throw {
testError: true,
message: errorText
};
} finally {
// Resolve after the next tick - so the uncaught exception handler can be called.
setTimeout(promise.resolve, 1);
}
});
promise.then(function () {
fluid.failureEvent.removeListener("jqUnit");
fluid.onUncaughtException.removeListener("fail");
gpii.tests.eventLog.checkLogFile(logFile, expected)
.then(jqUnit.start);
});
});
jqUnit.asyncTest("fluid.fail test", function () {
var logFile = gpii.tests.eventLog.prepareLogFile();
gpii.tests.eventLog.createEventLogComponent(logFile);
// Clear the log file
fs.unlinkSync(logFile);
var errorText = "eventLog failure logging test";
var expected = [
{
"installID": gpii.tests.eventLog.installID,
"timestamp": gpii.tests.eventLog.timestamp,
"version": gpii.tests.eventLog.version,
"sessionID": gpii.tests.eventLog.sessionID,
"level": "FAIL",
"module": "GPII",
"event": "Error.Fail",
"data": {
"error": {
"message": errorText,
"stack": ""
}
}
}
];
// Test fluid.fail gets logged.
jqUnit.expectFrameworkDiagnostic("testing fluid.fail gets logged", function () {
fluid.fail(errorText);
}, [errorText]);
gpii.tests.eventLog.checkLogFile(logFile, expected)
.then(jqUnit.start);
});
jqUnit.asyncTest("eventLog tests #1", function () {
var tests = gpii.tests.eventLog.testDefs;
var logFile = gpii.tests.eventLog.prepareLogFile();
var expected = [];
var that = gpii.tests.eventLog.createEventLogComponent(logFile);
// mock hrtime so it return something expected
var hrtime = process.hrtime;
process.hrtime = function (arg) {
return arg ? [2, 0] : [3, 0];
};
// Log the test data
for (var n = 0; n < tests.length; n++) {
var test = tests[n];
var calls = fluid.makeArray(test.testData);
while (calls.length) {
var call = calls.shift();
var args = fluid.transform(call.args, function (c) {
return (c === "{that}") ? that : c;
});
gpii.eventLog[call.func].apply(null, args);
}
expected.push.apply(expected, fluid.makeArray(test.expected));
}
process.hrtime = hrtime;
gpii.tests.eventLog.checkLogFile(logFile, expected)
.then(jqUnit.start);
});
/**
* Creates a TCP server, calling a callback for every line received.
* @param {Number} port The TCP port to listen on.
* @param {Function} gotLine The function to call for each line.
* @return {net.Server} The TCP server.
*/
gpii.tests.eventLog.createLogServer = function (port, gotLine) {
var buffer = "";
var logServer = net.createServer();
logServer.listen(port, "127.0.0.1");
logServer.on("listening", function () {
fluid.log("[test] log server listening");
});
logServer.on("connection", function (socket) {
fluid.log("[test] log server connection");
socket.setEncoding("utf8");
socket.on("data", function (chunk) {
buffer += chunk;
if (buffer.indexOf("\n") >= 0) {
var lines = buffer.split("\n");
buffer = lines.pop();
fluid.each(lines, gotLine);
}
});
});
logServer.on("close", function () {
});
logServer.on("error", function (err) {
jqUnit.fail(err);
});
return logServer;
};
/**
* Resolve after a timeout
* @param {Number} timeout The millisecond timeout.
* @return {Promise} Resolves after the timeout.
*/
gpii.tests.eventLog.promiseTimeout = function (timeout) {
var promise = fluid.promise();
setTimeout(promise.resolve, timeout);
return promise;
};
/**
* Tests writing logs to a log server.
*/
jqUnit.asyncTest("eventLog tests - writeLogTcp", function () {
var port = 50000 + Math.floor(Math.random() * 5000);
var logFile = "tcp://127.0.0.1:" + port;
var that = gpii.tests.eventLog.createEventLogComponent(logFile);
var linesWrote = [];
var lineCount = 0;
var stopTest = false;
var firstLine = true;
// Check each line as it comes in.
var gotLine = function (line) {
if (firstLine) {
// Ignore the first line - it's the gpii.start event
firstLine = false;
} else {
// All lines written are expected to be received.
var expected = linesWrote.shift();
jqUnit.assertEquals("Line received must match the one sent in sequence", expected, line);
}
};
// Continuously write log lines.
var writeLine = function () {
if (!stopTest) {
var line = "message" + lineCount++;
linesWrote.push(line);
gpii.eventLog.writeLogTcp(that, line + "\n");
setTimeout(writeLine, 100);
}
};
// Start writing logs before the server is started. Log lines should be buffered until the connection is made.
writeLine();
// Start the log server, resolving some time after a client connection is received
var startServer = function (state) {
fluid.log("[test] Starting log server");
state.logServer = gpii.tests.eventLog.createLogServer(port, gotLine);
var p = fluid.promise();
state.logServer.on("connection", function (socket) {
state.logServer.close();
state.socket = socket;
setTimeout(p.resolve, 500);
});
return p;
};
var actions = [
gpii.tests.eventLog.promiseTimeout(500),
// Start the log server.
startServer,
// Stop the log server. Outgoing lines should be buffered until reconnected.
function (state) {
fluid.log("[test] Stopping log server");
state.socket.end();
return gpii.tests.eventLog.promiseTimeout(700);
},
// Restart the log server. eventLog should reconnect.
startServer,
// End the test
function (state) {
stopTest = true;
state.socket.end();
jqUnit.expect(lineCount);
return linesWrote.length > 0 && gpii.tests.eventLog.promiseTimeout(500);
}
];
fluid.promise.sequence(actions, {}).then(jqUnit.start);
});