├── .travis.yml ├── .gitignore ├── test ├── fixtures │ ├── lib │ │ ├── add.js │ │ └── multiply.js │ └── test │ │ └── add.js ├── .jshintrc └── main.js ├── SECURITY.md ├── .editorconfig ├── .jshintrc ├── LICENSE-MIT ├── package.json ├── index.js └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4 4 | - 6 5 | - node 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | cov-foo/ 4 | json/ 5 | lcovonly/ 6 | *.orig 7 | -------------------------------------------------------------------------------- /test/fixtures/lib/add.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.add = function (a, b) { 4 | return a + b; 5 | }; 6 | 7 | exports.missed = function () { 8 | return "not covered"; 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/lib/multiply.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.multiply = function (a, b) { 4 | return a * b; 5 | }; 6 | 7 | exports.missed = function () { 8 | return "This entire file is not covered because it's never required."; 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/test/add.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var mod = require('../lib/add'); 5 | 6 | describe('#add', function () { 7 | it('add numbers', function () { 8 | assert.equal(mod.add(1, 1), 2); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.jshintrc", 3 | "globals": { 4 | "describe": false, 5 | "it": false, 6 | "beforeEach": false, 7 | "afterEach": false, 8 | "before": false, 9 | "after": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security contact information 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | 5 | You can also send an email to admin@simonboudrias.com for direct contact with the maintainer. 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [test/fixtures/*] 15 | insert_final_newline = false 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "bitwise": true, 4 | "camelcase": true, 5 | "eqeqeq": true, 6 | "forin": true, 7 | "freeze": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": true, 11 | "newcap": true, 12 | "noarg": true, 13 | "noempty": true, 14 | "nonew": true, 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "white": true 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2013 Simon Boudrias 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-istanbul", 3 | "version": "1.1.3", 4 | "description": "Istanbul unit test coverage plugin for gulp.", 5 | "keywords": [ 6 | "gulpplugin", 7 | "coverage", 8 | "istanbul", 9 | "unit test" 10 | ], 11 | "homepage": "https://github.com/SBoudrias/gulp-istanbul", 12 | "bugs": "https://github.com/SBoudrias/gulp-istanbul/issues", 13 | "author": { 14 | "name": "Simon Boudrias", 15 | "email": "admin@simonboudrias.com", 16 | "url": "https://github.com/SBoudrias" 17 | }, 18 | "main": "index.js", 19 | "files": [ 20 | "index.js" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "git://github.com/SBoudrias/gulp-istanbul.git" 25 | }, 26 | "scripts": { 27 | "pretest": "jshint index.js ./test/.", 28 | "test": "mocha -R spec" 29 | }, 30 | "dependencies": { 31 | "istanbul": "^0.4.0", 32 | "istanbul-threshold-checker": "^0.2.1", 33 | "lodash": "^4.0.0", 34 | "plugin-error": "^1.0.0", 35 | "through2": "^3.0.0", 36 | "vinyl-sourcemaps-apply": "^0.2.1" 37 | }, 38 | "devDependencies": { 39 | "gulp": "^3.6.2", 40 | "gulp-mocha": "^3.0.1", 41 | "gulp-sourcemaps": "^2.2.0", 42 | "isparta": "^4.0.0", 43 | "jshint": "^2.5.0", 44 | "mocha": "^5.0.0", 45 | "rimraf": "^2.2.8", 46 | "vinyl": "^2.1.0" 47 | }, 48 | "license": "MIT" 49 | } 50 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var through = require('through2').obj; 4 | var path = require('path'); 5 | var checker = require('istanbul-threshold-checker'); 6 | // Make sure istanbul is `require`d after the istanbul-threshold-checker to use the istanbul version 7 | // defined in this package.json instead of the one defined in istanbul-threshold-checker. 8 | var istanbul = require('istanbul'); 9 | var _ = require('lodash'); 10 | var applySourceMap = require('vinyl-sourcemaps-apply'); 11 | var Report = istanbul.Report; 12 | var Collector = istanbul.Collector; 13 | var PluginError = require('plugin-error'); 14 | 15 | var PLUGIN_NAME = 'gulp-istanbul'; 16 | var COVERAGE_VARIABLE = '$$cov_' + new Date().getTime() + '$$'; 17 | 18 | function normalizePathSep(filepath) { 19 | return filepath.replace(/\//g, path.sep); 20 | } 21 | 22 | var plugin = module.exports = function (opts) { 23 | opts = opts || {}; 24 | _.defaults(opts, { 25 | coverageVariable: COVERAGE_VARIABLE, 26 | instrumenter: istanbul.Instrumenter 27 | }); 28 | opts.includeUntested = opts.includeUntested === true; 29 | 30 | return through(function (file, enc, cb) { 31 | var fileContents = file.contents.toString(); 32 | var fileOpts = _.cloneDeep(opts); 33 | 34 | if (file.sourceMap) { 35 | fileOpts = _.defaultsDeep(fileOpts, { 36 | codeGenerationOptions: { 37 | sourceMap: file.sourceMap.file, 38 | sourceMapWithCode: true, 39 | sourceContent: fileContents, 40 | sourceMapRoot: file.sourceMap.sourceRoot, 41 | file: normalizePathSep(file.path) 42 | } 43 | }); 44 | } 45 | var instrumenter = new opts.instrumenter(fileOpts); 46 | 47 | cb = _.once(cb); 48 | if (!(file.contents instanceof Buffer)) { 49 | return cb(new PluginError(PLUGIN_NAME, 'streams not supported')); 50 | } 51 | 52 | var filepath = normalizePathSep(file.path); 53 | instrumenter.instrument(fileContents, filepath, function (err, code) { 54 | if (err) { 55 | return cb(new PluginError( 56 | PLUGIN_NAME, 57 | 'Unable to parse ' + filepath + '\n\n' + err.message + '\n' 58 | )); 59 | } 60 | 61 | var sourceMap = instrumenter.lastSourceMap(); 62 | if (sourceMap !== null) { 63 | applySourceMap(file, sourceMap.toString()); 64 | } 65 | 66 | file.contents = new Buffer(code); 67 | 68 | // Parse the blank coverage object from the instrumented file and save it 69 | // to the global coverage variable to enable reporting on non-required 70 | // files, a workaround for 71 | // https://github.com/gotwarlost/istanbul/issues/112 72 | if (opts.includeUntested) { 73 | var instrumentedSrc = file.contents.toString(); 74 | var covStubRE = /\{.*"path".*"fnMap".*"statementMap".*"branchMap".*\}/g; 75 | var covStubMatch = covStubRE.exec(instrumentedSrc); 76 | if (covStubMatch !== null) { 77 | var covStub = JSON.parse(covStubMatch[0]); 78 | global[opts.coverageVariable] = global[opts.coverageVariable] || {}; 79 | global[opts.coverageVariable][path.resolve(filepath)] = covStub; 80 | } 81 | } 82 | 83 | return cb(err, file); 84 | }); 85 | }); 86 | }; 87 | 88 | plugin.hookRequire = function (options) { 89 | var fileMap = {}; 90 | 91 | istanbul.hook.unhookRequire(); 92 | istanbul.hook.hookRequire(function (path) { 93 | return !!fileMap[normalizePathSep(path)]; 94 | }, function (code, path) { 95 | return fileMap[normalizePathSep(path)]; 96 | }, options); 97 | 98 | return through(function (file, enc, cb) { 99 | // If the file is already required, delete it from the cache otherwise the covered 100 | // version will be ignored. 101 | delete require.cache[path.resolve(file.path)]; 102 | fileMap[normalizePathSep(file.path)] = file.contents.toString(); 103 | return cb(); 104 | }); 105 | }; 106 | 107 | plugin.summarizeCoverage = function (opts) { 108 | opts = opts || {}; 109 | if (!opts.coverageVariable) opts.coverageVariable = COVERAGE_VARIABLE; 110 | 111 | if (!global[opts.coverageVariable]) throw new Error('no coverage data found, run tests before calling `summarizeCoverage`'); 112 | 113 | var collector = new Collector(); 114 | collector.add(global[opts.coverageVariable]); 115 | return istanbul.utils.summarizeCoverage(collector.getFinalCoverage()); 116 | }; 117 | 118 | plugin.writeReports = function (opts) { 119 | if (typeof opts === 'string') opts = { dir: opts }; 120 | opts = opts || {}; 121 | 122 | var defaultDir = path.join(process.cwd(), 'coverage'); 123 | opts = _.defaultsDeep(opts, { 124 | coverageVariable: COVERAGE_VARIABLE, 125 | dir: defaultDir, 126 | reportOpts: { 127 | dir: opts.dir || defaultDir 128 | } 129 | }); 130 | opts.reporters = opts.reporters || [ 'lcov', 'json', 'text', 'text-summary' ]; 131 | 132 | var reporters = opts.reporters.map(function(reporter) { 133 | if (reporter.TYPE) Report.register(reporter); 134 | return reporter.TYPE || reporter; 135 | }); 136 | 137 | var invalid = _.difference(reporters, Report.getReportList()); 138 | if (invalid.length) { 139 | // throw before we start -- fail fast 140 | throw new PluginError(PLUGIN_NAME, 'Invalid reporters: ' + invalid.join(', ')); 141 | } 142 | 143 | reporters = reporters.map(function (r) { 144 | var reportOpts = opts.reportOpts[r] || opts.reportOpts; 145 | return Report.create(r, _.clone(reportOpts)); 146 | }); 147 | 148 | var cover = through(); 149 | 150 | cover.on('end', function () { 151 | var collector = new Collector(); 152 | 153 | // Revert to an object if there are no matching source files. 154 | collector.add(global[opts.coverageVariable] || {}); 155 | 156 | reporters.forEach(function (report) { 157 | report.writeReport(collector, true); 158 | }); 159 | }).resume(); 160 | 161 | return cover; 162 | }; 163 | 164 | plugin.enforceThresholds = function (opts) { 165 | opts = opts || {}; 166 | opts = _.defaults(opts, { 167 | coverageVariable: COVERAGE_VARIABLE 168 | }); 169 | 170 | var cover = through(); 171 | 172 | cover.on('end', function () { 173 | var collector = new Collector(); 174 | 175 | // Revert to an object if there are no macthing source files. 176 | collector.add(global[opts.coverageVariable] || {}); 177 | 178 | var results = checker.checkFailures(opts.thresholds, collector.getFinalCoverage()); 179 | var criteria = function(type) { 180 | return (type.global && type.global.failed) || (type.each && type.each.failed); 181 | }; 182 | 183 | if (_.some(results, criteria)) { 184 | this.emit('error', new PluginError({ 185 | plugin: PLUGIN_NAME, 186 | message: 'Coverage failed' 187 | })); 188 | } 189 | 190 | }).resume(); 191 | 192 | return cover; 193 | }; 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gulp-istanbul [![NPM version][npm-image]][npm-url] 2 | =========================== 3 | 4 | [Istanbul][istanbul] unit test coverage plugin for [gulp][gulp]. 5 | 6 | Works on top of any Node.js unit test framework. 7 | 8 | Installation 9 | --------------- 10 | 11 | ```shell 12 | npm install --save-dev gulp-istanbul 13 | ``` 14 | 15 | Example 16 | --------------- 17 | 18 | In your `gulpfile.js`: 19 | 20 | #### Node.js testing 21 | 22 | ```javascript 23 | var istanbul = require('gulp-istanbul'); 24 | // We'll use mocha in this example, but any test framework will work 25 | var mocha = require('gulp-mocha'); 26 | 27 | gulp.task('pre-test', function () { 28 | return gulp.src(['lib/**/*.js']) 29 | // Covering files 30 | .pipe(istanbul()) 31 | // Force `require` to return covered files 32 | .pipe(istanbul.hookRequire()); 33 | }); 34 | 35 | gulp.task('test', ['pre-test'], function () { 36 | return gulp.src(['test/*.js']) 37 | .pipe(mocha()) 38 | // Creating the reports after tests ran 39 | .pipe(istanbul.writeReports()) 40 | // Enforce a coverage of at least 90% 41 | .pipe(istanbul.enforceThresholds({ thresholds: { global: 90 } })); 42 | }); 43 | ``` 44 | 45 | **Note:** Version 4.x.x of `gulp-mocha` is not supported (see issue [#115](https://github.com/SBoudrias/gulp-istanbul/issues/115) for details). In this example, you should use `gulp-mocha` version 3.0.1 for the time being. 46 | 47 | #### Browser testing 48 | 49 | For browser testing, you'll need to write the files covered by istanbul in a directory from where you'll serve these files to the browser running the test. You'll also need a way to extract the value of the [coverage variable](#coveragevariable) after the test have runned in the browser. 50 | 51 | Browser testing is hard. If you're not sure what to do, then I suggest you take a look at [Karma test runner](http://karma-runner.github.io) - it has built-in coverage using Istanbul. 52 | 53 | 54 | ```javascript 55 | var istanbul = require('gulp-istanbul'); 56 | 57 | 58 | gulp.task('pre-test', function () { 59 | return gulp.src(['lib/**/*.js']) 60 | // Covering files 61 | .pipe(istanbul()) 62 | // Write the covered files to a temporary directory 63 | .pipe(gulp.dest('test-tmp/')); 64 | }); 65 | 66 | gulp.task('test', ['pre-test'], function () { 67 | // Make sure your tests files are requiring files from the 68 | // test-tmp/ directory 69 | return gulp.src(['test/*.js']) 70 | .pipe(testFramework()) 71 | // Creating the reports after tests ran 72 | .pipe(istanbul.writeReports()); 73 | }); 74 | ``` 75 | 76 | #### Source Maps 77 | gulp-istanbul supports [gulp-sourcemaps][gulp-sourcemaps] when instrumenting: 78 | 79 | 80 | ```javascript 81 | gulp.task('pre-test', function () { 82 | return gulp.src(['lib/**/*.js']) 83 | // optionally load existing source maps 84 | .pipe(sourcemaps.init()) 85 | // Covering files 86 | .pipe(istanbul()) 87 | .pipe(sourcemaps.write('.')) 88 | // Write the covered files to a temporary directory 89 | .pipe(gulp.dest('test-tmp/')); 90 | }); 91 | ``` 92 | 93 | API 94 | -------------- 95 | 96 | ### istanbul(opt) 97 | 98 | Instrument files passed in the stream. 99 | 100 | #### opt 101 | Type: `Object` (optional) 102 | ```js 103 | { 104 | coverageVariable: 'someVariable', 105 | ...other Instrumeter options... 106 | } 107 | ``` 108 | 109 | ##### coverageVariable 110 | Type: `String` (optional) 111 | Default: `'$$cov_' + new Date().getTime() + '$$'` 112 | 113 | The global variable istanbul uses to store coverage 114 | 115 | See also: 116 | - [istanbul coverageVariable][istanbul-coverage-variable] 117 | - [SanboxedModule][sandboxed-module-coverage-variable] 118 | 119 | ##### includeUntested 120 | Type: `Boolean` (optional) 121 | Default: `false` 122 | 123 | Flag to include test coverage of files that aren't `require`d by any tests 124 | 125 | See also: 126 | - [istanbul "0% coverage" issue](https://github.com/gotwarlost/istanbul/issues/112) 127 | 128 | ##### instrumenter 129 | Type: `Instrumenter` (optional) 130 | Default: `istanbul.Instrumenter` 131 | 132 | Custom Instrumenter to be used instead of the default istanbul one. 133 | 134 | ```js 135 | var isparta = require('isparta'); 136 | var istanbul = require('gulp-istanbul'); 137 | 138 | gulp.src('lib/**.js') 139 | .pipe(istanbul({ 140 | // supports es6 141 | instrumenter: isparta.Instrumenter 142 | })); 143 | ``` 144 | 145 | See also: 146 | - [isparta](https://github.com/douglasduteil/isparta) 147 | 148 | ##### Other Istanbul Instrumenter options 149 | 150 | See: 151 | - [istanbul Instrumenter documentation][istanbul-coverage-variable] 152 | 153 | ### istanbul.hookRequire() 154 | 155 | Overwrite `require` so it returns the covered files. The method take an optional [option object](https://gotwarlost.github.io/istanbul/public/apidocs/classes/Hook.html#method_hookRequire). 156 | 157 | Always use this option if you're running tests in Node.js 158 | 159 | ### istanbul.summarizeCoverage(opt) 160 | 161 | get coverage summary details 162 | 163 | #### opt 164 | Type: `Object` (optional) 165 | ```js 166 | { 167 | coverageVariable: 'someVariable' 168 | } 169 | ``` 170 | ##### coverageVariable 171 | Type: `String` (optional) 172 | Default: `'$$cov_' + new Date().getTime() + '$$'` 173 | 174 | The global variable istanbul uses to store coverage 175 | 176 | See also: 177 | - [istanbul coverageVariable][istanbul-coverage-variable] 178 | - [SanboxedModule][sandboxed-module-coverage-variable] 179 | 180 | #### returns 181 | Type: `Object` 182 | ```js 183 | { 184 | lines: { total: 4, covered: 2, skipped: 0, pct: 50 }, 185 | statements: { total: 4, covered: 2, skipped: 0, pct: 50 }, 186 | functions: { total: 2, covered: 0, skipped: 0, pct: 0 }, 187 | branches: { total: 0, covered: 0, skipped: 0, pct: 100 } 188 | } 189 | ``` 190 | 191 | See also: 192 | - [istanbul utils.summarizeCoverage()][istanbul-summarize-coverage] 193 | 194 | 195 | ### istanbul.writeReports(opt) 196 | 197 | Create the reports on stream end. 198 | 199 | #### opt 200 | Type: `Object` (optional) 201 | ```js 202 | { 203 | dir: './coverage', 204 | reporters: [ 'lcov', 'json', 'text', 'text-summary', CustomReport ], 205 | reportOpts: { dir: './coverage' }, 206 | coverageVariable: 'someVariable' 207 | } 208 | ``` 209 | 210 | You can pass individual configuration to a reporter. 211 | ```js 212 | { 213 | dir: './coverage', 214 | reporters: [ 'lcovonly', 'json', 'text', 'text-summary', CustomReport ], 215 | reportOpts: { 216 | lcov: {dir: 'lcovonly', file: 'lcov.info'}, 217 | json: {dir: 'json', file: 'converage.json'} 218 | }, 219 | coverageVariable: 'someVariable' 220 | } 221 | ``` 222 | ##### dir 223 | Type: `String` (optional) 224 | Default: `./coverage` 225 | 226 | The folder in which the reports are to be outputted. 227 | 228 | ##### reporters 229 | Type: `Array` (optional) 230 | Default: `[ 'lcov', 'json', 'text', 'text-summary' ]` 231 | 232 | The list of available reporters: 233 | - `clover` 234 | - `cobertura` 235 | - `html` 236 | - `json` 237 | - `lcov` 238 | - `lcovonly` 239 | - `none` 240 | - `teamcity` 241 | - `text` 242 | - `text-summary` 243 | 244 | You can also specify one or more custom reporter objects as items in the array. These will be automatically registered with istanbul. 245 | 246 | See also `require('istanbul').Report.getReportList()` 247 | 248 | ##### reportOpts 249 | Type: `Object` (optional) 250 | ```js 251 | { 252 | dir: './coverage' 253 | } 254 | ``` 255 | 256 | You can also configure separate directory for each report. 257 | ```js 258 | { 259 | html: { 260 | dir: './coverage/html', 261 | watermarks: { 262 | statements: [ 50, 80 ], 263 | lines: [ 50, 80 ], 264 | functions: [ 50, 80], 265 | branches: [ 50, 80 ] 266 | } 267 | }, 268 | lcov: {dir: './coverage/lcov'}, 269 | lcovonly: {dir: './coverage/lcovonly'}, 270 | json: {dir: './coverage/json'}, 271 | } 272 | ``` 273 | `watermarks` can be used to confgure the color of the HTML report. 274 | Default colors are.. RED: below 50% coverage, YELLOW: 50-80% coverage, GREEN: above 80% 275 | 276 | ##### coverageVariable 277 | Type: `String` (optional) 278 | Default: `'$$cov_' + new Date().getTime() + '$$'` 279 | 280 | The global variable istanbul uses to store coverage 281 | 282 | See also: 283 | - [istanbul coverageVariable][istanbul-coverage-variable] 284 | - [SanboxedModule][sandboxed-module-coverage-variable] 285 | 286 | 287 | ### istanbul.enforceThresholds(opt) 288 | 289 | Checks coverage against minimum acceptable thresholds. Fails the build if any of the thresholds are not met. 290 | 291 | #### opt 292 | Type: `Object` (optional) 293 | ```js 294 | { 295 | coverageVariable: 'someVariable', 296 | thresholds: { 297 | global: 60, 298 | each: -10 299 | } 300 | } 301 | ``` 302 | 303 | ##### coverageVariable 304 | Type: `String` (optional) 305 | Default: `'$$cov_' + new Date().getTime() + '$$'` 306 | 307 | The global variable istanbul uses to store coverage 308 | 309 | 310 | ##### thresholds 311 | Type: `Object` (required) 312 | 313 | Minimum acceptable coverage thresholds. Any coverage values lower than the specified threshold will fail the build. 314 | 315 | Each threshold value can be: 316 | - A positive number - used as a percentage 317 | - A negative number - used as the maximum amount of coverage gaps 318 | - A falsey value will skip the coverage 319 | 320 | Thresholds can be specified across all files (`global`) or per file (`each`): 321 | ``` 322 | { 323 | global: 80, 324 | each: 60 325 | } 326 | ``` 327 | 328 | You can also specify a value for each metric: 329 | ``` 330 | { 331 | global: { 332 | statements: 80, 333 | branches: 90, 334 | lines: 70, 335 | functions: -10 336 | } 337 | each: { 338 | statements: 100, 339 | branches: 70, 340 | lines: -20 341 | } 342 | } 343 | ``` 344 | 345 | #### emits 346 | 347 | A plugin error in the stream if the coverage fails 348 | 349 | License 350 | ------------ 351 | 352 | [MIT License](http://en.wikipedia.org/wiki/MIT_License) (c) Simon Boudrias - 2013 353 | 354 | [istanbul]: http://gotwarlost.github.io/istanbul/ 355 | [gulp]: https://github.com/gulpjs/gulp 356 | [gulp-sourcemaps]: https://github.com/floridoo/gulp-sourcemaps 357 | 358 | [npm-url]: https://npmjs.org/package/gulp-istanbul 359 | [npm-image]: https://badge.fury.io/js/gulp-istanbul.svg 360 | 361 | [travis-url]: http://travis-ci.org/SBoudrias/gulp-istanbul 362 | [travis-image]: https://secure.travis-ci.org/SBoudrias/gulp-istanbul.svg?branch=master 363 | 364 | [depstat-url]: https://david-dm.org/SBoudrias/gulp-istanbul 365 | [depstat-image]: https://david-dm.org/SBoudrias/gulp-istanbul.svg 366 | 367 | [istanbul-coverage-variable]: http://gotwarlost.github.io/istanbul/public/apidocs/classes/Instrumenter.html 368 | [istanbul-summarize-coverage]: http://gotwarlost.github.io/istanbul/public/apidocs/classes/ObjectUtils.html#method_summarizeCoverage 369 | [sandboxed-module-coverage-variable]: https://github.com/felixge/node-sandboxed-module/blob/master/lib/sandboxed_module.js#L240 370 | -------------------------------------------------------------------------------- /test/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var assert = require('assert'); 5 | var rimraf = require('rimraf'); 6 | var File = require('vinyl'); 7 | var gulp = require('gulp'); 8 | var istanbul = require('../'); 9 | var isparta = require('isparta'); 10 | var mocha = require('gulp-mocha'); 11 | var sourcemaps = require('gulp-sourcemaps'); 12 | var Report = require('istanbul').Report; 13 | var path = require('path'); 14 | 15 | var out = process.stdout.write.bind(process.stdout); 16 | 17 | describe('gulp-istanbul', function () { 18 | 19 | afterEach(function () { 20 | process.stdout.write = out; // put it back even if test fails 21 | require.cache = {}; 22 | }); 23 | 24 | var libFile; 25 | 26 | describe('istanbul()', function () { 27 | beforeEach(function () { 28 | this.stream = istanbul(); 29 | libFile = new File({ 30 | path: 'test/fixtures/lib/add.js', 31 | cwd: 'test/', 32 | base: 'test/fixtures/lib', 33 | contents: fs.readFileSync('test/fixtures/lib/add.js') 34 | }); 35 | 36 | }); 37 | 38 | it('instrument files', function (done) { 39 | this.stream.on('data', function (file) { 40 | assert.equal(file.path, libFile.path); 41 | assert(file.contents.toString().indexOf('__cov_') >= 0); 42 | assert(file.contents.toString().indexOf('$$cov_') >= 0); 43 | done(); 44 | }); 45 | 46 | this.stream.write(libFile); 47 | this.stream.end(); 48 | }); 49 | 50 | it('throw when receiving a stream', function (done) { 51 | var srcFile = new File({ 52 | path: path.join('test', 'fixtures', 'lib', 'add.js'), 53 | cwd: 'test/', 54 | base: 'test/fixtures/lib', 55 | contents: fs.createReadStream('test/fixtures/lib/add.js') 56 | }); 57 | 58 | this.stream.on('error', function (err) { 59 | assert(err); 60 | done(); 61 | }); 62 | 63 | this.stream.write(srcFile); 64 | this.stream.end(); 65 | }); 66 | 67 | it('handles invalid JS files', function (done) { 68 | var srcFile = new File({ 69 | path: path.join('test', 'fixtures', 'lib', 'add.js'), 70 | cwd: 'test/', 71 | base: 'test/fixtures/lib', 72 | contents: new Buffer('var a {}') 73 | }); 74 | this.stream.on('error', function (err) { 75 | assert(err.message.indexOf(path.join('test', 'fixtures', 'lib', 'add.js')) >= 0); 76 | done(); 77 | }); 78 | this.stream.write(srcFile); 79 | this.stream.end(); 80 | }); 81 | 82 | it('is compatible to gulp-sourcemaps', function(done) { 83 | var initStream = sourcemaps.init(); 84 | var sourceMapStream = initStream.pipe(this.stream); 85 | sourceMapStream.on('data', function (file) { 86 | assert(file.sourceMap !== undefined); 87 | assert.equal(file.sourceMap.file, file.path.replace(/\\/g, '/')); 88 | done(); 89 | }); 90 | 91 | initStream.write(libFile); 92 | initStream.end(); 93 | }); 94 | 95 | it('handles existing source maps', function(done) { 96 | var initStream = sourcemaps.init(); 97 | var sourceMapStream = initStream.pipe(this.stream); 98 | sourceMapStream.on('data', function (file) { 99 | assert.equal(file.sourceMap.sourceRoot, 'testSourceRoot'); 100 | assert(file.sourceMap.sources.indexOf('testInputFile.js') >= 0); 101 | done(); 102 | }); 103 | 104 | libFile.sourceMap = { 105 | version: 3, 106 | sources: [ 'add.js' ], 107 | names: [ 'exports', 'add', 'a', 'b', 'missed' ], 108 | mappings: ';;;;;;;;;AAEAA,OAAA,CAAQC,GAAR,GAAc,UAAUC,CAAV,EAAaC,CAAb,EAAgB;AAAA,I,sCAAA;AAAA,I,sCAAA;AAAA,IAC5B,OAAOD,CAAA,GAAIC,CAAX,CAD4B;AAAA,CAA9B,C;;AAIAH,OAAA,CAAQI,MAAR,GAAiB,YAAY;AAAA,I,sCAAA;AAAA,I,sCAAA;AAAA,IAC3B,OAAO,aAAP,CAD2B;AAAA,CAA7B', 109 | file: 'testInputFile.js', 110 | sourcesContent: [ '' ], 111 | sourceRoot: 'testSourceRoot' 112 | }; 113 | initStream.write(libFile); 114 | initStream.end(); 115 | }); 116 | 117 | it('creates sourcemaps only if requested', function(done) { 118 | this.stream.on('data', function (file) { 119 | assert(file.sourceMap === undefined); 120 | done(); 121 | }); 122 | 123 | this.stream.write(libFile); 124 | this.stream.end(); 125 | }); 126 | }); 127 | 128 | describe('istanbul() with custom instrumentor', function() { 129 | beforeEach(function () { 130 | this.stream = istanbul({ 131 | instrumentor: isparta.Instrumentor 132 | }); 133 | }); 134 | 135 | it('instrument files', function (done) { 136 | this.stream.on('data', function (file) { 137 | assert.equal(file.path, libFile.path); 138 | assert(file.contents.toString().indexOf('__cov_') >= 0); 139 | assert(file.contents.toString().indexOf('$$cov_') >= 0); 140 | done(); 141 | }); 142 | 143 | this.stream.write(libFile); 144 | this.stream.end(); 145 | }); 146 | }); 147 | 148 | describe('.hookRequire()', function () { 149 | it('clear covered files from require.cache', function (done) { 150 | var add1 = require('./fixtures/lib/add'); 151 | var stream = istanbul() 152 | .pipe(istanbul.hookRequire()) 153 | .on('finish', function () { 154 | var add2 = require('./fixtures/lib/add'); 155 | assert.notEqual(add1, add2); 156 | done(); 157 | }); 158 | stream.write(libFile); 159 | stream.end(); 160 | }); 161 | }); 162 | 163 | describe('istanbul.summarizeCoverage()', function () { 164 | 165 | it('gets statistics about the test run', function (done) { 166 | gulp.src([ 'test/fixtures/lib/*.js' ]) 167 | .pipe(istanbul()) 168 | .pipe(istanbul.hookRequire()) 169 | .on('finish', function () { 170 | process.stdout.write = function () {}; 171 | gulp.src([ 'test/fixtures/test/*.js' ]) 172 | .pipe(mocha()) 173 | .on('end', function () { 174 | var data = istanbul.summarizeCoverage(); 175 | process.stdout.write = out; 176 | assert.equal(data.lines.pct, 75); 177 | assert.equal(data.statements.pct, 75); 178 | assert.equal(data.functions.pct, 50); 179 | assert.equal(data.branches.pct, 100); 180 | done(); 181 | }); 182 | }); 183 | }); 184 | 185 | it('allows inclusion of untested files', function (done) { 186 | var COV_VAR = 'untestedCovVar'; 187 | 188 | gulp.src([ 'test/fixtures/lib/*.js' ]) 189 | .pipe(istanbul({ 190 | coverageVariable: COV_VAR, 191 | includeUntested: true 192 | })) 193 | .pipe(istanbul.hookRequire()) 194 | .on('finish', function () { 195 | process.stdout.write = function () {}; 196 | gulp.src([ 'test/fixtures/test/*.js' ]) 197 | .pipe(mocha()) 198 | .on('end', function () { 199 | var data = istanbul.summarizeCoverage({ 200 | coverageVariable: COV_VAR 201 | }); 202 | process.stdout.write = out; 203 | 204 | // If untested files are included, line and statement coverage 205 | // drops to 25% 206 | assert.equal(data.lines.pct, 37.5); 207 | assert.equal(data.statements.pct, 37.5); 208 | assert.equal(data.functions.pct, 25); 209 | assert.equal(data.branches.pct, 100); 210 | done(); 211 | }); 212 | }); 213 | }); 214 | }); 215 | 216 | describe('istanbul.writeReports()', function () { 217 | beforeEach(function (done) { 218 | // set up coverage 219 | gulp.src([ 'test/fixtures/lib/*.js' ]) 220 | .pipe(istanbul()) 221 | .pipe(istanbul.hookRequire()) 222 | .on('finish', done); 223 | }); 224 | 225 | afterEach(function () { 226 | rimraf.sync('coverage'); 227 | rimraf.sync('cov-foo'); 228 | }); 229 | 230 | it('output coverage report', function (done) { 231 | gulp.src([ 'test/fixtures/test/*.js' ]) 232 | .pipe(mocha()) 233 | .pipe(istanbul.writeReports()); 234 | 235 | process.stdout.write = function (str) { 236 | if (str.indexOf('==== Coverage summary ====') >= 0) { 237 | done(); 238 | } 239 | }; 240 | }); 241 | 242 | it('create coverage report', function (done) { 243 | process.stdout.write = function () {}; 244 | gulp.src([ 'test/fixtures/test/*.js' ]) 245 | .pipe(mocha()) 246 | .pipe(istanbul.writeReports()) 247 | .on('end', function () { 248 | process.stdout.write = out; 249 | assert(fs.existsSync('./coverage')); 250 | assert(fs.existsSync('./coverage/lcov.info')); 251 | assert(fs.existsSync('./coverage/coverage-final.json')); 252 | done(); 253 | }); 254 | }); 255 | 256 | it('allow specifying report output dir (legacy way)', function (done) { 257 | process.stdout.write = function () {}; 258 | gulp.src([ 'test/fixtures/test/*.js' ]) 259 | .pipe(mocha()) 260 | .pipe(istanbul.writeReports('cov-foo')) 261 | .on('end', function () { 262 | process.stdout.write = out; 263 | assert(fs.existsSync('./cov-foo')); 264 | assert(fs.existsSync('./cov-foo/lcov.info')); 265 | assert(fs.existsSync('./cov-foo/coverage-final.json')); 266 | done(); 267 | }); 268 | }); 269 | 270 | it('allow specifying report output dir', function (done) { 271 | process.stdout.write = function () {}; 272 | gulp.src([ 'test/fixtures/test/*.js' ]) 273 | .pipe(mocha()) 274 | .pipe(istanbul.writeReports({ dir: 'cov-foo' })) 275 | .on('end', function () { 276 | process.stdout.write = out; 277 | assert(fs.existsSync('./cov-foo')); 278 | assert(fs.existsSync('./cov-foo/lcov.info')); 279 | assert(fs.existsSync('./cov-foo/coverage-final.json')); 280 | process.stdout.write = out; 281 | done(); 282 | }); 283 | }); 284 | 285 | it('allow specifying report output formats', function (done) { 286 | process.stdout.write = function () {}; 287 | gulp.src([ 'test/fixtures/test/*.js' ]) 288 | .pipe(mocha()) 289 | .pipe(istanbul.writeReports({ dir: 'cov-foo', reporters: ['cobertura'] })) 290 | .on('end', function () { 291 | process.stdout.write = out; 292 | assert(fs.existsSync('./cov-foo')); 293 | assert(!fs.existsSync('./cov-foo/lcov.info')); 294 | assert(!fs.existsSync('./cov-foo/coverage-final.json')); 295 | assert(fs.existsSync('./cov-foo/cobertura-coverage.xml')); 296 | process.stdout.write = out; 297 | done(); 298 | }); 299 | }); 300 | 301 | it('allow specifying configuration per report', function (done) { 302 | process.stdout.write = function () {}; 303 | var opts = { 304 | reporters: ['lcovonly', 'json'], 305 | reportOpts: { 306 | lcovonly: { dir: 'lcovonly', file: 'lcov-test.info' }, 307 | json: { dir: 'json', file: 'json-test.info' } 308 | } 309 | }; 310 | 311 | gulp.src(['test/fixtures/test/*.js']) 312 | .pipe(mocha()) 313 | .pipe(istanbul.writeReports(opts)) 314 | .on('end', function() { 315 | process.stdout.write = out; 316 | assert(fs.existsSync('./lcovonly')); 317 | assert(fs.existsSync('./lcovonly/lcov-test.info')); 318 | assert(fs.existsSync('./json')); 319 | assert(fs.existsSync('./json/json-test.info')); 320 | process.stdout.write = out; 321 | done(); 322 | }); 323 | }); 324 | 325 | it('allows specifying custom reporters', function (done) { 326 | var ExampleReport = function() {}; 327 | ExampleReport.TYPE = 'example'; 328 | ExampleReport.prototype = Object.create(Report.prototype); 329 | 330 | var reported = false; 331 | ExampleReport.prototype.writeReport = function () { 332 | reported = true; 333 | this.emit('done'); 334 | }; 335 | 336 | process.stdout.write = function () {}; 337 | gulp.src([ 'test/fixtures/test/*.js' ]) 338 | .pipe(mocha()) 339 | .pipe(istanbul.writeReports({ dir: 'cov-foo', reporters: [ExampleReport] })) 340 | .on('end', function () { 341 | process.stdout.write = out; 342 | assert(reported); 343 | done(); 344 | }); 345 | }); 346 | 347 | it('throws when specifying invalid reporters', function () { 348 | var actualErr; 349 | try { 350 | istanbul.writeReports({ reporters: ['not-a-valid-reporter'] }); 351 | } catch (err) { 352 | actualErr = err; 353 | } 354 | assert.equal(actualErr.plugin, 'gulp-istanbul'); 355 | }); 356 | 357 | }); 358 | 359 | describe('with defined coverageVariable option', function () { 360 | afterEach(function () { 361 | rimraf.sync('coverage'); 362 | }); 363 | 364 | it('allow specifying coverage variable', function (done) { 365 | process.stdout.write = function () {}; 366 | 367 | var coverageVariable = 'CUSTOM_COVERAGE_VARIABLE'; 368 | 369 | // set up coverage 370 | gulp.src([ 'test/fixtures/lib/*.js' ]) 371 | .pipe(istanbul({ coverageVariable: coverageVariable })) 372 | .pipe(istanbul.hookRequire()) 373 | .on('finish', function () { 374 | gulp.src([ 'test/fixtures/test/*.js' ]) 375 | .pipe(mocha()) 376 | .pipe(istanbul.writeReports({ coverageVariable: coverageVariable })) 377 | .on('end', function () { 378 | assert(fs.existsSync('./coverage')); 379 | assert(fs.existsSync('./coverage/lcov.info')); 380 | assert(fs.existsSync('./coverage/coverage-final.json')); 381 | process.stdout.write = out; 382 | done(); 383 | }); 384 | }); 385 | }); 386 | }); 387 | 388 | describe('istanbul.enforceThresholds()', function () { 389 | beforeEach(function (done) { 390 | // set up coverage 391 | gulp.src([ 'test/fixtures/lib/*.js' ]) 392 | .pipe(istanbul()) 393 | .pipe(istanbul.hookRequire()) 394 | .on('finish', done); 395 | }); 396 | 397 | afterEach(function () { 398 | rimraf.sync('coverage'); 399 | rimraf.sync('cov-foo'); 400 | }); 401 | 402 | it('checks coverage fails against global threshold', function (done) { 403 | var resolved = false; 404 | 405 | process.stdout.write = function () {}; 406 | gulp.src([ 'test/fixtures/test/*.js' ]) 407 | .pipe(mocha()) 408 | .pipe(istanbul.enforceThresholds({ thresholds: { global: 90 }})) 409 | .on('error', function (err) { 410 | if (!resolved) { 411 | resolved = true; 412 | process.stdout.write = out; 413 | assert.equal(err.message, 'Coverage failed'); 414 | done(); 415 | } 416 | }) 417 | .on('end', function () { 418 | if (!resolved) { 419 | resolved = true; 420 | process.stdout.write = out; 421 | done(new Error('enforceThresholds did not raise an error')); 422 | } 423 | }); 424 | }); 425 | 426 | it('checks coverage fails against per file threshold', function (done) { 427 | var resolved = false; 428 | 429 | process.stdout.write = function () {}; 430 | gulp.src([ 'test/fixtures/test/*.js' ]) 431 | .pipe(mocha()) 432 | .pipe(istanbul.enforceThresholds({ thresholds: { each: 80 }})) 433 | .on('error', function (err) { 434 | if (!resolved) { 435 | resolved = true; 436 | process.stdout.write = out; 437 | assert.equal(err.message, 'Coverage failed'); 438 | done(); 439 | } 440 | }) 441 | .on('end', function () { 442 | if (!resolved) { 443 | resolved = true; 444 | process.stdout.write = out; 445 | done(new Error('enforceThresholds did not raise an error')); 446 | } 447 | }); 448 | }); 449 | 450 | it('checks coverage passes against global and per file thresholds', function (done) { 451 | var resolved = false; 452 | 453 | process.stdout.write = function () {}; 454 | gulp.src([ 'test/fixtures/test/*.js' ]) 455 | .pipe(mocha()) 456 | .pipe(istanbul.enforceThresholds({ thresholds: { global: 50, each: 45 }})) 457 | .on('error', function () { 458 | if (!resolved) { 459 | resolved = true; 460 | process.stdout.write = out; 461 | done(new Error('enforceThresholds did not raise an error')); 462 | } 463 | }) 464 | .on('end', function () { 465 | if (!resolved) { 466 | resolved = true; 467 | process.stdout.write = out; 468 | done(); 469 | } 470 | }); 471 | }); 472 | 473 | it('checks coverage with a custom coverage variable', function (done) { 474 | var resolved = false; 475 | var coverageVariable = 'CUSTOM_COVERAGE_VARIABLE'; 476 | 477 | process.stdout.write = function () {}; 478 | gulp.src([ 'test/fixtures/lib/*.js' ]) 479 | .pipe(istanbul({ coverageVariable: coverageVariable })) 480 | .pipe(istanbul.hookRequire()) 481 | .on('finish', function () { 482 | gulp.src([ 'test/fixtures/test/*.js' ]) 483 | .pipe(mocha()) 484 | .pipe(istanbul.enforceThresholds({ 485 | coverageVariable: coverageVariable, 486 | thresholds: { global: 100 } 487 | })) 488 | .on('error', function (err) { 489 | if (!resolved) { 490 | resolved = true; 491 | process.stdout.write = out; 492 | assert.equal(err.message, 'Coverage failed'); 493 | done(); 494 | } 495 | }) 496 | .on('end', function () { 497 | if (!resolved) { 498 | resolved = true; 499 | process.stdout.write = out; 500 | done(new Error('enforceThresholds did not raise an error')); 501 | } 502 | }); 503 | }); 504 | }); 505 | }); 506 | }); 507 | --------------------------------------------------------------------------------