Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * jscoverage: index.js | |
3 | * Authors : fish |
|
4 | * Create : 2014-04-03 15:20:13 | |
5 | * CopyRight 2014 (c) Fish And Other Contributors | |
6 | * | |
7 | */ | |
8 | 1 | require('coffee-script').register(); |
9 | 1 | var debug = require('debug')('jscoverage'); |
10 | 1 | var fs = require('xfs'); |
11 | 1 | var path = require('path'); |
12 | 1 | var argv = require('optimist').argv; |
13 | 1 | var patch = require('./lib/patch'); |
14 | 1 | var rptUtil = require('./reporter/util'); |
15 | 1 | var cmd = argv['$0']; |
16 | 1 | var MODE_MOCHA = false; |
17 | 1 | process.__MOCHA_PREPARED = false; |
18 | 1 | var FLAG_LOCK = false; |
19 | if (/mocha/.test(cmd)) { | |
20 | 1 | MODE_MOCHA = true; |
21 | } | |
22 | ||
23 | if (MODE_MOCHA) { | |
24 | 1 | prepareMocha(); |
25 | } | |
26 | 1 | var COV_REPORT_NAME = argv.name || 'jscoverage reporter'; |
27 | /** | |
28 | * prepare env for mocha test | |
29 | * @covignore | |
30 | */ | |
31 | function prepareMocha() { | |
32 | var covIgnore = argv.covignore; | |
33 | var cwd = process.cwd(); | |
34 | var covlevel = argv.coverage; | |
35 | if (process.__MOCHA_PREPARED) { | |
36 | return; | |
37 | } | |
38 | process.__MOCHA_PREPARED = true; | |
39 | if (covlevel) { | |
40 | var tmp = covlevel.split(','); | |
41 | covlevel = { | |
42 | high: parseInt(tmp[0], 10) / 100, | |
43 | middle: parseInt(tmp[1], 10) / 100, | |
44 | low: parseInt(tmp[2], 10) / 100 | |
45 | }; | |
46 | } else { | |
47 | covlevel = { | |
48 | high: 0.9, | |
49 | middle: 0.7, | |
50 | low: 0.3 | |
51 | }; | |
52 | } | |
53 | debug('covlevel', covlevel); | |
54 | /** | |
55 | * add after hook | |
56 | * @return {[type]} [description] | |
57 | */ | |
58 | var supportReporters = ['list', 'spec', 'tap']; | |
59 | process.nextTick(function () { | |
60 | try { | |
61 | after(function () { | |
62 | if (FLAG_LOCK) { | |
63 | return; | |
64 | } | |
65 | FLAG_LOCK = true; | |
66 | if (typeof _$jscoverage === 'undefined') { | |
67 | return; | |
68 | } | |
69 | try { | |
70 | if (argv.covout === 'none') { | |
71 | return; | |
72 | } | |
73 | if (!argv.covout) { | |
74 | var mochaR = argv.reporter || argv.R; | |
75 | if (supportReporters.indexOf(mochaR) !== -1) { | |
76 | argv.covout = mochaR; | |
77 | } else { | |
78 | argv.covout = 'list'; | |
79 | } | |
80 | } | |
81 | var reporter; | |
82 | if (/^\w+$/.test(argv.covout)) { | |
83 | reporter = require('./reporter/' + argv.covout); | |
84 | } else { | |
85 | reporter = require(argv.covout); | |
86 | } | |
87 | reporter.process(_$jscoverage, exports.coverageStats(), covlevel, COV_REPORT_NAME, rptUtil); | |
88 | } catch (e) { | |
89 | console.error('jscoverage reporter error', e, e.stack); | |
90 | } | |
91 | }); | |
92 | } catch (e) { | |
93 | // do nothing | |
94 | } | |
95 | }); | |
96 | if (argv.covinject) { | |
97 | debug('covinject enabled'); | |
98 | patch.enableInject(true); | |
99 | } | |
100 | if (!covIgnore) { | |
101 | try { | |
102 | var stat = fs.statSync('.covignore'); | |
103 | stat && (covIgnore = '.covignore'); | |
104 | debug('.covignore file found!'); | |
105 | } catch (e) { | |
106 | return; | |
107 | } | |
108 | } | |
109 | try { | |
110 | covIgnore = fs.readFileSync(covIgnore).toString().split(/\r?\n/g); | |
111 | debug('loading .covignore file!'); | |
112 | } catch (e) { | |
113 | throw new Error('jscoverage loading covIgnore file error:' + covIgnore); | |
114 | } | |
115 | var _ignore = []; | |
116 | covIgnore.forEach(function (v, i, a) { | |
117 | if (!v) { | |
118 | return; | |
119 | } | |
120 | if (v.indexOf('/') === 0) { | |
121 | v = '^' + cwd + v; | |
122 | } | |
123 | _ignore.push(new RegExp(v.replace(/\./g, '\\.').replace(/\*/g, '.*'))); | |
124 | }); | |
125 | ||
126 | patch.setCovIgnore(_ignore); | |
127 | } | |
128 | ||
129 | 1 | var jscoverage = require('./lib/jscoverage'); |
130 | ||
131 | /** | |
132 | * enableInject description | |
133 | * @param {Boolean} true or false | |
134 | */ | |
135 | 1 | exports.enableInject = patch.enableInject; |
136 | /** | |
137 | * config the inject function names | |
138 | * @param {Object} obj {get, replace, call, reset} | |
139 | * @example | |
140 | * | |
141 | * jsc.config({get:'$get', replace:'$replace'}); | |
142 | * | |
143 | * ===================== | |
144 | * | |
145 | * testMod = require('testmodule'); | |
146 | * testMod.$get('name'); | |
147 | * testMod.$replace('name', obj); | |
148 | */ | |
149 | 1 | exports.config = function (obj) { |
150 | 1 | var inject_functions = patch.getInjectFunctions(); |
151 | for (var i in obj) { | |
152 | 4 | inject_functions[i] = obj[i]; |
153 | } | |
154 | }; | |
155 | /** | |
156 | * process Code, inject the coverage code to the input Code string | |
157 | * @param {String} filename jscoverage file flag | |
158 | * @param {Code} content | |
159 | * @return {Code} instrumented code | |
160 | */ | |
161 | 1 | exports.process = jscoverage.process; |
162 | ||
163 | /** | |
164 | * processFile, instrument singfile | |
165 | * @sync | |
166 | * @param {Path} source absolute Path | |
167 | * @param {Path} dest absolute Path | |
168 | * @param {Object} option [description] | |
169 | */ | |
170 | 1 | exports.processFile = function (source, dest, option) { |
171 | 6 | var content; |
172 | 6 | var stats; |
173 | // test source is file or dir, or not a file | |
174 | try { | |
175 | 6 | stats = fs.statSync(source); |
176 | if (stats.isDirectory()) { | |
177 | 1 | throw new Error('path is dir'); |
178 | } else if (!stats.isFile()) { | |
179 | 1 | throw new Error('path is not a regular file'); |
180 | } | |
181 | } catch (e) { | |
182 | 5 | throw new Error('source file error' + e); |
183 | } | |
184 | ||
185 | 1 | fs.sync().mkdir(path.dirname(dest)); |
186 | ||
187 | 1 | content = fs.readFileSync(source).toString(); |
188 | 1 | var sheBang = false; |
189 | if (content.charCodeAt(0) === 65279) { | |
190 | 0 | content = content.substr(1); |
191 | } | |
192 | // cut the shebang | |
193 | if (content.indexOf('#!') === 0) { | |
194 | 0 | var firstLineEnd = content.indexOf('\n'); |
195 | 0 | sheBang = content.substr(0, firstLineEnd + 1); |
196 | 0 | content = content.substr(firstLineEnd + 1); |
197 | } | |
198 | // check if coffee script | |
199 | 1 | var ext = path.extname(source); |
200 | if (ext === '.coffee' || ext === '.litcoffee') { | |
201 | 0 | var CoffeeScript = require('coffee-script'); |
202 | 0 | content = CoffeeScript.compile(content, { |
203 | filename: source | |
204 | }); | |
205 | } | |
206 | 1 | content = this.process(source, content); |
207 | if (sheBang) { | |
208 | 0 | content = sheBang + content; |
209 | } | |
210 | 1 | fs.writeFileSync(dest, content); |
211 | }; | |
212 | ||
213 | function fixData(num) { | |
214 | 14 | return Math.round(num * 10000) / 10000; |
215 | } | |
216 | /** | |
217 | * sum the coverage rate | |
218 | * @public | |
219 | */ | |
220 | 1 | exports.coverageStats = function () { |
221 | 1 | var file; |
222 | 1 | var tmp; |
223 | 1 | var lineTotal; |
224 | 1 | var lineHits; |
225 | 1 | var branchTotal; |
226 | 1 | var branchHits; |
227 | 1 | var n, len; |
228 | 1 | var stats = {}; |
229 | 1 | var branches, branchesMap, branch; |
230 | 1 | var line, start, offset; |
231 | if (typeof _$jscoverage === 'undefined') { | |
232 | 0 | return; |
233 | } | |
234 | for (var i in _$jscoverage) { | |
235 | 7 | file = i; |
236 | 7 | tmp = _$jscoverage[i]; |
237 | if (!tmp.length) { | |
238 | 0 | continue; |
239 | } | |
240 | // reset the counters; | |
241 | 7 | lineTotal = lineHits = 0; |
242 | 7 | branchTotal = branchHits = 0; |
243 | ||
244 | for (n = 0, len = tmp.length; n < len; n++) { | |
245 | if (tmp[n] !== undefined) { | |
246 | 301 | lineTotal ++; |
247 | if (tmp[n] > 0) { | |
248 | 261 | lineHits ++; |
249 | } | |
250 | } | |
251 | } | |
252 | // calculate the branches coverage | |
253 | 7 | branches = tmp.condition; |
254 | 7 | branchesMap = {}; |
255 | for (n in branches) { | |
256 | if (branches[n] === 0) { | |
257 | 12 | branch = n.split('_'); |
258 | 12 | line = branch[0]; |
259 | 12 | start = parseInt(branch[1], 10); |
260 | 12 | offset = parseInt(branch[2], 10); |
261 | if (!branchesMap[line]) { | |
262 | 11 | branchesMap[line] = []; |
263 | } | |
264 | 12 | branchesMap[line].push([start, offset]); |
265 | } else { | |
266 | 85 | branchHits ++; |
267 | } | |
268 | 97 | branchTotal ++; |
269 | } | |
270 | 7 | stats[file] = { |
271 | lineSloc: lineTotal, | |
272 | lineHits: lineHits, | |
273 | lineCoverage: lineTotal ? fixData(lineHits / lineTotal) : 0, | |
274 | branchSloc: branchTotal, | |
275 | branchHits: branchHits, | |
276 | branchCoverage: branchTotal ? fixData(branchHits / branchTotal) : 0, | |
277 | branches: branchesMap | |
278 | }; | |
279 | } | |
280 | 1 | return stats; |
281 | }; | |
282 | ||
283 | /** | |
284 | * get lcov report | |
285 | * @return {[type]} [description] | |
286 | */ | |
287 | 1 | exports.getLCOV = function () { |
288 | 1 | var tmp; |
289 | 1 | var total; |
290 | 1 | var touched; |
291 | 1 | var n, len; |
292 | 1 | var lcov = ''; |
293 | if (typeof _$jscoverage === 'undefined') { | |
294 | 0 | return; |
295 | } | |
296 | 1 | Object.keys(_$jscoverage).forEach(function (file) { |
297 | 6 | lcov += 'SF:' + file + '\n'; |
298 | 6 | tmp = _$jscoverage[file]; |
299 | if (!tmp.length) { | |
300 | 0 | return; |
301 | } | |
302 | 6 | total = touched = 0; |
303 | for (n = 0, len = tmp.length; n < len; n++) { | |
304 | if (tmp[n] !== undefined) { | |
305 | 249 | lcov += 'DA:' + n + ',' + tmp[n] + '\n'; |
306 | 249 | total ++; |
307 | if (tmp[n] > 0) { | |
308 | 173 | touched++; |
309 | } | |
310 | } | |
311 | } | |
312 | 6 | lcov += 'end_of_record\n'; |
313 | }); | |
314 | 1 | return lcov; |
315 | }; | |
316 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * jscoverage: lib/instrument.js | |
3 | * Authors : fish |
|
4 | * Create : 2014-04-03 15:20:13 | |
5 | * CopyRight 2014 (c) Fish And Other Contributors | |
6 | */ | |
7 | /** | |
8 | * instrument code | |
9 | * @example | |
10 | * var ist = new Instrument(); | |
11 | * var resCode = ist.process(str); | |
12 | */ | |
13 | 1 | var debug = require('debug')('cov:instrument'); |
14 | 1 | var Uglify = require('uglify-js'); |
15 | ||
16 | function Instrument() { | |
17 | /** | |
18 | * filename needed | |
19 | * @type {String} | |
20 | */ | |
21 | 1 | this.filename = null; |
22 | /** | |
23 | * store injected code | |
24 | * @type {String} | |
25 | */ | |
26 | 1 | this.code = null; |
27 | /** | |
28 | * 储存line信息 | |
29 | * @type {Array} | |
30 | */ | |
31 | 1 | this.lines = []; |
32 | /** | |
33 | * 储存condition信息 | |
34 | * @type {Object} | |
35 | */ | |
36 | 1 | this.conds = {}; |
37 | /** | |
38 | * source code in array | |
39 | * @type {Array} | |
40 | */ | |
41 | 1 | this.source = null; |
42 | } | |
43 | ||
44 | 1 | Instrument.prototype = { |
45 | // 行类型 | |
46 | T_LINE: 'line', | |
47 | T_COND: 'cond', | |
48 | /** | |
49 | * process code | |
50 | * @public | |
51 | * @param {String} code source code | |
52 | * @return {String} injected code | |
53 | */ | |
54 | process: function (filename, code) { | |
55 | if (!filename) { | |
56 | 0 | throw new Error('[jscoverage]instrument need filename!'); |
57 | } | |
58 | ||
59 | 1 | var ist = this; |
60 | // parse ast | |
61 | 1 | var ast = Uglify.parse(code); |
62 | ||
63 | 1 | this.filename = filename; |
64 | 1 | this.source = code.split(/\r?\n/); |
65 | ||
66 | // init walker | |
67 | 1 | var walker = new Uglify.TreeWalker(function (node) { |
68 | if (ist.checkIfIgnore(node, walker.stack)) { | |
69 | 4 | return; |
70 | } | |
71 | 263 | var parent = this.parent(); |
72 | if (node instanceof Uglify.AST_Conditional) { // 三元判断 | |
73 | 1 | debug('node type:', node.TYPE, 'parent type:', parent && parent.TYPE); |
74 | 1 | node.consequent = ist.inject('cond', node.consequent.start.line, node.consequent); |
75 | 1 | node.alternative = ist.inject('cond', node.alternative.start.line, node.alternative); |
76 | } else if (node.TYPE === 'Binary') { | |
77 | 3 | debug('node type:', node.TYPE, 'parent type:', parent && parent.TYPE); |
78 | if (node.operator && ['||', '&&'].indexOf(node.operator) === -1) { | |
79 | 3 | return; |
80 | } | |
81 | if (!(node.left instanceof Uglify.AST_Constant)) { | |
82 | 0 | node.left = ist.inject('cond', node.left.start.line, node.left); |
83 | } | |
84 | if (!(node.right instanceof Uglify.AST_Constant)) { | |
85 | 0 | node.right = ist.inject('cond', node.right.start.line, node.right); |
86 | } | |
87 | } | |
88 | ||
89 | else if (node instanceof Uglify.AST_If) { | |
90 | 0 | debug('node type:', node.TYPE, 'parent type:', parent && parent.TYPE); |
91 | 0 | node.condition = ist.inject('cond', node.condition.start.line, node.condition); |
92 | } | |
93 | ||
94 | 260 | var len = node.body ? node.body.length : 0; |
95 | if (len) { | |
96 | 7 | var res = []; |
97 | 7 | var subNode; |
98 | for (var i = 0; i < len; i++) { | |
99 | 30 | subNode = node.body[i]; |
100 | if (ist.checkIfIgnore(subNode, walker.stack)) { | |
101 | 1 | res.push(subNode); |
102 | 1 | continue; |
103 | } | |
104 | if (subNode instanceof Uglify.AST_Statement) { | |
105 | if (ist.ifExclude(subNode)) { | |
106 | 6 | res.push(subNode); |
107 | 6 | continue; |
108 | } | |
109 | 23 | res.push(ist.inject('line', subNode.start.line)); |
110 | } else if (subNode instanceof Uglify.AST_Var) { | |
111 | 0 | res.push(ist.inject('line', subNode.start.line)); |
112 | } | |
113 | 23 | res.push(subNode); |
114 | } | |
115 | 7 | node.body = res; |
116 | } | |
117 | }); | |
118 | // figure_out_scope | |
119 | 1 | ast.figure_out_scope(); |
120 | // walk process | |
121 | 1 | ast.walk(walker); |
122 | ||
123 | 1 | var out = Uglify.OutputStream({ |
124 | preserve_line : true, | |
125 | comments: 'all', | |
126 | beautify: true | |
127 | }); | |
128 | // rebuild file | |
129 | 1 | ast.print(out); |
130 | 1 | this.code = out.toString(); |
131 | 1 | return this; |
132 | }, | |
133 | /** | |
134 | * 注入覆盖率查询方法 | |
135 | * @private | |
136 | * @param {String} type inject type, line | conds | |
137 | * @param {Number} line line number | |
138 | * @param {Object} expr any expression, or node, or statement | |
139 | * @return {AST_Func} Object | |
140 | */ | |
141 | inject: function (type, line, expr) { | |
142 | 25 | var args = []; |
143 | if (type === this.T_LINE) { | |
144 | 23 | this.lines.push(line); |
145 | 23 | args = [ |
146 | new Uglify.AST_String({value: this.filename}), | |
147 | new Uglify.AST_String({value: type}), | |
148 | new Uglify.AST_Number({value: line}) | |
149 | ]; | |
150 | } else if (type === this.T_COND) { | |
151 | 2 | var start = expr.start.col; |
152 | 2 | var offset = expr.end.endpos - expr.start.pos; |
153 | 2 | var key = line + '_' + start + '_' + offset; // 编码 |
154 | 2 | this.conds[key] = 0; |
155 | 2 | args = [ |
156 | new Uglify.AST_String({value: this.filename}), | |
157 | new Uglify.AST_String({value: type}), | |
158 | new Uglify.AST_String({value: key}), | |
159 | expr | |
160 | ]; | |
161 | } | |
162 | ||
163 | 25 | var call = new Uglify.AST_Call({ |
164 | expression: new Uglify.AST_SymbolRef({name: '_$jscmd'}), | |
165 | //end: new Uglify.AST_ | |
166 | args: args | |
167 | }); | |
168 | ||
169 | if (type === this.T_LINE) { | |
170 | 23 | return new Uglify.AST_SimpleStatement({ |
171 | body: call, | |
172 | end: new Uglify.AST_Token({value: ';'}) | |
173 | }); | |
174 | } else { | |
175 | 2 | return call; |
176 | } | |
177 | }, | |
178 | /** | |
179 | * check if need inject | |
180 | * @param {AST_Node} node | |
181 | * @return {Boolean} | |
182 | */ | |
183 | ifExclude: function (node) { | |
184 | if (node instanceof Uglify.AST_LoopControl) { | |
185 | 2 | return false; |
186 | } | |
187 | if ( | |
188 | node instanceof Uglify.AST_IterationStatement || | |
189 | node instanceof Uglify.AST_StatementWithBody || | |
190 | node instanceof Uglify.AST_Block | |
191 | ) { | |
192 | 6 | return true; |
193 | } | |
194 | }, | |
195 | checkIfIgnore: function (node, stack) { | |
196 | 297 | var cmt; |
197 | if (node.start && node.start.comments_before.length) { | |
198 | 16 | cmt = node.start.comments_before[node.start.comments_before.length - 1]; |
199 | if (/@covignore/.test(cmt.value) && !(node instanceof Uglify.AST_Toplevel)) { | |
200 | 4 | node.__covignore = true; |
201 | } | |
202 | } | |
203 | if (node.__covignore) { | |
204 | 4 | return true; |
205 | } | |
206 | if (stack) { | |
207 | for (var i = stack.length - 1; i > 0; i--) { | |
208 | if (stack[i].__covignore) { | |
209 | 1 | return true; |
210 | } | |
211 | } | |
212 | } | |
213 | 292 | return false; |
214 | } | |
215 | }; | |
216 | ||
217 | 1 | module.exports = Instrument; |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * jscoverage: lib/jscoverage.js | |
3 | * Authors : fish |
|
4 | * Create : 2014-04-03 15:20:13 | |
5 | * CopyRight 2014 (c) Fish And Other Contributors | |
6 | */ | |
7 | 1 | var Instrument = require('./instrument'); |
8 | ||
9 | /** | |
10 | * do not exec this function | |
11 | * the function body will insert into instrument files | |
12 | * | |
13 | * _$jscoverage = { | |
14 | * filename : { | |
15 | * line1: 0 | |
16 | * line2: 1 | |
17 | * line3: undefined | |
18 | * .... | |
19 | * source: [], | |
20 | * condition: [ | |
21 | * line_start_offset | |
22 | * ] | |
23 | * } | |
24 | * } | |
25 | * @covignore | |
26 | */ | |
27 | function jscFunctionBody() { | |
28 | // instrument by jscoverage, do not modifly this file | |
29 | (function (file, lines, conds, source) { | |
30 | var BASE; | |
31 | if (typeof global === 'object') { | |
32 | BASE = global; | |
33 | } else if (typeof window === 'object') { | |
34 | BASE = window; | |
35 | } else { | |
36 | throw new Error('[jscoverage] unknow ENV!'); | |
37 | } | |
38 | if (BASE._$jscoverage) { | |
39 | BASE._$jscmd(file, 'init', lines, conds, source); | |
40 | return; | |
41 | } | |
42 | var cov = {}; | |
43 | /** | |
44 | * jsc(file, 'init', lines, condtions) | |
45 | * jsc(file, 'line', lineNum) | |
46 | * jsc(file, 'cond', lineNum, expr, start, offset) | |
47 | */ | |
48 | function jscmd(file, type, line, express, start, offset) { | |
49 | var storage; | |
50 | switch (type) { | |
51 | case 'init': | |
52 | if(cov[file]){ | |
53 | storage = cov[file]; | |
54 | } else { | |
55 | storage = []; | |
56 | for (var i = 0; i < line.length; i ++) { | |
57 | storage[line[i]] = 0; | |
58 | } | |
59 | var condition = express; | |
60 | var source = start; | |
61 | storage.condition = condition; | |
62 | storage.source = source; | |
63 | } | |
64 | cov[file] = storage; | |
65 | break; | |
66 | case 'line': | |
67 | storage = cov[file]; | |
68 | storage[line] ++; | |
69 | break; | |
70 | case 'cond': | |
71 | storage = cov[file]; | |
72 | storage.condition[line] ++; | |
73 | return express; | |
74 | } | |
75 | } | |
76 | ||
77 | BASE._$jscoverage = cov; | |
78 | BASE._$jscmd = jscmd; | |
79 | jscmd(file, 'init', lines, conds, source); | |
80 | })('$file$', $lines$, $conds$, $source$); | |
81 | } | |
82 | /** | |
83 | * gen coverage head | |
84 | */ | |
85 | function genCodeCoverage(instrObj) { | |
86 | if (!instrObj) { | |
87 | 0 | return ''; |
88 | } | |
89 | 1 | var code = []; |
90 | 1 | var filename = instrObj.filename; |
91 | // Fix windows path | |
92 | 1 | filename = filename.replace(/\\/g, '/'); |
93 | 1 | var lines = instrObj.lines; |
94 | 1 | var conditions = instrObj.conds; |
95 | 1 | var src = instrObj.source; |
96 | 1 | var jscfArray = jscFunctionBody.toString().split('\n'); |
97 | 1 | jscfArray = jscfArray.slice(1, jscfArray.length - 1); |
98 | 1 | var ff = jscfArray.join('\n').replace(/(^|\n) {2}/g, '\n') |
99 | .replace(/\$(\w+)\$/g, function (m0, m1){ | |
100 | switch (m1) { | |
101 | case 'file': | |
102 | 1 | return filename; |
103 | case 'lines': | |
104 | 1 | return JSON.stringify(lines); |
105 | case 'conds': | |
106 | 1 | return JSON.stringify(conditions); |
107 | case 'source': | |
108 | 1 | return JSON.stringify(src); |
109 | } | |
110 | }); | |
111 | 1 | code.push(ff); |
112 | 1 | code.push(instrObj.code); |
113 | 1 | return code.join('\n'); |
114 | } | |
115 | ||
116 | 1 | exports.process = function (filename, content) { |
117 | if (!filename) { | |
118 | 1 | throw new Error('jscoverage.process(filename, content), filename needed!'); |
119 | } | |
120 | 2 | filename = filename.replace(/\\/g, '/'); |
121 | if (!content) { | |
122 | 1 | return ''; |
123 | } | |
124 | 1 | var pwd = process.cwd(); |
125 | 1 | var fname; |
126 | if (filename.indexOf(pwd) === 0) { | |
127 | 1 | fname = filename.substr(pwd.length + 1); |
128 | } else { | |
129 | 0 | fname = filename; |
130 | } | |
131 | 1 | var instrObj; |
132 | 1 | var ist = new Instrument(); |
133 | 1 | instrObj = ist.process(fname, content); |
134 | 1 | return genCodeCoverage(instrObj); |
135 | }; | |
136 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * jscoverage: lib/patch.js | |
3 | * Authors : fish |
|
4 | * Create : 2014-04-03 15:20:13 | |
5 | * CopyRight 2014 (c) Fish And Other Contributors | |
6 | */ | |
7 | 1 | var Module = require('module'); |
8 | 1 | var path = require('path'); |
9 | 1 | var fs = require('fs'); |
10 | 1 | var argv = require('optimist').argv; |
11 | 1 | var jscoverage = require('./jscoverage'); |
12 | ||
13 | 1 | var covInject = false; |
14 | 1 | var defaultCovIgnore = [ |
15 | new RegExp('^' + process.cwd() + '/node_modules/'), | |
16 | new RegExp('^' + process.cwd() + '/test/') | |
17 | ]; | |
18 | 1 | var covIgnore = defaultCovIgnore; |
19 | ||
20 | 1 | var injectFunctions = { |
21 | get : '_get', | |
22 | replace : '_replace', | |
23 | call : '_call', | |
24 | reset : '_reset', | |
25 | test: '_test' | |
26 | }; | |
27 | ||
28 | 1 | exports.getInjectFunctions = function () { |
29 | 1 | return injectFunctions; |
30 | }; | |
31 | ||
32 | 1 | exports.enableInject = function (bool) { |
33 | 1 | covInject = bool; |
34 | }; | |
35 | 1 | exports.setCovIgnore = function (ignore) { |
36 | 2 | covIgnore = ignore.concat(defaultCovIgnore); |
37 | }; | |
38 | /** | |
39 | * do mock things here | |
40 | * @covignore | |
41 | */ | |
42 | (function () { | |
43 | if (Module.prototype.__jsc_patch__) { | |
44 | return; | |
45 | } | |
46 | Module.prototype.__jsc_patch__ = true; | |
47 | var origin_require = Module.prototype.require; | |
48 | var processExts = ['.js', '.coffee', '.litcoffee']; | |
49 | Module.prototype.require = function (filename) { | |
50 | var needinject = covInject; | |
51 | var ff = filename; | |
52 | filename = Module._resolveFilename(filename, this); | |
53 | var ext = path.extname(filename); | |
54 | var flagjsc = checkModule(filename); | |
55 | if (typeof filename === 'object') { | |
56 | filename = filename[0]; | |
57 | } | |
58 | if (!flagjsc || processExts.indexOf(ext) === -1) { | |
59 | return origin_require.call(this, filename); | |
60 | } | |
61 | ||
62 | var cachedModule = Module._cache[filename]; | |
63 | // take care of module cache | |
64 | if (flagjsc && cachedModule && cachedModule.__coveraged__) { | |
65 | return cachedModule.exports; | |
66 | } | |
67 | // console.log('jscoverage:', ff, 'cov', flagjsc, 'inject', needinject); | |
68 | var module = new Module(filename, this); | |
69 | try { | |
70 | module.filename = filename; | |
71 | module.paths = Module._nodeModulePaths(path.dirname(filename)); | |
72 | Module._extensions[ext](module, filename, { | |
73 | flagjsc : flagjsc, | |
74 | needinject : needinject | |
75 | }); | |
76 | module.__coveraged__ = flagjsc; | |
77 | module.loaded = true; | |
78 | Module._cache[filename] = module; | |
79 | } catch (err) { | |
80 | delete Module._cache[filename]; | |
81 | console.error(filename, err.stack); | |
82 | throw err; | |
83 | } | |
84 | return module.exports; | |
85 | }; | |
86 | function stripBOM(content) { | |
87 | // Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) | |
88 | // because the buffer-to-string conversion in `fs.readFileSync()` | |
89 | // translates it to FEFF, the UTF-16 BOM. | |
90 | if (content.charCodeAt(0) === 0xFEFF) { | |
91 | content = content.slice(1); | |
92 | } | |
93 | return content; | |
94 | } | |
95 | Module._extensions['.js'] = function (module, filename, status) { | |
96 | var content = fs.readFileSync(filename, 'utf8'); | |
97 | var tmpFuncBody; | |
98 | var injectFn = exports.getInjectFunctions(); | |
99 | // trim first line when script is a shell script | |
100 | // content = content.replace(/^\#\![^\n]+\n/, ''); | |
101 | if (status && status.flagjsc) { | |
102 | content = jscoverage.process(filename, content); | |
103 | } | |
104 | if (status && status.needinject) { | |
105 | tmpFuncBody = injectFunctionBody.toString().replace(/\$\$(\w+)\$\$/g, function (m0, m1) { | |
106 | return injectFunctions[m1]; | |
107 | }); | |
108 | tmpFuncBody = tmpFuncBody.split(/\n/); | |
109 | content += '\n' + tmpFuncBody.slice(1, tmpFuncBody.length - 1).join('\n'); | |
110 | } | |
111 | module._compile(stripBOM(content), filename); | |
112 | }; | |
113 | if (Module._extensions['.coffee']) { | |
114 | Module._extensions['.coffee'] = Module._extensions['.litcoffee'] = function (module, filename, status) { | |
115 | var CoffeeScript = require('coffee-script'); | |
116 | var content = CoffeeScript._compileFile(filename, false); | |
117 | var tmpFuncBody; | |
118 | var injectFn = exports.getInjectFunctions(); | |
119 | // trim first line when script is a shell script | |
120 | // content = content.replace(/^\#\![^\n]+\n/, ''); | |
121 | if (status && status.flagjsc) { | |
122 | content = jscoverage.process(filename, content); | |
123 | } | |
124 | if (status && status.needinject) { | |
125 | tmpFuncBody = injectFunctionBody.toString().replace(/\$\$(\w+)\$\$/g, function (m0, m1) { | |
126 | return injectFunctions[m1]; | |
127 | }); | |
128 | tmpFuncBody = tmpFuncBody.split(/\n/); | |
129 | content += '\n' + tmpFuncBody.slice(1, tmpFuncBody.length - 1).join('\n'); | |
130 | } | |
131 | module._compile(stripBOM(content), filename); | |
132 | }; | |
133 | } | |
134 | })(); | |
135 | ||
136 | function checkModule(module) { | |
137 | ||
138 | // native module | |
139 | if (!/\//.test(module)) { | |
140 | 2 | return false; |
141 | } | |
142 | // modules in node_modules | |
143 | 3 | var flagIgnore = false; |
144 | 3 | covIgnore.forEach(function (v) { |
145 | if (v.test(module)) { | |
146 | 2 | flagIgnore = true; |
147 | } | |
148 | }); | |
149 | 3 | return !flagIgnore; |
150 | } | |
151 | ||
152 | /** | |
153 | * do not exec this function | |
154 | * @covignore | |
155 | */ | |
156 | function injectFunctionBody() { | |
157 | (function (){ | |
158 | if (module.exports._i_n_j_e_c_t_e_d_) { | |
159 | return; | |
160 | } | |
161 | if (module.exports.$$call$$ || module.exports.$$get$$ || | |
162 | module.exports.$$replace$$ || module.exports.$$reset$$) { | |
163 | throw new Error("[jscoverage] jscoverage can not inject function for this module, because the function is exists! using jsc.config({inject:{}})"); | |
164 | } | |
165 | ||
166 | var __r_e_p_l_a_c_e__ = {}; | |
167 | module.exports.$$replace$$ = function (name, obj) { | |
168 | function stringify(obj) { | |
169 | if (obj === null) { | |
170 | return 'null'; | |
171 | } | |
172 | if (obj === undefined){ | |
173 | return 'undefined'; | |
174 | } | |
175 | if (!obj && isNaN(obj)){ | |
176 | return 'NaN'; | |
177 | } | |
178 | if (typeof obj === 'string') { | |
179 | return '"' + obj.replace(/"/g, '\\"') + '"'; | |
180 | } | |
181 | if (typeof obj === 'number') { | |
182 | return obj; | |
183 | } | |
184 | if (obj.constructor === Date) { | |
185 | return 'new Date(' + obj.getTime() + ')'; | |
186 | } | |
187 | if (obj.constructor === Function) { | |
188 | return obj.toString(); | |
189 | } | |
190 | if (obj.constructor === RegExp) { | |
191 | return obj.toString(); | |
192 | } | |
193 | var is_array = obj.constructor === Array ? true : false; | |
194 | var res, i; | |
195 | if (is_array) { | |
196 | res = ['[']; | |
197 | for (i = 0; i < obj.length; i++) { | |
198 | res.push(stringify(obj[i])); | |
199 | res.push(','); | |
200 | } | |
201 | if (res[res.length - 1] === ',') { | |
202 | res.pop(); | |
203 | } | |
204 | res.push(']'); | |
205 | } else { | |
206 | res = ['{']; | |
207 | for (i in obj) { | |
208 | res.push(i + ':' + stringify(obj[i])); | |
209 | res.push(','); | |
210 | } | |
211 | if (res[res.length - 1] === ',') | |
212 | res.pop(); | |
213 | res.push('}'); | |
214 | } | |
215 | return res.join(''); | |
216 | } | |
217 | if (!__r_e_p_l_a_c_e__.hasOwnProperty(name)) { | |
218 | __r_e_p_l_a_c_e__[name] = eval(name); | |
219 | } | |
220 | eval(name + "=" + stringify(obj)); | |
221 | }; | |
222 | module.exports.$$reset$$ = function (name) { | |
223 | var script; | |
224 | if (name) { | |
225 | script = 'if(__r_e_p_l_a_c_e__.hasOwnProperty("' + name + '"))' + name + ' = __r_e_p_l_a_c_e__["' + name + '"];'; | |
226 | } else { | |
227 | script = 'for(var i in __r_e_p_l_a_c_e__){eval( i + " = __r_e_p_l_a_c_e__[\'" + i + "\'];");}'; | |
228 | } | |
229 | eval(script); | |
230 | }; | |
231 | module.exports.$$call$$ = module.exports.$$test$$ = function (func, args) { | |
232 | var f, o; | |
233 | if (func.match(/\\./)) { | |
234 | func = func.split("."); | |
235 | f = func[func.length - 1]; | |
236 | func.pop(); | |
237 | o = func.join("."); | |
238 | } else { | |
239 | f = func; | |
240 | o = "this"; | |
241 | } | |
242 | return eval(f + ".apply(" + o + "," + JSON.stringify(args) + ")"); | |
243 | }; | |
244 | module.exports.$$get$$ = function (objstr) { | |
245 | return eval(objstr); | |
246 | }; | |
247 | module.exports._i_n_j_e_c_t_e_d_ = true; | |
248 | })(); | |
249 | } | |
250 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * jscoverage: reporter/html.js | |
3 | * Authors : fish |
|
4 | * Create : 2014-04-10 16:23:23 | |
5 | * CopyRight 2014 (c) Fish And Other Contributors | |
6 | */ | |
7 | ||
8 | /** | |
9 | * Module dependencies. | |
10 | */ | |
11 | ||
12 | 1 | var fs = require('fs'); |
13 | 1 | var ejs = require('ejs'); |
14 | 1 | var path = require('path'); |
15 | ||
16 | /** | |
17 | * Initialize a new `JsCoverage` reporter. | |
18 | * | |
19 | * @param {Runner} runner | |
20 | * @param {Boolean} output | |
21 | * @api public | |
22 | */ | |
23 | ||
24 | 1 | exports.process = function (_$jscoverage, stats, covlevel, name, utils) { |
25 | 2 | var result = map(_$jscoverage, stats); |
26 | 1 | var file = __dirname + '/templates/coverage.ejs'; |
27 | 1 | var str = fs.readFileSync(file, 'utf8').toString(); |
28 | 1 | result.instrumentation = name; |
29 | 1 | var html = ejs.render(str, { |
30 | debug: false, | |
31 | cov: result, | |
32 | coverageClass: function (n) { | |
33 | // n = n; | |
34 | if (n >= covlevel.high) { | |
35 | 16 | return 'high'; |
36 | } | |
37 | if (n >= covlevel.middle) { | |
38 | 1 | return 'medium'; |
39 | } | |
40 | if (n >= covlevel.low) { | |
41 | 5 | return 'low'; |
42 | } | |
43 | 8 | return 'terrible'; |
44 | }, | |
45 | formatCoverage:utils.formatCoverage, | |
46 | filename: path.join(__dirname, './templates/cached.ejs') | |
47 | }); | |
48 | ||
49 | 1 | fs.writeFileSync(process.cwd() + '/covreporter.html', html); |
50 | 1 | console.log('[REPORTER]: ', process.cwd() + '/covreporter.html'); |
51 | }; | |
52 | ||
53 | /** | |
54 | * Map jscoverage data to a JSON structure | |
55 | * suitable for reporting. | |
56 | * | |
57 | * @param {Object} cov | |
58 | * @return {Object} | |
59 | * @api private | |
60 | */ | |
61 | ||
62 | function map(cov, stats) { | |
63 | 2 | var ret = { |
64 | lineSloc: 0, | |
65 | lineHits: 0, | |
66 | lineMisses: 0, | |
67 | lineCoverage: 0, | |
68 | branchSloc: 0, | |
69 | branchHits: 0, | |
70 | branchMisses: 0, | |
71 | branchCoverage: 0, | |
72 | files: [] | |
73 | }; | |
74 | ||
75 | for (var filename in cov) { | |
76 | if (!cov[filename] || !cov[filename].length) { | |
77 | 0 | continue; |
78 | } | |
79 | 14 | var data = coverage(filename, cov[filename], stats[filename]); |
80 | 13 | ret.files.push(data); |
81 | 13 | ret.lineHits += data.lineHits; |
82 | 13 | ret.lineMisses += data.lineMisses; |
83 | 13 | ret.lineSloc += data.lineSloc; |
84 | 13 | ret.branchHits += data.branchHits; |
85 | 13 | ret.branchMisses += data.branchMisses; |
86 | 13 | ret.branchSloc += data.branchSloc; |
87 | } | |
88 | ||
89 | 1 | ret.files.sort(function(a, b) { |
90 | 12 | return a.filename.localeCompare(b.filename); |
91 | }); | |
92 | ||
93 | if (ret.lineSloc > 0) { | |
94 | 1 | ret.lineCoverage = Math.round(ret.lineHits / ret.lineSloc * 10000) / 100; |
95 | } | |
96 | if (ret.branchSloc > 0) { | |
97 | 1 | ret.branchCoverage = Math.round(ret.branchHits / ret.branchSloc * 10000) / 100; |
98 | } | |
99 | 1 | return ret; |
100 | } | |
101 | ||
102 | /** | |
103 | * Map jscoverage data for a single source file | |
104 | * to a JSON structure suitable for reporting. | |
105 | * | |
106 | * @param {String} filename name of the source file | |
107 | * @param {Object} data jscoverage coverage data | |
108 | * @return {Object} | |
109 | * @api private | |
110 | */ | |
111 | ||
112 | function coverage(filename, data, stats) { | |
113 | 14 | var ret = { |
114 | filename: filename, | |
115 | lineCoverage: stats.lineCoverage, | |
116 | lineHits: stats.lineHits, | |
117 | lineMisses: stats.lineSloc - stats.lineHits, | |
118 | lineSloc: stats.lineSloc, | |
119 | branchCoverage: stats.branchCoverage, | |
120 | branchHits: stats.branchHits, | |
121 | branchMisses: stats.branchSloc - stats.branchHits, | |
122 | branchSloc: stats.branchSloc, | |
123 | source: [] | |
124 | }; | |
125 | 14 | data.source.forEach(function(line, num){ |
126 | 2328 | num++; |
127 | 2329 | var branches = stats.branches[num]; |
128 | 2330 | var splits = []; |
129 | if (branches) { | |
130 | 40 | branches.forEach(function (v) { |
131 | if (!splits[v[0]]) { | |
132 | 49 | splits[v[0]] = {start:[], end:[]}; |
133 | } | |
134 | if (!splits[v[0] + v[1]]) { | |
135 | 49 | splits[v[0] + v[1]] = {start: [], end: []}; |
136 | } | |
137 | 50 | splits[v[0]].start.push(''); |
138 | 50 | splits[v[0] + v[1]].end.push(''); |
139 | }); | |
140 | 40 | var res = []; |
141 | 40 | var offset = 0; |
142 | 40 | splits.forEach(function (v, i) { |
143 | if (!v) { | |
144 | 0 | return; |
145 | } | |
146 | 98 | res.push(line.substr(offset, i - offset)); |
147 | 98 | res.push(v.end.join('')); |
148 | 98 | res.push(v.start.join('')); |
149 | 98 | offset = i; |
150 | }); | |
151 | 40 | res.push(line.substr(offset)); |
152 | 40 | line = res.join(''); |
153 | } | |
154 | 2356 | ret.source[num] = { |
155 | source: line, | |
156 | coverage: data[num] === undefined ? '' : data[num], | |
157 | branches: branches && branches.length ? true : false | |
158 | }; | |
159 | }); | |
160 | 13 | return ret; |
161 | } | |
162 |
Line | Hits | Source |
---|---|---|
1 | 1 | exports.formatCoverage = function (n) { |
2 | 14 | var str = Math.floor(n * 100) + '%'; |
3 | 14 | return str; |
4 | }; | |
5 | ||
6 | 1 | var symbols = { |
7 | ok: '✓', | |
8 | warn: '⁍', | |
9 | err: '✱' | |
10 | }; | |
11 | // With node.js on Windows: use symbols available in terminal default fonts | |
12 | if ('win32' === process.platform) { | |
13 | 0 | symbols.ok = '\u221A'; |
14 | 0 | symbols.warn = '\u204D'; |
15 | 0 | symbols.err = '\u2731'; |
16 | } | |
17 | ||
18 | 1 | exports.getType = function (covlevel, coverage) { |
19 | 0 | var type; |
20 | 0 | var head; |
21 | if (coverage >= covlevel.high) { | |
22 | 0 | type = 'GREEN'; |
23 | 0 | head = symbols.ok; |
24 | } else if (coverage >= covlevel.middle) { | |
25 | 0 | type = null; |
26 | 0 | head = symbols.ok; |
27 | } else if (coverage >= covlevel.low) { | |
28 | 0 | type = 'YELLOW'; |
29 | 0 | head = symbols.warn; |
30 | } else { | |
31 | 0 | type = 'RED'; |
32 | 0 | head = symbols.err; |
33 | } | |
34 | 0 | return [type, head]; |
35 | }; | |
36 | 1 | exports.colorful = function (str, type) { |
37 | if (!type) { | |
38 | 0 | return str; |
39 | } | |
40 | 0 | var head = '\x1B[', foot = '\x1B[0m'; |
41 | 0 | var color = { |
42 | LINENUM : 36, | |
43 | GREEN : 32, | |
44 | YELLOW : 33, | |
45 | RED : 31, | |
46 | DEFAULT: 0 | |
47 | }; | |
48 | 0 | return head + color[type] + 'm' + str + foot; |
49 | }; |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * jscoverage: test/abc.js | |
3 | * Authors : fish |
|
4 | * Create : 2014-04-10 16:23:23 | |
5 | * CopyRight 2014 (c) Fish And Other Contributors | |
6 | */ | |
7 | 2 | var cde = require('./cde'); |
8 | 2 | var a = 1; |
9 | 2 | var b = 2; |
10 | 2 | var c = 3; |
11 | 2 | var d; |
12 | 2 | var e = a > 1 ? 1 : 2; |
13 | ||
14 | 2 | var reset = { |
15 | abc:function () {} | |
16 | }; | |
17 | ||
18 | function abc() { | |
19 | 2 | var tmp = a + b; |
20 | 2 | var t = 1; |
21 | // test require ok | |
22 | 2 | cde.a(); |
23 | // test switch coverage | |
24 | 2 | testSwitch('first'); |
25 | /* @covignore */ | |
26 | testSwitch('second'); | |
27 | 2 | testSwitch(); |
28 | 2 | return tmp + c; |
29 | } | |
30 | ||
31 | function testSwitch(act) { | |
32 | 6 | var res = [ |
33 | 'a', | |
34 | 'b', | |
35 | 'c' | |
36 | ]; | |
37 | 6 | var tmp; |
38 | switch (act) { | |
39 | case 'first' : | |
40 | 2 | tmp = res[0]; |
41 | 2 | break; |
42 | case 'second' : | |
43 | 2 | tmp = res[1]; |
44 | 2 | break; |
45 | default: | |
46 | 2 | tmp = res.join(','); |
47 | } | |
48 | 6 | return tmp; |
49 | } | |
50 | 2 | abc(); |
51 | 2 | exports.abc = abc; |
52 |