From 77f1c9c67f43a14a1ccc593f93f64a83ec7f6c25 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 26 Feb 2018 10:44:22 +0100 Subject: [PATCH] Add test-code-coverage command --- .yarnrc | 1 + README.md | 39 +++++++ index.js | 6 ++ lib/commands/test-code-coverage.js | 160 +++++++++++++++++++++++++++++ package.json | 1 + yarn.lock | 26 +++-- 6 files changed, 226 insertions(+), 7 deletions(-) create mode 100644 .yarnrc create mode 100644 lib/commands/test-code-coverage.js diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 00000000..b4a8ce4f --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +registry "https://registry.yarnpkg.com" diff --git a/README.md b/README.md index 71c924ca..0eda7497 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,45 @@ If you are using [`ember-cli-pretender`](https://github.com/rwjblue/ember-cli-pr }); ``` +## Testing code coverage goals + +You can also test for code coverage goals with the included command `ember test-code-coverage`. + +It takes the following parameters: + +* target-lines: The target percentage for lines covered +* target-functions: The target percentage for functions covered +* target-statements: The target percentage for statements covered +* target-branches: The target percentage for branches covered + +For example: `ember test-code-coverage -target-lines=80 -target-branches=85` + +You can also configure those in the `config/coverage.js` file, in their camel cased form, like this: + +```js + module.exports = { + targetLines: 80, + targetFunctions: 85.5, + + // Other configuration + coverageEnvVar: 'COV' + } +``` + +These values will then be the defaults when running `ember test-code-coverage`. + +Running the command will output something like this to your console: + +```diff ++ Lines covered: 60.00% 69.17% ++ Functions covered: --.--% 72.18% ++ Statements covered: --.--% 68.93% +- Branches covered: 70.00% 60.16% +- Test coverage check failed +``` + +For any type of check where no target is specified, it will simply output the actual value without doing any comparison. + ## Inspiration This addon was inspired by [`ember-cli-blanket`](https://github.com/sglanzer/ember-cli-blanket). diff --git a/index.js b/index.js index e70a454d..7caf5a70 100644 --- a/index.js +++ b/index.js @@ -274,5 +274,11 @@ module.exports = { } return this._inRepoAddons; + }, + + includedCommands: function() { + return { + 'test-code-coverage': require('./lib/commands/test-code-coverage') + }; } }; diff --git a/lib/commands/test-code-coverage.js b/lib/commands/test-code-coverage.js new file mode 100644 index 00000000..76abff65 --- /dev/null +++ b/lib/commands/test-code-coverage.js @@ -0,0 +1,160 @@ +'use strict'; + +let fs = require('fs'); +let path = require('path'); +let chalk = require('chalk'); +let RSVP = require('rsvp'); + +function camelize(str) { + str = str.split('-').map((str) => str.charAt(0).toUpperCase() + str.slice(1)).join(''); + return str.charAt(0).toLowerCase() + str.slice(1); +} + +module.exports = { + + name: 'test-code-coverage', + description: 'Test the generated code coverage against given goals.', + works: 'insideProject', + + availableOptions: [ + { + name: 'coverage-folder', + type: String, + aliases: ['i'], + default: './coverage', + description: 'Directory of the coverage report' + }, + { + name: 'target-lines', + type: Number, + aliases: ['tl'], + description: 'The target percentage of lines to reach, e.g. 50 = 50% coverage minimum' + }, + { + name: 'target-statements', + type: Number, + aliases: ['ts'], + description: 'The target percentage of statements to reach, e.g. 50 = 50% coverage minimum' + }, + { + name: 'target-functions', + type: Number, + aliases: ['tf'], + description: 'The target percentage of functions to reach, e.g. 50 = 50% coverage minimum' + }, + { + name: 'target-branches', + type: Number, + aliases: ['tb'], + description: 'The target percentage of branches to reach, e.g. 50 = 50% coverage minimum' + } + ], + + /** + * Tries to extend `availableOptions` with globals from config. + * + * @public + * @method beforeRun + * @return {Void} + */ + beforeRun() { + this._super.apply(this, arguments); + + // try to read global options from `config/coverage.js` + let configOptions = {}; + let module = path.join(this.project.root, 'config', 'coverage'); + + try { + configOptions = require(module); + if (typeof configOptions === 'function') { + configOptions = configOptions(); + } + } catch(e) { + // do nothing, ignore the config + } + + // For all options that are specified in config/coverage.js, set the value there to be the actual default value + this.availableOptions.map((option) => { + let normalizedName = camelize(option.name); + let configOption = configOptions[normalizedName]; + + if (configOption !== undefined) { + option.default = configOption; + return option; + } + + return option; + }); + }, + + run(options) { + return new RSVP.Promise((resolve, reject) => { + let jsonSummaryPath = path.join(options.coverageFolder, 'coverage-summary.json'); + + // Try to read coverage-summary.json + let file = null; + try { + file = fs.readFileSync(jsonSummaryPath, 'utf8'); + } catch(e) { + this.ui.writeLine(chalk.red(`Could not read coverage report at ${jsonSummaryPath}, maybe you haven't generated a report yet?`)); + return reject(); + } + + // Try to load JSON content from the file + let jsonSummary = null; + try { + jsonSummary = JSON.parse(file); + } catch(e) { + this.ui.writeLine(chalk.red(`Could not read JSON content from the file at ${jsonSummaryPath}.`)); + return reject(); + } + + // Check lines, functions, statements & branches covered + let linesCovered = this._comparePercentages(jsonSummary.total.lines.pct, options.targetLines, 'Lines covered: '); + let functionsCovered = this._comparePercentages(jsonSummary.total.functions.pct, options.targetFunctions, 'Functions covered: '); + let statementsCovered = this._comparePercentages(jsonSummary.total.statements.pct, options.targetStatements, 'Statements covered:'); + let branchesCovered = this._comparePercentages(jsonSummary.total.branches.pct, options.targetBranches, 'Branches covered: '); + + if (!linesCovered || !functionsCovered || !statementsCovered || !branchesCovered) { + this.ui.writeLine(chalk.red('Test coverage check failed')); + return reject(); + } + + resolve(); + }); + }, + + /** + * Compare a given percentage with a comparison percentage. + * If no comparisonValue is given, it will just print out the actual value, + * without doing any assertions. + * + * This will print a line like this: + * Lines covered: 80.00% 81.23% + * + * @method _comparePercentages + * @param {Number} value The actual percentage value + * @param {Number} comparisonValue The percentage value that should be reached + * @param {String} title The prefix to add before the values, e.g. "Lines covered: " + * @return {Boolean} + * @private + */ + _comparePercentages(value, comparisonValue, title) { + if (!comparisonValue) { + this.ui.writeLine(chalk.green(`${title} --.--% ${value.toFixed(2)}%`)); + return true; + } + + // Normalize 0.5 to 60 + if (value < 1) { + value *= 50; + } + + let isSuccess = value >= comparisonValue; + let chalkColor = isSuccess ? chalk.green : chalk.red; + + this.ui.writeLine(chalkColor(`${title} ${comparisonValue.toFixed(2)}% ${value.toFixed(2)}%`)); + + return isSuccess; + } +}; diff --git a/package.json b/package.json index e6a2a81b..e884ea15 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "babel-plugin-istanbul": "^4.1.5", "babel-plugin-transform-async-to-generator": "^6.24.1", "body-parser": "^1.15.0", + "chalk": "^2.3.1", "co": "^4.6.0", "ember-cli-babel": "^6.6.0", "ember-cli-htmlbars": "^2.0.1", diff --git a/yarn.lock b/yarn.lock index a9d49b8f..d5713747 100644 --- a/yarn.lock +++ b/yarn.lock @@ -144,7 +144,7 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" -ansi-styles@^3.0.0, ansi-styles@^3.1.0: +ansi-styles@^3.0.0, ansi-styles@^3.1.0, ansi-styles@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" dependencies: @@ -1508,6 +1508,14 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0: escape-string-regexp "^1.0.5" supports-color "^4.0.0" +chalk@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796" + dependencies: + ansi-styles "^3.2.0" + escape-string-regexp "^1.0.5" + supports-color "^5.2.0" + chardet@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" @@ -3450,6 +3458,10 @@ has-flag@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + has-symbol-support-x@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.4.1.tgz#66ec2e377e0c7d7ccedb07a3a84d77510ff1bc4c" @@ -5031,12 +5043,6 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" -node-dir@^0.1.16: - version "0.1.17" - resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5" - dependencies: - minimatch "^3.0.2" - node-fetch-npm@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz#7258c9046182dca345b4208eda918daf33697ff7" @@ -6486,6 +6492,12 @@ supports-color@^5.1.0: dependencies: has-flag "^2.0.0" +supports-color@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.2.0.tgz#b0d5333b1184dd3666cbe5aa0b45c5ac7ac17a4a" + dependencies: + has-flag "^3.0.0" + symlink-or-copy@^1.0.0, symlink-or-copy@^1.0.1, symlink-or-copy@^1.1.3, symlink-or-copy@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/symlink-or-copy/-/symlink-or-copy-1.1.8.tgz#cabe61e0010c1c023c173b25ee5108b37f4b4aa3"