UNPKG

siesta-lite

Version:

Stress-free JavaScript unit testing and functional testing tool, works in NodeJS and browsers

480 lines (341 loc) 15.8 kB
/* Siesta 5.6.1 Copyright(c) 2009-2022 Bryntum AB https://bryntum.com/contact https://bryntum.com/products/siesta/license */ Role('Siesta.Launcher.CommandLineTool.BaseTool', { requires : [ 'print', 'printErr', 'readFile', 'printVersion', 'checkIsWindows', 'checkIsMacOS', 'checkIs64Bit', 'getTerminalWidth', 'doExit' ], does : [ Siesta.Util.Role.CanStyleOutput, Siesta.Util.Role.CanFormatStrings, Siesta.Launcher.Role.CanProcessArguments ], has : { // an array of the command line options, 1st one (with 0 index) must be a "binDir" value args : Joose.I.Array, argv : Joose.I.Array, options : null, optionGroups : null, positionalGroups : null, // with trailing slash! binDir : null, isWindows : function () { return this.checkIsWindows() }, isMacOS : function () { return this.checkIsMacOS() }, isLinux : function () { return !this.checkIsWindows() && !this.checkIsMacOS() }, is64Bit : function () { return this.checkIs64Bit() }, is32Bit : function () { return !this.checkIs64Bit() }, executableName : null, helpIntro : function () { return [ 'Usage: ' + this.executableName + ' [OPTIONS]', '' ] }, // should not be used directly, instead via `getTerminalWidth` terminalWidth : null, indentStr : ' ', sep : function () { return this.checkIsWindows() ? '\\' : '/' }, knownOptionGroups : { init : { // '00-sample' : { // name : 'Sample option group' // } } }, knownOptions : { init : [ // { // name : 'sample-option', // desc : [ // 'Description' // ], // group : '00-sample' // } ] } }, methods : { indentText : function (text, level) { level = level || 0 for (var i = 0, indent = ''; i < level; i++, indent += this.indentStr) ; var textArr = text.split('\n') return indent + textArr.join('\n' + indent) }, forEveryOption : function (func, scope) { scope = scope || this var processed = {} for (var meta = this.meta; meta.hasAttribute('knownOptions'); meta = meta.superClass.meta) { var res = Joose.A.each(meta.getAttribute('knownOptions').init, function (option, i) { // do not process option 2nd time (allow option override from parent class) if (processed[ option.name ]) return processed[ option.name ] = true return func.call(scope, option, i) }) if (res === false) return false } }, forEveryOptionGroup : function (func, scope) { scope = scope || this var processed = {} for (var meta = this.meta; meta.hasAttribute('knownOptionGroups'); meta = meta.superClass.meta) { var res = Joose.O.each(meta.getAttribute('knownOptionGroups').init, function (group, id) { // do not process option 2nd time (allow option override from parent class) if (processed[ id ]) return processed[ id ] = true return func.call(scope, group, id) }) if (res === false) return false } }, hasOption : function (name) { var found = this.forEveryOption(function (option) { if (option.name == name) return false }) // a bit unclear, but `found` will be set to `false` if some option has matching name // (early exit from the `forEveryOption` iterator) return found === false }, aliasOptions : function () { var options = this.options Joose.O.each(options, function (value, name) { if (/-/.test(name)) options[ name.replace(/-(\w)/g, function (m, match) { return match.toUpperCase() }) ] = value }) }, parseJson : function (str) { function extractLineFeeds(s) { return s.replace(/[^\n]+/g, ''); } // input is the HanSON string to convert. // if keepLineNumbers is set, toJSON() tried not to modify line numbers, so a JSON parser's // line numbers in error messages will still make sense. function toJSON(input, keepLineNumbers) { var UNESCAPE_MAP = { '\\"': '"', "\\`": "`", "\\'": "'" }; var ML_ESCAPE_MAP = {'\n': '\\n', "\r": '\\r', "\t": '\\t', '"': '\\"'}; function unescapeQuotes(r) { return UNESCAPE_MAP[r] || r; } return input.replace(/`(?:\\.|[^`])*`|'(?:\\.|[^'])*'|"(?:\\.|[^"])*"|\/\*[^]*?\*\/|\/\/.*\n?/g, // pass 1: remove comments function(s) { if (s.charAt(0) == '/') return keepLineNumbers ? extractLineFeeds(s) : ''; else return s; }) .replace(/(?:true|false|null)(?=[^\w_$]|$)|([a-zA-Z_$][\w_\-$]*)|`((?:\\.|[^`])*)`|'((?:\\.|[^'])*)'|"(?:\\.|[^"])*"|(,)(?=\s*[}\]])/g, // pass 2: requote function(s, identifier, multilineQuote, singleQuote, lonelyComma) { if (lonelyComma) return ''; else if (identifier != null) return '"' + identifier + '"'; else if (multilineQuote != null) return '"' + multilineQuote.replace(/\\./g, unescapeQuotes).replace(/[\n\r\t"]/g, function(r) { return ML_ESCAPE_MAP[r]; }) + '"' + (keepLineNumbers ? extractLineFeeds(multilineQuote) : ''); else if (singleQuote != null) return '"' + singleQuote.replace(/\\./g, unescapeQuotes).replace(/"/g, '\\"') + '"'; else return s; }); } return JSON.parse(toJSON(str, false)) }, readJsonFile : function (fileName, errIo, errParse) { var json, str try { str = this.readFile(fileName) } catch (e) { this.printError([ this.formatString(errIo || "Can't read the content of the JSON file: {fileName}", { fileName : fileName }) ]) return } try { json = this.parseJson(str) } catch (e) { this.printError([ this.formatString((errParse || "JSON file {fileName} does not contain valid JSON: ") + e, { fileName : fileName }) ]) return } return json }, readConfigFileOptions : function (fileName) { return this.readJsonFile( fileName, "Can't read the content of the configuration file: {fileName}", "Config file {fileName} does not contain valid JSON: " ) }, prepareOptions : function (callback) { var me = this var processed = this.processArguments(this.args) this.argv = processed.argv this.options = processed.options var options = this.options // add trailing slash if missing this.binDir = this.argv.shift().replace(/\/?$/, '/') if (options.version) { me.printVersion() callback(true) return } if (options.help) { me.printHelp() callback(true) return } if (options[ 'config-file' ]) { var config = this.readConfigFileOptions(options[ 'config-file' ]) if (!config) { callback(true); return } this.options = options = Joose.O.extend(config, options) } Joose.O.each(options, function (value, name) { if (!me.hasOption(name)) { me.warn("Unknown option provided: " + name) } }) callback() }, printHelp : function (callback) { this.printVersion() this.printCopyright() var options = [] var groups = {} this.forEveryOptionGroup(function (group, id) { if (groups[ id ]) throw "Group already defined: " + group.name groups[ id ] = group }) var maxNameLength = 0 this.forEveryOption(function (option) { // ignore options with leading `__` (used for indicating instrumented copy of the project for example) if (option.name.match(/^__/)) return if (!groups[ option.group ]) throw "Option with unknown group: " + option.name + ", " + option.group option.index = options.length options.push(option) if (option.name.length > maxNameLength) maxNameLength = option.name.length }) var terminalWidth = Math.max(this.getTerminalWidth() - 5, 80) var arr = [] arr.length = terminalWidth var spacesStr = arr.join(' ') var dashesStr = arr.join('-') //Header: // --option-name Description // 4 spaces + "--" + 1 space var optionSectionWidth = 4 + 2 + maxNameLength + 4 var descAvailableWidth = terminalWidth - optionSectionWidth options.sort(function (option1, option2) { return option1.group < option2.group ? -1 : option1.group > option2.group ? 1 : option1.index - option2.index }) var helpDesc = this.fitString(this.helpIntro.join(' '), descAvailableWidth, spacesStr) helpDesc.push('') var me = this var indentString = spacesStr.substr(0, optionSectionWidth) var currentGroup Joose.A.each(options, function (option, index) { var optionGroup = groups[ option.group ] if (currentGroup != optionGroup) { if (index > 0) helpDesc.push('') helpDesc.push(me.styled(optionGroup.name + ':', 'bold')) helpDesc.push(me.styled(dashesStr.substr(0, optionGroup.name.length + 1), 'bold')) } currentGroup = optionGroup var optionText = ' --' + me.styled(option.name, 'bold') + spacesStr.substr(0, maxNameLength - option.name.length) + ' ' var optionDesc = (option.desc instanceof Array) ? option.desc.join(' ') : option.desc var lines = me.fitString(optionDesc, descAvailableWidth, spacesStr) Joose.A.each(lines, function (line, index) { if (index == 0) lines[ 0 ] = optionText + lines[ 0 ] else lines[ index ] = indentString + lines[ index ] }) helpDesc.push.apply(helpDesc, lines) }) this.print(helpDesc.join('\n')) callback && callback() }, printCopyright : function () { this.print(this.style().bold("Copyright: ") + "Bryntum AB 2009-" + (new Date().getFullYear()) + "\n") }, fitString : function (string, maxLength, spacesStr) { var lines = [] var parts = string.split(/ /) while (parts.length) { var str = [] var len = 0 var forcedNewLine = false while ( parts.length && (len + parts[ 0 ].length + (str.length ? 1 : 0) <= maxLength || !str.length && parts[ 0 ].length > maxLength) ) { var part = parts.shift() len += part.length + (str.length ? 1 : 0) if (/\n$/.test(part)) forcedNewLine = true str.push(part.replace(/\n$/, '')) if (forcedNewLine) break } // can be negative in case of small `maxLength` var spaceLeft = Math.max(parts.length && !forcedNewLine ? maxLength - len : 0, 0) var fittedStr = '' for (var i = 0; i < str.length; i++) { if (i == 0) fittedStr = str[ 0 ] else { var addition = Math.ceil(spaceLeft / (str.length - i)) fittedStr += spacesStr.substr(0, addition + 1) + str[ i ] spaceLeft -= addition } } lines.push(fittedStr) } return lines }, getPlatformId : function () { if (this.isMacOS) return 'macos' if (this.isWindows) return 'windows' if (this.is64Bit) return 'linux64' return 'linux32' }, prepareText : function (text, addLineBreak, indentLevel, noColor) { if (text instanceof Array) text = text.join('\n') if (this.options[ 'no-color' ] || noColor) text = String(text).replace(/\x1B\[\d+m([\s\S]*?)\x1B\[\d+m/mg, '$1') // normalize line endings text = String(text).replace(/\x0d?\x0a/g, '\n') if (indentLevel) text = this.indentText(text, indentLevel) return text + (addLineBreak ? '\n' : '') }, printError : function (text, indentLevel) { if (text instanceof Array) text = text.join('\n') this.print( this.styled('[' + Siesta.Resource('Siesta.Role.ConsoleReporter', 'errorText') + '] ', 'red') + text, indentLevel ) }, info : function (text, indentLevel) { this.print( this.styled('[INFO] ', 'yellow') + text, indentLevel ) }, warn : function (text, indentLevel) { this.print( this.styled('[' + Siesta.Resource('Siesta.Role.ConsoleReporter', 'warnText') + '] ', 'red') + text, indentLevel ) }, debug : function (text) { if (this.options.debug) this.print(this.styled('[DEBUG] ', 'yellow') + text) }, exit : function (code) { this.doExit(code) } } // eof methods })