1
- //
2
- // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING
3
- //
4
- // This file is providing the test runner to use when running extension tests.
5
- // By default the test runner in use is Mocha based.
6
- //
7
- // You can provide your own test runner if you want to override it by exporting
8
- // a function run(testRoot: string, clb: (error:Error) => void) that the extension
9
- // host can call to run the tests. The test runner is expected to use console.log
10
- // to report the results back to the caller. When the tests are finished, return
11
- // a possible error to the callback or null if none.
12
-
13
- let testRunner = require ( 'vscode/lib/testrunner' ) ;
14
-
15
- // You can directly control Mocha options by uncommenting the following lines
16
- // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info
17
- testRunner . configure ( {
18
- ui : 'tdd' , // the TDD UI is being used in extension.test.ts (suite, test, etc.)
19
- useColors : true // colored output from test results
1
+ "use strict" ;
2
+
3
+ import * as fs from "fs" ;
4
+ import * as glob from "glob" ;
5
+ import * as paths from "path" ;
6
+
7
+ const istanbul = require ( "istanbul" ) ;
8
+ const Mocha = require ( "mocha" ) ;
9
+ const remapIstanbul = require ( "remap-istanbul" ) ;
10
+
11
+ // Linux: prevent a weird NPE when mocha on Linux requires the window size from the TTY
12
+ // Since we are not running in a tty environment, we just implementt he method statically
13
+ const tty = require ( "tty" ) ;
14
+ if ( ! tty . getWindowSize ) {
15
+ tty . getWindowSize = ( ) : number [ ] => {
16
+ return [ 80 , 75 ] ;
17
+ } ;
18
+ }
19
+
20
+ let mocha = new Mocha ( {
21
+ ui : "tdd" ,
22
+ useColors : true ,
20
23
} ) ;
21
24
22
- module . exports = testRunner ;
25
+ function configure ( mochaOpts ) : void {
26
+ mocha = new Mocha ( mochaOpts ) ;
27
+ }
28
+ exports . configure = configure ;
29
+
30
+ function _mkDirIfExists ( dir : string ) : void {
31
+ if ( ! fs . existsSync ( dir ) ) {
32
+ fs . mkdirSync ( dir ) ;
33
+ }
34
+ }
35
+
36
+ function _readCoverOptions ( testsRoot : string ) : ITestRunnerOptions {
37
+ let coverConfigPath = paths . join ( testsRoot , ".." , ".." , "coverconfig.json" ) ;
38
+ let coverConfig : ITestRunnerOptions = undefined ;
39
+ if ( fs . existsSync ( coverConfigPath ) ) {
40
+ let configContent = fs . readFileSync ( coverConfigPath , "utf-8" ) ;
41
+ coverConfig = JSON . parse ( configContent ) ;
42
+ }
43
+ return coverConfig ;
44
+ }
45
+
46
+ function run ( testsRoot , clb ) : any {
47
+ // Enable source map support
48
+ require ( "source-map-support" ) . install ( ) ;
49
+
50
+ // Read configuration for the coverage file
51
+ let coverOptions : ITestRunnerOptions = _readCoverOptions ( testsRoot ) ;
52
+ if ( coverOptions && coverOptions . enabled ) {
53
+ // Setup coverage pre-test, including post-test hook to report
54
+ let coverageRunner = new CoverageRunner ( coverOptions , testsRoot , clb ) ;
55
+ coverageRunner . setupCoverage ( ) ;
56
+ }
57
+
58
+ // Glob test files
59
+ glob ( "**/**.test.js" , { cwd : testsRoot } , ( error , files ) : any => {
60
+ if ( error ) {
61
+ return clb ( error ) ;
62
+ }
63
+ try {
64
+ // Fill into Mocha
65
+ files . forEach ( ( f ) : Mocha => {
66
+ return mocha . addFile ( paths . join ( testsRoot , f ) ) ;
67
+ } ) ;
68
+ // Run the tests
69
+ let failureCount = 0 ;
70
+
71
+ mocha . run ( )
72
+ . on ( "fail" , ( test , err ) : void => {
73
+ failureCount ++ ;
74
+ } )
75
+ . on ( "end" , ( ) : void => {
76
+ clb ( undefined , failureCount ) ;
77
+ } ) ;
78
+ } catch ( error ) {
79
+ return clb ( error ) ;
80
+ }
81
+ } ) ;
82
+ }
83
+ exports . run = run ;
84
+
85
+ interface ITestRunnerOptions {
86
+ enabled ?: boolean ;
87
+ relativeCoverageDir : string ;
88
+ relativeSourcePath : string ;
89
+ ignorePatterns : string [ ] ;
90
+ includePid ?: boolean ;
91
+ reports ?: string [ ] ;
92
+ verbose ?: boolean ;
93
+ }
94
+
95
+ class CoverageRunner {
96
+
97
+ private coverageVar : string = "$$cov_" + new Date ( ) . getTime ( ) + "$$" ;
98
+ private transformer : any = undefined ;
99
+ private matchFn : any = undefined ;
100
+ private instrumenter : any = undefined ;
101
+
102
+ constructor ( private options : ITestRunnerOptions , private testsRoot : string , private endRunCallback : any ) {
103
+ if ( ! options . relativeSourcePath ) {
104
+ return endRunCallback ( "Error - relativeSourcePath must be defined for code coverage to work" ) ;
105
+ }
106
+
107
+ }
108
+
109
+ public setupCoverage ( ) : void {
110
+ // Set up Code Coverage, hooking require so that instrumented code is returned
111
+ let self = this ;
112
+ self . instrumenter = new istanbul . Instrumenter ( { coverageVariable : self . coverageVar } ) ;
113
+ let sourceRoot = paths . join ( self . testsRoot , self . options . relativeSourcePath ) ;
114
+
115
+ // Glob source files
116
+ let srcFiles = glob . sync ( "**/**.js" , {
117
+ cwd : sourceRoot ,
118
+ ignore : self . options . ignorePatterns ,
119
+ } ) ;
120
+
121
+ // Create a match function - taken from the run-with-cover.js in istanbul.
122
+ let decache = require ( "decache" ) ;
123
+ let fileMap = { } ;
124
+ srcFiles . forEach ( ( file ) => {
125
+ let fullPath = paths . join ( sourceRoot , file ) ;
126
+ fileMap [ fullPath ] = true ;
127
+
128
+ // On Windows, extension is loaded pre-test hooks and this mean we lose
129
+ // our chance to hook the Require call. In order to instrument the code
130
+ // we have to decache the JS file so on next load it gets instrumented.
131
+ // This doesn"t impact tests, but is a concern if we had some integration
132
+ // tests that relied on VSCode accessing our module since there could be
133
+ // some shared global state that we lose.
134
+ decache ( fullPath ) ;
135
+ } ) ;
136
+
137
+ self . matchFn = ( file ) : boolean => { return fileMap [ file ] ; } ;
138
+ self . matchFn . files = Object . keys ( fileMap ) ;
139
+
140
+ // Hook up to the Require function so that when this is called, if any of our source files
141
+ // are required, the instrumented version is pulled in instead. These instrumented versions
142
+ // write to a global coverage variable with hit counts whenever they are accessed
143
+ self . transformer = self . instrumenter . instrumentSync . bind ( self . instrumenter ) ;
144
+ let hookOpts = { verbose : false , extensions : [ ".js" ] } ;
145
+ istanbul . hook . hookRequire ( self . matchFn , self . transformer , hookOpts ) ;
146
+
147
+ // initialize the global variable to stop mocha from complaining about leaks
148
+ global [ self . coverageVar ] = { } ;
149
+
150
+ // Hook the process exit event to handle reporting
151
+ // Only report coverage if the process is exiting successfully
152
+ process . on ( "exit" , ( code ) => {
153
+ self . reportCoverage ( ) ;
154
+ } ) ;
155
+ }
156
+
157
+ /**
158
+ * Writes a coverage report. Note that as this is called in the process exit callback, all calls must be synchronous.
159
+ *
160
+ * @returns {void }
161
+ *
162
+ * @memberOf CoverageRunner
163
+ */
164
+ public reportCoverage ( ) : void {
165
+ let self = this ;
166
+ istanbul . hook . unhookRequire ( ) ;
167
+ let cov : any ;
168
+ if ( typeof global [ self . coverageVar ] === "undefined" || Object . keys ( global [ self . coverageVar ] ) . length === 0 ) {
169
+ console . error ( "No coverage information was collected, exit without writing coverage information" ) ;
170
+ return ;
171
+ } else {
172
+ cov = global [ self . coverageVar ] ;
173
+ }
174
+
175
+ // TODO consider putting this under a conditional flag
176
+ // Files that are not touched by code ran by the test runner is manually instrumented, to
177
+ // illustrate the missing coverage.
178
+ self . matchFn . files . forEach ( ( file ) => {
179
+ if ( ! cov [ file ] ) {
180
+ self . transformer ( fs . readFileSync ( file , "utf-8" ) , file ) ;
181
+
182
+ // When instrumenting the code, istanbul will give each FunctionDeclaration a value of 1 in coverState.s,
183
+ // presumably to compensate for function hoisting. We need to reset this, as the function was not hoisted,
184
+ // as it was never loaded.
185
+ Object . keys ( self . instrumenter . coverState . s ) . forEach ( ( key ) => {
186
+ self . instrumenter . coverState . s [ key ] = 0 ;
187
+ } ) ;
188
+
189
+ cov [ file ] = self . instrumenter . coverState ;
190
+ }
191
+ } ) ;
192
+
193
+ // TODO Allow config of reporting directory with
194
+ let reportingDir = paths . join ( self . testsRoot , self . options . relativeCoverageDir ) ;
195
+ let includePid = self . options . includePid ;
196
+ let pidExt = includePid ? ( "-" + process . pid ) : "" ;
197
+ let coverageFile = paths . resolve ( reportingDir , "coverage" + pidExt + ".json" ) ;
198
+
199
+ _mkDirIfExists ( reportingDir ) ; // yes, do this again since some test runners could clean the dir initially created
200
+
201
+ fs . writeFileSync ( coverageFile , JSON . stringify ( cov ) , "utf8" ) ;
202
+
203
+ let remappedCollector = remapIstanbul . remap ( cov , { warn : warning => {
204
+ // We expect some warnings as any JS file without a typescript mapping will cause this.
205
+ // By default, we"ll skip printing these to the console as it clutters it up
206
+ if ( self . options . verbose ) {
207
+ console . warn ( warning ) ;
208
+ }
209
+ } } ) ;
210
+
211
+ let reporter = new istanbul . Reporter ( undefined , reportingDir ) ;
212
+ let reportTypes = ( self . options . reports instanceof Array ) ? self . options . reports : [ "lcov" ] ;
213
+ reporter . addAll ( reportTypes ) ;
214
+ reporter . write ( remappedCollector , true , ( ) => {
215
+ console . log ( `reports written to ${ reportingDir } ` ) ;
216
+ } ) ;
217
+ }
218
+ }
0 commit comments