diff --git a/.circleci/config.yml b/.circleci/config.yml
index 799407b9..e5e67f83 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -194,6 +194,30 @@ workflows:
../../node_modules/.bin/only-covered main.js
working_directory: examples/before-all-visit
+ - cypress/run:
+ attach-workspace: true
+ name: example-docker-paths
+ requires:
+ - cypress/install
+ # there are no jobs to follow this one
+ # so no need to save the workspace files (saves time)
+ no-workspace: true
+ working_directory: examples/docker-paths
+ command: '../../node_modules/.bin/cypress run'
+ post-steps:
+ # 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/docker-paths/coverage
+ - run:
+ name: Check code coverage 📈
+ command: |
+ ../../node_modules/.bin/check-coverage main.js
+ ../../node_modules/.bin/check-coverage second.js
+ ../../node_modules/.bin/only-covered main.js second.js
+ working_directory: examples/docker-paths
+
- cypress/run:
attach-workspace: true
name: example-ts-example
@@ -395,3 +419,4 @@ workflows:
- example-use-plugins-and-support
- example-one-spec
- example-exclude-files
+ - example-docker-paths
diff --git a/examples/docker-paths/README.md b/examples/docker-paths/README.md
new file mode 100644
index 00000000..0e8c20d1
--- /dev/null
+++ b/examples/docker-paths/README.md
@@ -0,0 +1,38 @@
+# example-docker-paths
+
+In this example, the source files are "instrumented" as if they were instrumented inside a Docker container. Still, Cypress code coverage plugin should find the matching current folder where same files exist and update `.nyc_output/out.json` file before generating reports.
+
+Source files from `app` folder were instrumented into `dist` folder with command
+
+```shell
+$ npx nyc instrument app dist
+```
+
+Then the `index.html` file was copied into `dist` folder.
+
+Then the source paths in [dist/main.js](dist/main.js) and [dist/second.js](dist/second.js) were changed to non-existent prefix folder `/var/www/test/site`.
+
+When Cypress runs, the `.nyc_output/out.json` is updated, so the path is valid local path like:
+
+```
+{
+ "/var/www/test/site/app/main.js": {
+ "path": "/Users/gleb/git/code-coverage/examples/docker-paths/app/main.js",
+ "statementMap": {
+ ...
+```
+
+And the report has valid HTML with sources
+
+
+
+
+
+**Note:** remember to remove existing `.nyc_output` folder if running Cypress in non-interactive mode `rm -rf .nyc_output/`.
+
+When running with [debug logs](https://github.com/cypress-io/code-coverage#debugging) you should see messages:
+
+```
+found common folder /var/www/test/site that matches
+current working directory /Users/gleb/git/code-coverage/examples/docker-paths
+```
diff --git a/examples/docker-paths/app/index.html b/examples/docker-paths/app/index.html
new file mode 100644
index 00000000..993f0c18
--- /dev/null
+++ b/examples/docker-paths/app/index.html
@@ -0,0 +1,17 @@
+
+ Page body
+
+
+
+
diff --git a/examples/docker-paths/app/main.js b/examples/docker-paths/app/main.js
new file mode 100644
index 00000000..5dd69be2
--- /dev/null
+++ b/examples/docker-paths/app/main.js
@@ -0,0 +1,3 @@
+window.add = (a, b) => a + b
+
+window.sub = (a, b) => a - b
diff --git a/examples/docker-paths/app/second.js b/examples/docker-paths/app/second.js
new file mode 100644
index 00000000..494a0c5f
--- /dev/null
+++ b/examples/docker-paths/app/second.js
@@ -0,0 +1,7 @@
+// this file should be excluded from the final coverage numbers
+// using "nyc.exclude" list in package.json
+window.reverse = s =>
+ s
+ .split('')
+ .reverse()
+ .join('')
diff --git a/examples/docker-paths/cypress.json b/examples/docker-paths/cypress.json
new file mode 100644
index 00000000..3681940e
--- /dev/null
+++ b/examples/docker-paths/cypress.json
@@ -0,0 +1,5 @@
+{
+ "pluginsFile": "../../plugins",
+ "supportFile": "../../support",
+ "fixturesFolder": false
+}
diff --git a/examples/docker-paths/cypress/integration/spec.js b/examples/docker-paths/cypress/integration/spec.js
new file mode 100644
index 00000000..3da439de
--- /dev/null
+++ b/examples/docker-paths/cypress/integration/spec.js
@@ -0,0 +1,11 @@
+///
+describe('docker-paths', () => {
+ it('works', () => {
+ cy.visit('dist/index.html')
+ cy.contains('Page body')
+
+ cy.window()
+ .invoke('reverse', 'super')
+ .should('equal', 'repus')
+ })
+})
diff --git a/examples/docker-paths/dist/index.html b/examples/docker-paths/dist/index.html
new file mode 100644
index 00000000..993f0c18
--- /dev/null
+++ b/examples/docker-paths/dist/index.html
@@ -0,0 +1,17 @@
+
+ Page body
+
+
+
+
diff --git a/examples/docker-paths/dist/main.js b/examples/docker-paths/dist/main.js
new file mode 100644
index 00000000..9bb7cd41
--- /dev/null
+++ b/examples/docker-paths/dist/main.js
@@ -0,0 +1,58 @@
+function cov_25n278q6qe() {
+ var path = '/var/www/test/site/app/main.js'
+ var hash = '8817d7301264befa6a587eeb9305a1bf2cbde8bc'
+ var global = new Function('return this')()
+ var gcv = '__coverage__'
+ var coverageData = {
+ path: '/var/www/test/site/app/main.js',
+ statementMap: {
+ '0': { start: { line: 1, column: 0 }, end: { line: 1, column: 28 } },
+ '1': { start: { line: 1, column: 23 }, end: { line: 1, column: 28 } },
+ '2': { start: { line: 3, column: 0 }, end: { line: 3, column: 28 } },
+ '3': { start: { line: 3, column: 23 }, end: { line: 3, column: 28 } }
+ },
+ fnMap: {
+ '0': {
+ name: '(anonymous_0)',
+ decl: { start: { line: 1, column: 13 }, end: { line: 1, column: 14 } },
+ loc: { start: { line: 1, column: 23 }, end: { line: 1, column: 28 } },
+ line: 1
+ },
+ '1': {
+ name: '(anonymous_1)',
+ decl: { start: { line: 3, column: 13 }, end: { line: 3, column: 14 } },
+ loc: { start: { line: 3, column: 23 }, end: { line: 3, column: 28 } },
+ line: 3
+ }
+ },
+ branchMap: {},
+ s: { '0': 0, '1': 0, '2': 0, '3': 0 },
+ f: { '0': 0, '1': 0 },
+ b: {},
+ _coverageSchema: '1a1c01bbd47fc00a2c39e90264f33305004495a9',
+ hash: '8817d7301264befa6a587eeb9305a1bf2cbde8bc'
+ }
+ var coverage = global[gcv] || (global[gcv] = {})
+ if (!coverage[path] || coverage[path].hash !== hash) {
+ coverage[path] = coverageData
+ }
+ var actualCoverage = coverage[path]
+ cov_25n278q6qe = function() {
+ return actualCoverage
+ }
+ return actualCoverage
+}
+cov_25n278q6qe()
+cov_25n278q6qe().s[0]++
+window.add = (a, b) => {
+ cov_25n278q6qe().f[0]++
+ cov_25n278q6qe().s[1]++
+ return a + b
+}
+cov_25n278q6qe().s[2]++
+window.sub = (a, b) => {
+ cov_25n278q6qe().f[1]++
+ cov_25n278q6qe().s[3]++
+ return a - b
+}
+//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm1haW4uanMiXSwibmFtZXMiOlsid2luZG93IiwiYWRkIiwiYSIsImIiLCJzdWIiXSwibWFwcGluZ3MiOiJtdkNBQUFBLE1BQU0sQ0FBQ0MsR0FBUCxDQUFhLENBQUNDLENBQUQsQ0FBSUMsQ0FBSixHQUFVLHVEQUFBRCxDQUFBQSxDQUFDLENBQUdDLENBQUosQ0FBSyxDQUE1QixDLHdCQUVBSCxNQUFNLENBQUNJLEdBQVAsQ0FBYSxDQUFDRixDQUFELENBQUlDLENBQUosR0FBVSx1REFBQUQsQ0FBQUEsQ0FBQyxDQUFHQyxDQUFKLENBQUssQ0FBNUIiLCJzb3VyY2VzQ29udGVudCI6WyJ3aW5kb3cuYWRkID0gKGEsIGIpID0+IGEgKyBiXG5cbndpbmRvdy5zdWIgPSAoYSwgYikgPT4gYSAtIGJcbiJdfQ==
diff --git a/examples/docker-paths/dist/second.js b/examples/docker-paths/dist/second.js
new file mode 100644
index 00000000..d5dfce4c
--- /dev/null
+++ b/examples/docker-paths/dist/second.js
@@ -0,0 +1,48 @@
+function cov_oh5ama61f() {
+ var path = '/var/www/test/site/app/second.js'
+ var hash = 'b86d13c912cff987dc7ed851327509c7a170ff4b'
+ var global = new Function('return this')()
+ var gcv = '__coverage__'
+ var coverageData = {
+ path: '/var/www/test/site/app/second.js',
+ statementMap: {
+ '0': { start: { line: 3, column: 0 }, end: { line: 7, column: 13 } },
+ '1': { start: { line: 4, column: 2 }, end: { line: 7, column: 13 } }
+ },
+ fnMap: {
+ '0': {
+ name: '(anonymous_0)',
+ decl: { start: { line: 3, column: 17 }, end: { line: 3, column: 18 } },
+ loc: { start: { line: 4, column: 2 }, end: { line: 7, column: 13 } },
+ line: 4
+ }
+ },
+ branchMap: {},
+ s: { '0': 0, '1': 0 },
+ f: { '0': 0 },
+ b: {},
+ _coverageSchema: '1a1c01bbd47fc00a2c39e90264f33305004495a9',
+ hash: 'b86d13c912cff987dc7ed851327509c7a170ff4b'
+ }
+ var coverage = global[gcv] || (global[gcv] = {})
+ if (!coverage[path] || coverage[path].hash !== hash) {
+ coverage[path] = coverageData
+ }
+ var actualCoverage = coverage[path]
+ cov_oh5ama61f = function() {
+ return actualCoverage
+ }
+ return actualCoverage
+}
+cov_oh5ama61f()
+cov_oh5ama61f().s[0]++ // this file should be excluded from the final coverage numbers
+// using "nyc.exclude" list in package.json
+window.reverse = s => {
+ cov_oh5ama61f().f[0]++
+ cov_oh5ama61f().s[1]++
+ return s
+ .split('')
+ .reverse()
+ .join('')
+}
+//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNlY29uZC5qcyJdLCJuYW1lcyI6WyJ3aW5kb3ciLCJyZXZlcnNlIiwicyIsInNwbGl0Iiwiam9pbiJdLCJtYXBwaW5ncyI6InErQkFBQTtBQUNBO0FBQ0FBLE1BQU0sQ0FBQ0MsT0FBUCxDQUFpQkMsQ0FBQyxFQUNoQixxREFBQUEsQ0FBQUEsQ0FBQyxDQUNFQyxLQURILENBQ1MsRUFEVCxFQUVHRixPQUZILEdBR0dHLElBSEgsQ0FHUSxFQUhSLEVBR1csQ0FKYiIsInNvdXJjZXNDb250ZW50IjpbIi8vIHRoaXMgZmlsZSBzaG91bGQgYmUgZXhjbHVkZWQgZnJvbSB0aGUgZmluYWwgY292ZXJhZ2UgbnVtYmVyc1xuLy8gdXNpbmcgXCJueWMuZXhjbHVkZVwiIGxpc3QgaW4gcGFja2FnZS5qc29uXG53aW5kb3cucmV2ZXJzZSA9IHMgPT5cbiAgc1xuICAgIC5zcGxpdCgnJylcbiAgICAucmV2ZXJzZSgpXG4gICAgLmpvaW4oJycpXG4iXX0=
diff --git a/examples/docker-paths/images/file.png b/examples/docker-paths/images/file.png
new file mode 100644
index 00000000..8a050cb4
Binary files /dev/null and b/examples/docker-paths/images/file.png differ
diff --git a/examples/docker-paths/images/files.png b/examples/docker-paths/images/files.png
new file mode 100644
index 00000000..29be1b4f
Binary files /dev/null and b/examples/docker-paths/images/files.png differ
diff --git a/examples/docker-paths/package.json b/examples/docker-paths/package.json
new file mode 100644
index 00000000..72104d30
--- /dev/null
+++ b/examples/docker-paths/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "example-docker-paths",
+ "private": true,
+ "version": "1.0.0",
+ "description": "Instrumented files in Docker container are found locally",
+ "main": "index.js",
+ "scripts": {
+ "cy:open": "../../node_modules/.bin/cypress open",
+ "cy:run": "../../node_modules/.bin/cypress run"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC"
+}
diff --git a/package.json b/package.json
index ad715ebe..68d98088 100644
--- a/package.json
+++ b/package.json
@@ -66,5 +66,8 @@
"serve": "11.3.0",
"start-server-and-test": "1.10.11",
"typescript": "3.8.3"
+ },
+ "nyc": {
+ "exclude": ["utils.js"]
}
}
diff --git a/task.js b/task.js
index c48f5e65..8d9135ff 100644
--- a/task.js
+++ b/task.js
@@ -1,10 +1,15 @@
// @ts-check
const istanbul = require('istanbul-lib-coverage')
-const { join, resolve, isAbsolute } = require('path')
+const { join, resolve } = require('path')
const { existsSync, mkdirSync, readFileSync, writeFileSync } = require('fs')
const execa = require('execa')
-const fs = require('fs')
-const { fixSourcePathes } = require('./utils')
+const {
+ fixSourcePathes,
+ showNycInfo,
+ resolveRelativePaths,
+ checkAllPathsNotFound,
+ tryFindingLocalFiles
+} = require('./utils')
const NYC = require('nyc')
const debug = require('debug')('code-coverage')
@@ -20,8 +25,8 @@ const nycFilename = join(coverageFolder, 'out.json')
// potentially there might be "nyc" options in other configuration files
// it allows, but for now ignore those options
const pkgFilename = join(processWorkingDirectory, 'package.json')
-const pkg = fs.existsSync(pkgFilename)
- ? JSON.parse(fs.readFileSync(pkgFilename, 'utf8'))
+const pkg = existsSync(pkgFilename)
+ ? JSON.parse(readFileSync(pkgFilename, 'utf8'))
: {}
const nycOptions = pkg.nyc || {}
const scripts = pkg.scripts || {}
@@ -37,52 +42,6 @@ function saveCoverage(coverage) {
writeFileSync(nycFilename, JSON.stringify(coverage, null, 2))
}
-/**
- * Looks at all coverage objects in the given JSON coverage file
- * and if the file is relative, and exists, changes its path to
- * be absolute.
- */
-function resolvePaths(nycFilename) {
- const nycCoverage = JSON.parse(readFileSync(nycFilename, 'utf8'))
-
- const coverageKeys = Object.keys(nycCoverage)
- if (!coverageKeys.length) {
- console.error('⚠️ file %s has no coverage information', nycFilename)
- return
- }
- debug('NYC file %s has %d key(s)', nycFilename, coverageKeys.length)
-
- let changed
- const maxPrintKeys = 3
-
- Object.keys(nycCoverage).forEach((key, k) => {
- const coverage = nycCoverage[key]
-
- // printing a few found keys and file paths from the coverage file
- // will make debugging any problems much much easier
- if (k < maxPrintKeys) {
- debug('%d key %s file path %s', k + 1, key, coverage.path)
- }
-
- if (coverage.path && !isAbsolute(coverage.path)) {
- if (existsSync(coverage.path)) {
- debug('resolving path %s', coverage.path)
- coverage.path = resolve(coverage.path)
- changed = true
- }
- }
- })
-
- if (changed) {
- debug('saving updated file %s', nycFilename)
- writeFileSync(
- nycFilename,
- JSON.stringify(nycCoverage, null, 2) + '\n',
- 'utf8'
- )
- }
-}
-
const tasks = {
/**
* Clears accumulated code coverage information.
@@ -143,7 +102,13 @@ const tasks = {
return null
}
- resolvePaths(nycFilename)
+ showNycInfo(nycFilename)
+ const allSourceFilesMissing = checkAllPathsNotFound(nycFilename)
+ if (allSourceFilesMissing) {
+ tryFindingLocalFiles(nycFilename)
+ }
+
+ resolveRelativePaths(nycFilename)
if (customNycReportScript) {
debug(
diff --git a/utils.js b/utils.js
index 75a43f84..be17e815 100644
--- a/utils.js
+++ b/utils.js
@@ -1,4 +1,30 @@
+// @ts-check
///
+const { readFileSync, writeFileSync, existsSync } = require('fs')
+const { isAbsolute, resolve, join } = require('path')
+const debug = require('debug')('code-coverage')
+
+function checkAllPathsNotFound(nycFilename) {
+ const nycCoverage = JSON.parse(readFileSync(nycFilename, 'utf8'))
+
+ const coverageKeys = Object.keys(nycCoverage)
+ if (!coverageKeys.length) {
+ console.error('⚠️ file %s has no coverage information', nycFilename)
+ return
+ }
+
+ const allFilesAreMissing = coverageKeys.every((key, k) => {
+ const coverage = nycCoverage[key]
+ return !existsSync(coverage.path)
+ })
+
+ debug(
+ 'in file %s all files are not found? %o',
+ nycFilename,
+ allFilesAreMissing
+ )
+ return allFilesAreMissing
+}
/**
* remove coverage for the spec files themselves,
@@ -6,6 +32,7 @@
*/
const filterSpecsFromCoverage = (totalCoverage, config = Cypress.config) => {
const integrationFolder = config('integrationFolder')
+ // @ts-ignore
const testFilePattern = config('testFiles')
// test files chould be:
@@ -58,7 +85,176 @@ function fixSourcePathes(coverage) {
})
}
+/**
+ * A small debug utility to inspect paths saved in NYC output JSON file
+ */
+function showNycInfo(nycFilename) {
+ const nycCoverage = JSON.parse(readFileSync(nycFilename, 'utf8'))
+
+ const coverageKeys = Object.keys(nycCoverage)
+ if (!coverageKeys.length) {
+ console.error('⚠️ file %s has no coverage information', nycFilename)
+ return
+ }
+ debug('NYC file %s has %d key(s)', nycFilename, coverageKeys.length)
+
+ const maxPrintKeys = 3
+ const showKeys = coverageKeys.slice(0, maxPrintKeys)
+
+ showKeys.forEach((key, k) => {
+ const coverage = nycCoverage[key]
+
+ // printing a few found keys and file paths from the coverage file
+ // will make debugging any problems much much easier
+ if (k < maxPrintKeys) {
+ debug('%d key %s file path %s', k + 1, key, coverage.path)
+ }
+ })
+}
+
+/**
+ * Looks at all coverage objects in the given JSON coverage file
+ * and if the file is relative, and exists, changes its path to
+ * be absolute.
+ */
+function resolveRelativePaths(nycFilename) {
+ const nycCoverage = JSON.parse(readFileSync(nycFilename, 'utf8'))
+
+ const coverageKeys = Object.keys(nycCoverage)
+ if (!coverageKeys.length) {
+ console.error('⚠️ file %s has no coverage information', nycFilename)
+ return
+ }
+ debug('NYC file %s has %d key(s)', nycFilename, coverageKeys.length)
+
+ let changed
+
+ coverageKeys.forEach((key, k) => {
+ const coverage = nycCoverage[key]
+
+ if (!coverage.path) {
+ debug('key %s does not have path', key)
+ return
+ }
+
+ if (!isAbsolute(coverage.path)) {
+ if (existsSync(coverage.path)) {
+ debug('resolving path %s', coverage.path)
+ coverage.path = resolve(coverage.path)
+ changed = true
+ }
+ return
+ }
+
+ // path is absolute, let's check if it exists
+ if (!existsSync(coverage.path)) {
+ debug('⚠️ cannot find file %s with hash %s', coverage.path, coverage.hash)
+ }
+ })
+
+ if (changed) {
+ debug('saving updated file %s', nycFilename)
+ writeFileSync(
+ nycFilename,
+ JSON.stringify(nycCoverage, null, 2) + '\n',
+ 'utf8'
+ )
+ }
+}
+
+/**
+ * @param {string[]} filepaths
+ * @returns {string | undefined} common prefix that corresponds to current folder
+ */
+function findCommonRoot(filepaths) {
+ if (!filepaths.length) {
+ debug('cannot find common root without any files')
+ return
+ }
+
+ // assuming / as file separator
+ const splitParts = filepaths.map(name => name.split('/'))
+ const lengths = splitParts.map(arr => arr.length)
+ const shortestLength = Math.min.apply(null, lengths)
+ debug('shorted file path has %d parts', shortestLength)
+
+ const cwd = process.cwd()
+ let commonPrefix = []
+ let foundCurrentFolder
+
+ for (let k = 0; k < shortestLength; k += 1) {
+ const part = splitParts[0][k]
+ const prefix = commonPrefix.concat(part).join('/')
+ debug('testing prefix %o', prefix)
+ const allFilesStart = filepaths.every(name => name.startsWith(prefix))
+ if (!allFilesStart) {
+ debug('stopped at non-common prefix %s', prefix)
+ break
+ }
+
+ commonPrefix.push(part)
+
+ const removedPrefixNames = filepaths.map(filepath =>
+ filepath.slice(prefix.length)
+ )
+ debug('removedPrefix %o', removedPrefixNames)
+ const foundAllPaths = removedPrefixNames.every(filepath =>
+ existsSync(join(cwd, filepath))
+ )
+ debug('all files found at %s? %o', prefix, foundAllPaths)
+ if (foundAllPaths) {
+ debug('found prefix that matches current folder: %s', prefix)
+ foundCurrentFolder = prefix
+ break
+ }
+ }
+
+ return foundCurrentFolder
+}
+
+function tryFindingLocalFiles(nycFilename) {
+ const nycCoverage = JSON.parse(readFileSync(nycFilename, 'utf8'))
+ const coverageKeys = Object.keys(nycCoverage)
+ const filenames = coverageKeys.map(key => nycCoverage[key].path)
+ const commonFolder = findCommonRoot(filenames)
+ if (!commonFolder) {
+ debug('could not find common folder %s', commonFolder)
+ return
+ }
+ const cwd = process.cwd()
+ debug(
+ 'found common folder %s that matches current working directory %s',
+ commonFolder,
+ cwd
+ )
+ const length = commonFolder.length
+ let changed
+
+ coverageKeys.forEach(key => {
+ const from = nycCoverage[key].path
+ if (from.startsWith(commonFolder)) {
+ const to = join(cwd, from.slice(length))
+ nycCoverage[key].path = to
+ debug('replaced %s -> %s', from, to)
+ changed = true
+ }
+ })
+
+ if (changed) {
+ debug('saving updated file %s', nycFilename)
+ writeFileSync(
+ nycFilename,
+ JSON.stringify(nycCoverage, null, 2) + '\n',
+ 'utf8'
+ )
+ }
+}
+
module.exports = {
fixSourcePathes,
- filterSpecsFromCoverage
+ filterSpecsFromCoverage,
+ showNycInfo,
+ resolveRelativePaths,
+ checkAllPathsNotFound,
+ tryFindingLocalFiles
}