├── .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 | 
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 |
--------------------------------------------------------------------------------