diff --git a/.travis.yml b/.travis.yml index 56152dc1..2b45b141 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: node_js node_js: - - "0.10" - - "0.12" + - "6.3.1" + - "7.5" 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 + - if [[ `node --version` == *v7.5* ]]; then cat ./build/coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js; fi diff --git a/lib/command/current-coverage.js b/lib/command/current-coverage.js new file mode 100644 index 00000000..bab75d47 --- /dev/null +++ b/lib/command/current-coverage.js @@ -0,0 +1,170 @@ +/* + 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' ]; + +var CURRENT_COVERAGE_FILE = 'current-coverage.txt'; + +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; + 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(); + var errors = []; + + 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; + if (masterCovAverage < masterBaseline) { + errors.push('ERROR: Current Coverage (' + masterCovAverage + + '%) does not meet baseline threshold (' + masterBaseline + '%)'); + } else { + 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")); + }); + } +}); + +module.exports = CurrentCoverageCommand; diff --git a/package.json b/package.json index eeb01a72..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": [ @@ -93,11 +93,11 @@ "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": { - "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", 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 new file mode 100644 index 00000000..4318be79 --- /dev/null +++ b/test/cli/test-current-coverage-command.js @@ -0,0 +1,90 @@ +/*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(/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 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(/SUCCESS: Current Coverage/)); + test.ok(results.grepOutput(/71\.64%/)); + test.ok(results.grepOutput(/is either equal or better than the baseline/)); + test.done(); + }); + } + } +};