├── .coveralls.yml ├── .gitignore ├── LICENSE ├── README.md ├── contrib ├── cover.js ├── coverage_store.js ├── reporters │ ├── .DS_Store │ ├── html.js │ ├── json.js │ ├── lcov.js │ └── templates │ │ ├── coverage.jade │ │ ├── menu.jade │ │ ├── script.html │ │ └── style.html └── templates │ └── instrumentation_header.js ├── debug └── chaindebug.js ├── gulpfile.js ├── index.js ├── package.json ├── screenshots └── gulp-coverage.png ├── test ├── cover.js └── gulp-coverage.js └── testsupport ├── c2_cov.js ├── c2_test.js ├── chain.js ├── chainable.js ├── myModule.js ├── rewire.js ├── src.js ├── src2.js ├── src3.js ├── src4.js ├── srcchain.js ├── srcjasmine.js ├── test.js ├── test2.js └── test3.js /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: wYYsVegLIowvPCKXUg0e5KXMzFFZtNBD7 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | debug 17 | .coverrun 18 | .coverdata 19 | coverage.html 20 | chain.html 21 | json.json 22 | jasmine.html 23 | blnkt.html 24 | testoutput 25 | .DS_Store 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dylan Barrell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gulp-coverage 2 | 3 | Gulp coverage reporting for Node.js that is independent of the test runner 4 | 5 | # Report 6 | 7 | gulp-coverage generates block, line, chainable and statement coverage for Node.js JavaScript files. This is equivalent to function, statement, branch and "modified condition/decision" coverage, where a "statement" is equivalent to a "modified condition/decision". The HTML report gives summary information for the block, line and statement covereage across all the files as well as for each file. 8 | 9 | ## Chainables 10 | The chainable coverage supports Array-like chainables and will record misses where the chained member calls both receive and result-in an empty array. 11 | 12 | ## HTML report 13 | For each covered file, a chain of links is built allowing you to click through from the summary to the first and then all subsequent instances of each type of miss (line misses and statement misses). 14 | 15 | ![Example Report Showing missed lines, missed statements and chains of links](https://raw.githubusercontent.com/dylanb/gulp-coverage/master/screenshots/gulp-coverage.png "Example Report") 16 | 17 | The HTML report format has been desiged to be accessible and conformant with WCAG 2 Level AA. 18 | 19 | ## Excluding code from coverage reporting 20 | 21 | To exclude a single line from the coverage report, you can append a comment with the content of "cover:false" to the end of the line. 22 | 23 | ``` 24 | var uncovered = "this line will not be covered"; // cover:false 25 | ``` 26 | 27 | If you would like to exclude a block of code from coverage reporting, then wrap the block in a pair of comments to turn coverage off and then back on. Note that the start and end comments should be at the same indent level or the outcome will not be what you expect. To turn off the coverage, put the text "#JSCOVERAGE_IF" into a comment. To turn it back on, use a comment with one of the following texts "#JSCOVERAGE_IF 0", or "#JSCOVERAGE_ENDIF". 28 | 29 | ``` 30 | var covered = "this line is covered"; 31 | //#JSCOVERAGE_IF 32 | if (false) { 33 | // this code will never be covered 34 | console.log("Why does nobody listen to me?!?!?"); 35 | } 36 | //#JSCOVERAGE_ENDIF 37 | var alsoCovered = "this line is also covered"; 38 | ``` 39 | 40 | # Example 41 | 42 | To instrument and report on a file using Mocha as your test runner: 43 | 44 | ```js 45 | mocha = require('gulp-mocha'); 46 | cover = require('gulp-coverage'); 47 | 48 | gulp.task('test', function () { 49 | return gulp.src('tests/**/*.js', { read: false }) 50 | .pipe(cover.instrument({ 51 | pattern: ['src/**/*.js'], 52 | debugDirectory: 'debug' 53 | })) 54 | .pipe(mocha()) 55 | .pipe(cover.gather()) 56 | .pipe(cover.format()) 57 | .pipe(gulp.dest('reports')); 58 | }); 59 | ``` 60 | 61 | To instrument and report using Jasmine as your test system: 62 | 63 | ```js 64 | jasmine = require('gulp-jasmine'); 65 | cover = require('gulp-coverage'); 66 | 67 | gulp.task('jasmine', function () { 68 | return gulp.src('tests/**/*.js') 69 | .pipe(cover.instrument({ 70 | pattern: ['src/**/*.js'], 71 | debugDirectory: 'debug' 72 | })) 73 | .pipe(jasmine()) 74 | .pipe(cover.gather()) 75 | .pipe(cover.format()) 76 | .pipe(gulp.dest('reports')); 77 | }); 78 | ``` 79 | 80 | To report coverage with gulp-coveralls: 81 | 82 | ```js 83 | mocha = require('gulp-mocha'); 84 | cover = require('gulp-coverage'); 85 | coveralls = require('gulp-coveralls'); 86 | 87 | gulp.task('coveralls', function () { 88 | return gulp.src('tests/**/*.js', { read: false }) 89 | .pipe(cover.instrument({ 90 | pattern: ['src/**/*.js'] 91 | })) 92 | .pipe(mocha()) // or .pipe(jasmine()) if using jasmine 93 | .pipe(cover.gather()) 94 | .pipe(cover.format({ reporter: 'lcov' })) 95 | .pipe(coveralls()); 96 | }); 97 | ``` 98 | 99 | ## Tasks 100 | 101 | There are five different tasks, `instrument`, `gather`, `report`, `format` and `enforce`. The `instrument` task must be run before any of the others can be called and either the `gather`, or the `report` task must be run before the `enforce` or the `format` task can be run. Enforce can follow `format` and should be run last if you want the reports generated even when the thresholds have not been met. 102 | 103 | After the `instrument` call, the target files must be required and executed (preferably using some sort of test runner such as gulp-mocha or gulp-jasmine). This will allow the instrumentation to capture the required data (in the example above, this is done by the mocha test runner). 104 | 105 | The gulp-coverage `gather` task will NOT pass any of the original stream files to the subsequent tasks. It must therefore be called after all other stream transformations have occurred. 106 | 107 | ## The Instrument task 108 | 109 | ### options 110 | 111 | `pattern` - A multimatch glob pattern or list of glob patterns. The globa patterns are relative to the CWD of the process running the gulp file. The pattern `'index.js'` will match the index.js file in the same directory as the gulpfile. Globs for file relative to this directory may contain the './'. For example `'./index.js'` is equivalent to `'index.js'` (you can also use `'**/index.js'` to match this file but this will match any other index.js files within the project's directory structure). 112 | 113 | The patterns can include match and exclude patterns. In the Mocha and Jasmine examples shown above, there are two JavaScript files `test.js` and `test2.js` that are required by the two test files `src.js` and `src2.js`. The `**` will match these files no matter which directory they live in. Patterns are additive while negations (eg ['**/foo*', '!**/node_modules/**/*']) are based on the current set. Exception is if the first pattern is negation, then it will get the full set, so to match user expectation (eg. ['!**/foo*'] will match everything except a file with foo in its name). Order matters. 114 | 115 | `debugDirectory` - a string pointing to a directory into which the instrumented source will be written. This is useful for debugging gulp-coverage itself 116 | 117 | ## The Gather Task 118 | 119 | The gather task will collate all of the collected data and form this data into a JSON structure that will be placed into the Gulp task stream. It will not pass the original stream files into the stream, so gather cannot be used before another Gulp task that requires the original stream contents. 120 | 121 | The format of the JSON structure is a Modified LCOV format. The format has been modified to make it much easier for a template engine like JADE or HAML to generate decorated source code output. 122 | 123 | ### The Modified LCOV JSON format 124 | 125 | ``` 126 | lcov : { 127 | sloc: Integer - how many source lines of code there were in total 128 | ssoc: Integer - how many statements of code there were in total 129 | sboc: Integer - how many blocks of code there were in total 130 | coverage: Float - percentage of lines covered 131 | statements: Float - percentage of statements covered 132 | blocks: Float: percentage of blocks covered 133 | files: Array[Object] - array of information about each file 134 | uncovered: Array[String] - array of the files that match the pattern but were not tested at all 135 | } 136 | ``` 137 | 138 | Each `file` has the following structure 139 | 140 | ``` 141 | file : { 142 | filename: String - the file 143 | basename: String - the file short name 144 | segments: String - the file's directory 145 | coverage: Float - the percentage of lines covered 146 | statements: Float - the percentage of statements covered 147 | blocks: Float - the percentage of blocks covered 148 | source: Array[Object] - array of objects, one for each line of code 149 | sloc: Integer - the number of lines of code 150 | ssoc: Integer - the number of statements of code 151 | sboc: Integer - the number of blocks of code 152 | } 153 | ``` 154 | 155 | Source contains `line` objects which have the following structure 156 | ``` 157 | line : { 158 | count: Integer - number of times the line was hit 159 | statements: Float - the percentage of statements covered 160 | segments: Array[Object] - the segments of statements that make up the line 161 | } 162 | ``` 163 | 164 | Each statement `segment` has the following structure 165 | ``` 166 | segment : { 167 | code: String - the string of code for the segment 168 | count: Integer - the hit count for the segment 169 | } 170 | ``` 171 | 172 | ## The Enforce Task 173 | 174 | The `enforce` task can be used to emit an error (throw an exception) when the overall coverage values fall below your specified thresholds. The default thesholds are 100% for all metrics. This task is useful if you would like the Gulp task to fail in a way that your CI or build system can easily detect. 175 | 176 | ### options 177 | 178 | If you would like to specify thresholds lower than 100%, pass in the thresholds in the first argument to the task. The defaults are: 179 | 180 | ``` 181 | options : { 182 | statements: 100, 183 | blocks: 100, 184 | lines: 100, 185 | uncovered: undefined 186 | } 187 | ``` 188 | 189 | If uncovered is undefined, no threshold will be defined, otherwise it must be an integer of the number of files that are allowed to not be covered. Setting this to 0 will enforce that there are no uncovered files that match the patterm passed into the instrument task. 190 | 191 | ## The Format Task 192 | 193 | The `format` task can be used to generate a textual, formatted version of the coverage data and emit this to the Gulp stream. By default, it will call the 'html' formatter (currently only `'html'` and `'json'` are supported). It will add the formatted text the stream. After calling both `gather` and `format`, the stream will contain a vinyl file with an additional attribute called `coverage`, that contains the JSON formatted data. 194 | 195 | The coverage file will be called "coverage.{reporter}" by default and can be output to the disk using gulp.dest(). if the reporter is `'html'`, then it will be "coverage.html" and if the reporter is `'json'`, then it will be "coverage.json". 196 | 197 | You must call `gather` prior to calling `format`. 198 | 199 | ### options 200 | 201 | The task takes one optional argument that is either an object that contains the options, or is an array of objects that contain options. If the argument is an array, then two vinyl files will be created and added to the gulp stream. 202 | 203 | There are 2 options, here are the default values: 204 | 205 | ``` 206 | { 207 | reporter: 'html', 208 | outFile: 'coverage.html' 209 | } 210 | ``` 211 | 212 | Calling format with the following arguments will all create two vinyl files in the gulp stream that have a path of coverage.html and coverage.json respectively: 213 | 214 | #### Multiple Formats Example 1 215 | 216 | ``` 217 | [ { reporter: 'html' }, { reporter: 'json' } ] 218 | ``` 219 | 220 | #### Multiple Formats Example 2 221 | 222 | ``` 223 | [ { reporter: 'html', outFile: 'coverage.html' }, { reporter: 'json', outFile: 'coverage.json' } ] 224 | ``` 225 | 226 | #### Multiple Formats Example 3 227 | 228 | ``` 229 | [ 'html', 'json'] 230 | ``` 231 | 232 | ## The Report Task (Deprecated) 233 | 234 | This task is deprecated because it does not support the gulp.dest task and will probably be removed before the first 1.0.0 release. Use [The Gather Task](#the-gather-task) and [The Format Task](#the-format-task) instead. 235 | 236 | Report will generate the reports for the instrumented files and can only be called after `instrument` has been called. It will also change the stream content for the tasks and pass through the LCOV JSON data so that the enforce task can be run. 237 | 238 | ### options 239 | 240 | `outFile` - the name of the file into which the report output will be written 241 | 242 | `reporter` - defaults to 'html' - this is the name of the reporter to use. Currently there are an HTML reporter ('html') and a JSON ('json') reporter. 243 | 244 | -------------------------------------------------------------------------------- /contrib/cover.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Original code from https://github.com/itay/node-cover licensed under MIT did 3 | * not have a Copyright message in the file. 4 | * 5 | * Significantly re-written. The changed code is Copyright (C) 2014 Dylan Barrell 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | * this software and associated documentation files (the "Software"), to deal in 9 | * the Software without restriction, including without limitation the rights to 10 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 11 | * the Software, and to permit persons to whom the Software is furnished to do so, 12 | * subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in all 15 | * copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 19 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 20 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 21 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | var instrument = require('instrumentjs'); 26 | var Module = require('module').Module; 27 | var path = require('path'); 28 | var fs = require('fs'); 29 | var vm = require('vm'); 30 | var _ = require('underscore'); 31 | var multimatch = require('multimatch'); 32 | var extend = require('extend'); 33 | 34 | /** 35 | * Class used to track the coverage data for a single source code file 36 | * 37 | * @class FileCoverageData 38 | * @constructor 39 | * @param {String} filename - the name of the file 40 | * @param {Object} instrumentor - the object that will help with instrumentation 41 | */ 42 | function FileCoverageData (filename, instrumentor) { 43 | var theLines = {}; 44 | /* 45 | * Create a map between the lines and the nodes 46 | * This is used later for calculating the code coverage stats 47 | */ 48 | Object.keys(instrumentor.nodes).forEach(function(index) { 49 | var node = instrumentor.nodes[index], 50 | lineStruct; 51 | 52 | if (!theLines[node.loc.start.line]) { 53 | lineStruct = theLines[node.loc.start.line] = { 54 | nodes: [] 55 | }; 56 | } else { 57 | lineStruct = theLines[node.loc.start.line]; 58 | } 59 | if (lineStruct.nodes.indexOf(node) === -1) { 60 | lineStruct.nodes.push(node); 61 | } 62 | if (!theLines[node.loc.end.line]) { 63 | lineStruct = theLines[node.loc.end.line] = { 64 | nodes: [] 65 | }; 66 | } else { 67 | lineStruct = theLines[node.loc.end.line]; 68 | } 69 | if (lineStruct.nodes.indexOf(node) === -1) { 70 | lineStruct.nodes.push(node); 71 | } 72 | }); 73 | this.lines = theLines; 74 | this.instrumentor = instrumentor; 75 | this.filename = filename; 76 | this.nodes = {}; 77 | this.visitedBlocks = {}; 78 | this.source = instrumentor.source; 79 | } 80 | 81 | /** 82 | * calculate the block coverage stats 83 | * 84 | * @private 85 | * @method _block 86 | * @return {Object} - structure containing `total` and `seen` counts for the blocks 87 | */ 88 | FileCoverageData.prototype._blocks = function() { 89 | var totalBlocks = this.instrumentor.blockCounter; 90 | var numSeenBlocks = 0; 91 | for(var index in this.visitedBlocks) { 92 | numSeenBlocks++; 93 | } 94 | var toReturn = { 95 | total: totalBlocks, 96 | seen: numSeenBlocks 97 | }; 98 | return toReturn; 99 | }; 100 | 101 | /** 102 | * read the instrumentation data from the store into memory 103 | * 104 | * @private 105 | * @method _prepare 106 | * @return {undefined} 107 | */ 108 | FileCoverageData.prototype._prepare = function() { 109 | // console.log('PREPARE'); 110 | var data = require('./coverage_store').getStoreData(this.filename), 111 | rawData, store, index; 112 | 113 | data = '[' + data + '{}]'; 114 | // console.log('DATA: ', data); 115 | rawData = JSON.parse(data); 116 | store = {nodes: {}, blocks: {}}; 117 | rawData.forEach(function(item) { 118 | var it; 119 | if (item.hasOwnProperty('block')) { 120 | store.blocks[item.block] = store.blocks[item.block] || {count: 0}; 121 | store.blocks[item.block].count += 1; 122 | } else { 123 | if (item.expression) { 124 | it = item.expression; 125 | } else if (item.statement) { 126 | it = item.statement; 127 | } else if (item.chain) { 128 | it = item.chain; 129 | } else { 130 | return; 131 | } 132 | store.nodes[it.node] = store.nodes[it.node] || {count: 0}; 133 | store.nodes[it.node].count += 1; 134 | } 135 | }); 136 | for (index in store.nodes) { 137 | if (store.nodes.hasOwnProperty(index)) { 138 | this.instrumentor.nodes[index].count = store.nodes[index].count; 139 | } 140 | } 141 | for (index in store.blocks) { 142 | if (store.blocks.hasOwnProperty(index)) { 143 | this.visitedBlocks[index] = {count: store.blocks[index].count}; 144 | } 145 | } 146 | }; 147 | 148 | /** 149 | * 150 | * Get statistics for the entire file, including per-line code coverage 151 | * statement coverage and block-level coverage 152 | * This function returns an object with the following structure: 153 | * { 154 | * lines: Integer - the number of lines covered 155 | * blocks: Integer - the number of blocks covered 156 | * statements: Integer - the number of statements covered 157 | * lineDetails: Array[Object] - a sparse array of the detailed information on each line 158 | * sloc: Integer - the number of relevant lines in the file 159 | * sboc: Integer - the number of relevant blocks in the file 160 | * ssoc: Integer - the number of relevant statements in the file 161 | * code: Array[String] - an Array of strings, one for each line of the file 162 | * } 163 | * 164 | * The line detail objects have the following structure 165 | * { 166 | * number: Integer - the line number 167 | * count: Integer - the number of times the line was executed 168 | * statements: Integer - the number of statements covered 169 | * ssoc: Integer - the number of statements in the line 170 | * statementDetails : Array[Object] - an array of the statement details 171 | * } 172 | * 173 | * The statement detail objects have the following structure 174 | * { 175 | * loc: Object - a location object 176 | * count: the number of times the statement was executed 177 | * } 178 | * 179 | */ 180 | 181 | FileCoverageData.prototype.stats = function() { 182 | // console.log('STATS'); 183 | this._prepare(); 184 | var filedata = this.instrumentor.source.split('\n'); 185 | var lineDetails = [], 186 | lines = 0, fileStatements = 0, fileSsoc = 0, fileSloc = 0, 187 | theLines = this.lines, 188 | blockInfo; 189 | 190 | Object.keys(theLines).forEach(function(index) { 191 | var line = theLines[index], 192 | lineStruct, 193 | lineCount = 0, 194 | statements = 0, 195 | ssoc = 0, 196 | statementDetails = []; 197 | line.nodes.forEach(function(node) { 198 | var loc; 199 | if (node.count === null || node.count === undefined) { 200 | node.count = 0; 201 | } 202 | lineCount = Math.max(lineCount, node.count); 203 | ssoc += 1; 204 | if (node.count) { 205 | statements += 1; 206 | } 207 | loc = {}; 208 | extend(loc, node.loc); 209 | statementDetails.push({ 210 | loc: loc, 211 | count: node.count 212 | }); 213 | }); 214 | lineStruct = { 215 | number: index, 216 | count: lineCount, 217 | ssoc: ssoc, 218 | statements: statements, 219 | statementDetails: statementDetails 220 | }; 221 | lines += (lineStruct.count ? 1 : 0); 222 | fileSloc += 1; 223 | fileStatements += lineStruct.statements; 224 | fileSsoc += lineStruct.ssoc; 225 | lineDetails[index-1] = lineStruct; 226 | }); 227 | blockInfo = this._blocks(); 228 | retVal = { 229 | lines: lines, 230 | statements: fileStatements, 231 | blocks: blockInfo.seen, 232 | sloc: fileSloc, 233 | ssoc: fileSsoc, 234 | sboc: blockInfo.total, 235 | lineDetails: lineDetails, 236 | code: filedata 237 | }; 238 | return retVal; 239 | }; 240 | 241 | 242 | /** 243 | * Generate the header at the top of the instrumented file that sets up the data structures that 244 | * are used to collect instrumentation data. 245 | * 246 | * @private 247 | * @method addInstrumentationHeader 248 | * @param {String} template - the contents of the template file 249 | * @param {String} filename - the full path name of the file being instrumented 250 | * @param {String} instrumented - the instrumented source code of the file 251 | * @param {String} coverageStorePath - the path to the coverage store 252 | * @return {String} the rendered file with instrumentation and instrumentation header 253 | */ 254 | var addInstrumentationHeader = function(template, filename, instrumented, coverageStorePath) { 255 | var templ = _.template(template), 256 | renderedSource = templ({ 257 | instrumented: instrumented, 258 | coverageStorePath: coverageStorePath, 259 | filename: filename, 260 | source: instrumented.instrumentedSource 261 | }); 262 | return renderedSource; 263 | }; 264 | 265 | function relatify(arr) { 266 | var retVal = []; 267 | if (!Array.isArray(arr)) { 268 | arr = [arr]; 269 | } 270 | arr.forEach(function(item, index) { 271 | if (item.indexOf('./') === 0 || item.indexOf('.\\') === 0) { 272 | retVal[index] = item.substring(2); 273 | } else if (item.charAt(0) === '!') { 274 | if (item.indexOf('./') === 1 || item.indexOf('.\\') === 1) { 275 | retVal[index] = '!' + item.substring(3); 276 | } else { 277 | retVal[index] = item; 278 | } 279 | } else { 280 | retVal[index] = item; 281 | } 282 | }); 283 | return retVal; 284 | } 285 | 286 | function createFullPathSync(fullPath) { 287 | if (!fullPath) { 288 | return false; 289 | } 290 | var parts, 291 | working = '/', 292 | pathList = []; 293 | 294 | if (fullPath[0] !== '/') { 295 | fullPath = path.join(process.cwd(), fullPath); 296 | } 297 | parts = path.normalize(fullPath).split('/'); 298 | for(var i = 0, max = parts.length; i < max; i++) { 299 | working = path.join(working, parts[i]); 300 | pathList.push(working); 301 | } 302 | var recursePathList = function recursePathList(paths) { 303 | var working; 304 | 305 | if (!paths.length) { 306 | return true; 307 | } 308 | working = paths.shift(); 309 | if( !fs.existsSync(working)) { 310 | try { 311 | fs.mkdirSync(working, 0755); 312 | } 313 | catch(e) { 314 | return false; 315 | } 316 | } 317 | return recursePathList(paths); 318 | } 319 | return recursePathList(pathList); 320 | } 321 | 322 | /** 323 | * @class CoverageSession 324 | * @constructor 325 | * @param {Array[glob]} pattern - the array of glob patterns of includes and excludes 326 | * @param {String} debugDirectory - the name of the director to contain debug instrumentation files 327 | */ 328 | var CoverageSession = function(pattern, debugDirectory) { 329 | var normalizedPattern; 330 | function stripBOM(content) { 331 | // Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) 332 | // because the buffer-to-string conversion in `fs.readFileSync()` 333 | // translates it to FEFF, the UTF-16 BOM. 334 | if (content.charCodeAt(0) === 0xFEFF) { 335 | content = content.slice(1); 336 | } 337 | return content; 338 | } 339 | var originalRequire = this.originalRequire = require.extensions['.js']; 340 | var coverageData = this.coverageData = {}; 341 | var instrumentList = this.instrumentList = {}; 342 | // console.log('new coverageData'); 343 | var pathToCoverageStore = path.resolve(path.resolve(__dirname), 'coverage_store.js').replace(/\\/g, '/'); 344 | var templatePath = path.resolve(path.resolve(__dirname), 'templates', 'instrumentation_header.js'); 345 | var template = fs.readFileSync(templatePath, 'utf-8'); 346 | this.pattern = normalizedPattern = relatify(pattern); 347 | require.extensions['.js'] = function(module, filename) { 348 | var shortFilename; 349 | filename = filename.replace(/\\/g, '/'); 350 | 351 | shortFilename = path.relative(process.cwd(), filename); 352 | // console.log('filename: ', filename, ', shortFilename:', shortFilename, ', normalizedPattern: ', normalizedPattern, ', match: ', multimatch(shortFilename, pattern)); 353 | if (!multimatch(shortFilename, normalizedPattern).length) { 354 | return originalRequire(module, filename); 355 | } 356 | if (filename === pathToCoverageStore) { 357 | return originalRequire(module, filename); 358 | } 359 | 360 | var data = stripBOM(fs.readFileSync(filename, 'utf8').trim()); 361 | data = data.replace(/^\#\!.*/, ''); 362 | 363 | instrumentList[filename] = true; 364 | var instrumented = instrument(data); 365 | coverageData[filename] = new FileCoverageData(filename, instrumented); 366 | 367 | var newCode = addInstrumentationHeader(template, filename, instrumented, pathToCoverageStore); 368 | 369 | if (debugDirectory) { 370 | createFullPathSync(debugDirectory); 371 | var outputPath = path.join(debugDirectory, filename.replace(/[\/|\:|\\]/g, '_') + '.js'); 372 | fs.writeFileSync(outputPath, newCode); 373 | } 374 | 375 | return module._compile(newCode, filename); 376 | }; 377 | 378 | }; 379 | 380 | /** 381 | * Release the original require function 382 | * 383 | * @method release 384 | */ 385 | CoverageSession.prototype.release = function() { 386 | for (var att in this.instrumentList) { 387 | // console.log('deleting: ', att); 388 | if (require.cache.hasOwnProperty(att)) { 389 | delete require.cache[att]; 390 | } 391 | } 392 | require.extensions['.js'] = this.originalRequire; 393 | }; 394 | 395 | 396 | /** 397 | * This function returns the unique segments for the lines that are covered by the statementDetails 398 | * argument. This is necessary because the segments, as generated by the parser/instrumentor, overlap 399 | * so in order to determine which segment is actually the segment responsible for a particular 400 | * piece of code, we have to split and eliminate the overlaps. Because we are interested only in the 401 | * segments that were missed, we can also try to consolidate the adjacent segments that were hit 402 | * 403 | * @private 404 | * @method getSegments 405 | * @param {Array[String]} code - array of the lines for the entire file 406 | * @param {Array[Object]} lines - array of the line ojects for the entire file 407 | * @param Integer count - the hit count for the outermost statement 408 | * @param Array{object} - array of overlapping statement segments 409 | */ 410 | function getSegments(code, lines, count, statementDetails) { 411 | var lengths = [], beginLine = code.length+1, endLine = 0, sd = [], 412 | linesCode, i, j, k, splintered, segments; 413 | // calculate the lengths of each line 414 | code.forEach(function (codeLine) { 415 | lengths.push(codeLine.length); 416 | }); 417 | // work out which lines we are talking about 418 | statementDetails.forEach(function(item) { 419 | if (item.loc.start.line < beginLine) { 420 | beginLine = item.loc.start.line; 421 | } 422 | if (item.loc.end.line > endLine) { 423 | endLine = item.loc.end.line; 424 | } 425 | }); 426 | // modify all the coordinates into a single number 427 | statementDetails.forEach(function(item) { 428 | var lineNo = beginLine, 429 | startOff = 0; 430 | while (item.loc.start.line > lineNo) { 431 | startOff += lengths[lineNo] + 1; 432 | lineNo += 1; 433 | } 434 | startOff += item.loc.start.column; 435 | endOff = 0; 436 | lineNo = beginLine; 437 | while (item.loc.end.line > lineNo) { 438 | endOff += lengths[lineNo] + 1; 439 | lineNo += 1; 440 | } 441 | endOff += item.loc.end.column; 442 | sd.push({ 443 | start: startOff, 444 | end: endOff, 445 | count: item.count 446 | }); 447 | }); 448 | linesCode = code.filter(function(item, index) { 449 | return (index >= beginLine-1 && index <= endLine-1); 450 | }).join('\n'); 451 | 452 | // push on a synthetic segment to catch all the parts of the line(s) 453 | sd.push({ 454 | start: 0, 455 | end: linesCode.length, 456 | count: count 457 | }); 458 | 459 | // reconcile the overlapping segments 460 | sd.sort(function(a, b) { 461 | return (a.end - b.end); 462 | }); 463 | sd.sort(function(a, b) { 464 | return (a.start - b.start); 465 | }); 466 | // Will now be sorted in start order with end as the second sort criterium 467 | splintered = []; 468 | for ( i = 0; i < sd.length; i++) { 469 | var size = (sd[i].end - sd[i].start + 1 < 0) ? 0 : sd[i].end - sd[i].start + 1; 470 | var us = new Array(size); 471 | for (k = sd[i].end - sd[i].start; k >= 0; k--) { 472 | us[k] = 1; 473 | } 474 | for (j = 0; j < sd.length; j++) { 475 | if (j !== i) { 476 | if (sd[i].start <= sd[j].start && sd[i].end >= sd[j].end && 477 | (sd[i].count !== sd[j].count || !sd[i].count || !sd[j].count)) { 478 | for ( k = sd[j].start; k <= sd[j].end; k++) { 479 | us[k - sd[i].start] = 0; 480 | } 481 | } 482 | } 483 | } 484 | if (us.indexOf(0) !== -1) { 485 | // needs to be split 486 | splitStart = undefined; 487 | splitEnd = undefined; 488 | for (k = 0; k < us.length; k++) { 489 | if (us[k] === 1 && splitStart === undefined) { 490 | splitStart = k; 491 | } else if (us[k] === 0 && splitStart !== undefined) { 492 | splitEnd = k - 1; 493 | splintered.push({ 494 | start: splitStart + sd[i].start, 495 | end: splitEnd + sd[i].start, 496 | count: sd[i].count 497 | }); 498 | splitStart = undefined; 499 | } 500 | } 501 | if (splitStart !== undefined) { 502 | splintered.push({ 503 | start: splitStart + sd[i].start, 504 | end: k - 1 + sd[i].start, 505 | count: sd[i].count 506 | }); 507 | } 508 | } else { 509 | splintered.push(sd[i]); 510 | } 511 | } 512 | if (splintered.length === 0) { 513 | return []; 514 | } 515 | splintered.sort(function(a, b) { 516 | return (b.end - a.end); 517 | }); 518 | splintered.sort(function(a, b) { 519 | return (a.start - b.start); 520 | }); 521 | var combined = [splintered[0]]; 522 | splintered.reduce(function(p, c) { 523 | if (p && p.start <= c.start && p.end >= c.end && 524 | (p.count === c.count || (p.count && c.count))) { 525 | // Can get rid of c 526 | return p; 527 | } else { 528 | combined.push(c); 529 | return c; 530 | } 531 | }); 532 | // combine adjacent segments 533 | currentItem = { 534 | start: combined[0].start, 535 | end: combined[0].end, 536 | count: combined[0].count 537 | }; 538 | segments = []; 539 | combined.splice(0,1); 540 | combined.forEach(function(item) { 541 | if (item.count === currentItem.count || (item.count && currentItem.count)) { 542 | currentItem.end = item.end; 543 | } else { 544 | segments.push(currentItem); 545 | currentItem = { 546 | start: item.start, 547 | end: item.end, 548 | count: item.count 549 | }; 550 | } 551 | }); 552 | segments.push(currentItem); 553 | // Now add the code to each segment 554 | segments.forEach(function(item) { 555 | item.code = linesCode.substring(item.start, item.end); 556 | }); 557 | return segments; 558 | } 559 | 560 | /** 561 | * The nodes (lines) that are returned by the parser overlap. There is one node generated for the beginning 562 | * and one for the end of each block and individual nodes may span multiple lines. This function 563 | * eliminates the duplicates and the overlaps such that there is at most one line responsible for each 564 | * source code line. The function does this by splitting the line objects that wrap other lines into the 565 | * piece before and the piece after. 566 | * 567 | * @private 568 | * @method splitOverlaps 569 | * @param {Array[Object]} lines - array of line (node) objects 570 | * @param {Array[String]} code - array of source code lines 571 | */ 572 | function splitOverlaps(lines, code) { 573 | var i, j, left, right, insertAt; 574 | 575 | for ( i = 0; i < lines.length; i++) { 576 | if (lines[i]) { 577 | for (j = lines[i].statementDetails.length; j--;) { 578 | if (lines[i].statementDetails[j].loc.start.line < i+1) { 579 | if (lines[i].statementDetails[j].loc.end.line >= i+1) { 580 | lines[i].statementDetails[j].loc.start.line = i+1; 581 | lines[i].statementDetails[j].loc.start.column = 0; 582 | } else { 583 | lines[i].statementDetails.splice(j, 1); 584 | } 585 | } 586 | } 587 | if (lines[i].statementDetails[0] && lines[i].statementDetails[0].loc.start.column) { 588 | lines[i].statementDetails[0].loc.start.column = 0; 589 | } 590 | for (j = i + 1; j < lines.length; j++) { 591 | if (lines[i] && lines[j]) { 592 | left = { 593 | start: undefined, 594 | end: undefined 595 | }; 596 | right = { 597 | start: undefined, 598 | end: undefined 599 | }; 600 | lines[i].statementDetails.forEach(function(item, index) { 601 | if (left.start === undefined) { 602 | left.start = item.loc.start.line; 603 | } 604 | if (left.end === undefined) { 605 | left.end = item.loc.end.line; 606 | } else { 607 | left.end = Math.max(left.end, item.loc.end.line); 608 | } 609 | }); 610 | lines[j].statementDetails.forEach(function(item) { 611 | if (right.start === undefined) { 612 | right.start = item.loc.start.line; 613 | } 614 | if (right.end === undefined) { 615 | right.end = item.loc.end.line; 616 | } else { 617 | right.end = Math.max(right.end, item.loc.end.line); 618 | } 619 | }); 620 | } 621 | if (i !== j && 622 | lines[i] && lines[j] && left.start <= right.start && left.end >= right.end) { 623 | if (left.start === right.start && left.end <= right.end) { 624 | lines[j] = undefined; 625 | } else { 626 | lines[i].statementDetails.forEach(function(item, index) { 627 | if (item.loc.start.line !== item.loc.end.line) { 628 | if (i > j || left.start === right.start) { 629 | lines[i] = undefined; 630 | } 631 | if (item.loc.end.line > right.end) { 632 | // need to split it 633 | insertAt = right.end+1; 634 | while (lines[insertAt-1] && insertAt <= left.end) { 635 | insertAt += 1; 636 | } 637 | if (insertAt <= left.end) { 638 | lines[insertAt-1] = { 639 | number: (insertAt).toString(), 640 | count: undefined, 641 | statementDetails: [{ 642 | loc: { 643 | start: { 644 | line: insertAt, 645 | column: 0 646 | }, 647 | end: { 648 | line: item.loc.end.line, 649 | column: item.loc.end.column 650 | } 651 | } 652 | }] 653 | }; 654 | } 655 | } 656 | if (i < j && left.start !== right.start) { 657 | item.loc.end.line = right.start - 1; 658 | item.loc.end.column = code[item.loc.start.line-1].length; 659 | } 660 | } 661 | }); 662 | } 663 | } 664 | } 665 | } else { 666 | lines[i] = undefined; 667 | } 668 | } 669 | for (j = lines.length; j--;) { 670 | if (!lines[j]) { 671 | lines[j] = undefined; 672 | } else { 673 | if (lines[j].statementDetails[0] && 674 | lines[j].statementDetails[0].loc.start.column === 0 && 675 | lines[j].statementDetails[0].loc.end.column === 0) { 676 | lines[j] = undefined; 677 | } 678 | } 679 | } 680 | return lines; 681 | } 682 | 683 | function linesWithData(lines) { 684 | var interim = [], 685 | unique, i; 686 | lines.forEach(function(item, index) { 687 | if (item) { 688 | item.statementDetails.forEach(function(statement) { 689 | for (i = statement.loc.start.line; i <= statement.loc.end.line; i++) { 690 | interim.push(i); 691 | } 692 | }); 693 | } 694 | }); 695 | interim.sort(function(a,b) {return a - b;}); 696 | if (interim.length) { 697 | unique = [interim[0]]; 698 | interim.reduce(function(p, c) { 699 | if (p === c) { 700 | // Can get rid of c 701 | return p; 702 | } else { 703 | unique.push(c); 704 | return c; 705 | } 706 | }); 707 | } else { 708 | unique = []; 709 | } 710 | return unique; 711 | } 712 | 713 | function getAllFiles(dir) { 714 | var retVal = [], 715 | subDir, 716 | files = fs.readdirSync(dir); 717 | 718 | //console.log('precessing:', dir); 719 | files.forEach(function (file) { 720 | var fullPath = path.join(dir, file); 721 | try { 722 | fs.readdirSync(fullPath); 723 | subDir = getAllFiles(fullPath); 724 | retVal = retVal.concat(subDir); 725 | } catch (err) { 726 | retVal.push(path.relative(process.cwd(),fullPath)); 727 | } 728 | }); 729 | return retVal; 730 | } 731 | 732 | /** 733 | * Generate a coverage statistics structure for all of the instrumented files given all the data that 734 | * has been generated for them to date 735 | * { 736 | * sloc: Integer - how many source lines of code there were in total 737 | * ssoc: Integer - how many statements of code there were in total 738 | * sboc: Integer - how many blocks of code there were in total 739 | * coverage: Float - percentage of lines covered 740 | * statements: Float - percentage of statements covered 741 | * blocks: Float: percentage of blocks covered 742 | * files: Array[Object] - array of information about each file 743 | * uncovered: Array[String] - array of the files that match the patterns that were not tested at all 744 | * } 745 | * 746 | * Each file has the following structure 747 | * { 748 | * filename: String - the file 749 | * basename: String - the file short name 750 | * segments: String - the file's directory 751 | * coverage: Float - the percentage of lines covered 752 | * statements: Float - the percentage of statements covered 753 | * blocks: Float - the percentage of blocks covered 754 | * source: Array[Object] - array of objects, one for each line of code 755 | * sloc: Integer - the number of lines of code 756 | * ssoc: Integer - the number of statements of code 757 | * sboc: Integer - the number of blocks of code 758 | * } 759 | * 760 | * Each line has the following structure 761 | * { 762 | * count: Integer - number of times the line was hit 763 | * statements: Float - the percentage of statements covered 764 | * segments: Array[Object] - the segments of statements that make up the line 765 | * } 766 | * 767 | * Each statement segment has the following structure 768 | * { 769 | * code: String - the string of code for the segment 770 | * count: Integer - the hit count for the segment 771 | * } 772 | * 773 | * @method allStats 774 | * @return {Object} - the structure containing all the coverage stats for the coverage instance 775 | * 776 | */ 777 | CoverageSession.prototype.allStats = function () { 778 | var stats = { files : []}, 779 | allFiles = [], 780 | that = this, 781 | shouldBeCovered = [], 782 | filename, item, lines, sourceArray, segments, 783 | totSloc, totCovered, totBloc, totStat, totStatCovered, totBlocCovered, 784 | coverageData = this.coverageData; 785 | 786 | totSloc = totCovered = totBloc = totStat = totStatCovered = totBlocCovered = 0; 787 | allFiles = getAllFiles(process.cwd()); 788 | allFiles.forEach(function (filename) { 789 | shortFilename = path.relative(process.cwd(), filename); 790 | //console.log('filename: ', filename, ', shortFilename:', shortFilename, ', this.pattern: ', that.pattern, ', match: ', multimatch(shortFilename, that.pattern)); 791 | if (multimatch(shortFilename, that.pattern).length && 792 | shortFilename.indexOf('node_modules') === -1) { 793 | shouldBeCovered.push(shortFilename); 794 | } 795 | }); 796 | 797 | // console.log(shouldBeCovered); 798 | // console.log(coverageData); 799 | Object.keys(coverageData).forEach(function(filename) { 800 | var fstats, lines, code, dataLines, shortFilename, index; 801 | 802 | fstats = coverageData[filename].stats(); 803 | shortFilename = path.relative(process.cwd(), filename); 804 | index = shouldBeCovered.indexOf(shortFilename); 805 | if (index !== -1) { 806 | shouldBeCovered.splice(index, 1); 807 | } 808 | // console.log('fstats: ', fstats, ', filename; ', filename); 809 | code = fstats.code; 810 | splitOverlaps(fstats.lineDetails, code); 811 | dataLines = linesWithData(fstats.lineDetails); 812 | lines = fstats.lineDetails; 813 | sourceArray = []; 814 | code.forEach(function(codeLine, index){ 815 | var count = -1, statements = null, numStatements = 0, segs, lineNo, allSame = true, lineStruct; 816 | line = lines[index]; 817 | if (line && line.statementDetails[0]) { 818 | count = line.count; 819 | statements = 0; 820 | lineNo = line.statementDetails[0].loc.start.line; 821 | line.statementDetails.forEach(function(statement) { 822 | numStatements += 1; 823 | if (statement.count) { 824 | statements += 1; 825 | } 826 | if (statement.loc.start.line !== statement.loc.end.line || statement.loc.start.line !== lineNo) { 827 | allSame = false; 828 | } 829 | }); 830 | if (count) { 831 | segs = getSegments(code, lines, count, line.statementDetails); 832 | } else { 833 | segs = [{ 834 | code: codeLine, 835 | count: count 836 | }]; 837 | } 838 | } else { 839 | segs = [{ 840 | code: codeLine, 841 | count: 0 842 | }]; 843 | } 844 | lineStruct = { 845 | coverage: count, 846 | statements: statements === null ? 100 : (statements / numStatements) * 100, 847 | segments: segs 848 | }; 849 | sourceArray.push(lineStruct); 850 | }); 851 | filename = path.relative(process.cwd(), filename).replace(/\\/g, '/'); 852 | segments = filename.split('/'); 853 | item = { 854 | filename: filename, 855 | basename: segments.pop(), 856 | segments: segments.join('/') + '/', 857 | coverage: (fstats.lines / fstats.sloc) * 100, 858 | statements: (fstats.statements / fstats.ssoc) * 100, 859 | blocks: (fstats.blocks / fstats.sboc) * 100, 860 | source: sourceArray, 861 | sloc: fstats.sloc, 862 | sboc: fstats.sboc, 863 | ssoc: fstats.ssoc 864 | }; 865 | // console.log('item: ', item); 866 | totStat += fstats.ssoc; 867 | totBloc += fstats.sboc; 868 | totSloc += fstats.sloc; 869 | totCovered += fstats.lines; 870 | totStatCovered += fstats.statements; 871 | totBlocCovered += fstats.blocks; 872 | stats.files.push(item); 873 | }); 874 | stats.sloc = totSloc; 875 | stats.ssoc = totStat; 876 | stats.sboc = totBloc; 877 | stats.coverage = totCovered / totSloc * 100; 878 | stats.statements = totStatCovered / totStat * 100; 879 | stats.blocks = totBlocCovered / totBloc * 100; 880 | stats.uncovered = shouldBeCovered; 881 | // console.log('stats: ', stats); 882 | // console.log(shouldBeCovered); 883 | return stats; 884 | }; 885 | 886 | /** 887 | * create a new CoverageSession object 888 | * 889 | * @method cover 890 | * @param {Array[glob]} pattern - the array of glob patterns of includes and excludes 891 | * @param {String} debugDirectory - the name of the director to contain debug instrumentation files 892 | * @return {Object} the CoverageSession instance 893 | */ 894 | var cover = function(pattern, debugDirectory) { 895 | return new CoverageSession(pattern, debugDirectory); 896 | }; 897 | 898 | 899 | function removeDir(dirName) { 900 | fs.readdirSync(dirName).forEach(function(name) { 901 | if (name !== '.' && name !== '..') { 902 | try { 903 | fs.unlinkSync(path.join(dirName, name)); 904 | } catch (err) {} 905 | } 906 | }); 907 | try { 908 | fs.rmdirSync(dirName); 909 | } catch(err) {} 910 | } 911 | 912 | /** 913 | * This initializes a new coverage run. It does this by creating a randomly generated directory 914 | * in the .coverdata and updating the .coverrun file in the process' cwd with the directory's 915 | * name, so that the data collection can write data into this directory 916 | */ 917 | var init = function() { 918 | var directoryName = '.cover_' + Math.random().toString().substring(2), 919 | dataDir = path.join(process.cwd(), '.coverdata'); 920 | if (!fs.existsSync(dataDir)) { 921 | fs.mkdirSync(dataDir); 922 | } else { 923 | fs.readdirSync(dataDir).forEach(function(name) { 924 | if (name !== '.' && name !== '..') { 925 | removeDir(path.join(dataDir, name)); 926 | } 927 | }); 928 | } 929 | fs.mkdirSync(path.join(dataDir, directoryName)); 930 | fd = fs.writeFileSync(path.join(process.cwd(), '.coverrun'), '{ "run" : "' + directoryName + '" }'); 931 | }; 932 | 933 | var cleanup = function() { 934 | var store = require('./coverage_store'); 935 | 936 | store.clearStore(); 937 | }; 938 | 939 | module.exports = { 940 | cover: cover, 941 | init: init, 942 | cleanup: cleanup, 943 | reporters: { 944 | html: require('./reporters/html'), 945 | lcov: require('./reporters/lcov'), 946 | json: require('./reporters/json') 947 | } 948 | }; -------------------------------------------------------------------------------- /contrib/coverage_store.js: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Itay Neeman 2 | // 3 | // Licensed under the MIT License 4 | (function() { 5 | global.coverageStore = global.coverageStore || {}; 6 | var coverageStore = global.coverageStore, 7 | fs = require('fs'); 8 | 9 | module.exports = {}; 10 | module.exports.register = function(filename) { 11 | var run = JSON.parse(fs.readFileSync(process.cwd() + '/.coverrun')).run, 12 | runDirectory = process.cwd() + '/.coverdata/' + run + '/'; 13 | 14 | filename = filename.replace(/[\/|\:|\\]/g, "_"); 15 | if (!coverageStore[filename] || !fs.existsSync(runDirectory + filename)) { 16 | if (coverageStore.hasOwnProperty(filename)) { 17 | fs.closeSync(coverageStore[filename]); 18 | coverageStore[filename] = undefined; 19 | delete coverageStore[filename]; 20 | } 21 | coverageStore[filename] = fs.openSync(runDirectory + filename, 'w'); 22 | } 23 | return coverageStore[filename]; 24 | }; 25 | 26 | module.exports.getStore = function(filename) { 27 | var run = JSON.parse(fs.readFileSync(process.cwd() + '/.coverrun')).run, 28 | runDirectory = process.cwd() + '/.coverdata/' + run + '/'; 29 | 30 | filename = filename.replace(/[\/|\:|\\]/g, "_"); 31 | if (!coverageStore[filename]) { 32 | coverageStore[filename] = fs.openSync(runDirectory + filename, 'a'); 33 | } 34 | return coverageStore[filename]; 35 | }; 36 | module.exports.getStoreData = function(filename) { 37 | var run = JSON.parse(fs.readFileSync(process.cwd() + '/.coverrun')).run, 38 | runDirectory = process.cwd() + '/.coverdata/' + run + '/'; 39 | 40 | filename = filename.replace(/[\/|\:|\\]/g, "_"); 41 | return fs.readFileSync(runDirectory + filename); 42 | }; 43 | module.exports.clearStore = function() { 44 | var filename; 45 | for (filename in coverageStore) { 46 | if (coverageStore.hasOwnProperty(filename)) { 47 | fs.closeSync(coverageStore[filename]); 48 | coverageStore[filename] = undefined; 49 | delete coverageStore[filename]; 50 | } 51 | } 52 | }; 53 | })(); -------------------------------------------------------------------------------- /contrib/reporters/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dylanb/gulp-coverage/2d76c425cd9984aabcb476bc35bae87445dda527/contrib/reporters/.DS_Store -------------------------------------------------------------------------------- /contrib/reporters/html.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var fs = require('fs'), 6 | path = require('path'); 7 | 8 | /** 9 | * Expose `HTMLCov`. 10 | */ 11 | 12 | exports = module.exports = HTMLCov; 13 | 14 | function HTMLCov (coverageData, filename) { 15 | var jade = require('jade'), 16 | file = path.join(__dirname, 'templates', 'coverage.jade'), 17 | str = fs.readFileSync(file, 'utf8'), 18 | fn = jade.compile(str, { filename: file }), 19 | output = fn({ 20 | cov: coverageData, 21 | coverageClass: coverageClass, 22 | coverageCategory: coverageCategory 23 | }); 24 | if (!filename) { 25 | return output; 26 | } else { 27 | fs.writeFileSync(filename, output); 28 | } 29 | } 30 | 31 | function coverageCategory(line) { 32 | return line.coverage === 0 ? 33 | 'miss' : 34 | (line.statements ? 35 | 'hit ' + (line.statements.toFixed(0) != 100 ? 'partial' : '') 36 | : ''); 37 | } 38 | 39 | function coverageClass (n) { 40 | if (n >= 75) { 41 | return 'high'; 42 | } 43 | if (n >= 50) { 44 | return 'medium'; 45 | } 46 | if (n >= 25) { 47 | return 'low'; 48 | } 49 | return 'terrible'; 50 | } 51 | -------------------------------------------------------------------------------- /contrib/reporters/json.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var fs = require('fs'); 6 | 7 | /** 8 | * Expose `JSONCov`. 9 | */ 10 | 11 | exports = module.exports = JSONCov; 12 | 13 | function JSONCov (coverageData, filename) { 14 | if (!filename) { 15 | return JSON.stringify(coverageData, null, ' '); 16 | } else { 17 | fs.writeFileSync(filename, JSON.stringify(coverageData, null, ' ')); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contrib/reporters/lcov.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var fs = require('fs'), 6 | path = require('path'); 7 | 8 | /** 9 | * Expose `'LCOVCov'`. 10 | */ 11 | 12 | exports = module.exports = LCOVCov; 13 | 14 | function LCOVCov (coverageData, filename) { 15 | var output = 'TN:gulp-coverage output\n'; 16 | 17 | coverageData.files.forEach(function (fileData) { 18 | var fileOutput = 'SF:' + path.join(process.cwd(), fileData.filename) + '\n', 19 | instrumented = 0; 20 | fileOutput += 'BRF:' + fileData.ssoc + '\n'; 21 | fileOutput += 'BRH:' + Math.round(fileData.ssoc * fileData.statements/100) + '\n'; 22 | fileData.source.forEach(function (lineData, index) { 23 | if (lineData.coverage !== null) { 24 | instrumented += 1; 25 | fileOutput += 'DA:' + (index + 1) + ',' + lineData.coverage + '\n'; 26 | } 27 | }); 28 | fileOutput += 'LH:' + instrumented + '\n'; 29 | fileOutput += 'LF:' + fileData.source.length + '\n'; 30 | fileOutput += 'end_of_record\n'; 31 | output += fileOutput; 32 | }); 33 | 34 | if (!filename) { 35 | return output; 36 | } else { 37 | fs.writeFileSync(filename, output); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /contrib/reporters/templates/coverage.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Coverage 5 | include script.html 6 | include style.html 7 | body 8 | include menu 9 | #coverage(role="main") 10 | h1#overview Coverage 11 | 12 | #stats(class="stats #{coverageClass(cov.coverage)}") 13 | .percentage #{cov.coverage | 0}% line coverage 14 | .statements #{cov.statements | 0}% statement coverage 15 | .blocks #{cov.blocks | 0}% block coverage 16 | .sloc #{cov.sloc} SLOC 17 | .hits= cov.hits 18 | .misses= cov.misses 19 | 20 | #files 21 | for file in cov.files 22 | .file 23 | h2(id=file.filename)= file.filename 24 | .stats(class=coverageClass(file.coverage)) 25 | .percentage #{file.coverage | 0}% line coverage 26 | if file.coverage < 100 27 | a(href="#miss0") 28 | = ' go ' 29 | span.offscreen jump to first missed line 30 | .statements #{file.statements | 0}% statement coverage 31 | if file.statements < 100 32 | a(href="#partial0") 33 | = ' go ' 34 | span.offscreen jump to first missed statement 35 | .blocks #{file.blocks | 0}% block coverage 36 | .sloc #{file.sloc} SLOC 37 | .hits= file.hits 38 | .misses= file.misses 39 | 40 | div.table 41 | table.source 42 | thead 43 | tr 44 | th Line 45 | th Hits 46 | th Statements 47 | th Source 48 | th Action 49 | tbody 50 | for line, number in file.source 51 | tr(class=coverageCategory(line)) 52 | td.line #{number + 1} 53 | td.hits #{line.coverage > 0 ? line.coverage : (line.coverage === 0 ? 0 : '')} 54 | td.statements #{line.coverage > 0 ? line.statements.toFixed(0) + '%' : ''} 55 | td.source 56 | for segment in line.segments 57 | span(class="statement #{segment.count ? '' : 'notok'}") 58 | if !segment.count 59 | span.offscreen= ' not covered ' 60 | = segment.code 61 | td.action 62 | for missed in cov.uncovered 63 | .file 64 | h2(id=missed)= missed 65 | .stats(class=coverageClass(0)) 66 | .percentage #{0}% line coverage 67 | .statements #{0}% statement coverage 68 | .blocks #{0}% block coverage 69 | -------------------------------------------------------------------------------- /contrib/reporters/templates/menu.jade: -------------------------------------------------------------------------------- 1 | #menu(role="navigation") 2 | li 3 | a(href='#overview') overview 4 | for file in cov.files 5 | li 6 | span.cov(class=coverageClass(file.coverage)) #{file.coverage | 0} 7 | a(href='##{file.filename}') 8 | if file.segments 9 | span.dirname= file.segments 10 | span.basename= file.basename 11 | -------------------------------------------------------------------------------- /contrib/reporters/templates/script.html: -------------------------------------------------------------------------------- 1 | 2 | 55 | -------------------------------------------------------------------------------- /contrib/reporters/templates/style.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /contrib/templates/instrumentation_header.js: -------------------------------------------------------------------------------- 1 | 2 | // Instrumentation Header 3 | { 4 | var _fs = require('fs'); 5 | var <%= instrumented.names.statement %>, <%= instrumented.names.expression %>, <%= instrumented.names.block %>; 6 | var store = require('<%= coverageStorePath %>'); 7 | 8 | <%= instrumented.names.statement %> = function(i) { 9 | var fd = store.register('<%= filename %>'); 10 | _fs.writeSync(fd, '{"statement": {"node": ' + i + '}},\n'); 11 | }; 12 | 13 | <%= instrumented.names.expression %> = function(i) { 14 | var fd = store.register('<%= filename %>'); 15 | _fs.writeSync(fd, '{"expression": {"node": ' + i + '}},\n'); 16 | }; 17 | 18 | <%= instrumented.names.block %> = function(i) { 19 | var fd = store.register('<%= filename %>'); 20 | _fs.writeSync(fd, '{"block": ' + i + '},\n'); 21 | }; 22 | <%= instrumented.names.intro %> = function(id, obj) { 23 | // console.log('__intro: ', id, ', obj.__instrumented_miss: ', obj.__instrumented_miss, ', obj.length: ', obj.length); 24 | (typeof obj === 'object' || typeof obj === 'function') && 25 | Object.defineProperty && Object.defineProperty(obj, '__instrumented_miss', {enumerable: false, writable: true}); 26 | obj.__instrumented_miss = obj.__instrumented_miss || []; 27 | if ('undefined' !== typeof obj && null !== obj && 'undefined' !== typeof obj.__instrumented_miss) { 28 | if (obj.length === 0) { 29 | // console.log('interim miss: ', id); 30 | obj.__instrumented_miss[id] = true; 31 | } else { 32 | obj.__instrumented_miss[id] = false; 33 | } 34 | } 35 | return obj; 36 | }; 37 | function isProbablyChainable(obj, id) { 38 | return obj && 39 | obj.__instrumented_miss[id] !== undefined && 40 | 'number' === typeof obj.length; 41 | } 42 | <%= instrumented.names.extro %> = function(id, obj) { 43 | var fd = store.register('<%= filename %>'); 44 | // console.log('__extro: ', id, ', obj.__instrumented_miss: ', obj.__instrumented_miss, ', obj.length: ', obj.length); 45 | if ('undefined' !== typeof obj && null !== obj && 'undefined' !== typeof obj.__instrumented_miss) { 46 | if (isProbablyChainable(obj, id) && obj.length === 0 && obj.__instrumented_miss[id]) { 47 | // if the call was not a "constructor" - i.e. it did not add things to the chainable 48 | // and it did not return anything from the chainable, it is a miss 49 | // console.log('miss: ', id); 50 | } else { 51 | _fs.writeSync(fd, '{"chain": {"node": ' + id + '}},\n'); 52 | } 53 | obj.__instrumented_miss[id] = undefined; 54 | } else { 55 | _fs.writeSync(fd, '{"chain": {"node": ' + id + '}},\n'); 56 | } 57 | return obj; 58 | }; 59 | }; 60 | //////////////////////// 61 | 62 | // Instrumented Code 63 | <%= source %> 64 | -------------------------------------------------------------------------------- /debug/chaindebug.js: -------------------------------------------------------------------------------- 1 | var instrument = require('../contrib/instrument'), 2 | fs = require('fs'), 3 | src = fs.readFileSync('./testsupport/chain.js').toString(), 4 | inst; 5 | 6 | inst = instrument(src); 7 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | cover = require('./index'), 3 | mochaTask = require('gulp-mocha'), 4 | jshint = require('gulp-jshint'), 5 | exec = require('child_process').exec, 6 | jasmineTask = require('gulp-jasmine'), 7 | coverallsTask = require('gulp-coveralls'), 8 | through2 = require('through2'); 9 | 10 | /* 11 | * Define the dependency arrays 12 | */ 13 | 14 | var lintDeps = [], 15 | testDeps = [], 16 | debugDeps = [], 17 | mochaDeps = [], 18 | jsonDeps = [], 19 | jasmineDeps = [], 20 | testchainDeps = [], 21 | rewireDeps = [], 22 | classPatternDeps = [], 23 | coverallsDeps = []; 24 | 25 | /* 26 | * Define the task functions 27 | */ 28 | 29 | function test () { 30 | return gulp.src(['test/**.js'], { read: false }) 31 | .pipe(mochaTask({ 32 | reporter: 'spec', 33 | })); 34 | } 35 | 36 | function lint () { 37 | return gulp.src(['test/**/*.js', 'index.js', 'contrib/cover.js', 'contrib/coverage_store.js', 'contrib/reporters/**/*.js']) 38 | .pipe(jshint()) 39 | .pipe(jshint.reporter('default')); 40 | } 41 | 42 | function debug (cb) { 43 | exec('node --debug-brk debug/chaindebug.js', {}, function (error, stdout, stderr) { 44 | console.log('STDOUT'); 45 | console.log(stdout); 46 | console.log('STDERR'); 47 | console.log(stderr); 48 | if (error) { 49 | console.log('-------ERROR-------'); 50 | console.log(error); 51 | } 52 | }); 53 | cb(); 54 | } 55 | 56 | function mocha () { 57 | return gulp.src(['testsupport/src.js', 'testsupport/src3.js'], { read: false }) 58 | .pipe(cover.instrument({ 59 | pattern: ['**/test*'], 60 | debugDirectory: 'debug/info' 61 | })) 62 | .pipe(mochaTask({ 63 | reporter: 'spec' 64 | })) 65 | .pipe(cover.gather()) 66 | .pipe(cover.format({ 67 | outFile: 'blnkt.html' 68 | })) 69 | .pipe(gulp.dest('./testoutput')); 70 | } 71 | 72 | function classPattern () { 73 | return gulp.src(['testsupport/src4.js'], { read: false }) 74 | .pipe(cover.instrument({ 75 | pattern: ['**/test3.js'], 76 | debugDirectory: 'debug/info' 77 | })) 78 | .pipe(mochaTask({ 79 | reporter: 'spec' 80 | })) 81 | .pipe(cover.gather()) 82 | .pipe(cover.format({ 83 | outFile: 'classPattern.html' 84 | })) 85 | .pipe(gulp.dest('./testoutput')); 86 | } 87 | 88 | 89 | 90 | function coveralls () { 91 | return gulp.src(['testsupport/src.js', 'testsupport/src3.js'], { read: false }) 92 | .pipe(cover.instrument({ 93 | pattern: ['**/test*'], 94 | debugDirectory: 'debug/info' 95 | })) 96 | .pipe(mochaTask({ 97 | reporter: 'spec' 98 | })) 99 | .pipe(cover.gather()) 100 | .pipe(cover.format({ 101 | reporter: 'lcov' 102 | })) 103 | .pipe(coverallsTask()) 104 | .pipe(gulp.dest('./testoutput')); 105 | } 106 | 107 | function rewire () { 108 | return gulp.src(['testsupport/rewire.js'], { read: false }) 109 | .pipe(cover.instrument({ 110 | pattern: ['testsupport/myModule.js'], 111 | debugDirectory: 'debug/info' 112 | })) 113 | .pipe(mochaTask({ 114 | reporter: 'spec' 115 | })) 116 | .pipe(cover.gather()) 117 | .pipe(cover.format({ 118 | outFile: 'rewire.html' 119 | })) 120 | .pipe(gulp.dest('./testoutput')); 121 | } 122 | 123 | function json () { 124 | return gulp.src(['testsupport/src.js', 'testsupprt/src3.js'], { read: false }) 125 | .pipe(cover.instrument({ 126 | pattern: ['**/test*'], 127 | debugDirectory: 'debug/info' 128 | })) 129 | .pipe(mochaTask({ 130 | reporter: 'spec' 131 | })) 132 | .pipe(cover.report({ 133 | reporter: 'json', 134 | outFile: 'testoutput/json.json' 135 | })); 136 | } 137 | 138 | function jasmine () { 139 | return gulp.src('testsupport/srcjasmine.js') 140 | .pipe(cover.instrument({ 141 | pattern: ['**/test*'], 142 | debugDirectory: 'debug/info' 143 | })) 144 | .pipe(jasmineTask()) 145 | .pipe(cover.gather()) 146 | .pipe(cover.format({ 147 | outFile: 'jasmine.html' 148 | })) 149 | .pipe(gulp.dest('./testoutput')); 150 | } 151 | 152 | gulp.task('test', function() { 153 | // Be sure to return the stream 154 | }); 155 | 156 | function testchain () { 157 | return gulp.src(['testsupport/srcchain.js'], { read: false }) 158 | .pipe(cover.instrument({ 159 | pattern: ['**/chain.js'], 160 | debugDirectory: 'debug/info' 161 | })) 162 | .pipe(mochaTask({ 163 | reporter: 'spec' 164 | })) 165 | .pipe(cover.gather()) 166 | .pipe(cover.format({ 167 | outFile: 'chain.html' 168 | })) 169 | .pipe(gulp.dest('./testoutput')) 170 | .pipe(cover.format({ 171 | outFile: 'chain.json', 172 | reporter: 'json' 173 | })) 174 | .pipe(gulp.dest('./testoutput')); 175 | } 176 | 177 | 178 | function testc2 () { 179 | return gulp.src(['testsupport/c2_test.js'], { read: false }) 180 | .pipe(cover.instrument({ 181 | pattern: ['**/c2_cov.js'], 182 | debugDirectory: 'debug/info' 183 | })) 184 | .pipe(mochaTask({ 185 | reporter: 'spec' 186 | })) 187 | .pipe(cover.gather()) 188 | .pipe(cover.format({ 189 | outFile: 'c2.html' 190 | })) 191 | .pipe(gulp.dest('./testoutput')) 192 | .pipe(cover.format({ 193 | outFile: 'c2.json', 194 | reporter: 'json' 195 | })) 196 | .pipe(gulp.dest('./testoutput')); 197 | } 198 | /* 199 | * setup function 200 | */ 201 | 202 | function setup () { 203 | gulp.task('coveralls', coverallsDeps, coveralls); 204 | gulp.task('rewire', rewireDeps, rewire); 205 | gulp.task('classPattern', classPatternDeps, classPattern); 206 | gulp.task('test', testDeps, test); 207 | gulp.task('lint', lintDeps, lint); 208 | gulp.task('mocha', mochaDeps, mocha); 209 | gulp.task('json', jsonDeps, json); 210 | gulp.task('jasmine', jasmineDeps, jasmine); 211 | gulp.task('testchain', testchainDeps, testchain); 212 | } 213 | 214 | /* 215 | * Actual task defn 216 | */ 217 | 218 | gulp.task('default', function() { 219 | // Setup the chain of dependencies 220 | coverallsDeps = ['classPattern']; 221 | rewireDeps = ['coveralls']; 222 | testchainDeps = ['rewire']; 223 | jasmineDeps = ['testchain']; 224 | jsonDeps = ['jasmine']; 225 | mochaDeps = ['json']; 226 | testDeps = ['mocha']; 227 | setup(); 228 | gulp.run('test'); 229 | }); 230 | 231 | gulp.task('debug', debugDeps, debug); 232 | 233 | gulp.task('c2', [], testc2); 234 | 235 | setup(); 236 | 237 | gulp.task('watch', function () { 238 | jasmineDeps = ['mocha']; 239 | setup(); 240 | gulp.watch(['testsupport/src.js', 'testsupport/src3.js', 'testsupport/test.js', 'testsupport/test2.js'], function(event) { 241 | gulp.run('jasmine'); 242 | }); 243 | }); 244 | 245 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Dylan Barrell, all rights reserved 3 | * 4 | * Licensed under the MIT license 5 | * 6 | */ 7 | 8 | var path = require('path'); 9 | var fs = require('fs'); 10 | var cover = require('./contrib/cover'); 11 | var through2 = require('through2'); 12 | var gutil = require('gulp-util'); 13 | var coverInst; 14 | 15 | module.exports.instrument = function (options) { 16 | options = options || {}; 17 | cover.cleanup(); 18 | cover.init(); 19 | if (coverInst) { 20 | coverInst.release(); 21 | } 22 | coverInst = cover.cover(options.pattern, options.debugDirectory); 23 | 24 | return through2.obj(function (file, encoding, cb) { 25 | if (!file.path) { 26 | this.emit('error', new gutil.PluginError('gulp-coverage', 'Streaming not supported')); 27 | return cb(); 28 | } 29 | 30 | this.push(file); 31 | cb(); 32 | }, 33 | function (cb) { 34 | cb(); 35 | }); 36 | }; 37 | 38 | module.exports.report = function (options) { 39 | options = options || {}; 40 | var reporter = options.reporter || 'html'; 41 | 42 | return through2.obj( 43 | function (file, encoding, cb) { 44 | if (!file.path) { 45 | this.emit('error', new gutil.PluginError('gulp-coverage', 'Streaming not supported')); 46 | return cb(); 47 | } 48 | cb(); 49 | }, function (cb) { 50 | var stats; 51 | 52 | if (!coverInst) { 53 | throw new Error('Must call instrument before calling report'); 54 | } 55 | stats = coverInst.allStats(); 56 | cover.reporters[reporter](stats, options.outFile ? options.outFile : undefined); 57 | this.push({ coverage: stats }); 58 | cb(); 59 | }); 60 | }; 61 | 62 | module.exports.gather = function () { 63 | return through2.obj( 64 | function (file, encoding, cb) { 65 | if (!file.path) { 66 | this.emit('error', new gutil.PluginError('gulp-coverage', 'Streaming not supported')); 67 | return cb(); 68 | } 69 | cb(); 70 | }, function (cb) { 71 | var stats; 72 | 73 | if (!coverInst) { 74 | throw new Error('Must call instrument before calling report'); 75 | } 76 | stats = coverInst.allStats(); 77 | this.push({ coverage: stats }); 78 | cb(); 79 | }); 80 | }; 81 | 82 | module.exports.enforce = function (options) { 83 | options = options || {}; 84 | var statements = options.statements || 100, 85 | blocks = options.blocks || 100, 86 | lines = options.lines || 100, 87 | uncovered = options.uncovered; 88 | return through2.obj( 89 | function (data, encoding, cb) { 90 | if (!data.coverage) { 91 | this.emit('error', new gutil.PluginError('gulp-coverage', 92 | 'Must call gather or report before calling enforce')); 93 | return cb(); 94 | } 95 | if (data.coverage.statements < statements) { 96 | this.emit('error', new gutil.PluginError('gulp-coverage', 97 | 'statement coverage of ' + data.coverage.statements + 98 | ' does not meet the threshold of ' + statements)); 99 | } 100 | if (data.coverage.coverage < lines) { 101 | this.emit('error', new gutil.PluginError('gulp-coverage', 102 | 'line coverage of ' + data.coverage.coverage + 103 | ' does not meet the threshold of ' + lines)); 104 | } 105 | if (data.coverage.blocks < blocks) { 106 | this.emit('error', new gutil.PluginError('gulp-coverage', 107 | 'block coverage of ' + data.coverage.blocks + 108 | ' does not meet the threshold of ' + blocks)); 109 | } 110 | if (data.coverage.uncovered && uncovered !== undefined && data.coverage.uncovered.length > uncovered) { 111 | this.emit('error', new gutil.PluginError('gulp-coverage', 112 | 'uncovered files of ' + data.coverage.uncovered.length + 113 | ' does not meet the threshold of ' + uncovered)); 114 | } 115 | cb(); 116 | }, function (cb) { 117 | cb(); 118 | }); 119 | }; 120 | 121 | module.exports.format = function (options) { 122 | var reporters = options || [{}]; 123 | if (!Array.isArray(reporters)) reporters = [reporters]; 124 | return through2.obj( 125 | function (data, encoding, cb) { 126 | var file; 127 | if (!data.coverage) { 128 | this.emit('error', new gutil.PluginError('gulp-coverage', 129 | 'Must call gather before calling enforce')); 130 | cb(); 131 | return; 132 | } 133 | reporters.forEach(function(opts) { 134 | if (typeof opts === 'string') opts = { reporter: opts }; 135 | var reporter = opts.reporter || 'html'; 136 | var outfile = opts.outFile || 'coverage.' + reporter; 137 | file = new gutil.File({ 138 | base: path.join(__dirname, './'), 139 | cwd: __dirname, 140 | path: path.join(__dirname, './', outfile), 141 | contents: new Buffer(cover.reporters[reporter](data.coverage)) 142 | }); 143 | file.coverage = data.coverage; 144 | this.push(file); 145 | }, this); 146 | cb(); 147 | }, function (cb) { 148 | cb(); 149 | }); 150 | }; 151 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-coverage", 3 | "version": "0.3.38", 4 | "description": "Instrument and generate code coverage independent of test runner", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "gulp test" 8 | }, 9 | "dependencies": { 10 | "extend": "~2.0.2", 11 | "gulp-util": "~2.2.13", 12 | "instrumentjs": "0.0.2", 13 | "jade": "~1.1.4", 14 | "multimatch": "~0.3.0", 15 | "through2": "~0.4.0", 16 | "underscore": "~1.5.2" 17 | }, 18 | "devDependencies": { 19 | "gulp-mocha": "~0.4.1", 20 | "gulp-jshint": "~1.3.4", 21 | "gulp-jasmine": "~0.1.3", 22 | "gulp": "~3.4.0", 23 | "rewire": "^2.1.0", 24 | "gulp-coveralls": "^0.1.2" 25 | }, 26 | "keywords": [ 27 | "coverage", 28 | "code coverage", 29 | "mocha", 30 | "jasmine", 31 | "gulpplugin" 32 | ], 33 | "author": "dylan@barrell.com", 34 | "license": "MIT", 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/dylanb/gulp-coverage.git" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /screenshots/gulp-coverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dylanb/gulp-coverage/2d76c425cd9984aabcb476bc35bae87445dda527/screenshots/gulp-coverage.png -------------------------------------------------------------------------------- /test/cover.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | cover = require('../contrib/cover.js'), 3 | fs = require('fs'), 4 | path = require('path'); 5 | 6 | function clearStore() { 7 | var filename; 8 | for (filename in coverageStore) { 9 | if (coverageStore.hasOwnProperty(filename)) { 10 | fs.closeSync(coverageStore[filename]); 11 | coverageStore[filename] = undefined; 12 | delete coverageStore[filename]; 13 | } 14 | } 15 | } 16 | 17 | function removeDir(dir) { 18 | fs.readdirSync(dir).forEach(function(name) { 19 | if (name !== '.' && name !== '..') { 20 | fs.unlinkSync(path.join(dir, name)); 21 | } 22 | }); 23 | fs.rmdirSync(dir); 24 | } 25 | 26 | function removeDirTree(dir) { 27 | clearStore(); 28 | fs.readdirSync(dir).forEach(function(name) { 29 | if (name !== '.' && name !== '..') { 30 | removeDir(path.join(dir, name)); 31 | } 32 | }); 33 | } 34 | 35 | describe('cover.js', function () { 36 | describe('cover', function () { 37 | beforeEach(function () { 38 | if (fs.existsSync(path.join(process.cwd(), '.coverdata'))) { 39 | removeDirTree(path.join(process.cwd(), '.coverdata')); 40 | } 41 | if (fs.existsSync(path.join(process.cwd(), '.coverrun'))) { 42 | fs.unlinkSync(path.join(process.cwd(), '.coverrun')); 43 | } 44 | }); 45 | it('cover.init() will create the coverdata directory', function () { 46 | cover.init(); 47 | assert.ok(fs.existsSync(path.join(process.cwd(), '.coverdata'))); 48 | }); 49 | it('cover.init() will create the .coverrun file', function () { 50 | cover.init(); 51 | assert.ok(fs.existsSync(path.join(process.cwd(), '.coverrun'))); 52 | }); 53 | it('cover.init() will put the run directory into the .coverrun file', function () { 54 | var run; 55 | cover.init(); 56 | run = JSON.parse(fs.readFileSync(path.join(process.cwd(), '.coverrun'))).run; 57 | assert.ok(fs.existsSync(path.join(process.cwd(), '.coverdata', run))); 58 | }); 59 | it('cover.init() will remove prior run directories', function () { 60 | var run; 61 | cover.init(); 62 | run = JSON.parse(fs.readFileSync(path.join(process.cwd(), '.coverrun'))).run; 63 | assert.ok(fs.existsSync(path.join(process.cwd(), '.coverdata', run))); 64 | cover.init(); 65 | assert.ok(!fs.existsSync(path.join(process.cwd(), '.coverdata', run))); 66 | }); 67 | it('cover.init() will remove prior run directories', function () { 68 | var run; 69 | cover.init(); 70 | run = JSON.parse(fs.readFileSync(path.join(process.cwd(), '.coverrun'))).run; 71 | assert.ok(fs.existsSync(path.join(process.cwd(), '.coverdata', run))); 72 | cover.init(); 73 | assert.ok(!fs.existsSync(path.join(process.cwd(), '.coverdata', run))); 74 | }); 75 | }); 76 | describe('coverInst', function () { 77 | var coverInst; 78 | beforeEach(function () { 79 | delete require.cache[require.resolve('../testsupport/test')]; 80 | cover.cleanup(); 81 | cover.init(); 82 | // Note: the cover pattern is relative to the process.cwd() 83 | coverInst = cover.cover('./testsupport/test.js', path.join(process.cwd(), 'debug')); 84 | }); 85 | it('will cause the require function to instrument the file', function () { 86 | var test = require('../testsupport/test'), 87 | filename = require.resolve('../testsupport/test'), 88 | outputPath = path.join(process.cwd(), 'debug', filename.replace(/[\/|\:|\\]/g, "_") + ".js"); 89 | assert.ok(fs.existsSync(outputPath)); 90 | }); 91 | it('will cause the data to be collected when the instrumented file is executed', function () { 92 | var test = require('../testsupport/test'), 93 | run = JSON.parse(fs.readFileSync(path.join(process.cwd(), '.coverrun'))).run, 94 | filename = require.resolve('../testsupport/test'), 95 | dataPath = path.join(process.cwd(), '.coverdata', run, filename.replace(/[\/|\:|\\]/g, "_")); 96 | test(); 97 | assert.ok(fs.existsSync(dataPath)); 98 | }); 99 | }); 100 | describe('coverInst.coverageData[filename].stats()', function () { 101 | var test, coverInst, stats, filename; 102 | cover.cleanup(); 103 | cover.init(); 104 | coverInst = cover.cover('**/test2.js'); 105 | test = require('../testsupport/test2'); 106 | filename = require.resolve('../testsupport/test2'); 107 | test(); 108 | filename = filename.replace(/\\/g, '/'); 109 | stats = coverInst.coverageData[filename].stats(); 110 | it('will return the correct number of covered lines', function () { 111 | assert.equal(stats.lines, 7); 112 | }); 113 | it('will return the correct number of code lines', function () { 114 | assert.equal(stats.sloc, 9); 115 | }); 116 | it('will return the correct number of covered statements', function () { 117 | assert.equal(stats.statements, 9); 118 | }); 119 | it('will return the correct number of statements', function () { 120 | assert.equal(stats.ssoc, 13); 121 | }); 122 | it('will return the correct number of covered blocks', function () { 123 | assert.equal(stats.blocks, 3); 124 | }); 125 | it('will return the correct number of blocks', function () { 126 | assert.equal(stats.sboc, 4); 127 | }); 128 | it('will return the lines of code as an array', function () { 129 | assert.equal(stats.code.length, 33); 130 | }); 131 | it('will return the code correctly', function () { 132 | var codeArray = fs.readFileSync(filename).toString().trim().split('\n'); 133 | assert.deepEqual(stats.code, codeArray); 134 | }); 135 | it('will return the correct lineDetails sparse array', function () { 136 | assert.equal(stats.lineDetails.length, 17); 137 | assert.equal(stats.lineDetails.filter(function(item){return item;}).length, 9); 138 | }); 139 | it('will return the correct count for lines that were not covered', function () { 140 | assert.equal(stats.lineDetails[7].count, 0); 141 | }); 142 | it('will return the correct count for lines that are blocks around other code', function () { 143 | assert.equal(stats.lineDetails[0].count, 1); 144 | assert.equal(stats.lineDetails[16].count, 1); 145 | }); 146 | it('will return the correct line and position information for the statements', function () { 147 | assert.equal(stats.lineDetails[4].statementDetails[0].loc.start.line, 5); 148 | assert.equal(stats.lineDetails[4].statementDetails[0].loc.start.column, 16); 149 | assert.equal(stats.lineDetails[4].statementDetails[0].loc.end.line, 5); 150 | assert.equal(stats.lineDetails[4].statementDetails[0].loc.end.column, 22); 151 | assert.equal(stats.lineDetails[4].statementDetails[2].loc.start.line, 5); 152 | assert.equal(stats.lineDetails[4].statementDetails[2].loc.start.column, 24); 153 | assert.equal(stats.lineDetails[4].statementDetails[2].loc.end.line, 5); 154 | assert.equal(stats.lineDetails[4].statementDetails[2].loc.end.column, 27); 155 | }); 156 | it('will return the correct coverage count for the covered statements', function () { 157 | assert.equal(stats.lineDetails[4].statementDetails[0].count, 11); 158 | assert.equal(stats.lineDetails[4].statementDetails[2].count, 10); 159 | }); 160 | it('will return the correct coverage count for the uncovered statements', function () { 161 | assert.equal(stats.lineDetails[6].statementDetails[0].count, 0); 162 | assert.equal(stats.lineDetails[6].statementDetails[2].count, 0); 163 | }); 164 | }); 165 | describe('coverInst.allStats()', function () { 166 | var test, coverInst, stats, filename; 167 | delete require.cache[require.resolve('../testsupport/test2')]; 168 | cover.cleanup(); 169 | cover.init(); 170 | coverInst = cover.cover('**/testsupport/*.js'); 171 | test = require('../testsupport/test2'); 172 | filename = require.resolve('../testsupport/test2'); 173 | test(); 174 | stats = coverInst.allStats(); 175 | // console.log(stats); 176 | it('will return the uncovered files', function () { 177 | //console.log(stats.uncovered); 178 | assert.deepEqual(stats.uncovered, [ 179 | 'testsupport/c2_cov.js', 180 | 'testsupport/c2_test.js', 181 | 'testsupport/chain.js', 182 | 'testsupport/chainable.js', 183 | 'testsupport/myModule.js', 184 | 'testsupport/rewire.js', 185 | 'testsupport/src.js', 186 | 'testsupport/src2.js', 187 | 'testsupport/src3.js', 188 | 'testsupport/src4.js', 189 | 'testsupport/srcchain.js', 190 | 'testsupport/srcjasmine.js', 191 | 'testsupport/test.js', 192 | 'testsupport/test3.js' ]); 193 | }); 194 | it('will return the correct number of code lines', function () { 195 | assert.equal(stats.sloc, 9); 196 | }); 197 | it('will return the correct number of statements', function () { 198 | assert.equal(stats.ssoc, 13); 199 | }); 200 | it('will return the correct number of blocks', function () { 201 | assert.equal(stats.sboc, 4); 202 | }); 203 | it('will return the correct coverage', function () { 204 | assert.equal(Math.floor(stats.coverage), 77); 205 | }); 206 | it('will return the correct statements coverage', function () { 207 | assert.equal(Math.floor(stats.statements), 69); 208 | }); 209 | it('will return the correct blocks coverage', function () { 210 | assert.equal(Math.floor(stats.blocks), 75); 211 | }); 212 | it('will return the file data', function () { 213 | assert.equal(stats.files.length, 1); 214 | }); 215 | 216 | }); 217 | }); 218 | -------------------------------------------------------------------------------- /test/gulp-coverage.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | cover = require('../index.js'), 3 | through2 = require('through2'), 4 | fs = require('fs'), 5 | path = require('path'), 6 | mocha = require('gulp-mocha'); 7 | 8 | function clearStore() { 9 | var filename; 10 | for (filename in coverageStore) { 11 | if (coverageStore.hasOwnProperty(filename)) { 12 | fs.closeSync(coverageStore[filename]); 13 | coverageStore[filename] = undefined; 14 | delete coverageStore[filename]; 15 | } 16 | } 17 | } 18 | 19 | function removeDir(dir) { 20 | fs.readdirSync(dir).forEach(function(name) { 21 | if (name !== '.' && name !== '..') { 22 | fs.unlinkSync(dir+ '/' + name); 23 | } 24 | }); 25 | fs.rmdirSync(dir); 26 | } 27 | 28 | function removeDirTree(dir) { 29 | clearStore(); 30 | fs.readdirSync(dir).forEach(function(name) { 31 | if (name !== '.' && name !== '..') { 32 | removeDir(dir + '/' + name); 33 | } 34 | }); 35 | } 36 | 37 | describe('gulp-coverage', function () { 38 | var writer, reader; 39 | beforeEach(function () { 40 | delete require.cache[require.resolve('../testsupport/test')]; 41 | delete require.cache[require.resolve('../testsupport/src')]; 42 | if (fs.existsSync(process.cwd() + '/.coverdata')) { 43 | removeDirTree(process.cwd() + '/.coverdata'); 44 | } 45 | if (fs.existsSync(process.cwd() + '/.coverrun')) { 46 | fs.unlinkSync(process.cwd() + '/.coverrun'); 47 | } 48 | writer = through2.obj(function (chunk, enc, cb) { 49 | this.push(chunk); 50 | cb(); 51 | }, function (cb) { 52 | cb(); 53 | }); 54 | }); 55 | describe('instrument', function () { 56 | it('should instrument and collect data', function (done) { 57 | reader = through2.obj(function (chunk, enc, cb) { 58 | this.push(chunk); 59 | cb(); 60 | }, 61 | function (cb) { 62 | var filename = require.resolve('../testsupport/test'); 63 | // Should have created the coverdata directory 64 | assert.ok(fs.existsSync(path.join(process.cwd(), '.coverdata'))); 65 | // Should have created the run directory 66 | assert.ok(fs.existsSync(path.join(process.cwd(), '.coverrun'))); 67 | run = JSON.parse(fs.readFileSync(path.join(process.cwd(), '.coverrun'))).run; 68 | assert.ok(fs.existsSync(path.join(process.cwd(), '.coverdata', run))); 69 | // Should have collected data 70 | dataPath = path.join(process.cwd(), '.coverdata', run, filename.replace(/[\/|\:|\\]/g, "_")); 71 | assert.ok(fs.existsSync(dataPath)); 72 | cb(); 73 | done(); 74 | }); 75 | reader.on('error', function(){ 76 | console.log('error: ', arguments); 77 | }); 78 | 79 | writer.pipe(cover.instrument({ 80 | pattern: ['**/test*'], 81 | debugDirectory: path.join(process.cwd() , 'debug') 82 | })) 83 | .pipe(mocha({})) 84 | .pipe(reader); 85 | 86 | writer.push({ 87 | path: require.resolve('../testsupport/src.js') 88 | }); 89 | writer.end(); 90 | }); 91 | it('should throw if passed a real stream', function(done) { 92 | writer = through2(function (chunk, enc, cb) { 93 | this.push(chunk); 94 | cb(); 95 | }, function (cb) { 96 | cb(); 97 | }); 98 | writer.pipe(cover.instrument({ 99 | pattern: ['**/test*'], 100 | debugDirectory: path.join(process.cwd(), 'debug') 101 | }).on('error', function(err) { 102 | assert.equal(err.message, 'Streaming not supported'); 103 | done(); 104 | })); 105 | writer.write('Some bogus data'); 106 | writer.end(); 107 | }); 108 | }); 109 | describe('report', function () { 110 | beforeEach(function () { 111 | if (fs.existsSync('coverage.html')) { 112 | fs.unlinkSync('coverage.html'); 113 | } 114 | }); 115 | it('should throw if passed a real stream', function(done) { 116 | writer = through2(function (chunk, enc, cb) { 117 | this.push(chunk); 118 | cb(); 119 | }, function (cb) { 120 | cb(); 121 | }); 122 | writer.pipe(cover.report({ 123 | outFile: 'coverage.html', 124 | reporter: 'html' 125 | }).on('error', function(err) { 126 | assert.equal(err.message, 'Streaming not supported'); 127 | done(); 128 | })); 129 | writer.write('Some bogus data'); 130 | writer.end(); 131 | }); 132 | it('should create an HTML report', function (done) { 133 | reader = through2.obj(function (chunk, enc, cb) { 134 | cb(); 135 | }, 136 | function (cb) { 137 | assert.ok(fs.existsSync('coverage.html')); 138 | cb(); 139 | done(); 140 | }); 141 | writer.pipe(cover.instrument({ 142 | pattern: ['**/test*'], 143 | debugDirectory: path.join(process.cwd(), 'debug') 144 | })).pipe(mocha({ 145 | })).pipe(cover.report({ 146 | outFile: 'coverage.html', 147 | reporter: 'html' 148 | })).pipe(reader); 149 | 150 | writer.write({ 151 | path: require.resolve('../testsupport/src.js') 152 | }); 153 | writer.end(); 154 | }); 155 | it('will send the coverage data through as a JSON structure', function (done) { 156 | reader = through2.obj(function (data, enc, cb) { 157 | assert.ok(data.coverage); 158 | assert.equal('number', typeof data.coverage.coverage); 159 | assert.equal('number', typeof data.coverage.statements); 160 | assert.equal('number', typeof data.coverage.blocks); 161 | assert.ok(Array.isArray(data.coverage.files)); 162 | assert.equal(data.coverage.files[0].basename, 'test.js'); 163 | cb(); 164 | }, 165 | function (cb) { 166 | cb(); 167 | done(); 168 | }); 169 | writer.pipe(cover.instrument({ 170 | pattern: ['**/test*'], 171 | debugDirectory: path.join(process.cwd(), 'debug') 172 | })).pipe(mocha({ 173 | })).pipe(cover.report({ 174 | outFile: 'coverage.html', 175 | reporter: 'html' 176 | })).pipe(reader); 177 | writer.write({ 178 | path: require.resolve('../testsupport/src.js') 179 | }); 180 | writer.end(); 181 | }); 182 | }); 183 | describe('gather', function () { 184 | it('should throw if passed a real stream', function(done) { 185 | writer = through2(function (chunk, enc, cb) { 186 | this.push(chunk); 187 | cb(); 188 | }, function (cb) { 189 | cb(); 190 | }); 191 | writer.pipe(cover.gather().on('error', function(err) { 192 | assert.equal(err.message, 'Streaming not supported'); 193 | done(); 194 | })); 195 | writer.write('Some bogus data'); 196 | writer.end(); 197 | }); 198 | it('will send the coverage data through as a JSON structure', function (done) { 199 | reader = through2.obj(function (data, enc, cb) { 200 | assert.ok(data.coverage); 201 | assert.equal('number', typeof data.coverage.coverage); 202 | assert.equal('number', typeof data.coverage.statements); 203 | assert.equal('number', typeof data.coverage.blocks); 204 | assert.ok(Array.isArray(data.coverage.files)); 205 | assert.equal(data.coverage.files[0].basename, 'test.js'); 206 | cb(); 207 | }, 208 | function (cb) { 209 | cb(); 210 | done(); 211 | }); 212 | writer.pipe(cover.instrument({ 213 | pattern: ['./testsupport/test*'], 214 | debugDirectory: path.join(process.cwd(), 'debug') 215 | })).pipe(mocha({ 216 | })).pipe(cover.gather()).pipe(reader); 217 | writer.write({ 218 | path: require.resolve('../testsupport/src.js') 219 | }); 220 | writer.end(); 221 | }); 222 | it('Does correctly support module pattern', function (done) { 223 | reader = through2.obj(function (data, enc, cb) { 224 | assert.ok(data.coverage); 225 | // this next test makes sure that comments and other lines that do not 226 | // contain statements will get output to the HTML report 227 | assert.equal(data.coverage.files[0].source[18].coverage, -1); 228 | cb(); 229 | }, 230 | function (cb) { 231 | cb(); 232 | done(); 233 | }); 234 | writer.pipe(cover.instrument({ 235 | pattern: ['./testsupport/test*'], 236 | debugDirectory: path.join(process.cwd(), 'debug') 237 | })).pipe(mocha({ 238 | })).pipe(cover.gather()).pipe(reader); 239 | writer.write({ 240 | path: require.resolve('../testsupport/src4.js') 241 | }); 242 | writer.end(); 243 | }); 244 | }); 245 | describe('enforce', function () { 246 | it('should throw if not passed the correct data', function(done) { 247 | writer = through2(function (chunk, enc, cb) { 248 | this.push(chunk); 249 | cb(); 250 | }, function (cb) { 251 | cb(); 252 | }); 253 | writer.pipe(cover.enforce({}).on('error', function(err) { 254 | assert.equal(err.message, 'Must call gather or report before calling enforce'); 255 | done(); 256 | })); 257 | writer.write('Some bogus data'); 258 | writer.end(); 259 | }); 260 | it('will emit an error if the statement coverage is below the appropriate threshold', function (done) { 261 | writer.pipe(cover.enforce({ 262 | statements: 100, 263 | lines: 1, 264 | blocks: 1 265 | }).on('error', function(err) { 266 | assert.equal(err.message.indexOf('statement coverage of'), 0); 267 | done(); 268 | })); 269 | writer.push({ 270 | coverage: { 271 | statements: 99, 272 | coverage: 99, 273 | blocks: 99 274 | } 275 | }); 276 | writer.end(); 277 | }); 278 | it('will emit an error if the line coverage is below the appropriate threshold', function (done) { 279 | writer.pipe(cover.enforce({ 280 | statements: 1, 281 | lines: 100, 282 | blocks: 1 283 | }).on('error', function(err) { 284 | assert.equal(err.message.indexOf('line coverage of'), 0); 285 | done(); 286 | })); 287 | writer.push({ 288 | coverage: { 289 | statements: 99, 290 | coverage: 99, 291 | blocks: 99 292 | } 293 | }); 294 | writer.end(); 295 | }); 296 | it('will emit an error if the block coverage is below the appropriate threshold', function (done) { 297 | writer.pipe(cover.enforce({ 298 | statements: 1, 299 | lines: 1, 300 | blocks: 100 301 | }).on('error', function(err) { 302 | assert.equal(err.message.indexOf('block coverage of'), 0); 303 | done(); 304 | })); 305 | writer.push({ 306 | coverage: { 307 | statements: 99, 308 | coverage: 99, 309 | blocks: 99 310 | } 311 | }); 312 | writer.end(); 313 | }); 314 | it('will emit an error if the uncovered files count is above the appropriate threshold', function (done) { 315 | writer.pipe(cover.enforce({ 316 | statements: 100, 317 | lines: 1, 318 | blocks: 1, 319 | uncovered: 1 320 | }).on('error', function(err) { 321 | assert.equal(err.message.indexOf('uncovered files of'), 0); 322 | done(); 323 | })); 324 | writer.push({ 325 | coverage: { 326 | statements: 100, 327 | coverage: 100, 328 | blocks: 100, 329 | uncovered: ['one/file/name.js', 'second/file/name.js'] 330 | } 331 | }); 332 | writer.end(); 333 | }); 334 | it('will NOT emit an error if an uncovered threshold is not explicitly provided', function (done) { 335 | var finished = false; 336 | writer.pipe(cover.enforce({ 337 | statements: 100, 338 | lines: 1, 339 | blocks: 1 340 | }).on('error', function(err) { 341 | assert.ok(false); 342 | finished = true; 343 | done(); 344 | })); 345 | writer.push({ 346 | coverage: { 347 | statements: 100, 348 | coverage: 100, 349 | blocks: 100, 350 | uncovered: ['one/file/name.js', 'second/file/name.js'] 351 | } 352 | }); 353 | writer.end(); 354 | setTimeout(function () { 355 | if (!finished) { 356 | assert.ok(true); 357 | done(); 358 | } 359 | }, 100); 360 | }); 361 | }); 362 | describe('format', function () { 363 | it('should throw if not passed the correct data', function (done) { 364 | writer = through2(function (chunk, enc, cb) { 365 | this.push(chunk); 366 | cb(); 367 | }, function (cb) { 368 | cb(); 369 | }); 370 | writer.pipe(cover.format({}).on('error', function(err) { 371 | assert.equal(err.message, 'Must call gather before calling enforce'); 372 | done(); 373 | })); 374 | writer.write('Some bogus data'); 375 | writer.end(); 376 | }); 377 | it('will add a "contents" item to the stream object', function (done) { 378 | reader = through2.obj(function (data, enc, cb) { 379 | assert.ok(data.coverage); 380 | assert.ok(data.contents); 381 | assert.equal(typeof data.contents, 'object'); 382 | assert.ok(data.path.indexOf('coverage.html') !== -1); 383 | done(); 384 | cb(); 385 | }, 386 | function (cb) { 387 | cb(); 388 | }); 389 | writer.pipe(cover.instrument({ 390 | pattern: ['testsupport/test*'], 391 | debugDirectory: path.join(process.cwd(), 'debug') 392 | })).pipe(mocha({ 393 | })).pipe(cover.gather( 394 | )).pipe(cover.format( 395 | )).pipe(reader); 396 | writer.write({ 397 | path: require.resolve('../testsupport/src.js') 398 | }); 399 | writer.end(); 400 | }); 401 | it('will add a "contents" item to the stream object in JSON format if asked', function (done) { 402 | reader = through2.obj(function (data, enc, cb) { 403 | var strContents = data.contents.toString(), 404 | json = JSON.parse(strContents); 405 | 406 | assert.ok(json.hasOwnProperty('files')); 407 | assert.ok(data.path.indexOf('coverage.json') !== -1); 408 | done(); 409 | cb(); 410 | }, 411 | function (cb) { 412 | cb(); 413 | }); 414 | writer.pipe(cover.instrument({ 415 | pattern: ['testsupport/test*'], 416 | debugDirectory: path.join(process.cwd(), 'debug') 417 | })).pipe(mocha({ 418 | })).pipe(cover.gather( 419 | )).pipe(cover.format({ 420 | reporter: 'json' 421 | })).pipe(reader); 422 | 423 | writer.write({ 424 | path: require.resolve('../testsupport/src.js') 425 | }); 426 | writer.end(); 427 | }); 428 | it('will give the output file the name passed into the options', function (done) { 429 | reader = through2.obj(function (data, enc, cb) { 430 | assert.ok(data.path.indexOf('cvrg.html') !== -1); 431 | done(); 432 | cb(); 433 | }, 434 | function (cb) { 435 | cb(); 436 | }); 437 | writer.pipe(cover.instrument({ 438 | pattern: ['testsupport/test*'], 439 | debugDirectory: path.join(process.cwd(), 'debug') 440 | })).pipe(mocha({ 441 | })).pipe(cover.gather( 442 | )).pipe(cover.format({ 443 | outFile: 'cvrg.html' 444 | })).pipe(reader); 445 | writer.write({ 446 | path: require.resolve('../testsupport/src.js') 447 | }); 448 | writer.end(); 449 | }); 450 | it('can be chained with "enforce"', function (done) { 451 | reader = through2.obj(function (data, enc, cb) { 452 | assert.ok(data.coverage); 453 | assert.ok(data.output); 454 | assert.equal(typeof data.output, 'string'); 455 | cb(); 456 | }, 457 | function (cb) { 458 | cb(); 459 | done(); 460 | }); 461 | writer.pipe(cover.instrument({ 462 | pattern: ['testsupport/test*'], 463 | debugDirectory: path.join(process.cwd(), 'debug') 464 | })).pipe(mocha({ 465 | })).pipe(cover.gather( 466 | )).pipe(cover.format( 467 | )).pipe(cover.enforce({ 468 | statements: 80, 469 | lines: 83, 470 | blocks: 60, 471 | uncovered: 2 472 | })).pipe(reader); 473 | writer.write({ 474 | path: require.resolve('../testsupport/src.js') 475 | }); 476 | writer.end(); 477 | }); 478 | }); 479 | it('should accept array of options', function (done) { 480 | var expected = [ 481 | path.resolve(process.cwd(), 'coverage.html'), 482 | path.resolve(process.cwd(), 'coverage.json') 483 | ]; 484 | var actual = []; 485 | reader = through2.obj(function (chunk, enc, cb) { 486 | actual.push(chunk.path); 487 | cb(); 488 | }, 489 | function (cb) { 490 | assert.deepEqual(expected, actual); 491 | cb(); 492 | done(); 493 | }); 494 | writer.pipe(cover.instrument({ 495 | pattern: ['**/test*'], 496 | debugDirectory: path.join(process.cwd(), 'debug') 497 | })).pipe(mocha({ 498 | })).pipe(cover.gather({ 499 | })).pipe(cover.format([ 500 | 'html', { reporter: 'json' } 501 | ])).pipe(reader); 502 | 503 | writer.write({ 504 | path: require.resolve('../testsupport/src.js') 505 | }); 506 | writer.end(); 507 | }); 508 | it('should take an array of just one string', function (done) { 509 | var expected = [ 510 | path.resolve(process.cwd(), 'coverage.html') 511 | ]; 512 | var actual = []; 513 | reader = through2.obj(function (chunk, enc, cb) { 514 | actual.push(chunk.path); 515 | cb(); 516 | }, 517 | function (cb) { 518 | assert.deepEqual(expected, actual); 519 | cb(); 520 | done(); 521 | }); 522 | writer.pipe(cover.instrument({ 523 | pattern: ['**/test*'], 524 | debugDirectory: path.join(process.cwd(), 'debug') 525 | })).pipe(mocha({ 526 | })).pipe(cover.gather({ 527 | })).pipe(cover.format([ 528 | 'html' 529 | ])).pipe(reader); 530 | 531 | writer.write({ 532 | path: require.resolve('../testsupport/src.js') 533 | }); 534 | writer.end(); 535 | }); 536 | it('should accept array of options, with different outFile settings', function (done) { 537 | var expected = [ 538 | path.resolve(process.cwd(), 'blah.html'), 539 | path.resolve(process.cwd(), 'bugger.json') 540 | ]; 541 | var actual = []; 542 | reader = through2.obj(function (chunk, enc, cb) { 543 | actual.push(chunk.path); 544 | cb(); 545 | }, 546 | function (cb) { 547 | assert.deepEqual(expected, actual); 548 | cb(); 549 | done(); 550 | }); 551 | writer.pipe(cover.instrument({ 552 | pattern: ['**/test*'], 553 | debugDirectory: path.join(process.cwd(), 'debug') 554 | })).pipe(mocha({ 555 | })).pipe(cover.gather({ 556 | })).pipe(cover.format([ 557 | { reporter: 'html', outFile: 'blah.html'}, 558 | { reporter: 'json', outFile: 'bugger.json' } 559 | ])).pipe(reader); 560 | 561 | writer.write({ 562 | path: require.resolve('../testsupport/src.js') 563 | }); 564 | writer.end(); 565 | }); 566 | it('should accept array of options, with one default and one explicit outFile settings', function (done) { 567 | var expected = [ 568 | path.resolve(process.cwd(), 'coverage.html'), 569 | path.resolve(process.cwd(), 'bugger.json') 570 | ]; 571 | var actual = []; 572 | reader = through2.obj(function (chunk, enc, cb) { 573 | actual.push(chunk.path); 574 | cb(); 575 | }, 576 | function (cb) { 577 | assert.deepEqual(expected, actual); 578 | cb(); 579 | done(); 580 | }); 581 | writer.pipe(cover.instrument({ 582 | pattern: ['**/test*'], 583 | debugDirectory: path.join(process.cwd(), 'debug') 584 | })).pipe(mocha({ 585 | })).pipe(cover.gather({ 586 | })).pipe(cover.format([ 587 | { reporter: 'html' }, 588 | { reporter: 'json', outFile: 'bugger.json' } 589 | ])).pipe(reader); 590 | 591 | writer.write({ 592 | path: require.resolve('../testsupport/src.js') 593 | }); 594 | writer.end(); 595 | }); 596 | }); 597 | -------------------------------------------------------------------------------- /testsupport/c2_cov.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | var a = false, b = false; 3 | 4 | if (a && b) { 5 | console.log('a && b'); 6 | } 7 | 8 | a = true; 9 | 10 | if (a && b) { 11 | console.log('a && b'); 12 | } 13 | 14 | b = true; 15 | a = false; 16 | 17 | if (a && b) { 18 | console.log('a && b'); 19 | } 20 | 21 | a = true; 22 | b = true; 23 | 24 | if (a && b) { 25 | console.log('a && b'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /testsupport/c2_test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | test = require('./c2_cov'); 3 | 4 | describe('Test C2', function () { 5 | it('SShould just run', function () { 6 | test(); 7 | assert(true); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /testsupport/chain.js: -------------------------------------------------------------------------------- 1 | var Chainable = require('./chainable'); 2 | 3 | module.exports = function () { 4 | var str = '473628742687'; 5 | var f = function () {}; 6 | 7 | // Chanable function 8 | f.func = function () {return this}; 9 | f.func().func(); 10 | 11 | // Chainable object 12 | chain = new Chainable(); 13 | chain.find('zack').format({middle: false}).remove(0, 1).write(); 14 | 15 | // non-object method call (looks like a chainable from a syntax tree perspective) 16 | str = str.substr(0, 2); 17 | 18 | Math.floor( 19 | Math.random()* 20 | 10+ 21 | 0.5 22 | ); 23 | return chain; 24 | }; 25 | 26 | -------------------------------------------------------------------------------- /testsupport/chainable.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits; 2 | var extend = require('extend'); 3 | 4 | var Chainable = function() { 5 | }; 6 | 7 | inherits(Chainable, Array); 8 | 9 | var dataBase = [ 10 | 'karin eichmann', 11 | 'dylan barrell', 12 | 'john smith', 13 | 'laurel a. neighbor', 14 | 'donny evans', 15 | 'julie y. jankowicz', 16 | 'zack pearlfisher']; 17 | 18 | Chainable.prototype.find = function(qTerm) { 19 | var q = qTerm.toLowerCase(); 20 | dataBase.forEach(function(item) { 21 | if (item.indexOf(q) !== -1) { 22 | this.push(item); 23 | } 24 | }, this); 25 | return this; 26 | }; 27 | 28 | Chainable.prototype.remove = function(index, number) { 29 | if (index !== -1) { 30 | this.splice(index, number !== undefined ? number : 1); 31 | } 32 | return this; 33 | }; 34 | 35 | Chainable.prototype.format = function(options) { 36 | var defaultOptions = { 37 | first: true, 38 | last: true, 39 | middle : true 40 | }; 41 | extend(defaultOptions, options); 42 | this.forEach(function(item, index) { 43 | var arr = item.trim().split(' '), 44 | i; 45 | if (defaultOptions.middle && arr.length > 2) { 46 | for (i = 1; i < arr.length - 1; i++) { 47 | arr[i] = arr[i][0].toUpperCase() + arr[i].substring(1); 48 | } 49 | } 50 | if (defaultOptions.first) { 51 | arr[0] = arr[0][0].toUpperCase() + arr[0].substring(1); 52 | } 53 | if (defaultOptions.last) { 54 | arr[arr.length - 1] = arr[arr.length - 1][0].toUpperCase() + arr[arr.length - 1].substring(1); 55 | } 56 | this[index] = arr.join(' '); 57 | }, this); 58 | return this; 59 | }; 60 | 61 | Chainable.prototype.write = function() { 62 | // noop 63 | return this; 64 | }; 65 | 66 | 67 | module.exports = Chainable; 68 | -------------------------------------------------------------------------------- /testsupport/myModule.js: -------------------------------------------------------------------------------- 1 | var myLocalGlobal = function () {}; 2 | 3 | // Calls myLocalGlobal 4 | exports.myFunction = function () { 5 | myLocalGlobal(); 6 | } 7 | -------------------------------------------------------------------------------- /testsupport/rewire.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | rewire = require('rewire'), 3 | myModule = rewire('./myModule'); 4 | 5 | describe('Test Rewire', function () { 6 | var called = false; 7 | myModule.__set__('myLocalGlobal', function () { 8 | called = true; 9 | }) 10 | it('Should rewire the function and call the rewired function', function () { 11 | myModule.myFunction(); 12 | assert.ok(called); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /testsupport/src.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | test = require('./test'); 3 | 4 | describe('Test Src', function () { 5 | it('Should run this test Src', function () { 6 | test(); 7 | assert.ok(true); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /testsupport/src2.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | test = require('./test2'); 3 | 4 | describe('Test Src2', function () { 5 | it('Should run this test Src2', function () { 6 | assert.ok(!true); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /testsupport/src3.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | test = require('./test'); 3 | 4 | describe('Test Src3', function () { 5 | it('Should run this test Src3', function () { 6 | assert.ok(true); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /testsupport/src4.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | Controller = require('./test3'); 3 | 4 | describe('Test AlertController', function () { 5 | it('Should work', function () { 6 | var c = new Controller(); 7 | assert.equal(c.show(), 1); 8 | assert.equal(c.hide(), 0); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /testsupport/srcchain.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | test = require('./chain'); 3 | 4 | describe('Test Src', function () { 5 | it('Should run this test for the chainable and not find an enumerable __instrumented_miss attribute on the object', function () { 6 | var chain = test(), 7 | props = [], 8 | i; 9 | for(i in chain) { 10 | props[i] = true; 11 | } 12 | assert.ok(chain.hasOwnProperty('__instrumented_miss')); 13 | assert.ok(!props['__instrumented_miss']); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /testsupport/srcjasmine.js: -------------------------------------------------------------------------------- 1 | var test = require('./test'); 2 | 3 | describe('Test Src', function () { 4 | it('Should run this test Src', function () { 5 | test(); 6 | expect(true).toBe(true); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /testsupport/test.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | var i, matcher, 3 | retVal = 0, 4 | a = true, b = false; 5 | 6 | // throw new Error('bugger'); 7 | for (i = 0; i < 10; i++) { 8 | matcher = Math.floor( 9 | Math.random()* 10 | 10+ 11 | 0.5 12 | ); 13 | if (matcher === i) { 14 | retVal += 1; 15 | retVal += 1; 16 | } else { 17 | retVal = (retVal > 100 ? retVal + 1 : retVal + 2); 18 | } 19 | } 20 | if (a || b) { 21 | retVal += 2; 22 | } else { 23 | retVal += 1; 24 | } 25 | return retVal; 26 | }; 27 | -------------------------------------------------------------------------------- /testsupport/test2.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | var i, 3 | retVal = 0; 4 | 5 | for (i = 0; i < 10; i++) { 6 | if (false) { 7 | retVal/0; 8 | retVal += 1; 9 | //#JSCOVERAGE_IF 10 | retVal += 2; 11 | //#JSCOVERAGE_ENDIF 12 | } else { 13 | retVal = retVal; 14 | } 15 | } 16 | return retVal; 17 | }; 18 | var uncovered = true; //cover:false 19 | //#JSCOVERAGE_IF 20 | if (false) { 21 | var retVal = 19; 22 | } 23 | //#JSCOVERAGE_IF 0 24 | //#JSCOVERAGE_IF 25 | if (false) { 26 | retVal += 1; 27 | } 28 | if (false) { 29 | retVal += 1; 30 | } 31 | if (false) { 32 | retVal += 1; 33 | } -------------------------------------------------------------------------------- /testsupport/test3.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var AlertController = function () { 4 | this.count = 0; 5 | }; 6 | 7 | AlertController.prototype = { 8 | /** 9 | * Comment 10 | */ 11 | show: function () { 12 | this.count++; 13 | return this.count; 14 | }, 15 | 16 | /** 17 | * comment 18 | */ 19 | hide: function () { 20 | this.count--; 21 | return this.count; 22 | } 23 | }; 24 | 25 | // to pull into node namespace if included 26 | if (typeof module !== "undefined" && module.exports !== undefined) { 27 | module.exports = AlertController; 28 | } 29 | --------------------------------------------------------------------------------