Skip to content

Commit 4f08006

Browse files
authored
fix: handling the file placeholder when merging (#362)
1 parent 199c02d commit 4f08006

File tree

20 files changed

+687
-17
lines changed

20 files changed

+687
-17
lines changed

Diff for: .circleci/config.yml

+35
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,40 @@ workflows:
393393
../../node_modules/.bin/only-covered --from coverage/coverage-final.json main.js second.js not-covered.js
394394
working_directory: examples/all-files
395395

396+
- cypress/run:
397+
attach-workspace: true
398+
name: example-placeholders
399+
requires:
400+
- cypress/install
401+
# there are no jobs to follow this one
402+
# so no need to save the workspace files (saves time)
403+
no-workspace: true
404+
command: npx cypress run --project examples/placeholders
405+
# store screenshots and videos
406+
store_artifacts: true
407+
post-steps:
408+
- run: cat examples/placeholders/.nyc_output/out.json
409+
- run: cat examples/placeholders/coverage/coverage-final.json
410+
# store the created coverage report folder
411+
# you can click on it in the CircleCI UI
412+
# to see live static HTML site
413+
- store_artifacts:
414+
path: examples/placeholders/coverage
415+
# make sure the examples captures 100% of code
416+
- run:
417+
command: npx nyc report --check-coverage true --lines 100
418+
working_directory: examples/placeholders
419+
- run:
420+
name: Check code coverage 📈
421+
# we will check the final coverage report
422+
# to make sure it only has files we are interested in
423+
# because there are files covered at 0 in the report
424+
command: |
425+
../../node_modules/.bin/check-coverage src/a.js
426+
../../node_modules/.bin/check-coverage src/a.js
427+
../../node_modules/.bin/only-covered --from coverage/coverage-final.json src/a.js src/b.js
428+
working_directory: examples/placeholders
429+
396430
- cypress/run:
397431
attach-workspace: true
398432
name: example-exclude-files
@@ -544,4 +578,5 @@ workflows:
544578
- example-docker-paths
545579
- example-use-webpack
546580
- example-all-files
581+
- example-placeholders
547582
- Windows test

Diff for: README.md

+12-3
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ Register tasks in your `cypress/plugins/index.js` file
2121
```js
2222
module.exports = (on, config) => {
2323
require('@cypress/code-coverage/task')(on, config)
24-
24+
2525
// add other tasks to be registered here
26-
26+
2727
// IMPORTANT to return the config object
2828
// with the any changed environment variables
2929
return config
@@ -189,7 +189,7 @@ For any other server, define the endpoint yourself and return the coverage objec
189189
```js
190190
if (global.__coverage__) {
191191
// add method "GET /__coverage__" and response with JSON
192-
onRequest = response => response.sendJSON({ coverage: global.__coverage__ })
192+
onRequest = (response) => response.sendJSON({ coverage: global.__coverage__ })
193193
}
194194
```
195195

@@ -365,6 +365,7 @@ Full examples we use for testing in this repository:
365365
### External examples
366366

367367
Look up the list of examples under GitHub topic [cypress-code-coverage-example](https://github.com/topics/cypress-code-coverage-example)
368+
368369
- [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).
369370
- [cypress-io/cypress-example-todomvc-redux](https://github.com/cypress-io/cypress-example-todomvc-redux) is a React / Redux application with 100% code coverage.
370371
- [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
427428
code-coverage saving coverage report using command: "nyc report --report-dir ./coverage --reporter=lcov --reporter=clover --reporter=json" +3ms
428429
```
429430

431+
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
432+
433+
```shell
434+
$ DEBUG_DEPTH=10 DEBUG=code-coverage npm run dev
435+
```
436+
437+
### Common issues
438+
430439
Common issue: [not instrumenting your application when running Cypress](#instrument-your-application).
431440

432441
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 for: common-utils.js

+38-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,44 @@ const defaultNycOptions = {
2626
excludeAfterRemap: false
2727
}
2828

29+
/**
30+
* Returns an object with placeholder properties for files we
31+
* do not have coverage yet. The result can go into the coverage object
32+
*
33+
* @param {string} fullPath Filename
34+
*/
35+
const fileCoveragePlaceholder = (fullPath) => {
36+
return {
37+
path: fullPath,
38+
statementMap: {},
39+
fnMap: {},
40+
branchMap: {},
41+
s: {},
42+
f: {},
43+
b: {}
44+
}
45+
}
46+
47+
const isPlaceholder = (entry) => {
48+
// when the file has been instrumented, its entry has "hash" property
49+
return !('hash' in entry)
50+
}
51+
52+
/**
53+
* Given a coverage object with potential placeholder entries
54+
* inserted instead of covered files, removes them. Modifies the object in place
55+
*/
56+
const removePlaceholders = (coverage) => {
57+
Object.keys(coverage).forEach((key) => {
58+
if (isPlaceholder(coverage[key])) {
59+
delete coverage[key]
60+
}
61+
})
62+
}
63+
2964
module.exports = {
3065
combineNycOptions,
31-
defaultNycOptions
66+
defaultNycOptions,
67+
fileCoveragePlaceholder,
68+
removePlaceholders
3269
}

Diff for: cypress/fixtures/coverage.json

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"/src/index.js": {
3+
"path": "/src/index.js",
4+
"statementMap": {
5+
"0": {
6+
"start": {
7+
"line": 7,
8+
"column": 0
9+
},
10+
"end": {
11+
"line": 14,
12+
"column": 2
13+
}
14+
}
15+
},
16+
"fnMap": {},
17+
"branchMap": {},
18+
"s": {
19+
"0": 1
20+
},
21+
"f": {},
22+
"b": {},
23+
"_coverageSchema": "1a1c01bbd47fc00a2c39e90264f33305004495a9",
24+
"hash": "46f2efd10038593ec768c6cc815a6d6ee2924243"
25+
}
26+
}

Diff for: cypress/integration/combine-spec.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/// <reference types="Cypress" />
12
const { combineNycOptions, defaultNycOptions } = require('../../common-utils')
23
describe('Combine NYC options', () => {
34
it('overrides defaults', () => {

Diff for: cypress/integration/merge-spec.js

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/// <reference types="Cypress" />
2+
const istanbul = require('istanbul-lib-coverage')
3+
const coverage = require('../fixtures/coverage.json')
4+
const {
5+
fileCoveragePlaceholder,
6+
removePlaceholders
7+
} = require('../../common-utils')
8+
9+
/**
10+
* Extracts just the data from the coverage map object
11+
* @param {*} cm
12+
*/
13+
const coverageMapToCoverage = (cm) => {
14+
return JSON.parse(JSON.stringify(cm))
15+
}
16+
17+
describe('merging coverage', () => {
18+
const filename = '/src/index.js'
19+
20+
before(() => {
21+
expect(coverage, 'initial coverage has this file').to.have.property(
22+
filename
23+
)
24+
})
25+
26+
it('combines an empty coverage object', () => {
27+
const previous = istanbul.createCoverageMap({})
28+
const coverageMap = istanbul.createCoverageMap(previous)
29+
coverageMap.merge(Cypress._.cloneDeep(coverage))
30+
31+
const merged = coverageMapToCoverage(coverageMap)
32+
33+
expect(merged, 'merged coverage').to.deep.equal(coverage)
34+
})
35+
36+
it('combines the same full coverage twice', () => {
37+
const previous = istanbul.createCoverageMap(Cypress._.cloneDeep(coverage))
38+
const coverageMap = istanbul.createCoverageMap(previous)
39+
coverageMap.merge(Cypress._.cloneDeep(coverage))
40+
41+
const merged = coverageMapToCoverage(coverageMap)
42+
// it is almost the same - only the statement count has been doubled
43+
const expected = Cypress._.cloneDeep(coverage)
44+
expected[filename].s[0] = 2
45+
expect(merged, 'merged coverage').to.deep.equal(expected)
46+
})
47+
48+
it('does not merge correctly placeholders', () => {
49+
const coverageWithPlaceHolder = Cypress._.cloneDeep(coverage)
50+
const placeholder = fileCoveragePlaceholder(filename)
51+
coverageWithPlaceHolder[filename] = placeholder
52+
53+
expect(coverageWithPlaceHolder, 'placeholder').to.deep.equal({
54+
[filename]: placeholder
55+
})
56+
57+
// now lets merge full info
58+
const previous = istanbul.createCoverageMap(coverageWithPlaceHolder)
59+
const coverageMap = istanbul.createCoverageMap(previous)
60+
coverageMap.merge(coverage)
61+
62+
const merged = coverageMapToCoverage(coverageMap)
63+
const expected = Cypress._.cloneDeep(coverage)
64+
// the merge against the placeholder without valid statement map
65+
// removes the statement map and sets the counter to null
66+
expected[filename].s = { 0: null }
67+
expected[filename].statementMap = {}
68+
// and no hashes :(
69+
delete expected[filename].hash
70+
delete expected[filename]._coverageSchema
71+
expect(merged).to.deep.equal(expected)
72+
})
73+
74+
it('removes placeholders', () => {
75+
const inputCoverage = Cypress._.cloneDeep(coverage)
76+
removePlaceholders(inputCoverage)
77+
expect(inputCoverage, 'nothing to remove').to.deep.equal(coverage)
78+
79+
// add placeholder
80+
const placeholder = fileCoveragePlaceholder(filename)
81+
inputCoverage[filename] = placeholder
82+
83+
removePlaceholders(inputCoverage)
84+
expect(inputCoverage, 'the placeholder has been removed').to.deep.equal({})
85+
})
86+
})

Diff for: examples/all-files/cypress/plugins/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module.exports = (on, config) => {
22
require('../../../../task')(on, config)
3+
// instrument the specs and any source files loaded from specs
34
on('file:preprocessor', require('../../../../use-babelrc'))
45
return config
56
}

Diff for: examples/all-files/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "example-all-files",
33
"description": "Report all files",
4+
"private": true,
45
"scripts": {
56
"start": "../../node_modules/.bin/parcel serve index.html",
67
"start:windows": "npx bin-up parcel serve index.html",

Diff for: examples/placeholders/.babelrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"plugins": ["istanbul"]
3+
}

Diff for: examples/placeholders/cypress.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"fixturesFolder": false
3+
}

Diff for: examples/placeholders/cypress/integration/spec-a.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/// <reference types="cypress" />
2+
// this spec only loads "src/a" module
3+
import { myFunc } from '../../src/a.js'
4+
describe('spec-a', () => {
5+
it('exercises src/a.js', () => {
6+
expect(myFunc(), 'always returns 30').to.equal(30)
7+
})
8+
})

Diff for: examples/placeholders/cypress/integration/spec-b.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/// <reference types="cypress" />
2+
// this spec loads "src/b" module
3+
import { anotherFunction } from '../../src/b.js'
4+
describe('spec-b', () => {
5+
it('exercises src/b.js', () => {
6+
expect(anotherFunction(), 'always returns hello backwards').to.equal(
7+
'olleh'
8+
)
9+
})
10+
})

Diff for: examples/placeholders/cypress/plugins/index.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = (on, config) => {
2+
require('../../../../task')(on, config)
3+
// instrument the specs and any source files loaded from specs
4+
on('file:preprocessor', require('../../../../use-babelrc'))
5+
return config
6+
}

Diff for: examples/placeholders/cypress/support/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import '../../../../support'

0 commit comments

Comments
 (0)