Comparing version 0.9.0 to 0.9.1
@@ -0,1 +1,12 @@ | ||
e2e-helper | ||
========== | ||
## 0.9.1 | ||
- allow better customization for cover mode | ||
- add eslint definitions | ||
## 0.9.0 - official release | ||
predecessor: | ||
mocha-e2e | ||
@@ -2,0 +13,0 @@ ========== |
266
lib/index.js
'use strict' | ||
const assign = Object.assign | ||
const {assign} = Object | ||
const path = require('path') | ||
const fs = require('fs') | ||
const run = require('child_process').spawn | ||
let log | ||
@@ -13,3 +13,3 @@ const defaults = | ||
, readyNotice : 'listening on port' | ||
, console : console | ||
, console | ||
, args : [] | ||
@@ -20,19 +20,14 @@ , timeout : 10000 | ||
, term_timeout : 3000 | ||
, coverIgnore : | ||
[ '**/test-e2e/**' | ||
, '**/test/**' | ||
] | ||
, coverSvc : './node_modules/istanbul/lib/cli' | ||
, coverArgs : ['cover', '--dir', './coverage/e2e-test', '--handle-sigint'] | ||
, coverIgnore : [] | ||
} | ||
const coverArgs = | ||
[ './node_modules/istanbul/lib/cli', 'cover' | ||
, '--dir', './coverage/e2e-test' | ||
, '--handle-sigint' | ||
] | ||
/** | ||
returns a before-all mocha handler that setups the server with the provided | ||
@description | ||
returns a before-all mocha handler that setups the server with the provided | ||
options. | ||
while being called, it sets `e2e.teardown` with the matching teardown | ||
while being called, it sets `e2e.teardown` with the matching teardown | ||
function that shuts down the service gracefully. | ||
@@ -43,6 +38,6 @@ | ||
@param {string|object} options - when provided as string - it's understood as `options.svc` | ||
@param {string} options.svc - system under test - path to the script that | ||
@param {string} options.svc - system under test - path to the script that | ||
runs the target server | ||
@param {string} options.logPath - path to log file | ||
@param {string} options.readyNotice - output line expected on stdout of | ||
@param {string} options.readyNotice - output line expected on stdout of | ||
the started service that indicates that the service is ready | ||
@@ -66,5 +61,5 @@ @param {array} options.args - args to concat to the node command | ||
e2e.internal = | ||
{ initCtx : initCtx | ||
, setup : setup | ||
, teardown : teardown | ||
{ initCtx | ||
, setup | ||
, teardown | ||
} | ||
@@ -78,22 +73,24 @@ | ||
ctx_teardown | ||
return ctx_setup | ||
//TRICKY: these must be 'oldschool' functions - mocha relays on the `this` keyword | ||
function ctx_setup (done) { setup (ctx, this, done) } | ||
function ctx_setup(done) { setup(ctx, this, done) } | ||
function ctx_teardown(done) { teardown(ctx, this, done) } | ||
} | ||
function mocha_bdd(options) { | ||
const console = options.console || global.console | ||
describe( | ||
function mocha_functions_api(options, {suite, setup, teardown, console = options.console || global.console}) { | ||
suite( | ||
options.title || defaults.title | ||
, () => { | ||
process.env.SUT | ||
? console.log('test target: ', process.env.SUT) | ||
: [ before(e2e(options)), after(e2e.teardown) ] | ||
if (process.env.SUT) { | ||
console.log('test target: ', process.env.SUT) | ||
} else { | ||
setup(e2e(options)) | ||
teardown(e2e.teardown) | ||
} | ||
options.suites | ||
.map( p => path.resolve(p) ) | ||
.forEach(global.require /* istanbul ignore next */ || require) | ||
.forEach(global.require /* istanbul ignore next */|| require) | ||
} | ||
@@ -103,14 +100,16 @@ ) | ||
function mocha_bdd(options) { | ||
mocha_functions_api(options | ||
, { suite: global.describe | ||
, setup: global.before | ||
, teardown: global.after | ||
} | ||
) | ||
} | ||
function mocha_tdd(options) { | ||
const console = options.console || global.console | ||
suite( | ||
options.title || defaults.title | ||
, () => { | ||
process.env.SUT | ||
? console.log('test target: ', process.env.SUT) | ||
: [ suiteSetup(e2e(options)), suiteteardown(e2e.teardown) ] | ||
options.suites | ||
.map( p => path.resolve(p) ) | ||
.forEach(global.require /* istanbul ignore next */|| require) | ||
mocha_functions_api(options | ||
, { suite: global.suite | ||
, setup: global.suiteSetup | ||
, teardown: global.suiteTeardown | ||
} | ||
@@ -125,10 +124,11 @@ ) | ||
const suite = {} | ||
process.env.SUT | ||
? console.log('test target: ', process.env.SUT) | ||
: Object.assign(suite, { | ||
beforeAll: e2e(options), | ||
afterAll: e2e.teardown | ||
}) | ||
: Object.assign(suite | ||
, { beforeAll: e2e(options) | ||
, afterAll: e2e.teardown | ||
} | ||
) | ||
options.suites.forEach( | ||
@@ -138,5 +138,5 @@ endpointTest => | ||
) | ||
res[ options.title || defaults.title ] = suite | ||
return res | ||
@@ -146,37 +146,37 @@ } | ||
function initCtx(options) { | ||
let msg | ||
if ('object' != typeof options) options = { svc: options } | ||
options = assign({}, defaults, options) | ||
options.args = options.args.concat() | ||
msg = ( | ||
!options.svc && ('options.svc is not provided') | ||
|| 'string' != typeof options.svc && ('options.svc must be a string') | ||
|| 'string' != typeof options.cwd && ('options.cwd must be a string') | ||
|| !fs.existsSync(options.cwd) && (options.cwd + ' is not found on disk') | ||
|| !isFileFound(path.join(options.cwd, options.svc)) | ||
&& (options.svc + ' is not found on disk') | ||
|| !options.logPath && ('options.logPath is expected to be a path') | ||
|| 'string' != typeof options.logPath && ('options.logPath is expected to be a path') | ||
|| !options.readyNotice && ('options.readyNotice must be a string') | ||
|| 'string' != typeof options.readyNotice && ('options.readyNotice must be a string') | ||
|| !Array.isArray(options.args) && ('options.args must be an array') | ||
|| 'number' != typeof options.timeout && ('options.timeout must be a number') | ||
|| 'number' != typeof options.slow && ('options.slow must be a number') | ||
|| options.env | ||
&& 'object' != typeof options.env && ('options.env, when provided must be an object') | ||
options.coverArgs = options.coverArgs.concat() | ||
const stringArray = (arr) => Array.isArray(arr) && !arr.find(s => 'string' != typeof s) | ||
const targetSvc = path.join(String(options.cwd), String(options.svc)) | ||
const msg = | ||
!options.svc && 'options.svc is not provided' | ||
|| 'string' != typeof options.svc && 'options.svc must be a string' | ||
|| 'string' != typeof options.cwd && 'options.cwd must be a string' | ||
|| !fs.existsSync(options.cwd) && options.cwd + ' is not found on disk' | ||
|| !isFileFound(targetSvc) && options.svc + ' is not found on disk' | ||
|| !options.logPath && 'options.logPath is expected to be a path' | ||
|| 'string' != typeof options.logPath && 'options.logPath is expected to be a path' | ||
|| !options.readyNotice && 'options.readyNotice must be a string' | ||
|| 'string' != typeof options.readyNotice && 'options.readyNotice must be a string' | ||
|| !Array.isArray(options.args) && 'options.args must be an array' | ||
|| 'number' != typeof options.timeout && 'options.timeout must be a number' | ||
|| 'number' != typeof options.slow && 'options.slow must be a number' | ||
|| options.env | ||
&& 'object' != typeof options.env && 'options.env, when provided must be an object' | ||
|| !( 'number' == typeof options.term_timeout | ||
&& 0 < options.term_timeout | ||
) && ('options.term_timeout, when provided - must be a positive number') | ||
|| !~['SIGTERM','SIGINT','SIGQUIT', | ||
'SIGKILL','SIGHUP' | ||
].indexOf(options.term_code) && ('options.term_code, when provided - must be a valid process signal') | ||
|| options.coverIgnore | ||
&& ( !Array.isArray(options.coverIgnore) | ||
|| options.coverIgnore.find(s => 'string' != typeof s) | ||
) && ('options.coverIgnore, when provided - must be an array of glob pattern strings') | ||
) | ||
&& 0 < options.term_timeout | ||
) && 'options.term_timeout, when provided - must be a positive number' | ||
|| !~[ 'SIGTERM', 'SIGINT', 'SIGQUIT' | ||
, 'SIGKILL', 'SIGHUP' | ||
].indexOf(options.term_code) && 'options.term_code, when provided - must be a valid process signal' | ||
|| 'string' != typeof options.coverSvc && 'options.svc, when provided - must be path node CLI script, such as istanbul' | ||
|| !stringArray(options.coverArgs) && 'options.coverArgs, when provided - must be an array of CLI arguments' | ||
|| !stringArray(options.coverIgnore) && 'options.coverIgnore, when provided - must be an array of glob pattern strings' | ||
if (msg) throw assign( | ||
@@ -189,15 +189,21 @@ new Error( | ||
, " if you need to provide an absolute path - you may use .cwd as the absolute path" | ||
, " when options is string - it is uderstood as options.svc" | ||
, " when options is string - it is uderstood as options.svc, applying defaults to all the rest" | ||
, " - cwd - string, optional - the work directory the process should run in. defaults to current dir" | ||
, " - logPath - string, optional - path to logfile. default: './e2e.log'" | ||
, " - timeout - integer, optional - timeout for server setup" | ||
, " - slow - integer, optional - slow bar indicator for server setup" | ||
, " - timeout - integer, optional - timeout for server setup, default: " + defaults.timeout | ||
, " - slow - integer, optional - slow bar indicator for server setup, default: " + defaults.timeout | ||
, " - readyNotice - string, optional - message to expect on service output that" | ||
, " indicates the service is ready. default: 'listening on port'" | ||
, " - args - array, argumnets to be concatenated to the running command" | ||
, " - cwd - string, optional - the work directory the process should run in" | ||
, " - term_code - string, optional, the termination message to send to the child, default: SIGTERM" | ||
, " - term_ipc - optional, any value provided will be used to child.send(term_ipc) before escalating to child.kill(term_code)" | ||
, " indicates the service is ready. default: " + defaults.readyNotice | ||
, " - args - array, optional, argumnets to be concatenated to the running command" | ||
, " - term_code - string, optional, the termination message to send to the child, default: " + defaults.term_code | ||
, " - term_ipc - optional, any value provided will be used to child.send(term_ipc) before escalating" | ||
, " to term-code. When not provided - starts with term-code" | ||
, " - term_timeout - optional, number, timeout in miliseconds before escalations( ipc->term->kill)" | ||
, " - coverIgnore - optional, array of glob-pattern strings" | ||
, "reason: ", | ||
, " default: " + defaults.term_timeout | ||
, " - coverSvc - optional, path to cover-tool node-script which should run .svc to gather coverage" | ||
, " default: " + defaults.coverSvc | ||
, " - coverArgs - optional, array of CLI arguments to the cover tool" | ||
, " default: " + JSON.stringify(defaults.coverArgs) | ||
, " - coverIgnore - optional, array of glob-pattern strings for files to exclude from coverage" | ||
, "reason: " | ||
, " " + msg | ||
@@ -208,9 +214,8 @@ ].join('\n') | ||
) | ||
if (process.env.COVER) { | ||
const args = coverArgs.concat() | ||
const ignore = options.coverIgnore | ||
const args = [options.coverSvc].concat(options.coverArgs) | ||
options.coverIgnore.forEach((pattern) => args.push('-x', pattern)) | ||
args.push( | ||
@@ -221,5 +226,5 @@ options.svc.match(/\.js$/) | ||
) | ||
options.args = | ||
options.args.length | ||
options.args = | ||
options.args.length | ||
? args.concat(['--'], options.args) | ||
@@ -232,5 +237,5 @@ : args | ||
return options | ||
function isFileFound(path) { | ||
return fs.existsSync(path) | ||
return fs.existsSync(path) | ||
|| fs.existsSync(path + '.js') | ||
@@ -241,13 +246,13 @@ } | ||
/** | ||
@param {object} options | ||
@param {string} options.sut - system under test - path to the script that | ||
@param {object} ctx - context | ||
@param {string} ctx.sut - system under test - path to the script that | ||
runs the target server | ||
@param {string} options.logPath - path to log file | ||
@param {string} options.readyNotice - output line expected on stdout of | ||
@param {string} ctx.logPath - path to log file | ||
@param {string} ctx.readyNotice - output line expected on stdout of | ||
the started target service that indicates that the service is running | ||
@param {mocha.Test} test - the test context that implements .timetout(n), .slow(n) .... | ||
@param {callback} callback | ||
@param {callback} done - callback | ||
@returns {undefined} | ||
*/ | ||
function setup(ctx, test, done) { | ||
try { fs.unlinkSync( ctx.logPath ) } catch(e) {} | ||
@@ -261,15 +266,19 @@ | ||
fs.createWriteStream(ctx.logPath, { flags: 'a'} ) | ||
log.writable = true | ||
const writeLog = (...args) => log.writable && log.write.apply(log, args) | ||
const child = | ||
ctx.child = | ||
run(process.execPath, ctx.args, { | ||
env: assign({}, process.env, ctx.env || {}), | ||
cwd: ctx.cwd, | ||
stdio: ['pipe','pipe', 'pipe', 'ipc'] | ||
}) | ||
run(process.execPath, ctx.args | ||
, { env: assign({}, process.env, ctx.env || {}) | ||
, cwd: ctx.cwd | ||
, stdio: ['pipe', 'pipe', 'pipe', 'ipc'] | ||
} | ||
) | ||
/* istanbul ignore next */ | ||
child.on('error', (err) => { | ||
if (log.writable) log.write('ERR: ', err) | ||
writeLog('ERR: ', err) | ||
done(err) | ||
@@ -279,11 +288,12 @@ }) | ||
child.stderr.on('data', (data) => { | ||
data = data + '' | ||
if (log.writable) log.write('ERR: ' + data) | ||
if (~data.indexOf('Error: listen EADDRINUSE')) | ||
done(new Error('Server could not start: Address In Use')) | ||
data = data.toString() | ||
/* istanbul ignore if */ | ||
writeLog('ERR: ' + data) | ||
if (~data.indexOf('Error: listen EADDRINUSE')) done(new Error('Server could not start: Address In Use')) | ||
}) | ||
child.stdout.on('data', function(data) { | ||
child.stdout.on('data', (data) => { | ||
data = data.toString() | ||
if (log.writable) log.write(data) | ||
writeLog(data) | ||
@@ -295,8 +305,8 @@ if (~data.indexOf(ctx.readyNotice)) { | ||
}) | ||
child.on('exit', function(e) { | ||
child.on('exit', (e) => { | ||
child.exitted = true | ||
ctx.console.log('\n\nservice termination ended', e || 'OK') | ||
ctx.log.writable = false | ||
ctx.log.end(function() { | ||
ctx.log.end(() => { | ||
child.emit('--over--') | ||
@@ -308,5 +318,9 @@ }) | ||
/** | ||
@param {callback} callback | ||
@param {object} ctx - the context | ||
@param {mocha.Test} test - the Test object | ||
@param {callback} done - callback | ||
@returns {undefined} | ||
*/ | ||
function teardown(ctx, test, done) { | ||
/* istanbul ignore if */ | ||
if (ctx.child.exitted) return done() | ||
@@ -316,3 +330,3 @@ test.slow(ctx.term_timeout * 1.5) | ||
ctx.child.on('--over--', done) | ||
ctx.child.on('--over--', done) | ||
ctx.term_ipc | ||
@@ -328,3 +342,3 @@ ? ipc() | ||
function term() { | ||
ctx.child.kill(ctx.term_code) | ||
ctx.child.kill(ctx.term_code) | ||
setTimeout(kill, ctx.term_timeout) | ||
@@ -336,2 +350,2 @@ } | ||
} | ||
} | ||
} |
{ | ||
"name": "e2e-helper", | ||
"version": "0.9.0", | ||
"version": "0.9.1", | ||
"description": "end-to-end test helper, with facilitators for mocha", | ||
"main": "lib", | ||
"scripts": { | ||
"test": "mocha", | ||
"cover": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot", | ||
"postcover": "npm run cover-enforce", | ||
"cover-enforce": "node node_modules/istanbul/lib/cli.js check-coverage --function 90 --lines 90 --branches 90 --statements 90" | ||
}, | ||
"repository": { | ||
@@ -28,2 +22,4 @@ "type": "git", | ||
"devDependencies": { | ||
"coveralls": "^3.0.0", | ||
"eslint": "^4.15.0", | ||
"istanbul": "*", | ||
@@ -33,3 +29,10 @@ "mocha": "~2.3.4", | ||
"should": "^9.0.0" | ||
}, | ||
"scripts": { | ||
"test": "mocha", | ||
"cover": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot", | ||
"postcover": "npm run cover-enforce", | ||
"cover-enforce": "node node_modules/istanbul/lib/cli.js check-coverage --function 90 --lines 90 --branches 90 --statements 90", | ||
"coveralls": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- --reporter dot && cat coverage/lcov.info | coveralls --verbose" | ||
} | ||
} |
@@ -15,2 +15,3 @@ # e2e-helper | ||
- [![Build Status](https://secure.travis-ci.org/osher/e2e-helper.png?branch=master)](http://travis-ci.org/osher/e2e-helper) Tested on latests node versions of 6,7,8 | ||
- [![Coverage Status](https://coveralls.io/repos/github/osher/e2e-helper/badge.svg?branch=master)](https://coveralls.io/github/osher/e2e-helper?branch=master) | ||
@@ -69,18 +70,21 @@ # Features list: | ||
- `svc` - string, mandatory, should be relative path. a path to the script that starts the service. | ||
if you need to provide an absolute path - you may use .cwd as the absolute path | ||
when options is string - it is uderstood as options.svc | ||
- `logPath` - string, optional - path to logfile. default: './e2e.log' | ||
- `timeout` - integer, optional - timeout for server setup | ||
- `slow` - integer, optional - slow bar indicator for server setup | ||
if you need to provide an absolute path - you may use `.cwd` to provide the absolute path | ||
when options is string - it is uderstood as options.svc, applying defaults to all the rest | ||
- `cwd` - string, optional - the work directory the process should run in. defaults to current dir | ||
- `logPath` - string, optional - path to logfile. default: `'./e2e.log'` | ||
- `timeout` - integer, optional - timeout for server setup, default: `10000` | ||
- `slow` - integer, optional - slow bar indicator for server setup, default: `10000` | ||
- `readyNotice` - string, optional - message to expect on service output that | ||
indicates the service is ready. default: 'listening on port' | ||
- `args` - array, argumnets to be concatenated to the running command | ||
- `cwd` - string, optional - the work directory the process should run in | ||
- `term_code` - string, optional, the termination message to send to the child, default: SIGTERM | ||
- `term_ipc` - optional, any value provided will be used to child.send(term_ipc) before escalating to child.kill(term_code) | ||
indicates the service is ready. default: `'listening on port'` | ||
- `args` - array, optional, argumnets to be concatenated to the running command | ||
- `term_code` - string, optional, the termination message to send to the child, default: `'SIGINT'` | ||
- `term_ipc` - optional, any value provided will be used to child.send(term_ipc) before escalating | ||
to term-code. When not provided - starts with term-code | ||
- `term_timeout` - optional, number, timeout in miliseconds before escalations( ipc->term->kill) | ||
- `coverIgnore` - optional, array of glob-pattern strings to exclude from cover tool, meaningful only for istanbul COVER mode | ||
on top of that list, the higher facilitators accept as well | ||
- `title` - string - the root level test title | ||
default: `3000` | ||
- `coverSvc` - optional, path to cover-tool node-script which should run `.svc` to gather coverage | ||
default: `'./node_modules/istanbul/lib/cli'` | ||
- `coverArgs` - optional, array of CLI arguments to the cover tool | ||
default: `["cover","--dir","./coverage/e2e-test","--handle-sigint"]` | ||
- `coverIgnore` - optional, array of glob-pattern strings for files to exclude from coverage | ||
@@ -176,3 +180,3 @@ ## with mocha | ||
There may be edge-cases with cover mode on windows - that's the only part I'm yet to be satisfied with. | ||
I don't expect API changes for fixing any of these might-be issues. | ||
I don't expect API changes for fixing any of these issues. | ||
@@ -182,4 +186,3 @@ # Contribute | ||
- if you add functionality - add tests :) | ||
- don't really worry much about the style... | ||
I hope it doesn't freak you out (it just might if you're using IDE) | ||
- don't worry about the style. I tried to add eslint to help with that, although it's not bullet-proof. | ||
If I'll really need to - I'll ask you to permit me on your fork, I'll help as best I can with styles or with anything else :) | ||
@@ -186,0 +189,0 @@ |
20309
281
188
9
6
4