diff --git a/.circleci/config.yml b/.circleci/config.yml index 35134092..1969760b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -393,6 +393,40 @@ workflows: ../../node_modules/.bin/only-covered --from coverage/coverage-final.json main.js second.js not-covered.js working_directory: examples/all-files + - cypress/run: + attach-workspace: true + name: example-placeholders + requires: + - cypress/install + # there are no jobs to follow this one + # so no need to save the workspace files (saves time) + no-workspace: true + command: npx cypress run --project examples/placeholders + # store screenshots and videos + store_artifacts: true + post-steps: + - run: cat examples/placeholders/.nyc_output/out.json + - run: cat examples/placeholders/coverage/coverage-final.json + # store the created coverage report folder + # you can click on it in the CircleCI UI + # to see live static HTML site + - store_artifacts: + path: examples/placeholders/coverage + # make sure the examples captures 100% of code + - run: + command: npx nyc report --check-coverage true --lines 100 + working_directory: examples/placeholders + - run: + name: Check code coverage 📈 + # we will check the final coverage report + # to make sure it only has files we are interested in + # because there are files covered at 0 in the report + command: | + ../../node_modules/.bin/check-coverage src/a.js + ../../node_modules/.bin/check-coverage src/a.js + ../../node_modules/.bin/only-covered --from coverage/coverage-final.json src/a.js src/b.js + working_directory: examples/placeholders + - cypress/run: attach-workspace: true name: example-exclude-files @@ -544,4 +578,5 @@ workflows: - example-docker-paths - example-use-webpack - example-all-files + - example-placeholders - Windows test diff --git a/README.md b/README.md index c8ff09dd..28896aa9 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ Register tasks in your `cypress/plugins/index.js` file ```js module.exports = (on, config) => { require('@cypress/code-coverage/task')(on, config) - + // add other tasks to be registered here - + // IMPORTANT to return the config object // with the any changed environment variables return config @@ -189,7 +189,7 @@ For any other server, define the endpoint yourself and return the coverage objec ```js if (global.__coverage__) { // add method "GET /__coverage__" and response with JSON - onRequest = response => response.sendJSON({ coverage: global.__coverage__ }) + onRequest = (response) => response.sendJSON({ coverage: global.__coverage__ }) } ``` @@ -365,6 +365,7 @@ Full examples we use for testing in this repository: ### External examples Look up the list of examples under GitHub topic [cypress-code-coverage-example](https://github.com/topics/cypress-code-coverage-example) + - [cypress-io/cypress-realworld-app](https://github.com/cypress-io/cypress-realworld-app) is an easy to setup and run real-world application with E2E, API, and unit tests that achieves 100% code-coverage for both front and back end code. Its CI pipeline also reports code-coverage reports across parallelized test runs to [Codecov](https://codecov.io/gh/cypress-io/cypress-realworld-app). - [cypress-io/cypress-example-todomvc-redux](https://github.com/cypress-io/cypress-example-todomvc-redux) is a React / Redux application with 100% code coverage. - [cypress-io/cypress-example-conduit-app](https://github.com/cypress-io/cypress-example-conduit-app) shows how to collect the coverage information from both back and front end code and merge it into a single report. The E2E test step runs in parallel in several CI containers, each saving just partial test coverage information. Then a merge job runs taking artifacts and combining coverage into the final report to be sent to an exteral coverage as a service app. @@ -427,6 +428,14 @@ $ DEBUG=code-coverage npm run dev code-coverage saving coverage report using command: "nyc report --report-dir ./coverage --reporter=lcov --reporter=clover --reporter=json" +3ms ``` +Deeply nested object will sometimes have `[object Object]` values printed. You can print these nested objects by specifying a deeper depth by adding `DEBUG_DEPTH=` setting + +```shell +$ DEBUG_DEPTH=10 DEBUG=code-coverage npm run dev +``` + +### Common issues + Common issue: [not instrumenting your application when running Cypress](#instrument-your-application). If the plugin worked before in version X, but stopped after upgrading to version Y, please try the [released versions](https://github.com/cypress-io/code-coverage/releases) between X and Y to see where the breaking change was. diff --git a/common-utils.js b/common-utils.js index 473b63dc..573f7ada 100644 --- a/common-utils.js +++ b/common-utils.js @@ -26,7 +26,44 @@ const defaultNycOptions = { excludeAfterRemap: false } +/** + * Returns an object with placeholder properties for files we + * do not have coverage yet. The result can go into the coverage object + * + * @param {string} fullPath Filename + */ +const fileCoveragePlaceholder = (fullPath) => { + return { + path: fullPath, + statementMap: {}, + fnMap: {}, + branchMap: {}, + s: {}, + f: {}, + b: {} + } +} + +const isPlaceholder = (entry) => { + // when the file has been instrumented, its entry has "hash" property + return !('hash' in entry) +} + +/** + * Given a coverage object with potential placeholder entries + * inserted instead of covered files, removes them. Modifies the object in place + */ +const removePlaceholders = (coverage) => { + Object.keys(coverage).forEach((key) => { + if (isPlaceholder(coverage[key])) { + delete coverage[key] + } + }) +} + module.exports = { combineNycOptions, - defaultNycOptions + defaultNycOptions, + fileCoveragePlaceholder, + removePlaceholders } diff --git a/cypress/fixtures/coverage.json b/cypress/fixtures/coverage.json new file mode 100644 index 00000000..f2bea5aa --- /dev/null +++ b/cypress/fixtures/coverage.json @@ -0,0 +1,26 @@ +{ + "/src/index.js": { + "path": "/src/index.js", + "statementMap": { + "0": { + "start": { + "line": 7, + "column": 0 + }, + "end": { + "line": 14, + "column": 2 + } + } + }, + "fnMap": {}, + "branchMap": {}, + "s": { + "0": 1 + }, + "f": {}, + "b": {}, + "_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9", + "hash": "46f2efd10038593ec768c6cc815a6d6ee2924243" + } +} diff --git a/cypress/integration/combine-spec.js b/cypress/integration/combine-spec.js index a0c25473..8984e506 100644 --- a/cypress/integration/combine-spec.js +++ b/cypress/integration/combine-spec.js @@ -1,3 +1,4 @@ +/// const { combineNycOptions, defaultNycOptions } = require('../../common-utils') describe('Combine NYC options', () => { it('overrides defaults', () => { diff --git a/cypress/integration/merge-spec.js b/cypress/integration/merge-spec.js new file mode 100644 index 00000000..db093176 --- /dev/null +++ b/cypress/integration/merge-spec.js @@ -0,0 +1,86 @@ +/// +const istanbul = require('istanbul-lib-coverage') +const coverage = require('../fixtures/coverage.json') +const { + fileCoveragePlaceholder, + removePlaceholders +} = require('../../common-utils') + +/** + * Extracts just the data from the coverage map object + * @param {*} cm + */ +const coverageMapToCoverage = (cm) => { + return JSON.parse(JSON.stringify(cm)) +} + +describe('merging coverage', () => { + const filename = '/src/index.js' + + before(() => { + expect(coverage, 'initial coverage has this file').to.have.property( + filename + ) + }) + + it('combines an empty coverage object', () => { + const previous = istanbul.createCoverageMap({}) + const coverageMap = istanbul.createCoverageMap(previous) + coverageMap.merge(Cypress._.cloneDeep(coverage)) + + const merged = coverageMapToCoverage(coverageMap) + + expect(merged, 'merged coverage').to.deep.equal(coverage) + }) + + it('combines the same full coverage twice', () => { + const previous = istanbul.createCoverageMap(Cypress._.cloneDeep(coverage)) + const coverageMap = istanbul.createCoverageMap(previous) + coverageMap.merge(Cypress._.cloneDeep(coverage)) + + const merged = coverageMapToCoverage(coverageMap) + // it is almost the same - only the statement count has been doubled + const expected = Cypress._.cloneDeep(coverage) + expected[filename].s[0] = 2 + expect(merged, 'merged coverage').to.deep.equal(expected) + }) + + it('does not merge correctly placeholders', () => { + const coverageWithPlaceHolder = Cypress._.cloneDeep(coverage) + const placeholder = fileCoveragePlaceholder(filename) + coverageWithPlaceHolder[filename] = placeholder + + expect(coverageWithPlaceHolder, 'placeholder').to.deep.equal({ + [filename]: placeholder + }) + + // now lets merge full info + const previous = istanbul.createCoverageMap(coverageWithPlaceHolder) + const coverageMap = istanbul.createCoverageMap(previous) + coverageMap.merge(coverage) + + const merged = coverageMapToCoverage(coverageMap) + const expected = Cypress._.cloneDeep(coverage) + // the merge against the placeholder without valid statement map + // removes the statement map and sets the counter to null + expected[filename].s = { 0: null } + expected[filename].statementMap = {} + // and no hashes :( + delete expected[filename].hash + delete expected[filename]._coverageSchema + expect(merged).to.deep.equal(expected) + }) + + it('removes placeholders', () => { + const inputCoverage = Cypress._.cloneDeep(coverage) + removePlaceholders(inputCoverage) + expect(inputCoverage, 'nothing to remove').to.deep.equal(coverage) + + // add placeholder + const placeholder = fileCoveragePlaceholder(filename) + inputCoverage[filename] = placeholder + + removePlaceholders(inputCoverage) + expect(inputCoverage, 'the placeholder has been removed').to.deep.equal({}) + }) +}) diff --git a/examples/all-files/cypress/plugins/index.js b/examples/all-files/cypress/plugins/index.js index b17c48db..4df97b99 100644 --- a/examples/all-files/cypress/plugins/index.js +++ b/examples/all-files/cypress/plugins/index.js @@ -1,5 +1,6 @@ module.exports = (on, config) => { require('../../../../task')(on, config) + // instrument the specs and any source files loaded from specs on('file:preprocessor', require('../../../../use-babelrc')) return config } diff --git a/examples/all-files/package.json b/examples/all-files/package.json index 497422d3..3fe29694 100644 --- a/examples/all-files/package.json +++ b/examples/all-files/package.json @@ -1,6 +1,7 @@ { "name": "example-all-files", "description": "Report all files", + "private": true, "scripts": { "start": "../../node_modules/.bin/parcel serve index.html", "start:windows": "npx bin-up parcel serve index.html", diff --git a/examples/placeholders/.babelrc b/examples/placeholders/.babelrc new file mode 100644 index 00000000..7a016cf8 --- /dev/null +++ b/examples/placeholders/.babelrc @@ -0,0 +1,3 @@ +{ + "plugins": ["istanbul"] +} diff --git a/examples/placeholders/cypress.json b/examples/placeholders/cypress.json new file mode 100644 index 00000000..4ec54f37 --- /dev/null +++ b/examples/placeholders/cypress.json @@ -0,0 +1,3 @@ +{ + "fixturesFolder": false +} diff --git a/examples/placeholders/cypress/integration/spec-a.js b/examples/placeholders/cypress/integration/spec-a.js new file mode 100644 index 00000000..b5dea373 --- /dev/null +++ b/examples/placeholders/cypress/integration/spec-a.js @@ -0,0 +1,8 @@ +/// +// this spec only loads "src/a" module +import { myFunc } from '../../src/a.js' +describe('spec-a', () => { + it('exercises src/a.js', () => { + expect(myFunc(), 'always returns 30').to.equal(30) + }) +}) diff --git a/examples/placeholders/cypress/integration/spec-b.js b/examples/placeholders/cypress/integration/spec-b.js new file mode 100644 index 00000000..a5afb55b --- /dev/null +++ b/examples/placeholders/cypress/integration/spec-b.js @@ -0,0 +1,10 @@ +/// +// this spec loads "src/b" module +import { anotherFunction } from '../../src/b.js' +describe('spec-b', () => { + it('exercises src/b.js', () => { + expect(anotherFunction(), 'always returns hello backwards').to.equal( + 'olleh' + ) + }) +}) diff --git a/examples/placeholders/cypress/plugins/index.js b/examples/placeholders/cypress/plugins/index.js new file mode 100644 index 00000000..4df97b99 --- /dev/null +++ b/examples/placeholders/cypress/plugins/index.js @@ -0,0 +1,6 @@ +module.exports = (on, config) => { + require('../../../../task')(on, config) + // instrument the specs and any source files loaded from specs + on('file:preprocessor', require('../../../../use-babelrc')) + return config +} diff --git a/examples/placeholders/cypress/support/index.js b/examples/placeholders/cypress/support/index.js new file mode 100644 index 00000000..dd60efa2 --- /dev/null +++ b/examples/placeholders/cypress/support/index.js @@ -0,0 +1 @@ +import '../../../../support' diff --git a/examples/placeholders/package-lock.json b/examples/placeholders/package-lock.json new file mode 100644 index 00000000..412051f1 --- /dev/null +++ b/examples/placeholders/package-lock.json @@ -0,0 +1,407 @@ +{ + "name": "example-placeholders", + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/core": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", + "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.0", + "@babel/helper-module-transforms": "^7.9.0", + "@babel/helpers": "^7.9.0", + "@babel/parser": "^7.9.0", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.9.0", + "@babel/types": "^7.9.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + } + }, + "@babel/generator": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.10.tgz", + "integrity": "sha512-6mCdfhWgmqLdtTkhXjnIz0LcdVCd26wS2JXRtj2XY0u5klDsXBREA/pG5NVOuVnF2LUrBGNFtQkIqqTbblg0ww==", + "dev": true, + "requires": { + "@babel/types": "^7.12.10", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", + "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", + "dev": true, + "requires": { + "@babel/types": "^7.12.10" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", + "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", + "dev": true, + "requires": { + "@babel/types": "^7.12.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", + "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.5" + } + }, + "@babel/helper-module-transforms": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz", + "integrity": "sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-simple-access": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/helper-validator-identifier": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.1", + "@babel/types": "^7.12.1", + "lodash": "^4.17.19" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz", + "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==", + "dev": true, + "requires": { + "@babel/types": "^7.12.10" + } + }, + "@babel/helper-replace-supers": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz", + "integrity": "sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.12.1", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/traverse": "^7.12.5", + "@babel/types": "^7.12.5" + } + }, + "@babel/helper-simple-access": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz", + "integrity": "sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.1" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz", + "integrity": "sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==", + "dev": true, + "requires": { + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.5", + "@babel/types": "^7.12.5" + } + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.10.tgz", + "integrity": "sha512-PJdRPwyoOqFAWfLytxrWwGrAxghCgh/yTNCYciOz8QgjflA7aZhECPZAa2VUedKg2+QMWkI0L9lynh2SNmNEgA==", + "dev": true + }, + "@babel/template": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" + } + }, + "@babel/traverse": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz", + "integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.10", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.12.10", + "@babel/types": "^7.12.10", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "@babel/types": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.10.tgz", + "integrity": "sha512-sf6wboJV5mGyip2hIpDSKsr80RszPinEFjsHTalMxZAZkoQ2/2yQzxlcFN52SJqsyPfLtPmenL4g2KB3KJXPDw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "is-core-module": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", + "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + } + } +} diff --git a/examples/placeholders/package.json b/examples/placeholders/package.json new file mode 100644 index 00000000..09573462 --- /dev/null +++ b/examples/placeholders/package.json @@ -0,0 +1,20 @@ +{ + "name": "example-placeholders", + "description": "Fills placeholder values", + "private": true, + "scripts": { + "cy:open": "../../node_modules/.bin/cypress open", + "cy:run": "../../node_modules/.bin/cypress run", + "report": "../../node_modules/.bin/nyc report", + "clean": "rm -rf .nyc_output || true", + "test:a": "../../node_modules/.bin/cypress run --spec cypress/integration/spec-a.js", + "test:b": "../../node_modules/.bin/cypress run --spec cypress/integration/spec-b.js" + }, + "nyc": { + "all": true, + "include": "src/*.js" + }, + "devDependencies": { + "@babel/core": "7.9.0" + } +} diff --git a/examples/placeholders/src/a.js b/examples/placeholders/src/a.js new file mode 100644 index 00000000..4127294f --- /dev/null +++ b/examples/placeholders/src/a.js @@ -0,0 +1,5 @@ +export const myFunc = () => { + const a = 10 + const b = 20 + return a + b +} diff --git a/examples/placeholders/src/b.js b/examples/placeholders/src/b.js new file mode 100644 index 00000000..ff625dbc --- /dev/null +++ b/examples/placeholders/src/b.js @@ -0,0 +1,4 @@ +export const anotherFunction = () => { + const hello = 'hello' + return hello.split('').reverse().join('') +} diff --git a/task-utils.js b/task-utils.js index 81cdd039..cd4360c1 100644 --- a/task-utils.js +++ b/task-utils.js @@ -9,7 +9,11 @@ const debug = require('debug')('code-coverage') const chalk = require('chalk') const globby = require('globby') const yaml = require('js-yaml') -const { combineNycOptions, defaultNycOptions } = require('./common-utils') +const { + combineNycOptions, + defaultNycOptions, + fileCoveragePlaceholder +} = require('./common-utils') function readNycOptions(workingDirectory) { const pkgFilename = join(workingDirectory, 'package.json') @@ -363,15 +367,8 @@ function includeAllFiles(nycFilename, nycOptions) { debug('adding empty coverage for file %s', fullPath) changed = true // insert placeholder object for now - nycCoverage[fullPath] = { - path: fullPath, - statementMap: {}, - fnMap: {}, - branchMap: {}, - s: {}, - f: {}, - b: {} - } + const placeholder = fileCoveragePlaceholder(fullPath) + nycCoverage[fullPath] = placeholder }) if (changed) { diff --git a/task.js b/task.js index 3e4d3fc8..d420f9a5 100644 --- a/task.js +++ b/task.js @@ -12,6 +12,7 @@ const { includeAllFiles } = require('./task-utils') const { fixSourcePaths } = require('./support-utils') +const { removePlaceholders } = require('./common-utils') const debug = require('debug')('code-coverage') @@ -146,10 +147,19 @@ const tasks = { debug('parsed sent coverage') fixSourcePaths(coverage) - const previous = existsSync(nycFilename) + + const previousCoverage = existsSync(nycFilename) ? JSON.parse(readFileSync(nycFilename, 'utf8')) - : istanbul.createCoverageMap({}) - const coverageMap = istanbul.createCoverageMap(previous) + : {} + + // previous code coverage object might have placeholder entries + // for files that we have not seen yet, + // but the user expects to include in the coverage report + // the merge function messes up, so we should remove any placeholder entries + // and re-insert them again when creating the report + removePlaceholders(previousCoverage) + + const coverageMap = istanbul.createCoverageMap(previousCoverage) coverageMap.merge(coverage) saveCoverage(coverageMap) debug('wrote coverage file %s', nycFilename)