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 ,
2023} ) ;
2124
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