lamed_test
Version:
Mocha unit testing made easy
476 lines (428 loc) • 18.1 kB
JavaScript
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
}