Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Fix cli.js for testability. #853

Merged
merged 2 commits into from

1 participant

@mojit0

Reorganized cli.js to make it testable.
Now uses an enclosing function to isolate the use strict pragma.
Now uses a top-level object which is exported and which holds all testable functions/properties.
The internal functions now use _ prefixes and can successfully be mocked.
Added tests for all internal functions.

@mojit0 mojit0 merged commit 55af791 into from
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
Showing with 193 additions and 29 deletions.
  1. +84 −23 lib/management/cli.js
  2. +109 −6 tests/cli/lib/management/test-cli.js
View
107 lib/management/cli.js
@@ -5,17 +5,20 @@
*/
-/*jslint anon:true, node:true */
-'use strict';
+/*jslint anon:true, node:true, nomen:true */
-var utils = require('./utils'),
- libpath = require('path');
+(function() {
-/*
- * A command is expected to export the following:
+'use strict';
+
+/**
+ * @fileoverview Mojito command loader/execution harness. The cli parses the
+ * command line, identifies a command name, and tries to load a file of that
+ * name from the command directory (app/commands by default).
+ *
+ * A command module is expected to export the following:
* run - The function that executes the command. The signature is:
- * run(params, options, callback);
- * usage - Help information for this command, as a string.
+ * run(params, options, callback);
* options - Option info for supported options. Optional.
*
* Option info must be provided as an array of option objects, each with the
@@ -26,11 +29,41 @@ var utils = require('./utils'),
*/
+// ----------------------------------------------------------------------------
+// Prerequisites
+// ----------------------------------------------------------------------------
+
+var utils = require('./utils'),
+ libpath = require('path');
+
+// ----------------------------------------------------------------------------
+// CLI Root Object
+// ----------------------------------------------------------------------------
+
+/**
+ * The Command Line Interpreter object. All testable methods and attributes
+ * are properties of this object.
+ */
+function CLI() {
+};
+
+// ----------------------------------------------------------------------------
+// Internal Helper Functions
+// ----------------------------------------------------------------------------
+
/*
* Creates a map keyed by both short and long option names, to simplify lookup
* of option info from command line args.
+ * @param {Object} optionInfo Option information in the form of an array of
+ * objects containing option descriptions with the following keys:
+ * shortName - Short (1-char) option name, without leading dash (e.g. 'v').
+ * longName - Long option name, without leading dashes (e.g. 'verbose').
+ * hasValue - True if option requires a value. Optional; default false.
+ * @return {Object} The option map, an object whose keys are the expanded
+ * option values such as {'-v': {...}, '--verbose': {...}} and whose values
+ * are the option info.
*/
-function makeOptionMap(optionInfo) {
+CLI._makeOptionMap = function(optionInfo) {
var optionMap = {};
if (optionInfo) {
@@ -49,20 +82,30 @@ function makeOptionMap(optionInfo) {
/*
- * Parses command line args based on the provided option info. Returns a map
- * with three keys:
- * params - an array of the specified parameters
- * options - a map of options, keyed by long name
- * errors - an array of error strings for reporting
+ * Parses command line args based on the provided option info.
+ * @param {Array} args A command line argument string split by whitespace.
+ * @param {Object} optionInfo Option information in the form of an array of
+ * objects containing option descriptions with the following keys:
+ * shortName - Short (1-char) option name, without leading dash (e.g. 'v').
+ * longName - Long option name, without leading dashes (e.g. 'verbose').
+ * hasValue - True if option requires a value. Optional; default false.
+ * @return {Object} A map with three keys:
+ * params - an array of the specified parameters
+ * options - a map of options, keyed by long name
+ * errors - an array of error strings for reporting
*/
-function parseArgs(args, optionInfo) {
- var optionMap = makeOptionMap(optionInfo),
+CLI._parseArgs = function(args, optionInfo) {
+ var optionMap = CLI._makeOptionMap(optionInfo),
params = [],
options = {},
errors = [],
option,
arg;
+ if (!args) {
+ return { params: params, options: options, errors: errors };
+ }
+
while (args.length > 0) {
arg = args.shift();
if (arg.charAt(0) === '-') {
@@ -88,9 +131,17 @@ function parseArgs(args, optionInfo) {
return { params: params, options: options, errors: errors };
}
-// ---------- Start of mainline code ----------
+// ----------------------------------------------------------------------------
+// Public Functions
+// ----------------------------------------------------------------------------
-function main() {
+/**
+ * Executes command line processing. If a valid command is parsed and loaded the
+ * command module's run method will be invoked. The command module's exported
+ * options are used to assist in parsing the command line.
+ *
+ */
+CLI.run = function() {
var args = process.argv.slice(2),
commandName = (args.length === 0 ? 'help' : args.shift()),
command,
@@ -100,11 +151,11 @@ function main() {
command = require('mojito-cli-cmd-' + commandName);
} catch (e) {
try {
- /*jslint nomen: true */
- command = require(libpath.join(__dirname, '../app/commands/', commandName));
+ command = require(libpath.join(__dirname, '../app/commands/',
+ commandName));
} catch (e2) {
utils.error('Error loading command: ' + command + ' ' + e2.message,
- 'mojito <command> [<params>] [<options>]');
+ 'mojito <command> [<params>] [<options>]');
return;
}
}
@@ -112,7 +163,7 @@ function main() {
if (args.length === 0) {
argInfo = { command: 'help', params: [], options: {} };
} else {
- argInfo = parseArgs(args, command.options);
+ argInfo = CLI._parseArgs(args, command.options);
}
if (argInfo.errors && argInfo.errors.length > 0) {
@@ -132,4 +183,14 @@ function main() {
});
}
-exports.run = main;
+// ----------------------------------------------------------------------------
+// EXPORT(S)
+// ----------------------------------------------------------------------------
+
+/**
+ * Export the CLI object, which includes all testable functions.
+ * @type {Function}
+ */
+exports = module.exports = CLI;
+
+}());
View
115 tests/cli/lib/management/test-cli.js
@@ -11,20 +11,123 @@ YUI().use('test', function(Y) {
var suite = new Y.Test.Suite('cli tests'),
A = Y.Assert,
OA = Y.ObjectAssert,
- libpath = require('path');
+ AA = Y.ArrayAssert,
+ libpath = require('path'),
+ cli = require(libpath.join(__dirname,
+ '../../../../lib/management/cli.js')),
+ mockOptionMap = function(opts) {
+ return {};
+ },
+ opts = [{"longName":"coverage","shortName":"c","hasValue":false},
+ {"longName":"verbose","shortName":"v","hasValue":false},
+ {"longName":"tmpdir","shortName":"t","hasValue":true}];
suite.add(new Y.Test.Case({
name: 'cli tests',
- 'load check': function() {
- var cli = require(libpath.join(__dirname,
- '../../../../lib/management/cli.js'));
-
+ 'test require': function() {
A.isNotNull(cli);
+ A.isFunction(cli._makeOptionMap);
+ A.isFunction(cli._parseArgs);
+
+ },
+
+ 'test makeOptionMap': function() {
+ var map,
+ out;
+
+ out = {"-c": opts[0], "--coverage": opts[0],
+ "-v": opts[1], "--verbose": opts[1],
+ "-t": opts[2], "--tmpdir": opts[2]}
+
+ map = cli._makeOptionMap(opts);
+
+ OA.areEqual(map, out);
+ },
+
+ 'test parseArgs empty': function() {
+ var out,
+ result,
+ orig;
+
+ out = {"params": [],
+ "options": {},
+ "errors": []
+ }
+
+ orig = cli._makeOptionMap;
+ cli._makeOptionMap = mockOptionMap;
+
+ try {
+ result = cli._parseArgs([], {});
+ AA.itemsAreEqual(result.params, out.params);
+ OA.areEqual(result.options, out.options);
+ AA.itemsAreEqual(result.errors, out.errors);
+ } finally {
+ cli._makeOptionMap = orig;
+ }
+ },
+
+ 'test parseArgs with param arg': function() {
+ var out,
+ result,
+ orig;
+
+ out = {"params": ["foo"],
+ "options": {},
+ "errors": []
+ }
+
+ orig = cli._makeOptionMap;
+ cli._makeOptionMap = mockOptionMap;
+
+ try {
+ result = cli._parseArgs(["foo"], {});
+ AA.itemsAreEqual(result.params, out.params);
+ OA.areEqual(result.options, out.options);
+ AA.itemsAreEqual(result.errors, out.errors);
+ } finally {
+ cli._makeOptionMap = orig;
+ }
+ },
+
+ 'test parseArgs with option arg': function() {
+ var out,
+ result,
+ orig;
+
+ out = {"params": [],
+ "options": {"verbose": true},
+ "errors": []
+ }
+
+ result = cli._parseArgs(["-v"], opts);
+ AA.itemsAreEqual(result.params, out.params);
+ OA.areEqual(result.options, out.options);
+ AA.itemsAreEqual(result.errors, out.errors);
+ },
+
+ 'test parseArgs with invalid arg': function() {
+ var out,
+ result,
+ orig;
+
+ out = {"params": [],
+ "options": {},
+ "errors": ["Invalid option: -f"]
+ }
+
+ result = cli._parseArgs(["-f"], opts);
+
+ AA.itemsAreEqual(result.params, out.params);
+ OA.areEqual(result.options, out.options);
+ AA.itemsAreEqual(result.errors, out.errors);
+ },
+
+ 'test run': function() {
A.isFunction(cli.run);
}
-
}));
Y.Test.Runner.add(suite);
Something went wrong with that request. Please try again.