Skip to content

Commit a6316c6

Browse files
committed
Setup code coverage test
1 parent bca7758 commit a6316c6

File tree

2 files changed

+219
-20
lines changed

2 files changed

+219
-20
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@
112112
"mocha": "^2.3.3",
113113
"@types/node": "^6.0.40",
114114
"@types/mocha": "^2.2.32",
115+
"glob": "^7.1.1",
116+
"istanbul": "^0.4.5",
117+
"remap-istanbul": "^0.8.4",
115118
"tslint": "^4.0.2"
116119
}
117120
}

test/index.ts

Lines changed: 216 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,218 @@
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

Comments
 (0)