├── .gitignore ├── LICENSE.txt ├── package.json ├── bin ├── phl.js └── phl.es6 ├── README.md ├── index.es6 └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.log 3 | phl_output 4 | coverage 5 | node_modules 6 | test 7 | test.bak -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Contributors 2 | 3 | Permission to use, copy, modify, and/or distribute this software 4 | for any purpose with or without fee is hereby granted, provided 5 | that the above copyright notice and this permission notice 6 | appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 10 | OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE 11 | LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES 12 | OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 13 | WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 14 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phl", 3 | "description": "a thin wrapper around istanbul to support ava test framework.", 4 | "version": "1.0.2", 5 | "author": "David Chase", 6 | "bin": { 7 | "phl": "./bin/phl.js" 8 | }, 9 | "bugs": "https://github.com/davidchase/phl/issues", 10 | "config": { 11 | "phl": { 12 | "exclude": [ 13 | "node_modules", 14 | "bin", 15 | "test/index.js" 16 | ] 17 | } 18 | }, 19 | "dependencies": { 20 | "commander": "2.9.0", 21 | "foreground-child": "1.3.1", 22 | "istanbul": "^0.3.19", 23 | "mkdirp": "0.5.1", 24 | "rimraf": "^2.4.2", 25 | "signal-exit": "^2.1.1", 26 | "spawn-wrap": "^1.0.1", 27 | "strip-bom": "^2.0.0", 28 | "xtend": "4.0.1" 29 | }, 30 | "devDependencies": { 31 | "babel-preset-es2015": "6.1.2", 32 | "tape": "4.2.2" 33 | }, 34 | "homepage": "https://github.com/davidchase/phl", 35 | "keywords": [ 36 | "coverage", 37 | "reporter", 38 | "subprocess", 39 | "testing" 40 | ], 41 | "license": "ISC", 42 | "main": "index.js", 43 | "repository": { 44 | "type": "git", 45 | "url": "git+https://github.com/davidchase/phl.git" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /bin/phl.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var path = require('path'); 5 | var foreground = require('foreground-child'); 6 | var wrap = require('spawn-wrap'); 7 | var program = require('commander'); 8 | var phl = require('../'); 9 | var pkg = require('../package.json'); 10 | var internals = {}; 11 | 12 | internals.report = function report(args) { 13 | args = args || {}; 14 | if (args.silent) { 15 | return; 16 | } 17 | process.env.PHL_CWD = process.cwd(); 18 | phl.init({ 19 | reporter: args.reporter || 'text' 20 | }); 21 | phl.report(); 22 | }; 23 | 24 | internals.istanbulCheckCoverage = function istanbulCheckCoverage(options) { 25 | foreground(process.execPath, [require.resolve('istanbul/lib/cli'), 'check-coverage', '--lines=' + options.lines, '--functions=' + options.functions, '--branches=' + options.branches, '--statements=' + options.statements, path.resolve(process.cwd(), './.phl_output/*.json')]); 26 | }; 27 | 28 | program.version(pkg.version).usage('[cmd | options] '); 29 | 30 | program.command('report').option('-r, --reporter ', 'specify report to use, default:text').option('-s, --silent', 'dont output any coverage').description('run coverage report for .phl_output').action(internals.report); 31 | 32 | program.command('check-coverage').description('check whether coverage is within thresholds provided').option('-l, --lines ', 'what n of lines must be covered?').option('-b, --branches ', 'what n of branches must be covered?').option('-f, --functions ', 'what n of functions must be covered?').option('-s, --statements ', 'what n of statements must be covered?').action(internals.istanbulCheckCoverage); 33 | 34 | program.arguments('[dir...]').action(function (dirs) { 35 | if (process.env.PHL_CWD) { 36 | phl.init(); 37 | phl.wrap(); 38 | var name = require.resolve('../'); 39 | delete require.cache[name]; 40 | 41 | return wrap.runMain(); 42 | } 43 | phl.init(); 44 | phl.cleanup(); 45 | 46 | wrap([__filename], { 47 | PHL_CWD: process.cwd() 48 | }); 49 | 50 | foreground(dirs, function (done) { 51 | internals.report(program.args); 52 | return done(); 53 | }); 54 | }); 55 | 56 | program.parse(process.argv); 57 | -------------------------------------------------------------------------------- /bin/phl.es6: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const foreground = require('foreground-child'); 5 | const wrap = require('spawn-wrap'); 6 | const program = require('commander'); 7 | const phl = require('../'); 8 | const pkg = require('../package.json'); 9 | const internals = {}; 10 | 11 | internals.report = function report(args) { 12 | args = args || {}; 13 | if (args.silent) { 14 | return; 15 | } 16 | process.env.PHL_CWD = process.cwd(); 17 | phl.init({ 18 | reporter: args.reporter || 'text' 19 | }); 20 | phl.report(); 21 | }; 22 | 23 | internals.istanbulCheckCoverage = function istanbulCheckCoverage(options) { 24 | foreground(process.execPath, [require.resolve('istanbul/lib/cli'), 25 | 'check-coverage', 26 | '--lines=' + options.lines, 27 | '--functions=' + options.functions, 28 | '--branches=' + options.branches, 29 | '--statements=' + options.statements, 30 | path.resolve(process.cwd(), './.phl_output/*.json') 31 | ]); 32 | }; 33 | 34 | program 35 | .version(pkg.version) 36 | .usage('[cmd | options] '); 37 | 38 | program 39 | .command('report') 40 | .option('-r, --reporter ', 'specify report to use, default:text') 41 | .option('-s, --silent', 'dont output any coverage') 42 | .description('run coverage report for .phl_output') 43 | .action(internals.report); 44 | 45 | program 46 | .command('check-coverage') 47 | .description('check whether coverage is within thresholds provided') 48 | .option('-l, --lines ', 'what n of lines must be covered?') 49 | .option('-b, --branches ', 'what n of branches must be covered?') 50 | .option('-f, --functions ', 'what n of functions must be covered?') 51 | .option('-s, --statements ', 'what n of statements must be covered?') 52 | .action(internals.istanbulCheckCoverage); 53 | 54 | 55 | program 56 | .arguments('[dir...]') 57 | .action(function(dirs) { 58 | if (process.env.PHL_CWD) { 59 | phl.init(); 60 | phl.wrap(); 61 | const name = require.resolve('../'); 62 | delete require.cache[name]; 63 | 64 | return wrap.runMain(); 65 | } 66 | phl.init(); 67 | phl.cleanup(); 68 | 69 | wrap([__filename], { 70 | PHL_CWD: process.cwd() 71 | }); 72 | 73 | foreground(dirs, function(done) { 74 | internals.report(program.args); 75 | return done(); 76 | }); 77 | 78 | }); 79 | 80 | 81 | program.parse(process.argv); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # phl 2 | 3 | > **This is a slimmer es6 version and fork of [nyc](https://github.com/bcoe/nyc)** 4 | 5 | Supporting currently Mac OSX only at the time, if you want better support use [nyc](https://github.com/bcoe/nyc). 6 | 7 | 8 | ```shell 9 | phl npm test 10 | ``` 11 | 12 | a code coverage tool built on [istanbul](https://www.npmjs.com/package/istanbul) 13 | that works for applications that spawn subprocesses. 14 | 15 | ## Why? 16 | 17 | Because the place i work needed some features that nyc currently does not provide... simple as that. 18 | 19 | ## Instrumenting Your Code 20 | 21 | Simply run your tests with `phl`, and it will collect coverage information for 22 | each process and store it in `phl_output`. 23 | 24 | ```shell 25 | phl npm test 26 | ``` 27 | 28 | you can pass a list of Istanbul reporters that you'd like to run: 29 | 30 | ```shell 31 | phl --reporter=lcov --reporter=text-lcov npm test 32 | ``` 33 | 34 | If you're so inclined, you can simply add phl to the test stanza in your package.json: 35 | 36 | ```json 37 | { 38 | "script": { 39 | "test": "phl tap ./test/*.js" 40 | } 41 | } 42 | ``` 43 | 44 | ## Checking Coverage 45 | 46 | phl exposes istanbul's check-coverage tool. After running your tests with phl, 47 | simply run: 48 | 49 | ```shell 50 | phl check-coverage --lines 95 --functions 95 --branches 95 51 | ``` 52 | 53 | This feature makes it easy to fail your tests if coverage drops below a given threshold. 54 | 55 | ## Running Reports 56 | 57 | Once you've run your tests with phl, simply run: 58 | 59 | ```bash 60 | phl report 61 | ``` 62 | 63 | To view your coverage report: 64 | 65 | ```shell 66 | --------------------|-----------|-----------|-----------|-----------| 67 | File | % Stmts |% Branches | % Funcs | % Lines | 68 | --------------------|-----------|-----------|-----------|-----------| 69 | ./ | 85.96 | 50 | 75 | 92.31 | 70 | index.js | 85.96 | 50 | 75 | 92.31 | 71 | ./test/ | 98.08 | 50 | 95 | 98.04 | 72 | phl-test.js | 98.08 | 50 | 95 | 98.04 | 73 | ./test/fixtures/ | 100 | 100 | 100 | 100 | 74 | sigint.js | 100 | 100 | 100 | 100 | 75 | sigterm.js | 100 | 100 | 100 | 100 | 76 | --------------------|-----------|-----------|-----------|-----------| 77 | All files | 91.89 | 50 | 86.11 | 95.24 | 78 | --------------------|-----------|-----------|-----------|-----------| 79 | ``` 80 | 81 | you can use any reporters that are supported by istanbul: 82 | 83 | ```bash 84 | phl report --reporter=lcov 85 | ``` 86 | 87 | ## Including and Excluding Files 88 | 89 | By default phl does not instrument files in `node_modules`, or `test` 90 | for coverage. You can override this setting in your package.json, by 91 | adding the following configuration: 92 | 93 | ```js 94 | {"config": { 95 | "phl": { 96 | "exclude": [ 97 | "node_modules/" 98 | ] 99 | } 100 | }} 101 | ``` 102 | 103 | If you need coverage for files/directories inside `node_modules` you can include them 104 | like so: 105 | 106 | ```js 107 | {"config": { 108 | "phl": { 109 | "include": [ 110 | "node_modules/utils" 111 | ] 112 | } 113 | }} 114 | ``` 115 | 116 | For a better illustration the following: 117 | 118 | ```js 119 | {"config": { 120 | "phl": { 121 | "exclude": [ 122 | "node_modules/" 123 | ], 124 | "include": [ 125 | "node_modules/utils" 126 | ] 127 | } 128 | }} 129 | ``` 130 | 131 | excludes all files inside of `node_modules` directory other than the utils directory 132 | 133 | -------------------------------------------------------------------------------- /index.es6: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const mkdirp = require('mkdirp'); 3 | const path = require('path'); 4 | const rimraf = require('rimraf'); 5 | const onExit = require('signal-exit'); 6 | const stripBom = require('strip-bom'); 7 | const xtend = require("xtend"); 8 | 9 | const internals = {}; 10 | const PHL = {}; 11 | 12 | internals.defaultOpts = { 13 | subprocessBin: path.resolve(__dirname, './bin/PHL.js'), 14 | tempDirectory: './.phl_output', 15 | cwd: process.env.PHL_CWD || process.cwd(), 16 | reporter: 'text', 17 | istanbul: require('istanbul') 18 | }; 19 | 20 | internals.getPkgConfig = function getPkgConfig() { 21 | const getConfigObj = require(path.resolve(internals.config.cwd, './package.json')).config || {}; 22 | return getConfigObj.phl; 23 | }; 24 | 25 | internals.toList = xs => Array.isArray(xs) ? xs : [xs]; 26 | 27 | internals.createRegexPaths = list => internals.toList(list).map((path) => new RegExp(path)); 28 | 29 | internals.createInstrumenter = () => new internals.config.istanbul.Instrumenter(); 30 | 31 | internals.addFile = function addFile(filename) { 32 | let instrument = true; 33 | const relFile = path.relative(internals.config.cwd, filename); 34 | 35 | PHL.exclude.forEach(function(exclude) { 36 | if (exclude.test(filename)) { 37 | instrument = false; 38 | } 39 | }); 40 | 41 | PHL.include.forEach(function(include) { 42 | if (include.test(filename)) { 43 | instrument = true; 44 | } 45 | }); 46 | 47 | let content = stripBom(fs.readFileSync(filename, 'utf8')); 48 | 49 | if (instrument) { 50 | content = internals.instrumenter.instrumentSync(content, './' + relFile); 51 | } 52 | 53 | return { 54 | instrument: instrument, 55 | content: content, 56 | relFile: relFile 57 | }; 58 | }; 59 | 60 | internals.wrapRequire = function wrapRequire() { 61 | require.extensions['.js'] = function(module, filename) { 62 | const obj = internals.addFile(filename); 63 | module._compile(obj.content, filename); 64 | }; 65 | }; 66 | 67 | internals.createOutputDirectory = () => mkdirp.sync(internals.tmpDirectory()); 68 | 69 | internals.wrapExit = function wrapExit() { 70 | onExit(function() { 71 | internals.writeCoverageFile(); 72 | }, { 73 | alwaysLast: true 74 | }); 75 | }; 76 | 77 | internals.writeCoverageFile = function writeCoverageFile() { 78 | let coverage = global.__coverage__; 79 | if (typeof __coverage__ === 'object') { 80 | coverage = __coverage__; 81 | } 82 | if (!coverage) { 83 | return; 84 | } 85 | 86 | fs.writeFileSync(path.resolve(internals.tmpDirectory(), './', process.pid + '.json'), JSON.stringify(coverage), 'utf-8'); 87 | }; 88 | 89 | 90 | internals.loadReports = function loadReports() { 91 | const files = fs.readdirSync(internals.tmpDirectory()); 92 | 93 | return files.map(function(f) { 94 | try { 95 | return JSON.parse(fs.readFileSync(path.resolve(internals.tmpDirectory(), './', f), 'utf-8')); 96 | } catch (e) { 97 | return {}; 98 | } 99 | }); 100 | }; 101 | 102 | internals.tmpDirectory = () => path.resolve(internals.config.cwd, './', internals.config.tempDirectory); 103 | 104 | PHL.init = function init(opts) { 105 | internals.config = xtend(internals.defaultOpts, opts); 106 | const phlConfig = internals.getPkgConfig(); 107 | 108 | internals.reporterList = internals.toList(internals.config.reporter); 109 | 110 | internals.exclude = phlConfig.exclude || ['node_modules/', 'test/', 'test.js']; 111 | internals.include = phlConfig.include || []; 112 | 113 | PHL.exclude = internals.createRegexPaths(internals.exclude); 114 | PHL.include = internals.createRegexPaths(internals.include); 115 | 116 | internals.instrumenter = internals.createInstrumenter(); 117 | internals.createOutputDirectory(); 118 | }; 119 | 120 | 121 | PHL.cleanup = function cleanup() { 122 | if (!process.env.PHL_CWD) { 123 | rimraf.sync(internals.tmpDirectory()); 124 | } 125 | }; 126 | 127 | PHL.wrap = function wrap() { 128 | internals.wrapRequire(); 129 | internals.wrapExit(); 130 | }; 131 | 132 | PHL.report = function report(next, _collector, _reporter) { 133 | next = next || function() {}; 134 | 135 | const collector = _collector || new internals.config.istanbul.Collector(); 136 | const reporter = _reporter || new internals.config.istanbul.Reporter(); 137 | 138 | internals.loadReports().forEach(function(report) { 139 | collector.add(report); 140 | }); 141 | 142 | internals.reporterList.forEach(function(porter) { 143 | reporter.add(porter); 144 | }); 145 | 146 | reporter.write(collector, true, next); 147 | }; 148 | 149 | module.exports = PHL; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function _typeof(obj) { return obj && typeof Symbol !== "undefined" && obj.constructor === Symbol ? "symbol" : typeof obj; } 4 | 5 | var fs = require('fs'); 6 | var mkdirp = require('mkdirp'); 7 | var path = require('path'); 8 | var rimraf = require('rimraf'); 9 | var onExit = require('signal-exit'); 10 | var stripBom = require('strip-bom'); 11 | var xtend = require("xtend"); 12 | 13 | var internals = {}; 14 | var PHL = {}; 15 | 16 | internals.defaultOpts = { 17 | subprocessBin: path.resolve(__dirname, './bin/PHL.js'), 18 | tempDirectory: './.phl_output', 19 | cwd: process.env.PHL_CWD || process.cwd(), 20 | reporter: 'text', 21 | istanbul: require('istanbul') 22 | }; 23 | 24 | internals.getPkgConfig = function getPkgConfig() { 25 | var getConfigObj = require(path.resolve(internals.config.cwd, './package.json')).config || {}; 26 | return getConfigObj.phl; 27 | }; 28 | 29 | internals.toList = function (xs) { 30 | return Array.isArray(xs) ? xs : [xs]; 31 | }; 32 | 33 | internals.createRegexPaths = function (list) { 34 | return internals.toList(list).map(function (path) { 35 | return new RegExp(path); 36 | }); 37 | }; 38 | 39 | internals.createInstrumenter = function () { 40 | return new internals.config.istanbul.Instrumenter(); 41 | }; 42 | 43 | internals.addFile = function addFile(filename) { 44 | var instrument = true; 45 | var relFile = path.relative(internals.config.cwd, filename); 46 | 47 | PHL.exclude.forEach(function (exclude) { 48 | if (exclude.test(filename)) { 49 | instrument = false; 50 | } 51 | }); 52 | 53 | PHL.include.forEach(function (include) { 54 | if (include.test(filename)) { 55 | instrument = true; 56 | } 57 | }); 58 | 59 | var content = stripBom(fs.readFileSync(filename, 'utf8')); 60 | 61 | if (instrument) { 62 | content = internals.instrumenter.instrumentSync(content, './' + relFile); 63 | } 64 | 65 | return { 66 | instrument: instrument, 67 | content: content, 68 | relFile: relFile 69 | }; 70 | }; 71 | 72 | internals.wrapRequire = function wrapRequire() { 73 | require.extensions['.js'] = function (module, filename) { 74 | var obj = internals.addFile(filename); 75 | module._compile(obj.content, filename); 76 | }; 77 | }; 78 | 79 | internals.createOutputDirectory = function () { 80 | return mkdirp.sync(internals.tmpDirectory()); 81 | }; 82 | 83 | internals.wrapExit = function wrapExit() { 84 | onExit(function () { 85 | internals.writeCoverageFile(); 86 | }, { 87 | alwaysLast: true 88 | }); 89 | }; 90 | 91 | internals.writeCoverageFile = function writeCoverageFile() { 92 | var coverage = global.__coverage__; 93 | if ((typeof __coverage__ === 'undefined' ? 'undefined' : _typeof(__coverage__)) === 'object') { 94 | coverage = __coverage__; 95 | } 96 | if (!coverage) { 97 | return; 98 | } 99 | 100 | fs.writeFileSync(path.resolve(internals.tmpDirectory(), './', process.pid + '.json'), JSON.stringify(coverage), 'utf-8'); 101 | }; 102 | 103 | internals.loadReports = function loadReports() { 104 | var files = fs.readdirSync(internals.tmpDirectory()); 105 | 106 | return files.map(function (f) { 107 | try { 108 | return JSON.parse(fs.readFileSync(path.resolve(internals.tmpDirectory(), './', f), 'utf-8')); 109 | } catch (e) { 110 | return {}; 111 | } 112 | }); 113 | }; 114 | 115 | internals.tmpDirectory = function () { 116 | return path.resolve(internals.config.cwd, './', internals.config.tempDirectory); 117 | }; 118 | 119 | PHL.init = function init(opts) { 120 | internals.config = xtend(internals.defaultOpts, opts); 121 | var phlConfig = internals.getPkgConfig(); 122 | 123 | internals.reporterList = internals.toList(internals.config.reporter); 124 | 125 | internals.exclude = phlConfig.exclude || ['node_modules/', 'test/', 'test.js']; 126 | internals.include = phlConfig.include || []; 127 | 128 | PHL.exclude = internals.createRegexPaths(internals.exclude); 129 | PHL.include = internals.createRegexPaths(internals.include); 130 | 131 | internals.instrumenter = internals.createInstrumenter(); 132 | internals.createOutputDirectory(); 133 | }; 134 | 135 | PHL.cleanup = function cleanup() { 136 | if (!process.env.PHL_CWD) { 137 | rimraf.sync(internals.tmpDirectory()); 138 | } 139 | }; 140 | 141 | PHL.wrap = function wrap() { 142 | internals.wrapRequire(); 143 | internals.wrapExit(); 144 | }; 145 | 146 | PHL.report = function report(next, _collector, _reporter) { 147 | next = next || function () {}; 148 | 149 | var collector = _collector || new internals.config.istanbul.Collector(); 150 | var reporter = _reporter || new internals.config.istanbul.Reporter(); 151 | 152 | internals.loadReports().forEach(function (report) { 153 | collector.add(report); 154 | }); 155 | 156 | internals.reporterList.forEach(function (porter) { 157 | reporter.add(porter); 158 | }); 159 | 160 | reporter.write(collector, true, next); 161 | }; 162 | 163 | module.exports = PHL; 164 | --------------------------------------------------------------------------------