From 44e2988e290b6e302142959b60d1c34dae500fab Mon Sep 17 00:00:00 2001 From: Leo Gallucci Date: Tue, 14 Feb 2017 19:53:18 +0100 Subject: [PATCH 1/6] Add cli.js current-coverage --baseline {Num} --- lib/command/current-coverage.js | 159 ++++++++++++++++++++++ package.json | 2 +- test/cli/test-current-coverage-command.js | 72 ++++++++++ 3 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 lib/command/current-coverage.js create mode 100644 test/cli/test-current-coverage-command.js diff --git a/lib/command/current-coverage.js b/lib/command/current-coverage.js new file mode 100644 index 00000000..5dbae5d4 --- /dev/null +++ b/lib/command/current-coverage.js @@ -0,0 +1,159 @@ +/* + Copyright (c) 2012, Yahoo! Inc. All rights reserved. + Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. + */ + +var nopt = require('nopt'), + path = require('path'), + fs = require('fs'), + Collector = require('../collector'), + util = require('util'), + utils = require('../object-utils'), + filesFor = require('../util/file-matcher').filesFor, + Command = require('./index'); + +var COV_TYPES = [ "statements", + "branches", + "lines", + "functions" ]; + +function isAbsolute(file) { + if (path.isAbsolute) { + return path.isAbsolute(file); + } + + return path.resolve(file) === path.normalize(file); +} + +function CurrentCoverageCommand() { + Command.call(this); +} + +function removeFiles(covObj, root, files) { + var filesObj = {}, + obj = {}; + + // Create lookup table. + files.forEach(function (file) { + filesObj[file] = true; + }); + + Object.keys(covObj).forEach(function (key) { + // Exclude keys will always be relative, but covObj keys can be absolute or relative + var excludeKey = isAbsolute(key) ? path.relative(root, key) : key; + // Also normalize for files that start with `./`, etc. + excludeKey = path.normalize(excludeKey); + if (filesObj[excludeKey] !== true) { + obj[key] = covObj[key]; + } + }); + + return obj; +} + +CurrentCoverageCommand.TYPE = 'current-coverage'; +util.inherits(CurrentCoverageCommand, Command); + +Command.mix(CurrentCoverageCommand, { + synopsis: function () { + return "shows and optionally checks overall coverage from coverage JSON files."; + }, + + usage: function () { + console.error('\nUsage: ' + this.toolName() + ' ' + this.type() + ' []'); + console.error('\n\n'); + }, + + run: function (args, callback) { + + var template = { + root: path, + baseline: Number, + verbose: Boolean + }; + + var opts = nopt(template, { v : '--verbose' }, args, 0); + + var baseline = opts.baseline; + var includePattern = '**/coverage*.json'; + var root; + var collector = new Collector(); + var errors = []; + + if (opts.argv.remain.length > 0) { + includePattern = opts.argv.remain[0]; + } + + root = opts.root || process.cwd(); + filesFor({ + root: root, + includes: [ includePattern ] + }, function (err, files) { + if (err) { throw err; } + if (files.length === 0) { + return callback('ERROR: No coverage files found.'); + } + files.forEach(function (file) { + var coverageObject = JSON.parse(fs.readFileSync(file, 'utf8')); + collector.add(coverageObject); + }); + var rawCoverage = collector.getFinalCoverage(), + globalResults = utils.summarizeCoverage(removeFiles(rawCoverage, root, [])), + eachResults = removeFiles(rawCoverage, root, []); + + // Summarize per-file results and mutate original results. + Object.keys(eachResults).forEach(function (key) { + eachResults[key] = utils.summarizeFileCoverage(eachResults[key]); + }); + + var coverageValuesAry = {}; + COV_TYPES.forEach(function (key) { + coverageValuesAry[key] = []; + }); + + function processCov(name, actuals) { + COV_TYPES.forEach(function (key) { + var actual = actuals[key].pct; + + coverageValuesAry[key].push(actual); + }); + } + + processCov("global", globalResults); + + Object.keys(eachResults).forEach(function (key) { + processCov("per-file" + " (" + key + ") ", eachResults[key]); + }); + + var coverageAverages = {}; + COV_TYPES.forEach(function (key) { + var sum = 0; + coverageValuesAry[key].forEach(function (val) { + sum += val; + }); + var avg = sum / coverageValuesAry[key].length; + coverageAverages[key] = avg; + }); + + var masterCovSum = 0; + COV_TYPES.forEach(function (key) { + masterCovSum += coverageAverages[key]; + }); + var masterCovAverage = Number((masterCovSum / 4).toFixed(2)); + + var masterBaseline = baseline || 0; + if (masterCovAverage < masterBaseline) { + errors.push('ERROR: Current Coverage (' + masterCovAverage + + '%) does not meet baseline threshold (' + masterBaseline + '%)'); + } else { + console.log(masterCovAverage); + } + + return callback(errors.length === 0 ? null : errors.join("\n")); + }); + } +}); + +module.exports = CurrentCoverageCommand; + + diff --git a/package.json b/package.json index eeb01a72..ebed6bde 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "scripts": { "pretest": "jshint index.js lib/ test/ && ./download-escodegen-browser.sh", "test": "node --harmony test/run.js", - "posttest": "node ./lib/cli.js check-coverage --statements 95 --branches 80", + "posttest": "node ./lib/cli.js check-coverage --statements 95 --branches 80 && node ./lib/cli.js current-coverage --baseline 95", "docs": "npm install yuidocjs && node node_modules/yuidocjs/lib/cli.js ." }, "bin": { diff --git a/test/cli/test-current-coverage-command.js b/test/cli/test-current-coverage-command.js new file mode 100644 index 00000000..eb1fa31b --- /dev/null +++ b/test/cli/test-current-coverage-command.js @@ -0,0 +1,72 @@ +/*jslint nomen: true */ +var path = require('path'), + fs = require('fs'), + rimraf = require('rimraf'), + mkdirp = require('mkdirp'), + COMMAND = 'current-coverage', + COVER_COMMAND = 'cover', + DIR = path.resolve(__dirname, 'sample-project'), + OUTPUT_DIR = path.resolve(DIR, 'coverage'), + helper = require('../cli-helper'), + existsSync = fs.existsSync || path.existsSync, + run = helper.runCommand.bind(null, COMMAND), + runCover = helper.runCommand.bind(null, COVER_COMMAND); + +module.exports = { + setUp: function (cb) { + rimraf.sync(OUTPUT_DIR); + mkdirp.sync(OUTPUT_DIR); + helper.resetOpts(); + runCover([ 'test/run.js', '--report', 'none' ], function (/* results */) { + + // Mutate coverage.json to test relative key paths. + var covObj = require('./sample-project/coverage/coverage.json'); + var relCovObj = {}; + var relCovDotSlashObj = {}; + Object.keys(covObj).forEach(function (key) { + var relKey = path.relative(__dirname + '/sample-project', key); + relCovObj[relKey] = covObj[key]; + relCovDotSlashObj['./' + relKey] = covObj[key]; + }); + fs.writeFileSync(path.resolve(__dirname, 'sample-project/coverage/relative.json'), JSON.stringify(relCovObj)); + fs.writeFileSync(path.resolve(__dirname, 'sample-project/coverage/relative-dot-slash.json'), JSON.stringify(relCovDotSlashObj)); + + cb(); + }); + }, + tearDown: function (cb) { + rimraf.sync(OUTPUT_DIR); + cb(); + }, + "Current coverage": { + "should fail with a difficult baseline": function (test) { + test.ok(existsSync(path.resolve(OUTPUT_DIR, 'coverage.json'))); + run([ '--baseline', '99' ], function (results) { + test.ok(!results.succeeded()); + test.ok(!results.grepError(/No coverage files found/)); + test.ok(results.grepError(/does not meet baseline threshold/)); + test.done(); + }); + }, + "should pass with a reachable baseline": function (test) { + test.ok(existsSync(path.resolve(OUTPUT_DIR, 'coverage.json'))); + run([ '--baseline', '71' ], function (results) { + test.ok(results.succeeded()); + test.ok(!results.grepError(/No coverage files found/)); + test.ok(!results.grepError(/does not meet baseline threshold/)); + test.ok(results.grepOutput(/71\.64/)); + test.done(); + }); + }, + "should pass without a baseline": function (test) { + test.ok(existsSync(path.resolve(OUTPUT_DIR, 'coverage.json'))); + run([], function (results) { + test.ok(results.succeeded()); + test.ok(!results.grepError(/No coverage files found/)); + test.ok(!results.grepError(/does not meet baseline threshold/)); + test.ok(results.grepOutput(/71\.64/)); + test.done(); + }); + } + } +}; From dd1e419e6fe935af3db18b3c07cf416ccc0fab93 Mon Sep 17 00:00:00 2001 From: Leo Gallucci Date: Tue, 14 Feb 2017 20:03:02 +0100 Subject: [PATCH 2/6] Modernize NodeJS version in Travis --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 56152dc1..c78dad0c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: node_js node_js: - - "0.10" - - "0.12" + - "6.3.1" + - "7" sudo: false @@ -14,4 +14,4 @@ script: - npm test --cover after_script: - - if [[ `node --version` == *v0.12* ]]; then cat ./build/coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js; fi + - cat ./build/coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js From 84e1c5acf528785f07747281ffd46c9b0f104229 Mon Sep 17 00:00:00 2001 From: Leo Gallucci Date: Tue, 14 Feb 2017 20:45:42 +0100 Subject: [PATCH 3/6] Support a file based baseline from ./current-coverage.txt --- lib/command/current-coverage.js | 31 +++++++++++++++----- test/cli/sample-project/current-coverage.txt | 1 + test/cli/test-current-coverage-command.js | 24 +++++++++++++-- 3 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 test/cli/sample-project/current-coverage.txt diff --git a/lib/command/current-coverage.js b/lib/command/current-coverage.js index 5dbae5d4..46ab0acd 100644 --- a/lib/command/current-coverage.js +++ b/lib/command/current-coverage.js @@ -12,10 +12,12 @@ var nopt = require('nopt'), filesFor = require('../util/file-matcher').filesFor, Command = require('./index'); -var COV_TYPES = [ "statements", - "branches", - "lines", - "functions" ]; +var COV_TYPES = [ 'statements', + 'branches', + 'lines', + 'functions' ]; + +var CURRENT_COVERAGE_FILE = 'current-coverage.txt'; function isAbsolute(file) { if (path.isAbsolute) { @@ -74,7 +76,19 @@ Command.mix(CurrentCoverageCommand, { var opts = nopt(template, { v : '--verbose' }, args, 0); - var baseline = opts.baseline; + var baseline; + if (typeof opts.baseline !== "undefined") { + // CLI options should take priority + baseline = opts.baseline; + } else if (fs.existsSync(CURRENT_COVERAGE_FILE)) { + // If the file exists take the baseline from there + var fileContent = fs.readFileSync(CURRENT_COVERAGE_FILE, 'utf8'); + baseline = parseFloat(fileContent); + } else { + // Fallback default + baseline = 0; + } + var includePattern = '**/coverage*.json'; var root; var collector = new Collector(); @@ -141,12 +155,15 @@ Command.mix(CurrentCoverageCommand, { }); var masterCovAverage = Number((masterCovSum / 4).toFixed(2)); - var masterBaseline = baseline || 0; + var masterBaseline = baseline; if (masterCovAverage < masterBaseline) { errors.push('ERROR: Current Coverage (' + masterCovAverage + '%) does not meet baseline threshold (' + masterBaseline + '%)'); } else { - console.log(masterCovAverage); + console.log('SUCCESS: Current Coverage (' + masterCovAverage + + '%) is either equal or better than the baseline (' + masterBaseline + '%)'); + // Update the new baseline + fs.writeFileSync(CURRENT_COVERAGE_FILE, masterCovAverage, 'utf8'); } return callback(errors.length === 0 ? null : errors.join("\n")); diff --git a/test/cli/sample-project/current-coverage.txt b/test/cli/sample-project/current-coverage.txt new file mode 100644 index 00000000..47344b7f --- /dev/null +++ b/test/cli/sample-project/current-coverage.txt @@ -0,0 +1 @@ +71.64 \ No newline at end of file diff --git a/test/cli/test-current-coverage-command.js b/test/cli/test-current-coverage-command.js index eb1fa31b..4318be79 100644 --- a/test/cli/test-current-coverage-command.js +++ b/test/cli/test-current-coverage-command.js @@ -54,17 +54,35 @@ module.exports = { test.ok(results.succeeded()); test.ok(!results.grepError(/No coverage files found/)); test.ok(!results.grepError(/does not meet baseline threshold/)); - test.ok(results.grepOutput(/71\.64/)); + test.ok(results.grepOutput(/SUCCESS: Current Coverage/)); + test.ok(results.grepOutput(/71\.64%/)); + test.ok(results.grepOutput(/is either equal or better than the baseline/)); + test.ok(results.grepOutput(/71%/)); test.done(); }); }, - "should pass without a baseline": function (test) { + "should pass with a 0 baseline": function (test) { + test.ok(existsSync(path.resolve(OUTPUT_DIR, 'coverage.json'))); + run([ '--baseline', '0' ], function (results) { + test.ok(results.succeeded()); + test.ok(!results.grepError(/No coverage files found/)); + test.ok(!results.grepError(/does not meet baseline threshold/)); + test.ok(results.grepOutput(/SUCCESS: Current Coverage/)); + test.ok(results.grepOutput(/71\.64%/)); + test.ok(results.grepOutput(/is either equal or better than the baseline/)); + test.ok(results.grepOutput(/0%/)); + test.done(); + }); + }, + "should pass without a baseline but using the txt file": function (test) { test.ok(existsSync(path.resolve(OUTPUT_DIR, 'coverage.json'))); run([], function (results) { test.ok(results.succeeded()); test.ok(!results.grepError(/No coverage files found/)); test.ok(!results.grepError(/does not meet baseline threshold/)); - test.ok(results.grepOutput(/71\.64/)); + test.ok(results.grepOutput(/SUCCESS: Current Coverage/)); + test.ok(results.grepOutput(/71\.64%/)); + test.ok(results.grepOutput(/is either equal or better than the baseline/)); test.done(); }); } From aa50dc6b2d523ecd89f2044937478726a4a40c0b Mon Sep 17 00:00:00 2001 From: Leo Gallucci Date: Tue, 14 Feb 2017 20:54:18 +0100 Subject: [PATCH 4/6] Travis fix --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c78dad0c..2b45b141 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: node_js node_js: - "6.3.1" - - "7" + - "7.5" sudo: false @@ -14,4 +14,4 @@ script: - npm test --cover after_script: - - cat ./build/coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js + - if [[ `node --version` == *v7.5* ]]; then cat ./build/coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js; fi From 125b33120e2b168fad9a5a20119300e4dae81e4a Mon Sep 17 00:00:00 2001 From: Leo Gallucci Date: Tue, 14 Feb 2017 21:45:55 +0100 Subject: [PATCH 5/6] Create a separate tool called istanbul-checker --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index ebed6bde..a5061095 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "istanbul", + "name": "istanbul-checker", "version": "0.4.5", "description": "Yet another JS code coverage tool that computes statement, line, function and branch coverage with module loader hooks to transparently add coverage when running tests. Supports all JS coverage use cases including unit tests, server side functional tests and browser tests. Built for scale", "keywords": [ @@ -97,7 +97,7 @@ "docs": "npm install yuidocjs && node node_modules/yuidocjs/lib/cli.js ." }, "bin": { - "istanbul": "./lib/cli.js" + "istanbul-checker": "./lib/cli.js" }, "files": [ "index.js", @@ -105,7 +105,7 @@ ], "repository": { "type": "git", - "url": "git://github.com/gotwarlost/istanbul.git" + "url": "git://github.com/elgalu/istanbul-checker.git" }, "dependencies": { "abbrev": "1.0.x", From 3e015278bce64652707e340516be4d864b3130e1 Mon Sep 17 00:00:00 2001 From: Leo Gallucci Date: Tue, 14 Feb 2017 22:04:20 +0100 Subject: [PATCH 6/6] Fixate **/coverage*.json --- lib/command/current-coverage.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/command/current-coverage.js b/lib/command/current-coverage.js index 46ab0acd..bab75d47 100644 --- a/lib/command/current-coverage.js +++ b/lib/command/current-coverage.js @@ -94,10 +94,6 @@ Command.mix(CurrentCoverageCommand, { var collector = new Collector(); var errors = []; - if (opts.argv.remain.length > 0) { - includePattern = opts.argv.remain[0]; - } - root = opts.root || process.cwd(); filesFor({ root: root, @@ -172,5 +168,3 @@ Command.mix(CurrentCoverageCommand, { }); module.exports = CurrentCoverageCommand; - -