1
- import { extname , join , isAbsolute , sep , posix } from 'path' ;
1
+ import { extname , join , isAbsolute , sep , posix } from 'node: path' ;
2
2
import { CoverageMapData } from 'istanbul-lib-coverage' ;
3
3
import v8toIstanbulLib from 'v8-to-istanbul' ;
4
4
import { TestRunnerCoreConfig , fetchSourceMap } from '@web/test-runner-core' ;
5
5
import { Profiler } from 'inspector' ;
6
6
import picoMatch from 'picomatch' ;
7
7
import LruCache from 'lru-cache' ;
8
- import { readFile } from 'node:fs/promises' ;
9
-
10
- import { toFilePath } from './utils.js' ;
8
+ import { readFile , readdir , stat } from 'node:fs/promises' ;
9
+ import { Stats } from 'node:fs' ;
10
+ import { toFilePath , toBrowserPath } from './utils.js' ;
11
11
12
12
type V8Coverage = Profiler . ScriptCoverage ;
13
13
type Matcher = ( test : string ) => boolean ;
@@ -32,11 +32,10 @@ function hasOriginalSource(source: IstanbulSource): boolean {
32
32
typeof source . sourceMap . sourcemap === 'object' &&
33
33
source . sourceMap . sourcemap !== null &&
34
34
Array . isArray ( source . sourceMap . sourcemap . sourcesContent ) &&
35
- source . sourceMap . sourcemap . sourcesContent . length > 0
36
- ) ;
35
+ source . sourceMap . sourcemap . sourcesContent . length > 0 ) ;
37
36
}
38
37
39
- function getMatcher ( patterns ?: string [ ] ) {
38
+ function getMatcher ( patterns ?: string [ ] ) : picoMatch . Matcher {
40
39
if ( ! patterns || patterns . length === 0 ) {
41
40
return ( ) => true ;
42
41
}
@@ -60,63 +59,154 @@ export async function v8ToIstanbul(
60
59
testFiles : string [ ] ,
61
60
coverage : V8Coverage [ ] ,
62
61
userAgent ?: string ,
63
- ) {
62
+ ) : Promise < CoverageMapData > {
64
63
const included = getMatcher ( config ?. coverageConfig ?. include ) ;
65
64
const excluded = getMatcher ( config ?. coverageConfig ?. exclude ) ;
66
65
const istanbulCoverage : CoverageMapData = { } ;
67
66
68
67
for ( const entry of coverage ) {
69
- const url = new URL ( entry . url ) ;
70
- const path = url . pathname ;
71
- if (
72
- // ignore non-http protocols (for exmaple webpack://)
73
- url . protocol . startsWith ( ' http' ) &&
74
- // ignore external urls
75
- url . hostname === config . hostname &&
76
- url . port === ` ${ config . port } ` &&
77
- // ignore non-files
78
- ! ! extname ( path ) &&
79
- // ignore virtual files
80
- ! path . startsWith ( '/__web-test-runner' ) &&
81
- ! path . startsWith ( '/__web-dev-server' )
82
- ) {
83
- try {
68
+ try {
69
+ const url = new URL ( entry . url ) ;
70
+ const path = url . pathname ;
71
+ if (
72
+ // ignore non- http protocols (for exmaple webpack://)
73
+ url . protocol . startsWith ( 'http' ) &&
74
+ // ignore external urls
75
+ url . hostname === config . hostname &&
76
+ url . port === ` ${ config . port } ` &&
77
+ // ignore non-files
78
+ ! ! extname ( path ) &&
79
+ // ignore virtual files
80
+ ! path . startsWith ( '/__web-test-runner' ) &&
81
+ ! path . startsWith ( '/__web-dev-server' )
82
+ ) {
84
83
const filePath = join ( config . rootDir , toFilePath ( path ) ) ;
85
-
86
84
if ( ! testFiles . includes ( filePath ) && included ( filePath ) && ! excluded ( filePath ) ) {
87
85
const browserUrl = `${ url . pathname } ${ url . search } ${ url . hash } ` ;
88
- const cachedSource = cachedSources . get ( browserUrl ) ;
89
- const sources =
90
- cachedSource ??
91
- ( ( await fetchSourceMap ( {
92
- protocol : config . protocol ,
93
- host : config . hostname ,
94
- port : config . port ,
95
- browserUrl,
96
- userAgent,
97
- } ) ) as IstanbulSource ) ;
98
-
99
- if ( ! cachedSource ) {
100
- if ( ! hasOriginalSource ( sources ) ) {
101
- const contents = await readFile ( filePath , 'utf8' ) ;
102
- ( sources as IstanbulSource & { originalSource : string } ) . originalSource = contents ;
103
- }
104
- cachedSources . set ( browserUrl , sources ) ;
105
- }
106
-
107
- const converter = v8toIstanbulLib ( filePath , 0 , sources ) ;
108
- await converter . load ( ) ;
109
-
110
- converter . applyCoverage ( entry . functions ) ;
111
- Object . assign ( istanbulCoverage , converter . toIstanbul ( ) ) ;
86
+ const sources = await getIstanbulSource ( config , filePath , browserUrl , userAgent ) ;
87
+ await addCoverageForFilePath ( sources , filePath , entry , istanbulCoverage ) ;
112
88
}
113
- } catch ( error ) {
114
- console . error ( `Error while generating code coverage for ${ entry . url } .` ) ;
115
- console . error ( error ) ;
89
+ }
90
+ } catch ( error ) {
91
+ console . error ( `Error while generating code coverage for ${ entry . url } .` ) ;
92
+ console . error ( error ) ;
93
+ }
94
+ }
95
+
96
+ return istanbulCoverage ;
97
+ }
98
+
99
+ async function addCoverageForFilePath (
100
+ sources : IstanbulSource ,
101
+ filePath : string ,
102
+ entry : V8Coverage ,
103
+ istanbulCoverage : CoverageMapData ,
104
+ ) : Promise < void > {
105
+ const converter = v8toIstanbulLib ( filePath , 0 , sources ) ;
106
+ await converter . load ( ) ;
107
+
108
+ converter . applyCoverage ( entry . functions ) ;
109
+ Object . assign ( istanbulCoverage , converter . toIstanbul ( ) ) ;
110
+ }
111
+
112
+ async function getIstanbulSource (
113
+ config : TestRunnerCoreConfig ,
114
+ filePath : string ,
115
+ browserUrl : string ,
116
+ userAgent ?: string ,
117
+ doNotAddToCache ?: boolean ,
118
+ ) : Promise < IstanbulSource > {
119
+ const cachedSource = cachedSources . get ( browserUrl ) ;
120
+ const sources =
121
+ cachedSource ??
122
+ ( ( await fetchSourceMap ( {
123
+ protocol : config . protocol ,
124
+ host : config . hostname ,
125
+ port : config . port ,
126
+ browserUrl,
127
+ userAgent,
128
+ } ) ) as IstanbulSource ) ;
129
+
130
+ if ( ! cachedSource ) {
131
+ if ( ! hasOriginalSource ( sources ) ) {
132
+ const contents = await readFile ( filePath , 'utf8' ) ;
133
+ ( sources as IstanbulSource & { originalSource : string } ) . originalSource = contents ;
134
+ }
135
+ ! doNotAddToCache && cachedSources . set ( browserUrl , sources ) ;
136
+ }
137
+ return sources ;
138
+ }
139
+
140
+
141
+ async function recursivelyAddEmptyReports (
142
+ config : TestRunnerCoreConfig ,
143
+ testFiles : string [ ] ,
144
+ include : picoMatch . Matcher ,
145
+ exclude : picoMatch . Matcher ,
146
+ istanbulCoverage : CoverageMapData ,
147
+ dir = '' ,
148
+ ) : Promise < void > {
149
+ const contents = await readdir ( join ( coverageBaseDir , dir ) ) ;
150
+ for ( const file of contents ) {
151
+ const filePath = join ( coverageBaseDir , dir , file ) ;
152
+ if ( ! exclude ( filePath ) ) {
153
+ const stats = await stat ( filePath ) ;
154
+ const relativePath = join ( dir , file ) ;
155
+ if ( stats . isDirectory ( ) ) {
156
+ await recursivelyAddEmptyReports ( config , testFiles , include , exclude , istanbulCoverage , relativePath ) ;
157
+ } else if ( ! testFiles . includes ( filePath ) && include ( filePath ) ) {
158
+ await addEmptyReportIfFileUntouched ( config , istanbulCoverage , filePath , stats , relativePath ) ;
116
159
}
117
160
}
118
161
}
162
+ }
119
163
164
+ async function addEmptyReportIfFileUntouched (
165
+ config : TestRunnerCoreConfig ,
166
+ istanbulCoverage : CoverageMapData ,
167
+ filePath : string ,
168
+ stats : Stats ,
169
+ relativePath : string ,
170
+ ) : Promise < void > {
171
+ try {
172
+ const browserUrl = toBrowserPath ( relativePath ) ;
173
+ const fileHasBeenTouched = cachedSources . find ( ( _ , key ) => {
174
+ return key === browserUrl || key . startsWith ( browserUrl + '?' ) || key . startsWith ( browserUrl + '#' ) ;
175
+ } ) ;
176
+ if ( fileHasBeenTouched ) {
177
+ return ;
178
+ }
179
+ const sources = await getIstanbulSource ( config , filePath , browserUrl , undefined , true ) ;
180
+ const entry = {
181
+ scriptId : browserUrl ,
182
+ url : browserUrl ,
183
+ functions : [ {
184
+ functionName : '(empty-report)' ,
185
+ isBlockCoverage : true ,
186
+ ranges : [ {
187
+ startOffset : 0 ,
188
+ endOffset : stats . size ,
189
+ count : 0
190
+ } ]
191
+ } ]
192
+ } as V8Coverage ;
193
+ await addCoverageForFilePath ( sources , filePath , entry , istanbulCoverage ) ;
194
+ } catch ( error ) {
195
+ console . error ( `Error while generating empty code coverage for ${ filePath } .` ) ;
196
+ console . error ( error ) ;
197
+ }
198
+ }
199
+
200
+ export async function generateEmptyReportsForUntouchedFiles (
201
+ config : TestRunnerCoreConfig ,
202
+ testFiles : string [ ] ,
203
+ ) : Promise < CoverageMapData > {
204
+ const istanbulCoverage : CoverageMapData = { } ;
205
+ if ( config ?. coverageConfig ) {
206
+ const include = getMatcher ( config . coverageConfig . include ) ;
207
+ const exclude = getMatcher ( config . coverageConfig . exclude ) ;
208
+ await recursivelyAddEmptyReports ( config , testFiles , include , exclude , istanbulCoverage ) ;
209
+ }
120
210
return istanbulCoverage ;
121
211
}
122
212
0 commit comments