├── .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 |
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 | [](https://nodei.co/npm/tape-spawn/)
6 |
7 | [](https://travis-ci.org/maxogden/tape-spawn)
8 |
9 | [](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 |
--------------------------------------------------------------------------------