├── .gitignore ├── .travis.yml ├── LICENSE ├── collaborators.md ├── index.js ├── package.json ├── readme.md └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.12' 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Max Ogden 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /collaborators.md: -------------------------------------------------------------------------------- 1 | ## Collaborators 2 | 3 | tape-spawn is only possible due to the excellent work of the following collaborators: 4 | 5 | 6 | 7 |
maxogdenGitHub/maxogden
mafintoshGitHub/mafintosh
8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var spawn = require('npm-execspawn') 2 | var stripAnsi = require('strip-ansi') 3 | var defined = require('defined') 4 | 5 | module.exports = StreamMatch 6 | 7 | function StreamMatch (t, command, opts) { 8 | var self = this 9 | if (!(this instanceof StreamMatch)) return new StreamMatch(t, command, opts) 10 | if (!opts) opts = {} 11 | if (typeof command !== 'string') throw new Error('must specify command string') 12 | 13 | this.proc = spawn(command, opts) 14 | this.t = t 15 | this.opts = opts 16 | this.stdout = new StreamTest(t, this.proc.stdout, checkDone, 'stdout') 17 | this.stderr = new StreamTest(t, this.proc.stderr, checkDone, 'stderr') 18 | this.stdin = this.proc.stdin 19 | this.kill = this.proc.kill.bind(this.proc) 20 | 21 | var debug = process.env.DEBUG || '' 22 | if (debug.indexOf('tape-spawn') > -1) { 23 | this.stderr.stream.pipe(process.stderr) 24 | } 25 | 26 | // in case tape times out or something we should clean up 27 | t.on('end', function onTapeEnd () { 28 | self.proc.kill() 29 | }) 30 | 31 | function checkDone () { 32 | if (self.opts.exitCode === undefined && self.stdout.pending === 0 && self.stderr.pending === 0) { 33 | self.proc.kill() 34 | } 35 | } 36 | } 37 | 38 | StreamMatch.prototype.end = function (onDone) { 39 | var self = this 40 | self.proc.on('exit', function onExit (code) { 41 | code = code || 0 42 | if (self.timeoutId) clearTimeout(self.timeoutId) 43 | if (typeof self.opts.exitCode === 'number') self.t.equal(code, self.opts.exitCode, self.opts.exitCodeMessage) 44 | else if (self.opts.exitCode === 'nonzero') self.t.notEqual(code, 0, self.opts.exitCodeMessage) 45 | if (self.opts.end !== false) self.t.end() 46 | if (onDone) onDone() 47 | }) 48 | } 49 | 50 | StreamMatch.prototype.timeout = function (time, message) { 51 | var self = this 52 | if (self.timeoutId) clearTimeout(self.timeoutId) 53 | self.timeoutId = setTimeout(function timeout () { 54 | self.proc.kill() 55 | if (typeof message === 'function') message() // e.g. let the user handle the assertion themselves 56 | else self.t.ok(false, defined(message, 'timeout exceeded')) 57 | }, time) 58 | } 59 | 60 | StreamMatch.prototype.succeeds = function (message) { 61 | this.opts.exitCode = 0 62 | this.opts.exitCodeMessage = defined(message, 'exit code matched') 63 | } 64 | 65 | StreamMatch.prototype.fails = function (message) { 66 | this.opts.exitCode = 'nonzero' 67 | this.opts.exitCodeMessage = defined(message, 'non-zero exit code') 68 | } 69 | 70 | StreamMatch.prototype.exitCode = function (code, message) { 71 | this.opts.exitCode = code 72 | this.opts.exitCodeMessage = defined(message, 'exit code matched') 73 | } 74 | 75 | function StreamTest (t, stream, onDone, label) { 76 | if (!(this instanceof StreamTest)) return new StreamTest(t, stream, onDone, label) 77 | this.t = t 78 | this.stream = stream 79 | this.onDone = onDone 80 | this.label = label ? label + ' ' : '' 81 | this.pending = 0 82 | } 83 | 84 | StreamTest.prototype.empty = function empty () { 85 | this.match(/^$/, this.label + 'was empty', this.label + 'was not empty') 86 | } 87 | 88 | StreamTest.prototype.match = function match (pattern, message, failMessage) { 89 | var self = this 90 | var patternLabel = (typeof pattern === 'function') ? 'pattern function' : pattern 91 | this.pending++ 92 | var matched = false 93 | var buff = '' 94 | this.stream.setEncoding('utf8') 95 | this.stream.on('data', function onData (ch) { 96 | buff += ch 97 | matchOutput() 98 | }) 99 | 100 | this.stream.on('end', function onEnd () { 101 | matchOutput() 102 | if (!matched) { 103 | self.pending-- 104 | var outMessage = '"' + buff + '" did not match ' + patternLabel 105 | if (failMessage) outMessage = failMessage + ' ("' + buff + '")' 106 | self.t.ok(false, stripAnsi(outMessage)) 107 | self.onDone() 108 | } 109 | }) 110 | 111 | function matchOutput () { 112 | if (matched) return 113 | var match 114 | if (typeof pattern === 'function') { 115 | match = pattern(buff) 116 | } else if (typeof pattern === 'string') { 117 | match = pattern === buff 118 | } else { 119 | match = pattern.test(buff) 120 | } 121 | if (match) { 122 | matched = true 123 | self.pending-- 124 | self.t.ok(true, stripAnsi(defined(message, 'matched ' + patternLabel))) 125 | self.onDone() 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tape-spawn", 3 | "version": "1.4.2", 4 | "description": "spawn processes conveniently in tape tests and match against stdout/stderr streaming output", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && node test/test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/maxogden/tape-spawn.git" 12 | }, 13 | "keywords": [ 14 | "tape", 15 | "test" 16 | ], 17 | "author": "max ogden", 18 | "license": "BSD", 19 | "bugs": { 20 | "url": "https://github.com/maxogden/tape-spawn/issues" 21 | }, 22 | "homepage": "https://github.com/maxogden/tape-spawn", 23 | "dependencies": { 24 | "defined": "^1.0.0", 25 | "npm-execspawn": "^1.3.0", 26 | "strip-ansi": "^2.0.1" 27 | }, 28 | "devDependencies": { 29 | "cat": "^0.2.0", 30 | "standard": "^3.6.0", 31 | "tape": "^4.0.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # tape-spawn 2 | 3 | spawn processes conveniently in [tape](https://npmjs.org/tape) tests and match against stdout/stderr streaming output 4 | 5 | [![NPM](https://nodei.co/npm/tape-spawn.png)](https://nodei.co/npm/tape-spawn/) 6 | 7 | [![Build Status](https://travis-ci.org/maxogden/tape-spawn.svg?branch=master)](https://travis-ci.org/maxogden/tape-spawn) 8 | 9 | [![js-standard-style](https://raw.githubusercontent.com/feross/standard/master/badge.png)](https://github.com/feross/standard) 10 | 11 | ## installation 12 | 13 | ``` 14 | npm install tape-spawn 15 | ``` 16 | 17 | ## usage 18 | 19 | use this in conjunction with `tape`, e.g. 20 | 21 | ```js 22 | var test = require('tape') 23 | var spawn = require('tape-spawn') 24 | 25 | test('spawn ls', function (t) { 26 | var st = spawn(t, 'ls ' + __dirname) 27 | st.stdout.match(/example.js/) 28 | st.end() 29 | }) 30 | ``` 31 | 32 | ## debugging 33 | 34 | If you set `DEBUG=tape-spawn` in your ENV when running your tests then the STDERR of the spawned child process will be piped into the STDERR of your terminal. 35 | 36 | ## api 37 | 38 | ### `var spawn = require('tape-spawn')` 39 | 40 | returns a function, `spawn`, that can be used to spawn new processes and test their output with tape 41 | 42 | processes are spawned with [npm-execspawn](https://npmjs.org/npm-execspawn), meaning local node_modules bins will be matched first before looking in your PATH 43 | 44 | ### var spawnTest = spawn(tapeTest, commandString, [options]) 45 | 46 | returns `spawnTest`, which can be used to set up assertions. also spawns a process using `commandString`. `options` are optional, and can have the following properties: 47 | 48 | - `end` (default `true`) - if `false` no `t.end()` assertion will be set up for the test, which means you will have to use `spawnTest.end(onEnd)` to handle it yourself 49 | 50 | in addition, the entire `options` object will get passed as the second argument to `spawn`, so you can do e.g. `{env: {FOO: 'bar'}}` to pass env vars to pass custom spawn options (see the child_process node docs for more info) 51 | 52 | ### spawnTest.fails([message]) 53 | 54 | sets up a tape assertion that expects a non-zero exit code – with an optional `message` 55 | 56 | ### spawnTest.succeeds([message]) 57 | 58 | sets up a tape assertion that expects exit code to equal 0 – with an optional `message` 59 | 60 | ### spawnTest.exitCode(code, [message]) 61 | 62 | `code` must be a number. sets up a tape assertion that expects exit code to equal `code` – with an optional `message` 63 | 64 | ### spawnTest.timeout(time, [message]) 65 | 66 | waits for `time` milliseconds and then kills the spawned process and fails the test with the optional `message` string assert message. if `message` is a function it will be called after the timeout, and no fail assert will be created 67 | 68 | ### spawnTest.end([onDone]) 69 | 70 | sets up a tape assertion for `t.end()`. if you pass the optional `onDone` callback, no `t.end()` assertion will be created, and your `onDone` callback will be called when the test is done 71 | 72 | ### spawnTest.kill() 73 | 74 | kills the spawned process 75 | 76 | ### spawnTest.stdin 77 | 78 | this property is the internally spawned process `stdin` stream instance 79 | 80 | ### spawnTest.stdout.match(pattern, [message, failMessage]) 81 | 82 | matches `stdout` output (assumes utf8 encoding). if `pattern` is a RegExp it will set up a tape assertion that uses use `pattern.test(output)`. if `pattern` is a string it will use `t.equals()` to match the entire output against `message`. 83 | if `pattern` is a function it should return true/false and take 1 argument, the full output of the spawn 84 | 85 | You can pass the optional `message` or `failMessage` to customize the tape assertion messages 86 | 87 | ### spawnTest.stdout.empty() 88 | 89 | takes no args. sets up a tape assertion that expects output to match `/^$/` (e.g. to be empty) 90 | 91 | ### spawnTest.stderr.match(pattern, [message, failMessage]) 92 | 93 | the same as `spawnTest.stdout.match` but matches `stderr` instead 94 | 95 | ### spawnTest.stderr.empty() 96 | 97 | the same as `spawnTest.stdout.empty` but matches `stderr` instead 98 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var spawn = require('../') 3 | 4 | test('spawn ls', function (t) { 5 | var st = spawn(t, 'ls ' + __dirname) 6 | 7 | st.end() 8 | }) 9 | 10 | test('spawn ls w/ succeeds', function (t) { 11 | var st = spawn(t, 'ls ' + __dirname) 12 | 13 | st.succeeds() 14 | st.end() 15 | }) 16 | 17 | test('spawn ls w/ fails', function (t) { 18 | var st = spawn(t, 'pizzadonkeyultimate-highlylikelynotinstalled') 19 | 20 | st.fails() 21 | st.end() 22 | }) 23 | 24 | test('spawn ls w/ match regex', function (t) { 25 | var st = spawn(t, 'ls ' + __dirname) 26 | 27 | st.succeeds() 28 | st.stdout.match(/test.js/) 29 | st.stdout.match(/test/) 30 | st.end() 31 | }) 32 | 33 | test('spawn ls w/ match string', function (t) { 34 | var st = spawn(t, 'ls ' + __dirname) 35 | 36 | st.succeeds() 37 | st.stdout.match('test.js\n') 38 | st.end() 39 | }) 40 | 41 | test('spawn true w/ empty', function (t) { 42 | var st = spawn(t, 'true') // spawn /usr/bin/true which outputs nothing 43 | st.succeeds() 44 | st.stdout.empty() 45 | st.end() 46 | }) 47 | 48 | test('spawn with end false', function (t) { 49 | var st = spawn(t, 'pizzadonkeyultimate-highlylikelynotinstalled', { 50 | end: false 51 | }) 52 | 53 | st.end(function onEnd (err) { 54 | t.ifErr(err, 'got error in custom end fn') 55 | t.end() 56 | }) 57 | }) 58 | 59 | test('spawn and ensure proc was killed', function (t) { 60 | var st = spawn(t, 'cat -') 61 | 62 | st.stdin.write('x') 63 | st.stdout.match('x') 64 | 65 | st.end() 66 | }) 67 | 68 | test('spawn and ensure proc was killed (with delay)', function (t) { 69 | var st = spawn(t, 'cat -') 70 | 71 | st.stdout.match('x') 72 | 73 | setTimeout(function () { 74 | st.stdin.write('x') 75 | }, 250) 76 | 77 | st.end() 78 | }) 79 | 80 | test('spawn with timeout', function (t) { 81 | var st = spawn(t, 'cat -') 82 | 83 | st.timeout(250, function onTimeout () { 84 | t.ok(true, 'timeout happened') 85 | }) 86 | st.end() 87 | }) 88 | 89 | test('spawn with custom spawn option', function (t) { 90 | var st = spawn(t, 'echo $FOO', {env: {FOO: 'hi'}}) 91 | st.stdout.match('hi\n') 92 | st.end() 93 | }) 94 | 95 | test('custom match function', function (t) { 96 | var st = spawn(t, 'ls ' + __dirname) 97 | 98 | st.succeeds() 99 | st.stdout.match(function match (output) { 100 | return output === 'test.js\n' 101 | }) 102 | st.end() 103 | }) 104 | 105 | test('match function', function (t) { 106 | var st = spawn(t, 'echo one; sleep 0.5; echo two; exit 7') 107 | 108 | st.stdout.match(/^one/) 109 | st.exitCode(7) 110 | st.end() 111 | }) 112 | --------------------------------------------------------------------------------