Skip to content

Commit

Permalink
fix(coverage): istanbul provider to work with JSDOM and process.env
Browse files Browse the repository at this point in the history
… usage
  • Loading branch information
AriPerkkio committed Aug 6, 2023
1 parent f4e6e99 commit 7b5f36c
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 5 deletions.
53 changes: 48 additions & 5 deletions packages/coverage-istanbul/src/provider.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { existsSync, promises as fs } from 'node:fs'
import { resolve } from 'pathe'
import type { AfterSuiteRunMeta, CoverageIstanbulOptions, CoverageProvider, ReportContext, ResolvedCoverageOptions, Vitest } from 'vitest'
import type { RawSourceMap } from 'vite-node'
import { coverageConfigDefaults, defaultExclude, defaultInclude } from 'vitest/config'
import { BaseCoverageProvider } from 'vitest/coverage'
import libReport from 'istanbul-lib-report'
import reports from 'istanbul-reports'
import type { CoverageMap } from 'istanbul-lib-coverage'
import type { CoverageMap, CoverageMapData } from 'istanbul-lib-coverage'
import libCoverage from 'istanbul-lib-coverage'
import libSourceMaps from 'istanbul-lib-source-maps'
import { type Instrumenter, createInstrumenter } from 'istanbul-lib-instrument'
Expand All @@ -15,6 +16,7 @@ import _TestExclude from 'test-exclude'
import { COVERAGE_STORE_KEY } from './constants'

type Options = ResolvedCoverageOptions<'istanbul'>
type CoverageResult = CoverageMapData & { [key: string]: { inputSourceMap?: RawSourceMap } }

interface TestExclude {
new(opts: {
Expand Down Expand Up @@ -43,7 +45,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
* If storing in memory causes issues, we can simply write these into fs in `onAfterSuiteRun`
* and read them back when merging coverage objects in `onAfterAllFilesRun`.
*/
coverages: any[] = []
coverages: CoverageResult[] = []

initialize(ctx: Vitest) {
const config: CoverageIstanbulOptions = ctx.config.coverage
Expand Down Expand Up @@ -93,14 +95,17 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
const sourceMap = pluginCtx.getCombinedSourcemap()
sourceMap.sources = sourceMap.sources.map(removeQueryParameters)

const code = this.instrumenter.instrumentSync(sourceCode, id, sourceMap as any)
const code = this.instrumenter.instrumentSync(sourceCode, id, sourceMap)
const map = this.instrumenter.lastSourceMap() as any

return { code, map }
return {
code: prepareInputSourceMapForViteTransforms(code),
map,
}
}

onAfterSuiteRun({ coverage }: AfterSuiteRunMeta) {
this.coverages.push(coverage)
this.coverages.push(coverage as CoverageResult)
}

async clean(clean = true) {
Expand All @@ -121,6 +126,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
await this.includeUntestedFiles(mergedCoverage)

includeImplicitElseBranches(mergedCoverage)
revertViteTransformRewrites(mergedCoverage)

const sourceMapStore = libSourceMaps.createSourceMapStore()
const coverageMap: CoverageMap = await sourceMapStore.transformCoverage(mergedCoverage)
Expand Down Expand Up @@ -251,3 +257,40 @@ function isEmptyCoverageRange(range: libCoverage.Range) {
|| range.end.column === undefined
)
}

/**
* Instrumentation adds source map into the JS code. It contains source code in
* `sourcesContent` field. Vite transform is producing invalid syntax when
* `process.env` is present in `sourcesContent` string. As work-around we'll
* replace `process.env` before Vite transforms and revert it back after
* remapping the coverage.
*/
function prepareInputSourceMapForViteTransforms(code: string) {
if (code.includes('process.env')) {
return code.split('\n').map((line) => {
// Inline source map contains "sourcesContent" field which contains source code on a single line
if (line.includes('sourcesContent: ["'))
return line.replace(/process\.env/g, 'VITEST_PROCESS_ENV')

return line
}).join('\n')
}

return code
}

function revertViteTransformRewrites(coverageMap: CoverageMap) {
for (const file of coverageMap.files()) {
const { data } = coverageMap.fileCoverageFor(file) as { data: CoverageResult[string] }

if (data.inputSourceMap?.sourcesContent) {
data.inputSourceMap.sourcesContent = data.inputSourceMap.sourcesContent.map((code) => {
// Restore rewrites back to original ones
if (code.includes('VITEST_PROCESS_ENV'))
return code.replace(/VITEST_PROCESS_ENV/g, 'process.env')

return code
})
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ exports[`custom json report 1`] = `
"<process-cwd>/src/importEnv.ts",
"<process-cwd>/src/index.mts",
"<process-cwd>/src/multi-suite.ts",
"<process-cwd>/src/process-env.ts",
"<process-cwd>/src/utils.ts",
],
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1425,6 +1425,54 @@ exports[`istanbul json report 1`] = `
},
},
},
"<process-cwd>/src/process-env.ts": {
"b": {},
"branchMap": {},
"f": {
"0": 1,
},
"fnMap": {
"0": {
"decl": {
"end": {
"column": 29,
"line": 1,
},
"start": {
"column": 16,
"line": 1,
},
},
"loc": {
"end": {
"column": null,
"line": 3,
},
"start": {
"column": 29,
"line": 1,
},
},
"name": "getNodeEnv",
},
},
"path": "<process-cwd>/src/process-env.ts",
"s": {
"0": 1,
},
"statementMap": {
"0": {
"end": {
"column": null,
"line": 2,
},
"start": {
"column": 2,
"line": 2,
},
},
},
},
"<process-cwd>/src/untested-file.ts": {
"b": {
"0": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3528,6 +3528,109 @@ exports[`v8 json report 1`] = `
},
},
},
"<process-cwd>/src/process-env.ts": {
"all": false,
"b": {
"0": [
1,
],
},
"branchMap": {
"0": {
"line": 1,
"loc": {
"end": {
"column": 1,
"line": 3,
},
"start": {
"column": 7,
"line": 1,
},
},
"locations": [
{
"end": {
"column": 1,
"line": 3,
},
"start": {
"column": 7,
"line": 1,
},
},
],
"type": "branch",
},
},
"f": {
"0": 1,
},
"fnMap": {
"0": {
"decl": {
"end": {
"column": 1,
"line": 3,
},
"start": {
"column": 7,
"line": 1,
},
},
"line": 1,
"loc": {
"end": {
"column": 1,
"line": 3,
},
"start": {
"column": 7,
"line": 1,
},
},
"name": "getNodeEnv",
},
},
"path": "<process-cwd>/src/process-env.ts",
"s": {
"0": 1,
"1": 1,
"2": 1,
},
"statementMap": {
"0": {
"end": {
"column": 30,
"line": 1,
},
"start": {
"column": 0,
"line": 1,
},
},
"1": {
"end": {
"column": 29,
"line": 2,
},
"start": {
"column": 0,
"line": 2,
},
},
"2": {
"end": {
"column": 1,
"line": 3,
},
"start": {
"column": 0,
"line": 3,
},
},
},
},
"<process-cwd>/src/untested-file.ts": {
"all": true,
"b": {
Expand Down
13 changes: 13 additions & 0 deletions test/coverage-test/coverage-report-tests/istanbul.report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Istanbul coverage provider specific test cases
*/

import { resolve } from 'node:path'
import { readFileSync } from 'node:fs'
import { expect, test } from 'vitest'
import { readCoverageJson } from './utils'

Expand Down Expand Up @@ -50,3 +52,14 @@ test('tests with multiple suites are covered', async () => {
1: 1,
})
})

test('vite transforms should not show on coverage report', async () => {
const filePath = resolve('./coverage/src/process-env.ts.html')
const htmlContent = readFileSync(filePath, 'utf-8')

// Pattern used to replace `process.env` - should not end up in coverage report
expect(htmlContent).not.toContain('VITEST_PROCESS_ENV')

// Actual source code
expect(htmlContent).toContain('process.env.NODE_ENV')
})
3 changes: 3 additions & 0 deletions test/coverage-test/src/process-env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function getNodeEnv() {
return process.env.NODE_ENV
}
8 changes: 8 additions & 0 deletions test/coverage-test/test/jsdom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// @vitest-environment jsdom

import { expect, test } from 'vitest'
import { getNodeEnv } from '../src/process-env'

test('process.env works', () => {
expect(getNodeEnv()).toBe('test')
})

0 comments on commit 7b5f36c

Please sign in to comment.