diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8f9d77e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[{package.json,*.yml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1fd04da --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +coverage +.nyc_output diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a9694f9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: node_js + +node_js: + - 'stable' + - '0.12' + - '0.10' + +after_script: + - 'cat ./coverage/lcov.info | ./node_modules/.bin/coveralls' diff --git a/index.js b/index.js new file mode 100644 index 0000000..c69f84c --- /dev/null +++ b/index.js @@ -0,0 +1,124 @@ +'use strict'; +module.exports = makeTests; + +var recast = require('recast'); +var makeComment = require('inline-source-map-comment'); +var types = recast.types; +var n = types.namedTypes; + +function makeTests(inputSource, opts) { + var results = []; + + opts = opts || {}; + + var filename = opts.sourceMaps !== false && (opts.filename || opts.fileName); + + function parse() { + var parseOpts = filename ? {sourceFileName: filename} : null; + return recast.parse(inputSource, parseOpts); + } + + function print(ast) { + var printOpts = filename ? {sourceMapName: filename + '.map'} : null; + var result = recast.print(ast, printOpts); + + if (filename && opts.attachComment) { + result.code = result.code + '\n' + makeComment(result.map); + } + + return result; + } + + function copy(path) { + return rootNode(_copy(path)); + } + + function _copy(path) { + var copied; + if (path.parentPath) { + copied = _copy(path.parentPath).get(path.name); + } else { + copied = new types.NodePath({root: parse()}); + } + + var parent = copied.parent; + var node = copied.value; + if (!(n.Node.check(node) && parent && (n.BlockStatement.check(parent.node) || n.Program.check(parent.node)))) { + return copied; + } + + var body = parent.get('body').value; + var keeper = parent.get('body', path.name).node; + + var statementIdx = 0; + + while (statementIdx < body.length) { + var statement = body[statementIdx]; + if ((isDescribe(statement) || isIt(statement)) && statement !== keeper) { + parent.get('body', statementIdx).replace(); + } else { + statementIdx++; + } + } + + return copied; + } + + types.visit(parse(), { + visitExpressionStatement: function (path) { + var node = path.node; + if (isIt(node)) { + var result = print(copy(path)); + result.nestedName = nestedName(path); + results.push(result); + return false; + } + this.traverse(path); + } + }); + + return results; +} + +function isDescribe(node) { + if (!n.ExpressionStatement.check(node)) { + return false; + } + node = node.expression; + return n.CallExpression.check(node) && n.Identifier.check(node.callee) && (node.callee.name === 'describe'); +} + +function isIt(node) { + if (!n.ExpressionStatement.check(node)) { + return false; + } + node = node.expression; + return n.CallExpression.check(node) && n.Identifier.check(node.callee) && (node.callee.name === 'it'); +} + +// Walks the path up to the root. +function rootNode(path) { + while (path.parent) { + path = path.parent; + } + return path; +} + +function nestedName(path) { + var arr = []; + _nestedName(path, arr); + return arr.reverse(); +} + +function _nestedName(path, arr) { + if (!path) { + return; + } + if (isDescribe(path.node) || isIt(path.node)) { + var firstArg = path.get('expression', 'arguments', 0).node; + n.Literal.assert(firstArg); + arr.push(firstArg.value); + } + _nestedName(path.parent, arr); +} + diff --git a/license b/license new file mode 100644 index 0000000..ad5d021 --- /dev/null +++ b/license @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) James Talmage (github.com/jamestalmage) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/package.json b/package.json new file mode 100644 index 0000000..b56fc84 --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "name": "forking-tap", + "version": "0.0.0", + "description": "Run every single tap test in its own process.", + "license": "MIT", + "repository": "jamestalmage/forking-tap", + "author": { + "name": "James Talmage", + "email": "james@talmage.io", + "url": "github.com/jamestalmage" + }, + "engines": { + "node": ">=0.10.0" + }, + "scripts": { + "test": "xo && nyc --reporter=lcov --reporter=text ava" + }, + "files": [ + "index.js" + ], + "keywords": [ + "tap", + "forked", + "forking", + "test", + "tests", + "testing", + "isolation", + "globals" + ], + "dependencies": { + "inline-source-map-comment": "^1.0.5", + "recast": "^0.10.39" + }, + "devDependencies": { + "ava": "^0.7.0", + "convert-source-map": "^1.1.2", + "coveralls": "^2.11.4", + "nyc": "^4.0.1", + "source-map": "^0.5.3", + "xo": "^0.11.2" + }, + "xo": { + "ignores": [ + "test.js" + ] + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..1281254 --- /dev/null +++ b/readme.md @@ -0,0 +1,155 @@ +# forking-tap [![Build Status](https://travis-ci.org/jamestalmage/forking-tap.svg?branch=master)](https://travis-ci.org/jamestalmage/forking-tap) + +> Run every single tap test in its own process. + +`forking tap` takes a single test: + +```js +require('tap').mochaGlobals() +var foo = 'bar'; + +describe('foo', function () { + function fooHelper() {} + + it('foo-1', function () {}); + + it('foo-2', function () {}); +}); + +describe('bar', function () { + function barHelper() {} + + it('bar-1', function () {}); +}); +``` + +And splits it into three different files (one for each test): + +```js +require('tap').mochaGlobals(); +var foo = 'bar'; + +describe('foo', function () { + function fooHelper() {} + + it('foo-1', function () {}); +}); +``` + +```js +require('tap').mochaGlobals(); +var foo = 'bar'; + +describe('foo', function () { + function fooHelper() {} + + it('foo-2', function () {}); +}); +``` + +```js +require('tap').mochaGlobals(); +var foo = 'bar'; + +describe('bar', function () { + function barHelper() {} + + it('bar-1', function () {}); +}); +``` + +Notice how all the appropriate helper functions and shared variables make it into each test. + +## Install + +``` +$ npm install --save forking-tap +``` + + +## Usage + +```js +const forkingTap = require('forking-tap'); + +const results = forkingTap(fs.readFileSync('./all-the-tests.js', 'utf8')); + +results.forEach((result, testNum) => { + fs.writeFileSync('./test-number-' + testNum, result.code); +}); +``` + + +## API + +### testList = forkingTap(source, [options]) + +Returns `testList` an array of `testResult` objects that represent the input `source` split into individual test files with one test per file. + +#### source + +Type: `string` + +The original source code. + +#### options + +##### options.filename + +Type: `string` + +The name of the file being split up. Required for source map support. + +##### options.sourceMaps + +Type: `boolean` +Default: `true` + +Forcefully turn off source map support by setting this to `false`. Otherwise, source map support is turned on if the `filename` option is present. + +##### options.attachComment + +Type: `boolean` +Default: `false` + +Automatically attach an inline source map comment to the end of the generated code. + +### testResult + +#### testResult.code + +Type: `string` + +The full source code for an individual test + +#### testResult.map + +Type: `object` + +The source map descriptor object for the transform (or `undefined` if `filename` was not provided, or `options.sourceMaps === false`). + +#### testResult.nestedName + +Type: `Array.` + +A representation of the test name. The last element of the array will always be the string value passed to `it(str, fn)` test. The preceding elements of the array represent the names of any enclosing `describe` blocks. + +For example, the following: + +```js +describe('foo', function () { + describe('bar', function () { + it('baz', function () { /* ... */}); + }); +}); +``` + +would produce a `nestedName` of`: + +```js +['foo', 'bar', 'baz'] +``` + +## License + +MIT © [James Talmage](http://github.com/jamestalmage) diff --git a/test.js b/test.js new file mode 100644 index 0000000..f52e110 --- /dev/null +++ b/test.js @@ -0,0 +1,117 @@ +import test from 'ava'; +import fn from './'; +import {SourceMapConsumer} from 'source-map'; +import convertSourceMap from 'convert-source-map'; + +test('creates sources for individual test files', t => { + const result = fn(input); + + result.forEach(r => r.code = trim(r.code)); + + t.is(result.length, 3); + + t.is(result[0].code, trim(test0)); + t.is(result[1].code, trim(test1)); + t.is(result[2].code, trim(test2)); +}); + +test('it provides accurate source maps', t => { + const result = fn(input, {filename: 'foo.js'}); + + const consumer = new SourceMapConsumer(result[1].map); + + const loc = consumer.originalPositionFor({ + line: 7, + column: 8 + }); + + t.same(loc, { + source: 'foo.js', + line: 9, + column: 8, + name: null + }); +}); + +test('it will attach source map comments', t => { + const result = fn(input, { + filename: 'foo.js', + attachComment: true + }); + + const map = convertSourceMap.fromSource(result[1].code); + const consumer = new SourceMapConsumer(map.toObject()); + + const loc = consumer.originalPositionFor({ + line: 7, + column: 8 + }); + + t.same(loc, { + source: 'foo.js', + line: 9, + column: 8, + name: null + }); +}); + +test('nested names', t => { + const results = fn(input); + + t.same(results[0].nestedName, ['foo', 'foo-1']); + t.same(results[1].nestedName, ['foo', 'foo-2']); + t.same(results[2].nestedName, ['bar', 'bar-1']); +}); + +function trim(code) { + return code.replace(/[\s\n\t]+/g, ' '); +} + + +const input =` + var foo = 'bar'; + + describe('foo', function () { + function fooHelper() {} + + it('foo-1', function () {}); + + it('foo-2', function () {}); + }); + + describe('bar', function () { + function barHelper() {} + + it('bar-1', function () {}); + }); +`; + +const test0 = ` + var foo = 'bar'; + + describe('foo', function () { + function fooHelper() {} + + it('foo-1', function () {}); + }); +`; + +const test1 = ` + var foo = 'bar'; + + describe('foo', function () { + function fooHelper() {} + + it('foo-2', function () {}); + }); +`; + +const test2 = ` + var foo = 'bar'; + + describe('bar', function () { + function barHelper() {} + + it('bar-1', function () {}); + }); +`;