UNPKG

testarmada-magellan

Version:

Massively parallel automated testing

170 lines (145 loc) 6.09 kB
"use strict"; const _ = require("lodash"); const clc = require("cli-color"); const EventEmitter = require("events").EventEmitter; const StreamSlicer = require("stream-slic3r"); const { Transform } = require("stream"); const logger = require("../logger"); const logStamp = require("./logstamp"); const MESSAGE = "message"; const CLOSE = "close"; const NOT_GOOD_ENUFF_ERROR_MESSAGE = "Connection refused! Is selenium server started?"; const ADDED_ERROR_MESSAGE_CONTEXT = "If running on saucelabs, perhaps " + "you're out of capacity and should TRY RUN AGAIN LATER :)"; // MAKE NIGHWATCH ERROR & WARN LOGS VISIBLE IN MAGELLAN STDOUT: // ------------------------------------------------------------------------------------------------ // Currently the "ERROR" and "WARN" logs from nightwatch are suppressed when the "DEBUG" flag is // turned OFF.. and our customers ALWAYS use the "DEBUG" off, because if you turn it on your log // will be filled with base64 screenshot gobbledegook... Also, with the approach taken, we will // see all the "ERRORED" selenium request/response logs in our magellan log // // relevant code for this feature: // DEBUG, STDOUT_WHITE_LIST, SLICE_ON_TEXT, infoSlicer, isTextWhiteListed // ------------------------------------------------------------------------------------------------ // if the "this.handler.stdout" stream of the childprocess does not // include atleast one of these tokens then it will not be included in the "this.stdout" const NW_ERROR_TAG = "\x1B[1;33mERROR\x1B[0m"; const NW_WARN_TAG = "\x1B[1;32m\x1B[40mWARN\x1B[0m"; const NW_INFO_TAG = "\x1B[1;35m\x1B[40mINFO\x1B[0m"; const STDOUT_WHITE_LIST = [ NW_ERROR_TAG, NW_WARN_TAG, "Test Suite", "✖", "✔", "Running:", "OK." ]; // we slice the VERBOSE nighwatch stdout stream on the purple INFO text that has black background const SLICE_ON_TEXT = NW_INFO_TAG; const applyLineFilter = (text, lineFilter) => { const lines = text.split("\n"); const buff = []; lines.forEach(line => lineFilter(line, buff)); return buff.join("\n"); }; module.exports = class ChildProcess { constructor(handler) { this.stdout = `${clc.yellowBright(logStamp())} =====> Magellan child process start\n`; this.stderr = ""; this.handler = handler; // create the nightwtach INFO slicer: // if the stdout stream does not contain SLICE_ON_TEXT, // then the entire stdout will be emitted 'data' with nothing sliced out // otherwise the stream will be sliced on nighwatch INFO text (purple with black background) // and each "slice" will be emitted 'data' const infoSlicer = new StreamSlicer(SLICE_ON_TEXT); // create the stdout filtering transform, only nightwatch INFO chunks that are white-listed const self = this; const stdoutFilter = new Transform({ transform(data, encoding, callback) { let text = data.toString().trim(); // ignore info chunks that are reporting a failed /timeouts/async_script request // avoids cluttering the log because /timeouts/async_script is not well supported if (text.includes(NW_ERROR_TAG) && text.includes("/timeouts/async_script")) { // we are not losing any key information here, request and response payloads // sit between nw_info_tags and we slice data on nw_info_tag return callback(); } if (text.length > 0 && self.isTextWhiteListed(text)) { // slices that have any one of these indicators // need line-by-line filtering applied to them const lineFilterIndicators = ["✔", "Running:", "OK.", "✖"]; if (lineFilterIndicators.some(item => text.includes(item))) { // apply some line by line filtering const maxLineLength = 512; const lineFilter = (line, buff) => { if (self.isTextWhiteListed(line)) { // limit the length of each line, goal here is to "limit" verbosity buff.push(line.substring(0, maxLineLength)); } }; text = applyLineFilter(text, lineFilter); } text = text .split("\n") .filter((line) => !_.isEmpty(line.trim())) .map((line) => `${clc.yellowBright(logStamp())} ${line}`) .join("\n") + "\n"; self.stdout += text; self.addErrorMessageContext(); this.push(text); } callback(); } }); this.emitter = new EventEmitter(); this.emitter.stdout = stdoutFilter; this.emitter.stderr = handler.stderr; this.emitter.infoSlicer = infoSlicer; // pipe the stdout stream into the slicer and then into the filter this.handler.stdout.pipe(infoSlicer).pipe(stdoutFilter); } enableDebugMsg() { this.handler.on(MESSAGE, (msg) => { logger.debug(`Message from worker: ${JSON.stringify(msg)}`); }); } onMessage(callback) { this.handler.on(MESSAGE, (message) => callback(message)); } addErrorMessageContext() { if (this.stdout.includes(NOT_GOOD_ENUFF_ERROR_MESSAGE)) { if (!this.stdout.includes(ADDED_ERROR_MESSAGE_CONTEXT)) { const replacement = `${NOT_GOOD_ENUFF_ERROR_MESSAGE}\n${clc.yellowBright(logStamp())}` .concat(clc.red(ADDED_ERROR_MESSAGE_CONTEXT)); this.stdout = this.stdout.replace(NOT_GOOD_ENUFF_ERROR_MESSAGE, replacement); } } } isTextWhiteListed(text) { if (process.env.DEBUG) { // in debug mode we do not filter out any text return true; } return STDOUT_WHITE_LIST.some(item => text.includes(item)); } onClose(callback) { this.handler.on(CLOSE, callback); } send(message) { this.handler.send(message); } teardown() { this.handler.stdout.removeAllListeners(); this.handler.stderr.removeAllListeners(); this.handler.stdout.unpipe(); this.handler.stderr.unpipe(); this.handler.removeAllListeners(); this.emitter.stdout = null; this.emitter.stderr = null; } emitMessage(message) { this.emitter.emit(MESSAGE, message); } };