UNPKG

hay-caf

Version:

Haystacks Cloud Automation Framework: A demonstration testing automation framework application for the purposes of demonstrating the usage and integration of D-CAF, Hest, Hexiom, HayRunner, HayMonkey, and HayD-CAG in a single testing automation framework

585 lines (521 loc) 25.6 kB
/** * @file socketsServer.js * @module socketsServer * @description Creates a socket server through which clients will communicate. * @requires module:application.constants * @requires module:application.message.constants * @requires module:application.system.constants * @requires {@link https://www.npmjs.com/package/@haystacks/constants|@haystacks/constants} * @requires {@link https://www.npmjs.com/package/path|path} * @requires {@link https://nodejs.dev/learn/the-nodejs-process-module|process} * @requires {@link https://nodejs.org/api/net|net} * @author Karl-Edward F.P. Jean-Mehu * @date 2023/12/29 * @copyright Copyright © 2023-… by Karl-Edward F.P. Jean-Mehu. All rights reserved. */ // Internal imports import * as apc from '../constants/application.constants.js'; import * as app_msg from '../constants/application.message.constants.js'; import * as app_sys from '../constants/application.system.constants.js'; // External imports import haystacks from '@haystacks/async'; import hayConst from '@haystacks/constants'; import path from 'path'; import process from 'process'; import { createServer } from 'net'; const { bas, gen, msg, num, wrd } = hayConst; const baseFileName = path.basename(import.meta.url, path.extname(import.meta.url)); // application.hay-CAF.childProcess.socketsServer. const namespacePrefix = wrd.capplication + bas.cDot + apc.cApplicationName + bas.cDot + app_sys.cchildProcess + bas.cDot + baseFileName + bas.cDot; // Host and port to which the socket connection will be listening to. const SOCKET = { host: num.c127 + bas.cDot + num.c0 + bas.cDot + num.c0 + bas.cDot + num.c1, // '127.0.0.1', port: 3000, }; let persistentBuffer = ''; const messageDelimiter = bas.cHash.repeat(2) + wrd.cEND + bas.cHash.repeat(2); // ##END## /** * @function safeJsonParse * @description Safely parses socket chunks into javascript objects. * @param {buffer} buffer - The buffer value to be converted. * @return {object} A JSON object that is safely parsed from the input JSON string. * @author Karl-Edward FP Jean-Mehu * @date 2023/12/29 */ async function safeJsonParse(buffer) { const functionName = safeJsonParse.name; let chunkString = buffer.toString(gen.cUTF8).trim(); await haystacks.consoleLog(namespacePrefix, functionName, msg.cBEGIN_Function); await haystacks.consoleLog(namespacePrefix, functionName, msg.cbufferIs + buffer); let returnData = []; const REGEX = /\{([^{}]*)\}/g; // append the incoming chunkString to the persistent buffer. persistentBuffer += chunkString; // persistentBuffer after appending new chunk is now: await haystacks.consoleLog(namespacePrefix, functionName, app_msg.csafeJsonParseMessage01 + persistentBuffer); // Step 1: Split by messageDelimiter to identify complete messages let messages = persistentBuffer.split(messageDelimiter); // Messages array after messageDelimiter split: await haystacks.consoleLog(namespacePrefix, functionName, app_msg.csafeJsonParseMessage02 + JSON.stringify(messages)); // Step 2: Process each message that appears complete for (let i = 0; i < messages.length - 1; i++) { // BEGIN: i-th iterator await haystacks.consoleLog(namespacePrefix, functionName, msg.cBEGIN_ithIteration + i); const message = messages[i].trim(); await haystacks.consoleLog(namespacePrefix, functionName, msg.cmessageIs + message); if (message.length > 0) { try { // Attempt to parse as JSON; if successful, add to returnData const json = JSON.parse(message); // Successfully parsed json data is: await haystacks.consoleLog(namespacePrefix, functionName, app_msg.csafeJsonParseMessage03 + JSON.stringify(json)); returnData.push(json); // Add fully formed JSON objects } catch (error) { // If JSON parsing fails, use REGEX to extract valid JSON fragments // JSON parse error at iteration // attempting REGEX extraction: await haystacks.consoleLog(namespacePrefix, functionName, app_msg.csafeJsonParseMessage04 + i + app_msg.csafeJsonParseMessage05 + error.message); // Error-causing message content: await haystacks.consoleLog(namespacePrefix, functionName, app_msg.csafeJsonParseMessage06 + message); const tempData = message.matchAll(REGEX); // Attempting REGEX extraction for matches in error-causing message. await haystacks.consoleLog(namespacePrefix, functionName, app_msg.csafeJsonParseMessage07); // tempData is: await haystacks.consoleLog(namespacePrefix, functionName, msg.ctempDataIs + tempData); for (const match of tempData) { // REGEX match string is: await haystacks.consoleLog(namespacePrefix, functionName, app_msg.csafeJsonParseMessage08 + match); try { returnData.push(JSON.parse(match[0])); // Successfully parsed REGEX JSON fragment await haystacks.consoleLog(namespacePrefix, functionName, app_msg.csafeJsonParseMessage09); } catch (e) { // Skipping unparsable JSON fragment: await haystacks.consoleLog(namespacePrefix, functionName, app_msg.csafeJsonParseMessage10 + match[0]); } } // End-for (const match of tempData) } // End-catch (error) } // End-if (message.length > 0) await haystacks.consoleLog(namespacePrefix, functionName, msg.cEND_ithIteration + i); } // End-for (let i = 0; i < message.length - 1; i++) // Step 3: Retain any unprocessed part (last item) in the persistent buffer for next call. persistentBuffer = messages[messages.length - 1]; // persistentBuffer AFTER processing is: await haystacks.consoleLog(namespacePrefix, functionName, app_msg.csafeJsonParseMessage11 + persistentBuffer); await haystacks.consoleLog(namespacePrefix, functionName, msg.creturnDataIs + JSON.stringify(returnData)); await haystacks.consoleLog(namespacePrefix, functionName, msg.cEND_Function); return returnData; } /** * @function messageContainsTestResult * @description Checks to see if the message contains all of the string requirements * that would indicate the message contains a test result of some kind. * @param {string} message The message that should be evaluated if it contains some kind of a test result. * @returns {boolean} True or False to indicate if the message contains a test result or not. * @author Seth Hollingsead * @date 2024/02/20 */ async function messageContainsTestResult(message) { const functionName = messageContainsTestResult.name; await haystacks.consoleLog(namespacePrefix, functionName, msg.cBEGIN_Function); await haystacks.consoleLog(namespacePrefix, functionName, msg.cmessageIs + message); let returnData = false; if (message) { if (message.includes(app_msg.cTestResultsLog) && message.includes(wrd.cTest + bas.cUnderscore) && (message.toLowerCase().includes(wrd.cpass) || message.toLowerCase().includes(wrd.cwarning) || message.toLowerCase().includes(wrd.cfail))) { returnData = true; } } await haystacks.consoleLog(namespacePrefix, functionName, msg.creturnDataIs + returnData); await haystacks.consoleLog(namespacePrefix, functionName, msg.cEND_Function); return returnData; } /** * @function getTestResultFromMessage * @description Parses the input message to determine what the test result value should be. * valid test result values are: pass, warning, fail * @param {string} message The message that should be parsed into a test result. * @returns {string} The test result, pass, warning or fail. * @author Seth Hollingsead * @date 2024/02/20 */ async function getTestResultFromMessage(message) { const functionName = getTestResultFromMessage.name; await haystacks.consoleLog(namespacePrefix, functionName, msg.cBEGIN_Function); await haystacks.consoleLog(namespacePrefix, functionName, msg.cmessageIs + message); let returnData = wrd.cfail; if (message) { if (message.toLowerCase().includes(wrd.cpass)) { returnData = wrd.cpass; } else if (message.toLowerCase().includes(wrd.cwarning)) { returnData = wrd.cwarning; } else if (message.toLowerCase().includes(wrd.cfail)) { returnData = wrd.cfail; } else { returnData = wrd.cfail; } // For completeness } await haystacks.consoleLog(namespacePrefix, functionName, msg.creturnDataIs + returnData); await haystacks.consoleLog(namespacePrefix, functionName, msg.cEND_Function); return returnData; } // Messages queue const createMessageQueue = (state = { items: [] }) => ({ items: state.items || [], async enqueue(item) { return new Promise(resolve => { if (!Array.isArray(item)) item = [item]; this.items = [...this.items, ...item]; resolve(); }) }, async dequeue() { const [item, ...rest] = this.items; this.items = rest; return new Promise(resolve => resolve(item || null)); }, async isEmpty() { return new Promise(resolve => resolve(this.items.length === 0)); }, async size() { return new Promise(resolve => resolve(this.items.length)); } }); /** * @function socketsServer * @description manages the socket connection and communication. * @author Karl-Edward FP Jean-Mehu * @date 2023/12/29 */ export default function socketsServer() { const functionName = socketsServer.name; haystacks.consoleLog(namespacePrefix, functionName, msg.cBEGIN_Function); let server = null; try { // Flag to check if there is an active connection let isConnected = false; // Test result from client let testResult = null; // Check wether result was retrieved let testResultRetrieved = false; // Message queue let messageQueue = createMessageQueue(); // Checks if the server has ended let serverHasEnded = false; // Async proscess write const processWriteAsync = async (msg) => new Promise(resolve => { process.stdout.write(msg); resolve(); }); // Banner log const bannerLog = async (eventName, cb) => { console.log('\r\n'); await haystacks.consoleLog(namespacePrefix, functionName + eventName, msg.cBEGIN_Event); await cb(); await haystacks.consoleLog(namespacePrefix, functionName + eventName, msg.cEND_Event); console.log('\r\n'); } // Handles actions taken when an error occurs on the server. const handleError = async (error, serverInstance) => { const eventName = bas.cDot + wrd.cerror; await bannerLog(eventName, async () => { serverHasEnded = true; await haystacks.consoleLog(namespacePrefix, functionName + eventName, msg.cerrorIs + error); if (error[wrd.ccode] === gen.cEADDRINUSE && !isConnected){ await haystacks.consoleLog(namespacePrefix, functionName + eventName, msg.cEND_Event); return; } else if (error[wrd.ccode] !== gen.cECONNRESET) { // Error on socket server: console.error(app_msg.cErrorSocketServerMessage01 + error.message); } else if (error[wrd.ccode] === gen.cECONNRESET) { console.log(gen.cECONNRESET + bas.cExclamation.repeat(4)); serverInstance.close(); } }); return; }; // Handles actions to take when server begins to listen for connections from clients. const handleListening = async () => { const eventName = bas.cDot + wrd.clistening; await bannerLog(eventName, async () => { console.log(bas.cCarRetNewLin + wrd.cListening + bas.cDot.repeat(3) + bas.cCarRetNewLin); }); }; // Handles actions to take when a client is connected. const handleConnection = async () => { const eventName = bas.cDot + wrd.cconnection; await bannerLog(eventName, async () => { isConnected = true; console.log(bas.cCarRetNewLin + app_msg.cServerConnected + bas.cCarRetNewLin ); }); }; const test = async (json, childEventName) => { if (json[wrd.cmessage]){ const { message, timestamp } = json; // This is the ECHO from the testing framework back to the hay-CAF window. let logMessage = timestamp + bas.cColon + bas.cSpace + message; console.log(logMessage); // Again echo this to the haystacks.consoleLog, because it can be logged to the log file from there. haystacks.consoleLog(namespacePrefix, functionName + childEventName, logMessage); // Terminates child processes if the "end" message is received const str = message.split(bas.cSpace)[0].toLowerCase(); if (str === wrd.cend) { // Sending termination cmd to clients... haystacks.consoleLog(namespacePrefix, functionName + childEventName, app_msg.csendingTerminationCmdToClients); if (!testResultRetrieved) { // Test failed prematurely! console.log(bas.cCarRetNewLin + app_msg.cTestFailedPrematurely + bas.cExclamation + bas.cCarRetNewLin); server.close(); } } } } // Handles incoming messages as they come in from a socket client. const handleData = async (chunk, socket = null) => { const eventName = bas.cDot + wrd.cdata; await haystacks.consoleLog(namespacePrefix, functionName + eventName, msg.cBEGIN_Event); await haystacks.consoleLog(namespacePrefix, functionName + eventName, app_msg.cchunkIs + JSON.stringify(chunk)); try { const json = await safeJsonParse(chunk.toString().trim()); // Safe Parsed JSON data: await haystacks.consoleLog(namespacePrefix, functionName + eventName, app_msg.csocketServerDataEventHandlerMessage01 + JSON.stringify(json)); if (json) { let hasMessage = false; if (Array.isArray(json)) { // JSON is an array with length: await haystacks.consoleLog(namespacePrefix, functionName + eventName, app_msg.csocketServerDataEventHandlerMessage02 + json.length); for (const obj1 of json) { // Log the current object being processed // Processing array item: await haystacks.consoleLog(namespacePrefix, functionName + eventName, app_msg.csocketServerDataEventHandlerMessage03 + JSON.stringify(obj1)); const objMessage = obj1[wrd.cmessage]; // Extracted message from array item: await haystacks.consoleLog(namespacePrefix, functionName + eventName, app_msg.csocketServerDataEventHandlerMessage04 + objMessage); if (objMessage?.includes(app_msg.cTestResultsLog) && objMessage?.includes(wrd.cTest + bas.cUnderscore) && (objMessage?.toLowerCase()?.includes(wrd.cpass) || objMessage?.toLowerCase()?.includes(wrd.cwarning) || objMessage?.toLowerCase()?.includes(wrd.cfail))) { // Identified test result in array item. await haystacks.consoleLog(namespacePrefix, functionName + eventName, app_msg.csocketServerDataEventHandlerMessage05); if (objMessage?.toLowerCase()?.includes(wrd.cpass)) { testResult = wrd.cpass; } else if (objMessage?.toLowerCase()?.includes(wrd.cwarning)) { testResult = wrd.cwarning; } else if (objMessage?.toLowerCase()?.includes(wrd.cfail)) { testResult = wrd.cfail; } // Exit the loop as soon as a matching item is found break; } } hasMessage = json.every(v => v[wrd.cmessage]); // Result of every() check for messages: await haystacks.consoleLog(namespacePrefix, functionName + eventName, app_msg.csocketServerDataEventHandlerMessage06 + hasMessage); } else { // JSON is an object await haystacks.consoleLog(namespacePrefix, functionName + eventName, app_msg.csocketServerDataEventHandlerMessage07); hasMessage = json[wrd.cmessage]; if (hasMessage) { // Extracted message from JSON object: await haystacks.consoleLog(namespacePrefix, functionName + eventName, app_msg.csocketServerDataEventHandlerMessage08 + hasMessage); if (hasMessage.includes(app_msg.cTestResultsLog) && hasMessage.includes(wrd.cTest + bas.cUnderscore) && (hasMessage.toLowerCase().includes(wrd.cpass) || hasMessage.toLowerCase().includes(wrd.cwarning) || hasMessage.toLowerCase().includes(wrd.cfail))) { // Identified test result in JSON object. await haystacks.consoleLog(namespacePrefix, functionName + eventName, app_msg.csocketServerDataEventHandlerMessage09); if (hasMessage.toLowerCase().includes(wrd.cpass)) { testResult = wrd.cpass; } else if (hasMessage.toLowerCase().includes(wrd.cwarning)) { testResult = wrd.cwarning; } else if (hasMessage.toLowerCase().includes(wrd.cfail)) { testResult = wrd.cfail; } } } // End-if (hasMessage) } if (hasMessage) { // Enqueuing parsed JSON to message queue await haystacks.consoleLog(namespacePrefix, functionName + eventName, app_msg.csocketServerDataEventHandlerMessage10); await messageQueue.enqueue(json); // Enqueued successfully. Queue size: await haystacks.consoleLog(namespacePrefix, functionName + eventName, app_msg.csocketServerDataEventHandlerMessage11 + await messageQueue.size()); } while(!await messageQueue.isEmpty()) { const {message} = await messageQueue.dequeue(); // Dequeued message for console log: await haystacks.consoleLog(namespacePrefix, functionName + eventName, app_msg.csocketServerDataEventHandlerMessage12 + message); // Success raw chunk data: await haystacks.consoleLog(namespacePrefix, functionName + eventName, app_msg.csocketServerDataEventHandlerMessage13 + chunk.toString()); console.log(message); } } } catch ({ message }) { // Failed retrieving data from client: console.log(bas.cCarRetNewLin + app_msg.cErrorSocketServerMessage02 + message); await haystacks.consoleLog(namespacePrefix, functionName + eventName, app_msg.cErrorSocketServerMessage02 + message); // Failed raw chunk data: await haystacks.consoleLog(namespacePrefix, functionName + eventName, app_msg.csocketServerDataEventHandlerMessage14 + chunk.toString()); } await haystacks.consoleLog(namespacePrefix, functionName + eventName, msg.cEND_Event); }; // Handle drain const handleDrain = async (socket) => { if (!messageQueue.isEmpty()) { const eventName = bas.cDot + wrd.cdrain; await haystacks.consoleLog(namespacePrefix, functionName + eventName, msg.cBEGIN_Event); const json = await messageQueue.dequeue(); await test(json, eventName); await haystacks.consoleLog(namespacePrefix, functionName + eventName, msg.cEND_Event); socket.resume(); } } // Handles actions to take when the connection closes. const handleClose = async () => { const eventName = bas.cDot + wrd.cclose; await haystacks.consoleLog(namespacePrefix, functionName + eventName, msg.cBEGIN_Event); isConnected = false; // Show error only if connection did not close successfully await haystacks.consoleLog(namespacePrefix, functionName + eventName, msg.cEND_Event); await processWriteAsync(bas.cGreaterThan); }; // Gracefully exits process, when user attempts a "q" (quit) / ctrl-c. process.on(gen.cSIGINT, async () => { const eventName = bas.cDot + gen.csigint; await bannerLog(eventName, () => { isConnected = false; // Disconnecting gracefully console.log(bas.cCarRetNewLin + app_msg.cDisconnectingGracefully + bas.cCarRetNewLin); }); process.exit(); }); // Start listening for connections const handleConnect = async () => { const eventName = bas.cDot + wrd.cconnect; await haystacks.consoleLog(namespacePrefix, functionName + eventName, msg.cBEGIN_Event); testResultRetrieved = false; serverHasEnded = false; if (!isConnected) { server.listen(SOCKET.port, SOCKET.host, handleListening); } await haystacks.consoleLog(namespacePrefix, functionName + eventName, msg.cEND_Event); } // Stop listening and close connection const handleDisconnect = async () => { const eventName = bas.cDot + wrd.cdisconnect; await haystacks.consoleLog(namespacePrefix, functionName + eventName, msg.cBEGIN_Event); if (isConnected){ isConnected = false; haystacks.consoleLog(namespacePrefix, functionName + eventName, msg.cEND_Event); } await haystacks.consoleLog(namespacePrefix, functionName + eventName, msg.cEND_Event); } // Indicates the end of the server connection and properly closes server to enable smooth re-runs const handleEnd = async (serverInstance) => { const eventName = bas.cDot + wrd.cend; await bannerLog(eventName, async () => { isConnected = false; // Server connection has ended! // Message Queue: // console.log(app_msg.cMessageQueueColon, await messageQueue.size()) console.log(bas.cCarRetNewLin + app_msg.cErrorSocketServerMessage03 + bas.cCarRetNewLin); }); // await processWriteAsync(bas.cGreaterThan); // ! IMPORTANT allow re-runs serverHasEnded = true; testResult = null; testResultRetrieved = false serverInstance.close(); } // After retrieving testResult // Ends test after a number of seconds if it has not // already ended const beginEndOfScriptCountDown = async (allottedTimeInSeconds = 20) => { if (testResultRetrieved) { await new Promise(resolve => { setTimeout(() => { // Closing...Timeout reached for end of script! console.log(app_msg.cClosingTimeoutEndOfScript); server.close(); }, allottedTimeInSeconds * 1000); }) } } // Keep checking during the given allotted time for the test result. // If no value is provided or time has passed send error. const getTestResult = async (allottedTimeInSeconds) => { // console.log('calling getTestResult'); return await new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { // Error: The allotted time to retrieve the test result has passed. Try again later. reject(app_msg.cgetTestResultsError01); }, allottedTimeInSeconds * 1000); const checkResult = () => { if (serverHasEnded) { resolve(testResult || wrd.cfail); } else { if (typeof testResult === wrd.cstring && testResult.length){ clearTimeout(timeoutId); testResultRetrieved = true; beginEndOfScriptCountDown(); resolve(testResult); } else { setTimeout(checkResult, 100); } } } // Restart test. checkResult(); }); } // Return server instance server = createServer(async socket => { handleConnection(); socket.on(wrd.cdata, async chunk => { socket.emit(wrd.cdisconnect); await handleData(chunk, socket); }); socket.on(wrd.cerror, async (error) => { await handleError(error, server); }); socket.on(wrd.cdisconnect, handleDisconnect); socket.on(wrd.cclose, async () => { await handleClose(); }); socket.on(wrd.cend, async () => { await handleEnd(server) }); socket.on(wrd.cdrain, async () => { await handleDrain(socket) }); socket.on(wrd.cpause, () => { // Client has paused due to backpressure. console.log(app_msg.cClientPausedBackpressure); }); }); server.getTestResult = getTestResult; server.connect = handleConnect; server.terminate = async () => { await new Promise(resolve => { server.close(); resolve(); }); }; // invoke optional command if server.serverHasEndedCallback = async (callback, allottedTimeInSeconds = 5) => { await new Promise((resolve) => { // Calling serverHasEndedCallback!! console.log(app_msg.cCallingServerHasEndedCallbackMessage01); const timeoutId = setTimeout(() => { // doing bad!! console.log(app_msg.cCallingServerHasEndedCallbackMessage02) // Test has failed if nothing happened in the allottedTimeInSeconds! resolve(callback(true)); }, allottedTimeInSeconds * 1000); // Keep checking if the server has ended // return status whether result was returned via callback const checkStatus = () => { if (serverHasEnded && testResultRetrieved){ clearTimeout(timeoutId); // test has not (yet) failed resolve(callback(false)); } else { setTimeout(checkStatus, 100); } } // Restart check. checkStatus(); }); } return server; } catch ({ code, message }) { if (code == gen.cEADDRINUSE ) { // already in use... console.log(app_msg.calreadyInUse); } else { // Socket server failed: console.log(bas.cCarRetNewLin + app_msg.cSocketServerFailed + message); } } haystacks.consoleLog(namespacePrefix, functionName, msg.cEND_Function); }