Permalink
Browse files

Add --queue, --buffer flags, default options and args tests.

* Add --queue=NUM flag to 'concurrent'
* Add --buffer flag to 'concurrent'
* Add default options to flags in 'args'
* Add basic unit tests for 'args' parsing logic
* Add infrastructure for sinon, mocha, chai, karma
* Hook up Travis to coveralls for coverage reports
* Update README docs
* Add 'LOCAL_DEV' env variable option for 'npm link' local development
* BUGFIX: Make displayHelp() show unique tasks.
  • Loading branch information...
ryan-roemer committed Dec 5, 2015
1 parent f8c35a5 commit 89ba7b084fc89405fe2c054000f153f6e9195762
Showing with 393 additions and 30 deletions.
  1. +16 −0 .eslintrc-server-test
  2. +6 −0 .istanbul.server.yml
  3. +7 −1 .travis.yml
  4. +36 −7 README.md
  5. +39 −7 lib/args.js
  6. +15 −2 lib/config.js
  7. +27 −7 lib/runner.js
  8. +5 −3 lib/task.js
  9. +13 −3 package.json
  10. +2 −0 test/server/mocha.opts
  11. +13 −0 test/server/setup.js
  12. +26 −0 test/server/spec/base.spec.js
  13. +188 −0 test/server/spec/lib/args.spec.js
@@ -0,0 +1,16 @@
---
extends:
- "defaults/configurations/walmart/es5-test"
- "defaults/configurations/walmart/es5-node"
env:
mocha: true
globals:
fetch: false
expect: false
sandbox: false
rules:
no-unused-expressions: 0 # Disable for Chai expression assertions.
max-nested-callbacks: 0 # Disable for nested describes.
@@ -0,0 +1,6 @@
reporting:
dir: coverage/server
reports:
- lcov
- json
- text-summary
@@ -27,4 +27,10 @@ before_install:
script:
- npm --version
- npm run builder:check
- npm run builder:check-ci
# Manually send coverage reports to coveralls.
# - Aggregate client results
# - Single server and func test results
- ls coverage/server/lcov.info | cat
- cat coverage/server/lcov.info | node_modules/.bin/coveralls || echo "Coveralls upload failed"
@@ -1,4 +1,5 @@
[![Travis Status][trav_img]][trav_site]
[![Coverage Status][cov_img]][cov_site]
Builder
=======
@@ -122,27 +123,52 @@ to bind archetypes.
... and from here you are set for `builder`-controlled meta goodness!
#### Builder Commands
#### Builder Actions
Display general or command-specific help.
Display general or command-specific help (which shows available specific flags).
```sh
$ builder help
$ builder help run
$ builder help ACTION
```
Run a single `package.json` `scripts` task.
Run `builder help ACTION` for all available options. For a quick overview:
##### builder run
Run a single task from `script`. Analogous to `npm run TASK`
```sh
$ builder run foo-task
$ builder run TASK
```
Run multiple `package.json` `scripts` tasks.
Flags:
* `--builderrc`: Path to builder config file (default: `.builderrc`)
* `--tries`: Number of times to attempt a task (default: `1`)
##### builder concurrent
Run multiple tasks from `script` concurrently. Roughly analogous to
`npm run TASK1 | npm run TASK2 | npm run TASK3`, but kills all processes on
first non-zero exit (which makes it suitable for test tasks).
```sh
$ builder concurrent foo-task bar-task baz-task
$ builder concurrent TASK1 TASK2 TASK3
```
Flags:
* `--builderrc`: Path to builder config file (default: `.builderrc`)
* `--tries`: Number of times to attempt a task (default: `1`)
* `--queue`: Number of concurrent processes to run (default: unlimited - `0|null`)
* `--[no-]buffer`: Buffer output until process end (default: `false`)
Note that `tries` will retry _individual_ tasks that are part of the concurrent
group, not the group itself. So, if `builder concurrent --tries=3 foo bar baz`
is run and bar fails twice, then only `bar` would be retried. `foo` and `baz`
would only execute _once_ if successful.
## Tasks
@@ -383,3 +409,6 @@ things settle down in `v3.x`-on.
[builder-react-component]: https://github.com/FormidableLabs/builder-react-component
[trav_img]: https://api.travis-ci.org/FormidableLabs/builder.svg
[trav_site]: https://travis-ci.org/FormidableLabs/builder
[cov]: https://coveralls.io
[cov_img]: https://img.shields.io/coveralls/FormidableLabs/builder.svg
[cov_site]: https://coveralls.io/r/FormidableLabs/builder
@@ -11,7 +11,13 @@ var chalk = require("chalk");
// Generic flags:
var FLAG_TRIES = {
desc: "Number of times to attempt a task (default: `1`)",
types: [Number]
types: [Number],
default: function (val) { return val > 0 ? val : 1; }
};
var FLAG_QUEUE = {
desc: "Number of concurrent processes to run (default: unlimited - `0|null`)",
types: [Number],
default: function (val) { return val > 0 ? val : null; }
};
// Option flags.
@@ -20,7 +26,8 @@ var FLAGS = {
general: {
builderrc: {
desc: "Path to builder config file (default: `.builderrc`)",
types: [path]
types: [path],
default: ".builderrc"
}
},
@@ -29,7 +36,13 @@ var FLAGS = {
},
concurrent: {
tries: FLAG_TRIES
tries: FLAG_TRIES,
queue: FLAG_QUEUE,
buffer: {
desc: "Buffer output until process end (default: `false`)",
types: [Boolean],
default: false
}
}
};
@@ -55,18 +68,37 @@ var help = function (flagKey) {
};
// Option parser.
var createFn = function (opts) {
var createFn = function (flags) {
var opts = getOpts(flags);
return function (argv) {
argv = argv || process.argv;
return nopt(opts, {}, argv);
// Parse.
var parsedOpts = nopt(opts, {}, argv);
// Inject defaults and mutate parsed object.
_.extend(parsedOpts, _.mapValues(flags, function (val, key) {
var parsedVal = parsedOpts[key];
if (_.isFunction(val.default)) {
return val.default(parsedVal);
}
return _.isUndefined(parsedVal) ? val.default : parsedVal;
}));
// Camel-case flags.
return _.mapKeys(parsedOpts, function (val, key) {
return _.camelCase(key);
});
};
};
module.exports = _.extend({
FLAGS: FLAGS,
help: help
}, _.mapValues(FLAGS, function (val) {
}, _.mapValues(FLAGS, function (flags) {
// Add in `KEY()` methods.
return createFn(getOpts(val));
return createFn(flags);
}));
@@ -81,8 +81,20 @@ Config.prototype._loadConfig = function (cfg) {
Config.prototype._loadArchetypeScripts = function (name) {
/*eslint-disable global-require*/
var pkg;
// `npm link` makes the normal `require()` thing break. Give ourselves an
// environment variable to make imports more permissive.
if (process.env.LOCAL_DEV) {
try {
pkg = require(path.join(process.cwd(), "node_modules", name, "package.json"));
} catch (err) {
/*eslint-disable no-empty*/
// Pass through error
}
}
try {
pkg = require(name + "/package.json");
pkg = pkg || require(name + "/package.json");
} catch (err) {
log.error("config:load-archetype-scripts",
"Error loading package.json for: " + chalk.gray(name) + " " +
@@ -148,6 +160,8 @@ Config.prototype.displayScripts = function (archetypes) {
// Hack in a low-occuring string to prioritize "special" `:` names
return (name.indexOf(":") > -1 ? "0000" : "") + name;
})
// Filter to unique keys and we're in order (`true`)
.unique(true)
.value();
// Then, map in order to scripts.
@@ -205,4 +219,3 @@ Object.defineProperties(Config.prototype, {
}
}
});
@@ -12,21 +12,34 @@ var log = require("../lib/log");
*
* @param {String} cmd Shell command
* @param {Object} shOpts Shell options
* @param {Object} opts Runner options
* @param {Function} callback Callback `(err)`
* @returns {Object} Child process object
*/
var run = function (cmd, shOpts, callback) {
var run = function (cmd, shOpts, opts, callback) {
// Check if buffered output or piped.
var buffer = opts.buffer;
log.info("proc:start", cmd);
var proc = exec(cmd, shOpts, function (err) {
var proc = exec(cmd, shOpts, function (err, stdout, stderr) {
var code = err ? err.code || 1 : 0;
var level = code === 0 ? "info" : "warn";
// Write out buffered output.
if (buffer) {
process.stdout.write(stdout);
process.stderr.write(stderr);
}
log[level]("proc:end:" + code, cmd);
callback(err);
});
proc.stdout.pipe(process.stdout, { end: false });
proc.stderr.pipe(process.stderr, { end: false });
// Concurrent / "whenever" output.
if (!buffer) {
proc.stdout.pipe(process.stdout, { end: false });
proc.stderr.pipe(process.stderr, { end: false });
}
return proc;
};
@@ -42,10 +55,10 @@ var run = function (cmd, shOpts, callback) {
*/
var retry = function (cmd, shOpts, opts, callback) {
// Expand options.
var tries = opts.tries > 0 ? opts.tries : 1;
var tracker = opts.tracker;
// State.
var tries = opts.tries;
var success = false;
var error;
@@ -55,7 +68,7 @@ var retry = function (cmd, shOpts, opts, callback) {
return !success && tries > 0;
},
function (cb) {
var proc = run(cmd, shOpts, function (err) {
var proc = run(cmd, shOpts, opts, function (err) {
// Manage, update state.
tries--;
error = err;
@@ -156,8 +169,15 @@ module.exports = {
*/
concurrent: function (cmds, shOpts, opts, callback) {
var tracker = new Tracker();
var queue = opts.queue;
// Get mapper (queue vs. non-queue)
var map = queue ?
async.mapLimit.bind(async, cmds, queue) :
async.map.bind(async, cmds);
async.map(cmds, function (cmd, cb) {
log.info("concurrent", "Starting with queue size: " + chalk.magenta(queue || "unlimited"));
map(function (cmd, cb) {
retry(cmd, shOpts, _.extend({ tracker: tracker }, opts), cb);
}, function (err) {
tracker.kill();
@@ -142,11 +142,12 @@ Task.prototype.run = function (callback) {
var env = this._env.env; // Raw environment object.
var task = this.getCommand(this._command);
var flags = args.run(this.argv);
var flags = args.concurrent(this.argv);
var opts = _.extend({}, flags);
log.info(this._action, this._command + chalk.gray(" - " + task));
runner.run(task, { env: env }, { tries: flags.tries }, callback);
runner.run(task, { env: env }, opts, callback);
};
/**
@@ -160,12 +161,13 @@ Task.prototype.concurrent = function (callback) {
var cmds = this._commands;
var tasks = cmds.map(this.getCommand.bind(this));
var flags = args.concurrent(this.argv);
var opts = _.extend({}, flags);
log.info(this._action, cmds.join(", ") + tasks.map(function (t, i) {
return "\n * " + cmds[i] + chalk.gray(" - " + t);
}).join(""));
runner.concurrent(tasks, { env: env }, { tries: flags.tries }, callback);
runner.concurrent(tasks, { env: env }, opts, callback);
};
/**
@@ -14,8 +14,12 @@
"homepage": "https://github.com/FormidableLabs/builder",
"scripts": {
"builder:lint-server": "eslint --color -c .eslintrc-server lib bin",
"builder:lint": "npm run builder:lint-server",
"builder:check": "npm run builder:lint"
"builder:lint-server-test": "eslint --color -c .eslintrc-server-test test",
"builder:lint": "npm run builder:lint-server && npm run builder:lint-server-test",
"builder:test": "mocha --opts test/server/mocha.opts test/server/spec",
"builder:test-cov": "istanbul cover --config .istanbul.server.yml _mocha -- --opts test/server/mocha.opts test/server/spec",
"builder:check": "npm run builder:lint && npm run builder:test",
"builder:check-ci": "npm run builder:lint && npm run builder:test-cov"
},
"bin": {
"builder": "bin/builder.js"
@@ -28,8 +32,14 @@
"nopt": "^3.0.6"
},
"devDependencies": {
"chai": "^3.4.1",
"coveralls": "^2.11.4",
"eslint": "^1.7.3",
"eslint-config-defaults": "^7.0.1",
"eslint-plugin-filenames": "^0.1.2"
"eslint-plugin-filenames": "^0.1.2",
"istanbul": "^0.4.1",
"mocha": "^2.3.4",
"sinon": "^1.17.2",
"sinon-chai": "^2.8.0"
}
}
@@ -0,0 +1,2 @@
--require test/server/setup.js
--recursive
@@ -0,0 +1,13 @@
"use strict";
/**
* Test setup for server-side tests.
*/
var chai = require("chai");
var sinonChai = require("sinon-chai");
// Add chai plugins.
chai.use(sinonChai);
// Add test lib globals.
global.expect = chai.expect;
Oops, something went wrong.

0 comments on commit 89ba7b0

Please sign in to comment.