Permalink
Browse files

CLI stdin/out/err streams; evented mocks. Ref #95.

The CLI now expects to be provided with streams for:

 - stdin
 - stdout
 - stderr

This replaces the old options:

 - readableStream
 - writableStream
 - putsFn
 - errorFn

Removing putsFn and errorFn discourages usage of Node.js
stdio (e.g., console.*), which is one less thing to mock.
The CLI.prototype's puts() and error() methods still allow
for `printf`-like formatting by using util.format().

Create new mock Readable and Writable Streams for testing.

The MockWritableStream supports expect(), which will fire
a callback when the expected string is provided. This lets
us do async testing of the CLI.
  • Loading branch information...
1 parent 291be89 commit f1c8d51da2a0b4723635a73977ecc0e1ca6b164a @reid committed Oct 4, 2012
Showing with 158 additions and 95 deletions.
  1. +3 −4 cli.js
  2. +22 −33 lib/cli.js
  3. +19 −25 test/cli.js
  4. +114 −0 test/lib/streams.js
  5. +0 −33 test/lib/writable-stream.js
View
7 cli.js
@@ -5,11 +5,10 @@
var CLI = require("./lib/cli").CLI;
var frontend = new CLI({
- readableStream: process.stdin,
- writableStream: process.stderr,
+ stdin: process.stdin,
+ stdout: process.stdout,
+ stderr: process.stderr,
exitFn: process.exit,
- errorFn: console.error,
- putsFn: console.log
});
frontend.setupExceptionHandler();
View
@@ -21,8 +21,6 @@ var hubClient = require("./client");
var YetiEventEmitter2 = require("./events").EventEmitter2;
-var isTTY = process.stderr.isTTY;
-
var good = "";
var bad = "";
@@ -135,10 +133,9 @@ var getLocalIP = exports.getLocalIP = (function () {
* @extends YetiEventEmitter2
* @param {Object} config Configuration.
* @param {Function} config.exitFn Handler when process exit is requested, 1-arity.
- * @param {Function} config.putsFn Handler for stdout, expando arguments.
- * @param {Function} config.errorFn Handler for stderr, expando arguments.
- * @param {ReadableStream} config.readableStream Readable stream for creating a readline interface.
- * @param {WritableStream} config.writableStream Writable stream for creating a readline interface.
+ * @param {ReadableStream} config.stdin Readable stream for creating a readline interface.
+ * @param {WritableStream} config.stdout Writable stream for creating a readline interface.
+ * @param {WritableStream} config.stderr Writable stream for creating a readline interface.
*/
function CLI(config) {
/**
@@ -149,40 +146,32 @@ function CLI(config) {
* @param {Number} Return code.
*/
this.exitFn = config.exitFn;
+
/**
- * Fires for a line that should be printed to stdout.
- *
- * @property putsFn
- * @type {Function}
- * @param {String|Object} Multiple arguments.
- */
- this.putsFn = config.putsFn;
- /**
- * Fires for a line that should be printed to stderr.
+ * For readline.
*
- * @property errorFn
- * @type {Function}
- * @param {String|Object} Multiple arguments.
+ * @property stdin
+ * @type {ReadableStream}
*/
- this.errorFn = config.errorFn;
+ this.stdin = config.stdin;
/**
* For readline.
*
- * @property readableStream
- * @type {ReadableStream}
+ * @property stdout
+ * @type {WritableStream}
*/
- this.readableStream = config.readableStream;
+ this.stdout = config.stdout;
/**
* For readline.
*
- * @property writableStream
+ * @property stderr
* @type {WritableStream}
*/
- this.writableStream = config.writableStream;
+ this.stderr = config.stderr;
- this.rl = readline.createInterface(this.readableStream, this.writableStream);
+ this.rl = readline.createInterface(this.stdin, this.stderr);
}
util.inherits(CLI, YetiEventEmitter2);
@@ -204,10 +193,9 @@ CLI.prototype.exit = function (code) {
* @private
*/
CLI.prototype.error = function () {
- var args = Array.prototype.slice.apply(arguments);
- // This is not an event because this may called
- // when the event loop is already shut down.
- this.errorFn.apply(this, args);
+ var args = Array.prototype.slice.apply(arguments),
+ formattedString = util.format.apply(util, args);
+ this.stderr.write(formattedString + "\n", "utf8");
};
@@ -218,8 +206,9 @@ CLI.prototype.error = function () {
* @private
*/
CLI.prototype.puts = function () {
- var args = Array.prototype.slice.apply(arguments);
- this.putsFn.apply(this, args);
+ var args = Array.prototype.slice.apply(arguments),
+ formattedString = util.format.apply(util, args);
+ this.stdout.write(formattedString + "\n", "utf8");
};
/**
@@ -248,7 +237,7 @@ CLI.prototype.setupExceptionHandler = function () {
err = err.stack;
}
- if (isTTY) {
+ if (self.stderr.isTTY) {
message = [
color.red(bad + " Whoops!") + " " + err, "",
"If you believe this is a bug in Yeti, please report it.",
@@ -557,7 +546,7 @@ CLI.prototype.runBatch = function runBatch(options) {
self.rl.question("When ready, press Enter to begin testing.\n", function () {
self.rl.close();
- self.readableStream.destroy();
+ self.stdin.destroy();
self.submitBatch(client, batchOptions);
});
}
View
@@ -3,7 +3,7 @@
var vows = require("vows");
var assert = require("assert");
-var MockWritableStream = require("./lib/writable-stream");
+var streams = require("./lib/streams");
var cli = require("../lib/cli");
var YetiCLI = cli.CLI;
@@ -12,22 +12,20 @@ function cliTopic(fn) {
return function () {
var topic = {
fe: null,
- writableStream: new MockWritableStream(),
- log: new MockWritableStream(),
- error: new MockWritableStream(),
+ stdin: new streams.MockReadableStream(),
+ stdout: new streams.MockWritableStream(),
+ stderr: new streams.MockWritableStream(),
exit: 0
};
function mockExit(code) {
topic.exit = code;
}
-
topic.fe = new YetiCLI({
- writableStream: topic.writableStream,
- readableStream: process.stdin, // FIXME
- putsFn: topic.log.write.bind(topic.log),
- errorFn: topic.error.write.bind(topic.error),
+ stdin: topic.stdin,
+ stdout: topic.stdout,
+ stderr: topic.stderr,
exitFn: mockExit
});
@@ -38,58 +36,54 @@ function cliTopic(fn) {
vows.describe("Yeti CLI").addBatch({
"A Yeti CLI without arguments": {
topic: cliTopic(function (topic) {
+ topic.stderr.expect("usage", this.callback);
+
topic.fe.route([
"node",
"cli.js"
]);
-
- return topic;
}),
"returns usage on stderr": function (topic) {
- assert.ok(topic.error.$store.indexOf("usage:") === 0);
+ assert.ok(topic.indexOf("usage:") === 0);
},
"returns helpful information on stderr": function (topic) {
- assert.include(topic.error.$store, "launch the Yeti server");
+ assert.include(topic, "launch the Yeti server");
}
},
"A Yeti CLI with --server": {
topic: cliTopic(function (topic) {
+ topic.stderr.expect("started", this.callback);
topic.fe.route([
"node",
"cli.js",
"-s",
"-p", "9010"
]);
-
- return topic;
}),
"returns startup message on stderr": function (topic) {
- assert.ok(topic.error.$store.indexOf("Yeti Hub started") === 0);
+ assert.ok(topic.indexOf("Yeti Hub started") === 0);
}
},
"A Yeti CLI with files": {
topic: cliTopic(function (topic) {
- var vow = this;
+ topic.stderr.expect("When ready", this.callback);
+
topic.fe.route([
"node",
"cli.js",
"-p", "9011",
"fixture/basic.html",
]);
-
- setTimeout(function () {
- vow.callback(null, topic);
- }, 250);
}),
"prints hub creation message on stderr": function (topic) {
- assert.ok(topic.error.$store.indexOf("Creating a Hub.") === 0);
+ assert.ok(topic.indexOf("Creating a Hub.") === 0);
},
"waits for agents to connect on stderr": function (topic) {
- assert.include(topic.error.$store, "Waiting for agents to connect");
- assert.include(topic.error.$store, "also available locally at");
+ assert.include(topic, "Waiting for agents to connect");
+ assert.include(topic, "also available locally at");
},
"prompts on the writableStream": function (topic) {
- assert.include(topic.writableStream.$store, "When ready, press Enter");
+ assert.include(topic, "When ready, press Enter");
}
},
"parseArgv when given arguments": {
View
@@ -0,0 +1,114 @@
+"use strict";
+
+/**
+ * @module streams
+ */
+
+var util = require("util");
+var EventEmitter2 = require("../../lib/events").EventEmitter2;
+
+function makeString(data) {
+ if (Buffer.isBuffer(data)) {
+ data = data.toString("utf8");
+ }
+
+ return data;
+}
+
+function WRITE(data) {
+ this.emit("data", makeString(data));
+}
+
+function NOOP() {}
+
+/**
+ * @class MockReadableStream
+ * @constructor
+ * @extends YetiEventEmitter2
+ */
+function MockReadableStream() {
+ EventEmitter2.call(this);
+}
+
+util.inherits(MockReadableStream, EventEmitter2);
+
+/**
+ * No-op.
+ *
+ * @method setEncoding
+ */
+MockReadableStream.prototype.setEncoding = NOOP;
+
+/**
+ * No-op.
+ *
+ * @method resume
+ */
+MockReadableStream.prototype.resume = NOOP;
+
+/**
+ * Emit the `data` event with first argument
+ * as a String.
+ *
+ * @method write
+ * @param {String|Buffer} data Data.
+ */
+MockReadableStream.prototype.write = WRITE;
+
+/**
+ * @class MockWritableStream
+ * @constructor
+ * @extends YetiEventEmitter2
+ */
+function MockWritableStream() {
+ EventEmitter2.call(this);
+}
+
+util.inherits(MockWritableStream, EventEmitter2);
+
+/**
+ * No-op.
+ *
+ * @method end
+ */
+MockWritableStream.prototype.end = NOOP;
+
+/**
+ * Emit the `data` event with first argument
+ * as a String.
+ *
+ * @method write
+ * @param {String|Buffer} data Data.
+ */
+MockWritableStream.prototype.write = WRITE;
+
+/**
+ * Call the given callback when expectedString
+ * is written to this stream. The callback recieves
+ * a string of all data written since the expect call.
+ *
+ * @method expect
+ * @param {String} expectedString Expected string.
+ * @param {Function} cb Callback.
+ * @param {null} cb.err Error for callback, always null.
+ * @param {String} cb.data All data written between expectedString
+ * appearing and calling expect.
+ */
+MockWritableStream.prototype.expect = function (expectedString, cb) {
+ var self = this,
+ dataEvents = [];
+
+ self.on("data", function ondata(data) {
+ data = makeString(data);
+
+ dataEvents.push(data);
+
+ if (data.indexOf(expectedString) !== -1) {
+ self.removeListener("data", ondata);
+ cb(null, dataEvents.join(""));
+ }
+ });
+};
+
+exports.MockReadableStream = MockReadableStream;
+exports.MockWritableStream = MockWritableStream;
@@ -1,33 +0,0 @@
-"use strict";
-
-function MockWritableStream() {
- this.$store = "";
-}
-
-var proto = MockWritableStream.prototype;
-
-proto.writeHead = function (status, /* msg, */ headers) {
- this.$status = status;
- this.$headers = headers;
-};
-
-proto.end = function (input) {
- if (input) {
- this.write(input);
- }
- this.$end = true;
-};
-
-proto.write = function (input) {
- if (this.$end) {
- throw new Error("Unable to write: closed.");
- }
-
- if (Buffer.isBuffer(input)) {
- this.$store += input.toString("utf8");
- } else {
- this.$store += input;
- }
-};
-
-module.exports = MockWritableStream;

0 comments on commit f1c8d51

Please sign in to comment.