diff --git a/.gitignore b/.gitignore index b512c09..debc544 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,41 @@ -node_modules \ No newline at end of file +### https://raw.github.com/github/gitignore/9779d87d3d81ffacd90ae2470e7b3ffe566ffc71/node.gitignore + +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + + diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..051413b --- /dev/null +++ b/circle.yml @@ -0,0 +1,3 @@ +test: + override: + - npm run test diff --git a/cli.js b/cli.js index aefaef5..52870cf 100644 --- a/cli.js +++ b/cli.js @@ -64,7 +64,7 @@ for (var module in natives) { define_native(module); } -cli.output = console.log; +cli.output = console.error; cli.exit = require('exit'); cli.no_color = false; @@ -333,7 +333,7 @@ cli.parse = function (opts, command_def) { if (cli.version == null) { cli.parsePackageJson(); } - console.error(cli.app + ' v' + cli.version); + cli.output(cli.app + ' v' + cli.version); cli.exit(); break; } else if (enable.catchall && (o === 'c' || o === 'catch')) { @@ -453,13 +453,13 @@ cli.status = function (msg, type) { } msg = pre + ' ' + msg; if (type === 'fatal') { - console.error(msg); + cli.output(msg); return cli.exit(1); } if (enable.status && !show_debug && type === 'debug') { return; } - console.error(msg); + cli.output(msg); }; ['info','error','ok','debug','fatal'].forEach(function (type) { cli[type] = function (msg) { @@ -582,11 +582,11 @@ cli.getUsage = function (code) { usage = usage || cli.app + ' [OPTIONS]' + (command_list.length ? ' ' : '') + ' [ARGS]'; if (cli.no_color) { - console.error('Usage:\n ' + usage); - console.error('Options: '); + cli.output('Usage:\n ' + usage); + cli.output('Options: '); } else { - console.error('\x1b[1mUsage\x1b[0m:\n ' + usage); - console.error('\n\x1b[1mOptions\x1b[0m: '); + cli.output('\x1b[1mUsage\x1b[0m:\n ' + usage); + cli.output('\n\x1b[1mOptions\x1b[0m: '); } for (var opt in opt_list) { @@ -634,42 +634,42 @@ cli.getUsage = function (code) { line = pad(line, switch_pad); line += trunc_desc(line, desc); line += optional ? ' (Default is ' + optional + ')' : ''; - console.error(line.replace('%s', '%\0s')); + cli.output(line.replace('%s', '%\0s')); seen_opts.push(short); seen_opts.push(long); } if (enable.timeout && seen_opts.indexOf('t') === -1 && seen_opts.indexOf('timeout') === -1) { - console.error(pad(' -t, --timeout N', switch_pad) + 'Exit if the process takes longer than N seconds'); + cli.output(pad(' -t, --timeout N', switch_pad) + 'Exit if the process takes longer than N seconds'); } if (enable.status) { if (seen_opts.indexOf('k') === -1 && seen_opts.indexOf('no-color') === -1) { - console.error(pad(' -k, --no-color', switch_pad) + 'Omit color from output'); + cli.output(pad(' -k, --no-color', switch_pad) + 'Omit color from output'); } if (seen_opts.indexOf('debug') === -1) { - console.error(pad(' --debug', switch_pad) + 'Show debug information'); + cli.output(pad(' --debug', switch_pad) + 'Show debug information'); } } if (enable.catchall && seen_opts.indexOf('c') === -1 && seen_opts.indexOf('catch') === -1) { - console.error(pad(' -c, --catch', switch_pad) + 'Catch unanticipated errors'); + cli.output(pad(' -c, --catch', switch_pad) + 'Catch unanticipated errors'); } if (enable.version && seen_opts.indexOf('v') === -1 && seen_opts.indexOf('version') === -1) { - console.error(pad(' -v, --version', switch_pad) + 'Display the current version'); + cli.output(pad(' -v, --version', switch_pad) + 'Display the current version'); } if (enable.help && seen_opts.indexOf('h') === -1 && seen_opts.indexOf('help') === -1) { - console.error(pad(' -h, --help', switch_pad) + 'Display help and usage details'); + cli.output(pad(' -h, --help', switch_pad) + 'Display help and usage details'); } if (command_list.length) { - console.error('\n\x1b[1mCommands\x1b[0m: '); + cli.output('\n\x1b[1mCommands\x1b[0m: '); if (!Array.isArray(commands)) { for (var c in commands) { line = ' ' + pad(c, switch_pad - 2); line += trunc_desc(line, commands[c]); - console.error(line); + cli.output(line); } } else { command_list.sort(); - console.error(' ' + trunc_desc(' ', command_list.join(', '))); + cli.output(' ' + trunc_desc(' ', command_list.join(', '))); } } return cli.exit(code); @@ -970,7 +970,7 @@ cli.main = function (callback) { cli.createServer = function(/*layers*/) { var defaultStackErrorHandler = function (req, res, err) { if (err) { - console.error(err.stack); + cli.output(err.stack); res.writeHead(500, {"Content-Type": "text/plain"}); return res.end(err.stack + "\n"); } diff --git a/package.json b/package.json old mode 100755 new mode 100644 index 4d9f559..b7bf385 --- a/package.json +++ b/package.json @@ -17,6 +17,15 @@ "glob": "^7.1.1", "exit": "0.1.2" }, + "devDependencies": { + "intelli-espower-loader": "^1.0.1", + "mocha": "^3.2.0", + "power-assert": "^1.4.2", + "proxyquire": "^1.7.11" + }, + "scripts": { + "test": "mocha --require intelli-espower-loader" + }, "contributors": [ { "name": "Douglas Meyer", "github": "https://github.com/DouglasMeyer" } ], diff --git a/test/test_cli.js b/test/test_cli.js new file mode 100644 index 0000000..59ed208 --- /dev/null +++ b/test/test_cli.js @@ -0,0 +1,495 @@ +var assert = require('power-assert'); +var util = require('./util'); +var spawn = require('child_process').spawn; +var stream = require('stream'); + +/** + * test of parsing options + */ +describe('options', function() { + it('example options', function() { + process.argv = ['node', 'test.js', '--file=access.log', '--time=42', '--work=awake' ]; + var cli = util.createCliMock(); + cli.parse({ + file: [ 'f', 'A file to process', 'file', 'temp.log' ], + time: [ 't', 'An access time', 'time', false ], + work: [ false, 'What kind of work to do', 'string', 'sleep' ] + }); + assert(cli.options.file === 'access.log'); + assert(cli.options.time === 42); + assert(cli.options.work === 'awake'); + }); + + it('full name', function() { + process.argv = ['node', 'test.js', '--hoge=hogeoption1' ]; + var cli = util.createCliMock(); + cli.parse({ + hoge: [ 'o', '', 'string' ] + }); + assert(cli.options.hoge === 'hogeoption1'); + }); + + it('short name', function() { + process.argv = ['node', 'test.js', '-o', 'hogeoption2' ]; + var cli = util.createCliMock(); + cli.parse({ + hoge: [ 'o', '', 'string' ] + }); + assert(cli.options.hoge === 'hogeoption2'); + }); + + it('default option', function() { + process.argv = ['node', 'test.js' ]; + var cli = util.createCliMock(); + cli.parse({ + hoge: [ 'h', '', 'string', 'hogeoption3' ] + }); + assert(cli.options.hoge === 'hogeoption3'); + }); + + it('as-is', function() { + process.argv = ['node', 'test.js', '--string=x123x', '--int=123', '--boolean=true' ]; + var cli = util.createCliMock(); + cli.parse({ + string: [ false, '', 'string' ], + int: [ false, '', 'string' ], + boolean: [ false, '', 'string' ] + }); + assert(cli.options.string === 'x123x'); + assert(cli.options.int === 123); + assert(cli.options.boolean); + }); + + it('valid int', function() { + process.argv = ['node', 'test.js', '--int=123', '--number=0123', '--num=-42', '--time=-1', '--seconds=65536', '--secs=-65536', '--minutes=1', '--mins=3', '--x=110', '--n=119' ]; + var cli = util.createCliMock(); + cli.parse({ + int: [ false, '', 'int' ], + number: [ false, '', 'number' ], + num: [ false, '', 'num' ], + time: [ false, '', 'time' ], + seconds: [ false, '', 'seconds' ], + secs: [ false, '', 'secs' ], + minutes: [ false, '', 'minutes' ], + mins: [ false, '', 'mins' ], + x: [ false, '', 'x' ], + n: [ false, '', 'n' ], + }); + assert(cli.options.int === 123); + assert(cli.options.number === 123); + assert(cli.options.num === -42); + assert(cli.options.time === -1); + assert(cli.options.seconds === 65536); + assert(cli.options.secs === -65536); + assert(cli.options.minutes === 1); + assert(cli.options.mins === 3); + assert(cli.options.x === 110); + assert(cli.options.n === 119); + }); + + it('invalid int', function() { + process.argv = ['node', 'test.js', '--int', 'x123x' ]; + var cli = util.createCliMock(); + cli.output = function() {}; + cli.parse({ + int: [ false, '', 'int' ] + }); + assert(util.exitCode !== 0); + }); + + it('valid date', function() { + process.argv = ['node', 'test.js', '--date=2017-03-01', '--datetime="2017-02-28T15:42:43"', '--date_time="Mar 2 2018 16:44:45 UTC"' ]; + var cli = util.createCliMock(); + cli.parse({ + date: [ false, '', 'date' ], + datetime: [ false, '', 'datetime' ], + date_time: [ false, '', 'date_time' ] + }); + assert(cli.options.date.getTime() === 1488326400000); + assert(cli.options.datetime.getTime() === 1488296563000); + assert(cli.options.date_time.getTime() === 1520009085000); + }); + + it('invalid date', function() { + process.argv = ['node', 'test.js', '--date', 'this is invalid date string' ]; + var cli = util.createCliMock(); + cli.output = function() {}; + cli.parse({ + date: [ false, '', 'date' ] + }); + assert(util.exitCode !== 0); + }); + + it('valid float', function() { + process.argv = ['node', 'test.js', '--float=1.23', '--decimal=-9.99999' ]; + var cli = util.createCliMock(); + cli.parse({ + float: [ false, '', 'float' ], + decimal: [ false, '', 'decimal' ] + }); + assert(cli.options.float === 1.23); + assert(cli.options.decimal === -9.99999); + }); + + it('invalid float', function() { + process.argv = ['node', 'test.js', '--float', '--++**' ]; + var cli = util.createCliMock(); + cli.output = function() {}; + cli.parse({ + float: [ false, '', 'float' ] + }); + assert(util.exitCode !== 0); + }); + + it('valid file', function() { + process.argv = ['node', 'test.js', '--file=/absolute/path/to/file', '--path=./relative/path/to/file', '--directory=somedirectory/sub', '--dir=../parent/sub' ]; + var cli = util.createCliMock(); + cli.parse({ + file: [ false, '', 'file' ], + path: [ false, '', 'path' ], + directory: [ false, '', 'directory' ], + dir: [ false, '', 'dir' ], + }); + assert(cli.options.file === '/absolute/path/to/file'); + assert(cli.options.path === './relative/path/to/file'); + assert(cli.options.directory === 'somedirectory/sub'); + assert(cli.options.dir === '../parent/sub'); + }); + + it('invalid file', function() { + process.argv = ['node', 'test.js', '--file', 'path:with*invalid*%token' ]; + var cli = util.createCliMock(); + cli.output = function() {}; + cli.parse({ + file: [ false, '', 'path' ] + }); + assert(util.exitCode !== 0); + }); + + it('valid email', function() { + process.argv = ['node', 'test.js', '--email="sample@sample.com"' ]; + var cli = util.createCliMock(); + cli.parse({ + email: [ false, '', 'email' ] + }); + assert(cli.options.email === 'sample@sample.com'); + }); + + it('invalid email', function() { + process.argv = ['node', 'test.js', '--email', 'invaid@email@format/xyz.com$invalid' ]; + var cli = util.createCliMock(); + cli.output = function() {}; + cli.parse({ + email: [ false, '', 'email' ] + }); + assert(util.exitCode !== 0); + }); + + it('valid url', function() { + process.argv = ['node', 'test.js', '--url="http://sample.com/"', '--uri="ftp://user:password@sample.com/path"', '--domain=subdomain.domain.biz', '--host=hostname.local' ]; + var cli = util.createCliMock(); + cli.parse({ + url: [ false, '', 'url' ], + uri: [ false, '', 'uri' ], + domain: [ false, '', 'domain' ], + host: [ false, '', 'host' ] + }); + assert(cli.options.url === 'http://sample.com/'); + assert(cli.options.uri === 'ftp://user:password@sample.com/path'); + assert(cli.options.domain === 'subdomain.domain.biz'); + assert(cli.options.host === 'hostname.local'); + }); + + it('invalid url', function() { + process.argv = ['node', 'test.js', '--url', ':-)' ]; + var cli = util.createCliMock(); + cli.output = function() {}; + cli.parse({ + url: [ false, '', 'url' ] + }); + assert(util.exitCode !== 0); + }); + + it('valid ip', function() { + process.argv = ['node', 'test.js', '--ip=192.168.0.1' ]; + var cli = util.createCliMock(); + cli.parse({ + ip: [ false, '', 'ip' ] + }); + assert(cli.options.ip === '192.168.0.1'); + }); + + it('invalid ip', function() { + process.argv = ['node', 'test.js', '--ip', '00:ff:00:ff:00:ff' ]; + var cli = util.createCliMock(); + cli.output = function() {}; + cli.parse({ + ip: [ false, '', 'ip' ] + }); + assert(util.exitCode !== 0); + }); + + it('true presented', function() { + process.argv = ['node', 'test.js', '--bool=true', '--boolean=0', '--on=yes' ]; + var cli = util.createCliMock(); + cli.parse({ + bool: [ false, '', 'bool' ], + boolean: [ false, '', 'boolean' ], + on: [ false, '', 'on' ] + }); + assert(cli.options.bool); + assert(cli.options.boolean); + assert(cli.options.on); + }); + + it('true not presented', function() { + process.argv = ['node', 'test.js' ]; + var cli = util.createCliMock(); + cli.parse({ + bool: [ false, '', 'bool' ], + boolean: [ false, '', 'boolean' ], + on: [ false, '', 'on' ] + }); + assert(cli.options.bool === null); + assert(cli.options.boolean === null); + assert(cli.options.on === null); + }); + + it('false presented', function() { + process.argv = ['node', 'test.js', '--false=true', '--off=0', '--false_opt', '--0_opt' ]; + var cli = util.createCliMock(); + cli.parse({ + 'false': [ false, '', 'false' ], + off: [ false, '', 'off' ], + false_opt: [ false, '', false ], + '0_opt': [ false, '', 0 ] + }); + assert(cli.options.false === false); + assert(cli.options.off === false); + assert(cli.options.false_opt === false); + assert(cli.options['0_opt'] === false); + }); + + it('true not presented', function() { + process.argv = ['node', 'test.js' ]; + var cli = util.createCliMock(); + cli.parse({ + 'false': [ false, '', 'false' ], + off: [ false, '', 'off' ], + false_opt: [ false, '', false ], + '0_opt': [ false, '', 0 ] + }); + assert(cli.options.false === null); + assert(cli.options.off === null); + assert(cli.options.false_opt === null); + assert(cli.options['0_opt'] === null); + }); +}); + +/** + * test of parsing commands + */ +describe('commands', function() { + it('defined command', function() { + process.argv = ['node', 'test.js', 'command2']; + var cli = util.createCliMock(); + cli.parse({}, ['command1', 'command2']); + assert(cli.command === 'command2'); + }); + + it('undefined command', function() { + process.argv = ['node', 'test.js', 'command3']; + var cli = util.createCliMock(); + cli.output = function() {}; + cli.parse({}, ['command1', 'command2']); + assert(util.exitCode !== 0); + }); + + it('auto-completion', function() { + process.argv = ['node', 'test.js', 'i']; + var cli = util.createCliMock(); + cli.parse({}, ['install', 'uninstall']); + assert(cli.command === 'install'); + }); +}); + +/** + * test of parsing args + */ +describe('args', function() { + it('no args', function() { + process.argv = ['node', 'test.js']; + var cli = util.createCliMock(); + cli.parse(); + assert(cli.args.length === 0); + }); + + it('2 args', function() { + process.argv = ['node', 'test.js', 'arg1', 'arg2']; + var cli = util.createCliMock(); + cli.parse(); + assert.deepEqual(cli.args, ['arg1', 'arg2']); + }); +}); + + +/** + * test of helpers + */ +describe('helpers', function() { + it('withStdin', function(done) { + var child = spawn('node', ['test/withstdin.js']); + var input = ['this', 'is', 'sample input']; + child.on('close', function() { + }); + child.stdout.on('data', function(data) { + assert(data.toString() === 'withStdin return: ' + input.join('\n')); + done(); + }); + child.stdin.write(input.join('\n')); + child.stdin.end(); + }); + + it('withStdinLines', function(done) { + var child = spawn('node', ['test/withstdinlines.js']); + var input = ['this', 'is', 'sample input']; + child.on('close', function() { + }); + child.stdout.on('data', function(data) { + assert(data.toString() === 'withStdinLines return: ' + input.join(':')); + done(); + }); + child.stdin.write(input.join('\n')); + child.stdin.end(); + }); + + it('toType', function() { + process.argv = ['node', 'test.js' ]; + var cli = util.createCliMock(); + function myFunc() {} + assert(cli.toType(myFunc) === 'function'); + assert(cli.toType([]) === 'array'); + assert(cli.toType(new Date()) === 'date'); + assert(cli.toType(1) === 'integer'); + assert(cli.toType(1.1) === 'float'); + assert(cli.toType(Math) === 'math'); + assert(cli.toType(/a/) === 'regexp'); + assert(cli.toType(JSON) === 'json'); + }); + + it('progress', function(done) { + process.argv = ['node', 'test.js' ]; + var cli = util.createCliMock(); + var outStream = new stream.Writable(); + outStream._write = function (chunk, encoding, w_done) { + assert(chunk.toString() === '50%...'); + done(); + }; + cli.progress(0.5, 0, outStream); + }); + + it('exec', function(done) { + process.argv = ['node', 'test.js' ]; + var cli = util.createCliMock(); + cli.exec('echo \"hello\nworld\"', function(lines) { + assert(lines, ['hello', 'world']); + done(); + }); + }); +}); + + +/** + * test of plugins + */ +describe('plugins', function() { + it('help enabled', function(done) { + process.argv = ['node', 'test.js', '--help' ]; + var cli = util.createCliMock(); + // this plugin is enabled by default + //cli.enable('help'); + cli.no_color = true; + cli.output = function(line) { + assert(line.indexOf('Usage:') === 0); + done(); + }; + cli.parse(); + }); + + it('help disabled', function() { + process.argv = ['node', 'test.js', '--help' ]; + var cli = util.createCliMock(); + cli.disable('help'); + cli.parse(); + assert(util.exitCode !== 0); + }); + + it('version enabled', function(done) { + process.argv = ['node', 'test.js', '--version' ]; + var cli = util.createCliMock(); + cli.enable('version'); + cli.app ='test_app'; + cli.version = '0.1'; + cli.output = function(line) { + assert(line.indexOf('test_app v0.1') === 0); + done(); + }; + cli.parse(); + }); + + it('verstion disabled', function() { + process.argv = ['node', 'test.js', '--version' ]; + var cli = util.createCliMock(); + // this plugin is disabled by default + //cli.disable('version'); + cli.parse(); + assert(util.exitCode !== 0); + }); + + it('status enabled', function() { + process.argv = ['node', 'test.js', '-k' ]; + var cli = util.createCliMock(); + cli.enable('status'); + cli.parse(); + assert(cli.no_color === true); + }); + + it('status disabled', function() { + process.argv = ['node', 'test.js', '-k' ]; + var cli = util.createCliMock(); + // this plugin is disabled by default + //cli.disable('status'); + cli.parse(); + assert(cli.no_color === false); + }); + + // glob plugin is not working?? + // glob module is required in cli.js but is never used + + it('timeout enabled', function(done) { + process.argv = ['node', 'test.js', '--timeout=1' ]; + var cli = util.createCliMock(); + cli.enable('timeout'); + cli.output = function() {} + cli.parse(); + setTimeout(function() { + assert(util.exitCode !== 0); + done(); + }, 1100); + }); + + it('timeout disabled', function(done) { + process.argv = ['node', 'test.js' ]; + var cli = util.createCliMock(); + // this plugin is disabled by default + //cli.disable('timeout'); + cli.parse(); + setTimeout(function() { + assert(util.exitCode === undefined); + done(); + }, 1100); + }); + + // test of catchall plugin is difficult in test method +}); + diff --git a/test/util.js b/test/util.js new file mode 100644 index 0000000..64e480c --- /dev/null +++ b/test/util.js @@ -0,0 +1,21 @@ +var proxyquire = require('proxyquire'); + +/** exit code called from cli by using exit package */ +var exitCode = undefined; + +/** + * create cli mock using stub of exit package + */ +function createCliMock() { + exitCode = undefined; + return proxyquire('../cli', { + exit: function(code) { + exitCode = code; + } + }); +} + +module.exports = { + createCliMock: createCliMock, + exitCode: exitCode +} diff --git a/test/withstdin.js b/test/withstdin.js new file mode 100644 index 0000000..6b051e5 --- /dev/null +++ b/test/withstdin.js @@ -0,0 +1,7 @@ +var util = require('./util'); + +var cli = util.createCliMock(); +cli.output = function() {} +cli.withStdin(function(lines) { + process.stdout.write('withStdin return: ' + lines); +}); diff --git a/test/withstdinlines.js b/test/withstdinlines.js new file mode 100644 index 0000000..dc61b52 --- /dev/null +++ b/test/withstdinlines.js @@ -0,0 +1,7 @@ +var util = require('./util'); + +var cli = util.createCliMock(); +cli.output = function() {} +cli.withStdinLines(function(lines) { + process.stdout.write('withStdinLines return: ' + lines.join(':')); +});