UNPKG

gpii-universal

Version:

Cross platform, core components of the GPII personalization infrastructure.

866 lines (803 loc) 23.6 kB
/* * 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); });