Skip to content

fix: handling the file placeholder when merging #362

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Dec 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -544,4 +578,5 @@ workflows:
- example-docker-paths
- example-use-webpack
- example-all-files
- example-placeholders
- Windows test
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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__ })
}
```

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
39 changes: 38 additions & 1 deletion common-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
26 changes: 26 additions & 0 deletions cypress/fixtures/coverage.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
1 change: 1 addition & 0 deletions cypress/integration/combine-spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference types="Cypress" />
const { combineNycOptions, defaultNycOptions } = require('../../common-utils')
describe('Combine NYC options', () => {
it('overrides defaults', () => {
Expand Down
86 changes: 86 additions & 0 deletions cypress/integration/merge-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/// <reference types="Cypress" />
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({})
})
})
1 change: 1 addition & 0 deletions examples/all-files/cypress/plugins/index.js
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions examples/all-files/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
3 changes: 3 additions & 0 deletions examples/placeholders/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"plugins": ["istanbul"]
}
3 changes: 3 additions & 0 deletions examples/placeholders/cypress.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"fixturesFolder": false
}
8 changes: 8 additions & 0 deletions examples/placeholders/cypress/integration/spec-a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// <reference types="cypress" />
// 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)
})
})
10 changes: 10 additions & 0 deletions examples/placeholders/cypress/integration/spec-b.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/// <reference types="cypress" />
// 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'
)
})
})
6 changes: 6 additions & 0 deletions examples/placeholders/cypress/plugins/index.js
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions examples/placeholders/cypress/support/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '../../../../support'
Loading