diff --git a/.gitignore b/.gitignore index 62c5db9..f8990d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules /tmp /build +/test/config.json diff --git a/Makefile b/Makefile index dd5bf85..9c2dff8 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,11 @@ all: .PHONY: test test: - ./test/runtests + ./node_modules/.bin/tape test/unit/*.test.js + +.PHONY: test-integration +test-integration: + ./node_modules/.bin/tape test/integration/*.test.js .PHONY: dumpvar dumpvar: diff --git a/lib/common.js b/lib/common.js index 34083ae..2a5eaaa 100755 --- a/lib/common.js +++ b/lib/common.js @@ -151,7 +151,8 @@ function humanDurationFromMs(ms) { ['ms', 1000, 's'], ['s', 60, 'm'], ['m', 60, 'h'], - ['h', 24, 'd'] + ['h', 24, 'd'], + ['d', 7, 'w'] ]; if (ms === 0) { return '0ms'; @@ -169,7 +170,7 @@ function humanDurationFromMs(ms) { n = Math.floor(n / size[1]); if (n === 0) { break; - } else if (size[2] === 'd') { + } else if (i === sizes.length - 1) { bits.unshift(format('%d%s', n, size[2])); break; } diff --git a/lib/do_networks.js b/lib/do_networks.js index 215866b..ba964e2 100644 --- a/lib/do_networks.js +++ b/lib/do_networks.js @@ -106,10 +106,10 @@ do_networks.options = [ } ]; do_networks.help = ( - 'Show networks.\n' + 'List available networks.\n' + '\n' + 'Usage:\n' - + ' {{name}} account\n' + + ' {{name}} networks\n' + '\n' + '{{options}}' ); diff --git a/package.json b/package.json index de45a08..f477f4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "triton", - "description": "Joyent Triton tool and client (https://www.joyent.com/triton)", + "description": "Joyent Triton CLI and client (https://www.joyent.com/triton)", "version": "1.0.0", "author": "Joyent (joyent.com)", "private": true, @@ -22,6 +22,9 @@ "vasync": "1.6.3", "verror": "1.6.0" }, + "devDependencies": { + "tape": "4.2.0" + }, "bin": { "triton": "./bin/triton" }, diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..3e9ac52 --- /dev/null +++ b/test/README.md @@ -0,0 +1,36 @@ +The node-triton test suite. + +There are two sets of tests here: *unit* tests which can be run in your local +clone (see "test/unit/") and *integration* tests which are run against a +cloudapi. + +**WARNING**: While this test suite should strive to not be destructive to +existing data in the used account, one should take pause before blindly +running it with one's cloudapi creds. + + +# Usage + +Unit tests should be run before commits: + + make test + +Or you can run a specific test file via: + + cd test + ./runtest unit/foo.test.js + + +Integration tests: XXX how to run? + + +# Development Guidelines + +- We are using [tape](https://github.com/substack/tape). + +- Use "test/lib/\*.js" and "test/{unit,integration}/helpers.js" to help make + ".test.js" code more expressive: + +- Unit tests (i.e. not requiring the cloudapi endpoint) in "unit/\*.test.js". + Integration tests "integration/\*.test.js". + diff --git a/test/integration/cli-account.test.js b/test/integration/cli-account.test.js new file mode 100644 index 0000000..3112db7 --- /dev/null +++ b/test/integration/cli-account.test.js @@ -0,0 +1,65 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright (c) 2015, Joyent, Inc. + */ + +/* + * Integration tests for `triton account` + */ + +var h = require('./helpers'); +var test = require('tape'); + + + +// --- Globals + + + +// --- Tests + +test('triton account', function (tt) { + + tt.test(' triton account -h', function (t) { + h.triton('account -h', function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + t.ok(/Usage:\s+triton account/.test(stdout)); + t.end(); + }); + }); + + tt.test(' triton help account', function (t) { + h.triton('help account', function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + t.ok(/Usage:\s+triton account/.test(stdout)); + t.end(); + }); + }); + + tt.test(' triton account', function (t) { + h.triton('account', function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + t.ok(new RegExp('^login: ' + h.CONFIG.account, 'm').test(stdout)); + t.end(); + }); + }); + + tt.test(' triton account -j', function (t) { + h.triton('account -j', function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + var account = JSON.parse(stdout); + t.equal(account.login, h.CONFIG.account, 'account.login'); + t.end(); + }); + }); + +}); diff --git a/test/integration/cli-basics.test.js b/test/integration/cli-basics.test.js new file mode 100644 index 0000000..5e2f8ee --- /dev/null +++ b/test/integration/cli-basics.test.js @@ -0,0 +1,70 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright (c) 2015, Joyent, Inc. + */ + +/* + * Integration tests for `triton ...` CLI basics. + */ + +var h = require('./helpers'); +var test = require('tape'); + + + +// --- Globals + + + +// --- Tests + +test('triton (basics)', function (tt) { + + tt.test(' triton --version', function (t) { + h.triton('--version', function (err, stdout, stderr) { + if (h.ifErr(t, err, 'triton --version')) + return t.end(); + t.ok(/^triton \d+\.\d+\.\d+/.test(stdout)); + t.end(); + }); + }); + + tt.test(' triton -h', function (t) { + h.triton('-h', function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + t.ok(/^Usage:$/m.test(stdout)); + t.ok(/triton help COMMAND/.test(stdout)); + t.ok(/create-instance/.test(stdout)); + t.end(); + }); + }); + + tt.test(' triton --help', function (t) { + h.triton('--help', function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + t.ok(/^Usage:$/m.test(stdout)); + t.ok(/triton help COMMAND/.test(stdout)); + t.ok(/create-instance/.test(stdout)); + t.end(); + }); + }); + + tt.test(' triton help', function (t) { + h.triton('help', function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + t.ok(/^Usage:$/m.test(stdout)); + t.ok(/triton help COMMAND/.test(stdout)); + t.ok(/create-instance/.test(stdout)); + t.end(); + }); + }); + +}); diff --git a/test/integration/helpers.js b/test/integration/helpers.js new file mode 100644 index 0000000..ab26912 --- /dev/null +++ b/test/integration/helpers.js @@ -0,0 +1,86 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright (c) 2015, Joyent, Inc. + */ + +/* + * Test helpers for the integration tests + */ + +var error = console.error; +var assert = require('assert-plus'); +var path = require('path'); + +var testcommon = require('../lib/testcommon'); + + + +// --- globals + +try { + var CONFIG = require('../config.json'); + assert.object(CONFIG, 'test/config.json'); + assert.string(CONFIG.url, 'test/config.json#url'); + assert.string(CONFIG.account, 'test/config.json#account'); + assert.string(CONFIG.key_id, 'test/config.json#key_id'); + assert.optionalBool(CONFIG.insecure, 'test/config.json#insecure'); +} catch (e) { + error('* * *'); + error('node-triton integration tests require a ./test/config.json'); + error(''); + error(' {'); + error(' "url": "",'); + error(' "account": "",'); + error(' "key_id": "",'); + error(' "insecure": true|false // optional'); + error(' }'); + error(''); + error('Note: This test suite with create machines, images, et al using'); + error('this CloudAPI and account. That could *cost* you money. :)'); + error('* * *'); + throw e; +} + +var TRITON = 'node ' + path.resolve(__dirname, '../../bin/triton'); +var UA = 'node-triton-test'; + +var LOG = require('../lib/log'); + + + +// --- internal support routines + +/* + * Call the `triton` CLI with the given args. + */ +function triton(args, cb) { + testcommon.execPlus({ + command: TRITON + ' ' + args, + execOpts: { + maxBuffer: Infinity, + env: { + PATH: process.env.PATH, + HOME: process.env.HOME, + SDC_URL: CONFIG.url, + SDC_ACCOUNT: CONFIG.account, + SDC_KEY_ID: CONFIG.key_id, + SDC_TLS_INSECURE: CONFIG.insecure + } + }, + log: LOG + }, cb); +} + + +// --- exports + +module.exports = { + CONFIG: CONFIG, + triton: triton, + ifErr: testcommon.ifErr +}; diff --git a/test/lib/log.js b/test/lib/log.js new file mode 100644 index 0000000..7d0d12f --- /dev/null +++ b/test/lib/log.js @@ -0,0 +1,27 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright (c) 2015, Joyent, Inc. + */ + +/* + * bunyan logger for tests + */ + +var bunyan = require('bunyan'); +var restifyBunyan = require('restify-clients/lib/helpers/bunyan'); + +module.exports = bunyan.createLogger({ + name: 'node-triton-test', + serializers: restifyBunyan.serializers, + streams: [ + { + level: process.env.LOG_LEVEL || 'error', + stream: process.stderr + } + ] +}); diff --git a/test/lib/testcommon.js b/test/lib/testcommon.js new file mode 100644 index 0000000..03d1cab --- /dev/null +++ b/test/lib/testcommon.js @@ -0,0 +1,84 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright (c) 2015, Joyent, Inc. + */ + +var assert = require('assert-plus'); +var exec = require('child_process').exec; +var VError = require('verror').VError; + + + +// ---- exports + +/** + * A convenience wrapper around `child_process.exec` to take away some + * logging and error handling boilerplate. + * + * @param args {Object} + * - command {String} Required. + * - log {Bunyan Logger} Required. Use to log details at trace level. + * - execOpts {Array} Optional. child_process.exec options. + * - errMsg {String} Optional. Error string to use in error message on + * failure. + * @param cb {Function} `function (err, stdout, stderr)` where `err` here is + * an `VError` wrapper around the child_process error. + */ +function execPlus(args, cb) { + assert.object(args, 'args'); + assert.string(args.command, 'args.command'); + assert.optionalString(args.errMsg, 'args.errMsg'); + assert.optionalObject(args.execOpts, 'args.execOpts'); + assert.object(args.log, 'args.log'); + assert.func(cb); + var command = args.command; + var execOpts = args.execOpts; + + // args.log.trace({exec: true, command: command, execOpts: execOpts}, + // 'exec start'); + exec(command, execOpts, function (err, stdout, stderr) { + args.log.trace({exec: true, command: command, execOpts: execOpts, + err: err, stdout: stdout, stderr: stderr}, 'exec done'); + if (err) { + cb( + new VError(err, + '%s:\n' + + '\tcommand: %s\n' + + '\texit status: %s\n' + + '\tstdout:\n%s\n' + + '\tstderr:\n%s', + args.errMsg || 'exec error', command, err.code, + stdout.trim(), stderr.trim()), + stdout, stderr); + } else { + cb(null, stdout, stderr); + } + }); +} + + + +/** + * Calls t.ifError, outputs the error body for diagnostic purposes, and + * returns true if there was an error + */ +function ifErr(t, err, desc) { + t.ifError(err, desc); + if (err) { + t.deepEqual(err.body, {}, desc + ': error body'); + return true; + } + + return false; +} + + +module.exports = { + execPlus: execPlus, + ifErr: ifErr +}; diff --git a/test/runtests b/test/runtests new file mode 100755 index 0000000..f344565 --- /dev/null +++ b/test/runtests @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# + +# +# Copyright (c) 2015, Joyent, Inc. +# + +# +# Run *integration* tests. +# +# This creates .tap files in OUTPUT_DIR that can be processed by a TAP reader. +# Testing config and log files are also placed in this dir. +# +# Run `./runtests -h` for usage info. +# + +if [ "$TRACE" != "" ]; then + export PS4='${BASH_SOURCE}:${LINENO}: ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' + set -o xtrace +fi +set -o errexit +set -o pipefail + + + +#---- support functions + +function fatal +{ + echo "$(basename $0): fatal error: $*" + exit 1 +} + +function usage +{ + echo "Usage:" + echo " runtests [OPTIONS...]" + echo "" + echo "Options:" + echo " -f FILTER Filter pattern (substring match) for test files to run." + echo " -s Stop on first error." +} + + + +#---- mainline + +start_time=$(date +%s) + +API=docker +TOP=$(cd $(dirname $0)/../; pwd) +NODE_INSTALL=$TOP/build/node +OUTPUT_DIR=/var/tmp/${API}test +TAPE=$TOP/node_modules/.bin/tape +FAILING_LIST=$OUTPUT_DIR/failing-tests.txt + +# Options. +opt_test_pattern= +opt_stop_on_failure= +while getopts "hf:s" opt +do + case "$opt" in + h) + usage + exit 0 + ;; + f) + opt_test_pattern=$OPTARG + ;; + s) + opt_stop_on_failure="true" + ;; + *) + usage + exit 1 + ;; + esac +done + + +# Setup a clean output dir. +echo "# Setup a clean output dir ($OUTPUT_DIR)." +rm -rf $OUTPUT_DIR +mkdir -p /$OUTPUT_DIR +touch $FAILING_LIST + +cd $TOP + + +# Run the integration tests + +echo "" +test_files=$(ls -1 test/integration/*.test.js) +if [[ -n "$opt_test_pattern" ]]; then + test_files=$(echo "$test_files" | grep "$opt_test_pattern" || true) + echo "# Running filtered set of test files: $test_files" +fi + +set +o errexit + +for file in $test_files; do + test_file=$(basename $file) + echo "# $test_file" + PATH=$NODE_INSTALL/bin:$PATH $TAPE $file \ + | tee $OUTPUT_DIR/$test_file.tap + if [[ "$?" != "0" ]]; then + echo $file >> $OUTPUT_DIR/failing-tests.txt + [[ -n "$opt_stop_on_failure" ]] && break + fi +done + +set -o errexit + +echo "" +echo "# test output in $OUTPUT_DIR:" +cd $OUTPUT_DIR +ls *.tap + + +# Colored summary of results (borrowed from smartos-live.git/src/vm/run-tests). +echo "" +echo "# test results:" + +end_time=$(date +%s) +elapsed=$((${end_time} - ${start_time})) + +tests=$(grep "^# tests [0-9]" $OUTPUT_DIR/*.tap | cut -d ' ' -f3 | xargs | tr ' ' '+' | bc) +passed=$(grep "^# pass [0-9]" $OUTPUT_DIR/*.tap | tr -s ' ' | cut -d ' ' -f3 | xargs | tr ' ' '+' | bc) +[[ -z ${tests} ]] && tests=0 +[[ -z ${passed} ]] && passed=0 +fail=$((${tests} - ${passed})) +failing_tests=$(cat ${FAILING_LIST} | wc -l) + +echo "# Completed in ${elapsed} seconds." +echo -e "# \033[32mPASS: ${passed} / ${tests}\033[39m" +if [[ ${fail} -gt 0 ]]; then + echo -e "# \033[31mFAIL: ${fail} / ${tests}\033[39m" +fi + +if [[ ${failing_tests} -gt 0 ]]; then + echo "" + echo -e "# \033[31mFAILING TESTS:\033[39m" + cat $FAILING_LIST | sed -e 's,^,# ,' +fi +echo "" + +exit $failing_tests diff --git a/test/unit/common.test.js b/test/unit/common.test.js new file mode 100644 index 0000000..de25107 --- /dev/null +++ b/test/unit/common.test.js @@ -0,0 +1,40 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright (c) 2015, Joyent, Inc. + */ + +/* + * Unit tests for "lib/common.js". + */ + +var common = require('../../lib/common'); +var test = require('tape'); + + +// ---- globals + +var log = require('../lib/log'); + + +// ---- tests + +test('humanDurationFromMs', function (t) { + var humanDurationFromMs = common.humanDurationFromMs; + var ms = 1000; + var second = 1 * ms; + var minute = 60 * second; + var hour = minute * 60; + var day = hour * 24; + var week = day * 7; + var year = day * 365; + + t.equal(humanDurationFromMs(47*second), '47s'); + t.equal(humanDurationFromMs(1*week), '1w'); + + t.end(); +}); diff --git a/test/unit/helpers.js b/test/unit/helpers.js new file mode 100644 index 0000000..096ac9f --- /dev/null +++ b/test/unit/helpers.js @@ -0,0 +1,23 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright (c) 2015, Joyent, Inc. + */ + +/* + * Test helpers for unit tests + */ + +var testcommon = require('../lib/testcommon'); + + + +// --- Exports + +module.exports = { + ifErr: testcommon.ifErr +};