siesta-lite
Version:
Stress-free JavaScript unit testing and functional testing tool, works in NodeJS and browsers
480 lines (341 loc) • 15.8 kB
JavaScript
/*
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
})