UNPKG

lamed_test

Version:
476 lines (428 loc) 18.1 kB
console.log(`Starting ${__filename}...`) // comment line to remove simple logging // Purpose: The purpose of this module is to make testing in javascript easy // Date Created: 6/30/2018 // Created by : Perez Lamed van Niekerk // ------------------------------------------------------ /* jshint esversion: 6 */ const _core = require('lamed_core') const {Ok, notOk, notOk_Then, Ok_Then} = _core // eslint-disable-line const { con } = require('lamed_console') // con.useChalk(require('chalk')) const _isEqual = require('lodash.isequal') const { format, typeInfo, compareStr } = require('io_format') const { formatArray } = require('io_format_array') // Logging const { performance } = require('perf_hooks') const isTravis = require('is-travis') // /** // * Compare huge strings // * @param {string / array / number} result - The expected result // * @param {string / array / number} stringValue - The string value to compare against // * @param {int} format - The format level; Default = -1 // * @param {bool} trim - If true -> trim the line before compare. Default is true // * @param {string} lineBr - The line break character. Default is '\ n' // * @returns {boolean} - True if the items and the compare values are the same // */ // function Equal (result, stringValue, format = -1, trim = false, lineBr = '\n') { // return Compare(result, stringValue, format, trim, lineBr) // } /** * Compare huge strings * @param {string / array / number} result - The expected result * @param {string / array / number} stringValue - The string value to compare against * @param {int} format - The format level; Default = -1 * @param {bool} trim - If true -> trim the line before compare. Default is true * @param {string} lineBr - The line break character. Default is '\ n' * @returns {boolean} - True if the items and the compare values are the same */ // function notEqual (result, stringValue, format = -1, trim = false, lineBr = '\n') { // return !Compare(result, stringValue, format, trim, lineBr) // } /** * Test two values for equality * @param {any} result - Any value * @param {any} testValue - Any value * @returns {bool} - True if values are equal */ function notEqual (result, testValue) { return !_isEqual(result, testValue) } /** * Compare huge strings * @param {string / array / number} result - The expected result * @param {string / array / number} stringValue - The string value to compare against * @param {int} format - The format level; Default = -1 * @param {bool} trim - If true -> trim the line before compare. Default is true * @param {string} lineBr - The line break character. Default is '\ n' * @returns {boolean} - True if the items and the compare values are the same */ function Compare (result, stringValue, format1 = -1, trim = false, lineBr = '\n') { // eslint-disable-line if (result === undefined && stringValue === undefined) return true if (result === undefined || stringValue === undefined) { // if (_show.ConsoleOutputGet()) con.logBold('Compare strings mismatch:') // _show.ShowArrayMismatch(0, 0, result, stringValue, [result], [stringValue]) compareStr(result, stringValue, true) return false } result = format(result) stringValue = format(stringValue) // Split huge strings into lines let arrayResult = [] let arrayOutput = [] if (Array.isArray(result) || Array.isArray(stringValue)) { // Do not split if already split into array arrayResult = result arrayOutput = stringValue } else if (lineBr === '') { arrayResult = [result] arrayOutput = [stringValue] } else { // Split on line breaks result = _core.replaceAll(result, '\r', '') // Remove windows extra chars arrayResult = result.split(lineBr) stringValue = _core.replaceAll(stringValue, '\r', '') arrayOutput = stringValue.split(lineBr) } // More detail trace con.traceLine(1) con.trace({ arrayResult }, 1) con.trace({ arrayOutput }, 1) con.traceLine(1) // Do the compare let total = 0 const length = Math.min(arrayResult.length, arrayOutput.length) for (let ii = 0; ii < length; ii++) { let item1 = arrayResult[ii] if (typeof item1 === 'object') { item1 = JSON.stringify(item1) con.trace(' ' + item1, 2) // Only show with detail trace } let compare1 = arrayOutput[ii] if (typeof compare1 === 'object') compare1 = JSON.stringify(compare1) if (trim) { item1 = item1.trim() compare1 = compare1.trim() } // Lines differ if (item1 !== compare1) { // let index = compareStr(item1, compare1, false) // _show.ShowArrayMismatch(index, ii, item1, compare1, arrayResult, arrayOutput) formatArray(arrayResult, arrayOutput, ii, 'result', 'test') return false } con.trace(` ->Compare line: ${ii + 1} (ok)`, 2) total = ii + 1 } if (arrayResult.length !== arrayOutput.length) { // _show.ShowArrayMismatch(0, total, arrayResult[total], arrayOutput[total], arrayResult, arrayOutput) formatArray(arrayResult, arrayOutput, total, 'result', 'test') return false } return true } /** * Simple version of testing multiple values for truth. First argument is string that provide context. * @returns {bool} - Return true if all the arguments are true else return false. */ function testAND () { if (arguments.length === 0) return false let items = arguments /* if first argument is an array, use it */ if (Array.isArray(arguments[0])) { if (arguments.length === 1) items = arguments[0] else throw Error('in testAND()! Only one array argument allowed.') } const module1 = items[0] if (typeof module1 !== 'string') throw new Error(' in testAND(module.filename) not called with module.filename\n') let result = true for (let ii = 1; ii < items.length; ii++) { const item = items[ii] if (_core.notOk(item) || item === false) { result = false // Return false for first item break } } con.trace('') con.traceLine() con.trace({ items, result }) con.traceLine() con.trace('') return result } // /** // * Performs a deep comparison between two values to determine if they are equivalent. // * @param {*} object1 - Compare value1 // * @param {*} object2 - Compare value2 // * @param {bool} objectCheck - If true perform an object check on objects vs. strings // * @param {bool} looseNullCheck - If true then NaN === Null === '' === undefined // * @returns {bool} - Returns true if the objects are the same. // */ // function isEqual (object1, object2, looseNullCheck = false) { // // con.log({object1, object2}) // if (looseNullCheck) if (notOk(object1) && notOk(object2)) return true // // object1 = format(object1) // // object2 = format(object2) // return _isEqual(object1, object2) // } /** * Test the function for the error thrown * @param {function} func - The function to test * @param {bool} throwError - If true throws error * @returns {bool} - Returns false if no error was thrown */ function unThrow (func, message = '', throwError = true) { // Get the function const funcStr = formatFunction(func) // let result = '' // Error test let noErr = true try { // result = func() // Must throw error func() // Must throw error } catch (err) { noErr = false let errMsg = err.message // con.log({ funcStr, errMsg }) if (Ok(errMsg)) errMsg = _core.replaceAll(errMsg, '\n', '') // Remove new lines from error output if (message === '' || message !== errMsg) { con.logRed(`${funcStr}{ERROR}: "${errMsg}"`) if (message === '') return compareStr(message, errMsg, true) if (throwError) throw new Error(`Message not correct: '${message}' !== '${errMsg}'\n`) } else con.traceGreen(`${funcStr} {☺ EXCEPTION}: "${errMsg}"`) // Error thrown 100% correctly } if (noErr) { con.logRed(`\n${funcStr} ==>{NO ERROR}\n`) if (throwError) throw new Error(`Expected error to be thrown from:\n$> ${funcStr}\n`) } return noErr } // unThrow(() => unZip(() => { return [1, '2', 5] }, [1, 2, 5]), "unZip() evaluation failure!") // This should not return an error /** * Convert function() to its string representation * @param {function} func - The function to convert to string * @param {number} minLength - The minimum length of the function * @returns {string} - The function() as string */ function formatFunction (func, addArrow = false, minLength = 30) { // alternate way without eval: // https://gist.github.com/lamberta/3768814 let funcStr = eval(func).toString() // eslint-disable-line funcStr = funcStr.replace('() => ', '') if (addArrow === false) return funcStr // <------------------------------------- // Format the function for better console.log() let funcLen = minLength - funcStr.length if (funcLen < 1) funcLen = 1 funcStr = funcStr + ' '.repeat(funcLen) + '==> ' // Let function always be at least 30 chars long return funcStr } /** * Save the log information when node exit */ function exitHandler (options, exitCode) { if (options.cleanup === false) return // Node did not exit normally if (_log.stats.totalCalls < 20) return // define more test methods before we run if (_log.done) return // Only run once _log.done = true // Calculate averages and % _log.stats.avgTimePerCall = _log.stats.totalTime / _log.stats.totalCalls const maxTime = Math.floor(_log.stats.maxTime) const maxTimeP = Math.floor(_log.stats.maxTime * 100 / _log.stats.totalTime) _log.stats.maxTimeOfTotal = maxTimeP // Provide feedback const good = '√' const badd = 'X' let status = good // con.logGreen('Testing log:\n' + '-'.repeat(12)) if (_log.repeatCalls > 1) con.logGreen(` ${good} ${_log.repeatCalls} repeat(s) per test`) // Success!!! con.logGreen(` ${good} ${_log.stats.totalCalls} unZip() tests passing (${Math.floor(_log.stats.totalTime)}ms)`) // Success!!! if (maxTime > _log.maxTimePerTest) status = badd const maxMsg = ` ${status} Longest test was ${maxTimeP}% of total time @${maxTime}ms (average ${Math.floor(_log.stats.avgTimePerCall)}ms per test)` if (maxTimeP > 10) { // Failure con.logRed(maxMsg) const maxF = _log.stats.maxTimeFunction con.logRed(` --> ${maxF.definition} (${maxF.file}:${maxF.lineNo}:1)`) } else con.logGreen(maxMsg) // Success!!! if (Ok(_lio)) { // Write log file _lio.writeFileSync(_log.file, JSON.stringify(_log, null, 2)) } if (_log.setup === false) { con.log('\n\n // Call from your test runner to configure logging') con.log(' require(\'lamed_test\').unZipLogSetup(__dirname, require(\'lamed_io\'), 1) // npm i lamed_io') } // con.logLine() } /** * Setup logging to of functions to log file. * @param {object} lamedIO - require('lamed_io') * @param {string} dirname - The folder where logs should be saved. If folder does not exist, it will be created. * @param {string} file - The file to save the logs to. Default is 'unZip_date_time.json' * @returns {void} - void */ function unZipLogSetup (dirname, lamedIO, repeatCalls = 1, maxTimePerTest = 200, file = 'unZip.json') { _log.maxTimePerTest = maxTimePerTest _lio = lamedIO // Folder if (notOk(dirname)) dirname = __dirname dirname = _core.replaceAll(dirname, '\\', '/') + '/results/' if (Ok(_lio)) _lio.mkdir(dirname) // Make sure folder exist _log.file = dirname + file _log.repeatCalls = repeatCalls if (_log.setup === false) process.on('exit', exitHandler.bind(null, { cleanup: true })) // process and save log when app is closing _log.setup = true } let _lio // global add-on hook for lamed_io const _log = { setup: false, done: false, repeatCalls: 1, maxTimePerTest: 100, stats: { totalCalls: 0, totalTime: 0, avgTimePerCall: 0, maxTime: 0, maxTimeOfTotal: 0, maxTimeFunction: {} } } // the log file startup structure // async function execute (func) { // return await func() // } /** * Trace the function calling code and then execute it. If test value is provided and do not match -> throw error * @param {function} func - The function to print and execute * @param {any} testValue - The value to test the result against. If no value provided the function and the result will be printed * @param {object} settings - Defaults is {async: false, isSample: true, throwError: true, maxLineLen: 80}; isSample - If true - then this test can be used as a sample. Default is true; throwError - If true and the values does not match -> throw error; maxLineLen - The maximum line length to compare * @returns {bool} - True if the func() === testValue */ function unZip (func, testValue, settings = { isSample: true, throwError: true, maxLineLen: 80 }) { // con.log({ settings }) if (_core.isObject(settings) === false) { // Lets make sure unZip is called correctly con.log({ settings }) throw new Error('Settings needs to be an object') } // Other default values if a new setting was given if (notOk(settings.isSample)) settings.isSample = true if (notOk(settings.throwError)) settings.throwError = true if (notOk(settings.maxLineLen)) settings.maxLineLen = 80 // Get result, timer const t0 = performance.now() const result = func() for (let ii = 1; ii < _log.repeatCalls; ii++) { // Repeat test more than once func() } const timer = performance.now() - t0 // Get the function & type & caller & lineNo const funcStr = formatFunction(func, true) // ==> const funcStr2 = formatFunction(func) const funcName = funcStr.split('(')[0] const resultStr = format(result, 60, 80) const testValueStr = format(testValue, 60, 80) const type = typeInfo(result) const callee = traceCaller() const lineNo = callee.lineNo let caller = callee.caller if (Ok(_lio)) caller = _lio.fileFromPath(caller, true) // Remove path information // Update stats _log.stats.totalCalls = _log.stats.totalCalls + _log.repeatCalls _log.stats.totalTime = _log.stats.totalTime + timer if (timer > _log.stats.maxTime) { _log.stats.maxTimeFunction.file = caller _log.stats.maxTime = timer _log.stats.maxTimeFunction.function = funcName _log.stats.maxTimeFunction.definition = funcStr2 _log.stats.maxTimeFunction.timer = timer _log.stats.maxTimeFunction.lineNo = lineNo } // Output const logStr = ' $> ' + funcStr + type + resultStr if (testValue === undefined) { // Note: '' can be a value con.logGreen(logStr) return // <--------------------------------[ If no test value then show the result and exit } // if (isEqual(result, testValue)) { if (_isEqual(resultStr, testValueStr)) { // Result is correct!!! con.traceGreen(logStr) // Show result in green if trace is active if (Ok(_log.file && resultStr < settings.maxLineLen)) { // Log the results if (notOk(_log.functions)) _log.functions = {} if (notOk(_log.functions[caller])) _log.functions[caller] = {} if (Array.isArray(_log.functions[caller][funcName]) === false) _log.functions[caller][funcName] = [] const sample = '$>' + funcStr2 + ' // ===> ' + resultStr if (sample.includes('\n')) settings.isSample = false // Do not sample multi lines else if (sample.length > settings.maxLineLen) settings.isSample = false // Do not sample very complex function calls const data = { function: funcName, definition: funcStr2, result: resultStr, sample, timer, lineNo, isSample: settings.isSample } _log.functions[caller][funcName].push(data) } return true } else { // if (result !== testValue) // Lets give awesome feedback and show well formed output // ----------------------------------------------------- // result = format(result, 60, 80) // testValue = format(testValue, 60, 80) con.logLine() if (logStr.length < settings.maxLineLen) con.logRed(`${logStr}`) else { con.logRed(' $> ' + funcStr + type) con.log({ result }) } if (typeof resultStr === 'string') { con.reverseRed(`Expected value was -->${testValueStr}<--`) compareStr(resultStr, testValueStr, true, false) } else if (Array.isArray(resultStr)) Compare(resultStr, testValueStr) else con.logRed(`${resultStr} != ${testValueStr} \n`) con.logLine() if (settings.throwError) { const errMsg = 'ERROR:unZip() failure!!!\n' con.logRed(errMsg) throw new Error(errMsg) // unZip() throw this as an error because the test failed (result !== testValue) } return false } } // unZip(() => {return undefined}) // let test = format(undefined) // con.log({ test }) // con.unZip = unZip /** * Get calling function name and line no. */ function traceCaller () { // Use for testing const err = new Error() const info = err.stack.split('at ')[3].trim() // Object.<anonymous> (c:\Projects\lamed_test\test\compare.test.js:70:1) const pos1 = info.indexOf('(') const pos2 = info.indexOf(':', pos1 + 3) const pos3 = info.indexOf(':', pos2 + 1) const caller = info.substring(pos1 + 1, pos2) const lineNo = info.substring(pos2 + 1, pos3) // con.log({ info, caller, lineNo }) return { caller, lineNo } } // Exports -------------------------- module.exports = { // Comparing Functions About: _core.About, nodeVersion: _core.nodeVersion, Ok, notOk, notOk_Then, Ok_Then, rootFolder: _core.rootFolder, isRootFolder: _core.isRootFolder, isTestRunner: _core.isRootFolder, isTravis, Compare, Equal: _isEqual, isEqual: _isEqual, notEqual, unZip, unThrow, unZipLogSetup, compareStr, isObject: _core.isObject, // Testing functions con, testAND }