diff options
Diffstat (limited to 'src/test/index.ts')
| -rw-r--r-- | src/test/index.ts | 277 |
1 files changed, 63 insertions, 214 deletions
diff --git a/src/test/index.ts b/src/test/index.ts index 5dfa541..89f953b 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1,220 +1,69 @@ -"use strict"; - -import * as fs from "fs"; import * as glob from "glob"; -import * as paths from "path"; - -import istanbul = require("istanbul"); -import remapIstanbul = require("remap-istanbul"); -// tslint:disable-next-line:no-var-requires -const tty = require("tty"); -// tslint:disable-next-line:no-var-requires -const Mocha = require("mocha"); - -// Linux: prevent a weird NPE when mocha on Linux requires the window size from the TTY -// Since we are not running in a tty environment, we just implementt he method statically -if (!tty.getWindowSize) { - tty.getWindowSize = (): number[] => { - return [80, 75]; - }; -} - -let mocha = new Mocha({ - ui: "tdd", - useColors: true, -}); - -function configure(mochaOpts): void { - mocha = new Mocha(mochaOpts); -} -exports.configure = configure; - -function _mkDirIfExists(dir: string): void { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); - } -} - -function _readCoverOptions(testsRoot: string): ITestRunnerOptions { - const coverConfigPath = paths.join(testsRoot, "..", "..", "coverconfig.json"); - let coverConfig: ITestRunnerOptions; - if (fs.existsSync(coverConfigPath)) { - const configContent = fs.readFileSync(coverConfigPath, "utf-8"); - coverConfig = JSON.parse(configContent); - } - return coverConfig; -} - -function run(testsRoot, clb): any { - // Enable source map support - require("source-map-support").install(); - - // Read configuration for the coverage file - const coverOptions: ITestRunnerOptions = _readCoverOptions(testsRoot); - if (coverOptions && coverOptions.enabled) { - // Setup coverage pre-test, including post-test hook to report - const coverageRunner = new CoverageRunner(coverOptions, testsRoot, clb); - coverageRunner.setupCoverage(); - } - - // Glob test files - glob("**/**.test.js", { cwd: testsRoot }, (error, files): any => { - if (error) { - return clb(error); - } - try { - // Fill into Mocha - files.forEach((f): Mocha => { - return mocha.addFile(paths.join(testsRoot, f)); - }); - // Run the tests - let failureCount = 0; - - mocha.run() - .on("fail", (test, err): void => { - failureCount++; - }) - .on("end", (): void => { - clb(undefined, failureCount); - }); - } catch (error) { - return clb(error); - } +import * as Mocha from "mocha"; +import * as path from "path"; + +function setupNyc() { + const NYC = require("nyc"); + // create an nyc instance, config here is the same as your package.json + const nyc = new NYC({ + cache: false, + cwd: path.join(__dirname, "..", ".."), + exclude: [ + "**/**.test.js", + ], + extension: [ + ".ts", + ".tsx", + ], + hookRequire: true, + hookRunInContext: true, + hookRunInThisContext: true, + instrument: true, + reporter: ["text", "lcov", "cobertura"], + require: [ + "ts-node/register", + "source-map-support/register", + ], + sourceMap: true, }); + nyc.reset(); + nyc.wrap(); + return nyc; } -exports.run = run; - -interface ITestRunnerOptions { - enabled?: boolean; - relativeCoverageDir: string; - relativeSourcePath: string; - ignorePatterns: string[]; - includePid?: boolean; - reports?: string[]; - verbose?: boolean; -} - -class CoverageRunner { - - private coverageVar: string = "$$cov_" + new Date().getTime() + "$$"; - private transformer: any = undefined; - private matchFn: any = undefined; - private instrumenter: any = undefined; - - constructor(private options: ITestRunnerOptions, private testsRoot: string, private endRunCallback: any) { - if (!options.relativeSourcePath) { - return endRunCallback("Error - relativeSourcePath must be defined for code coverage to work"); - } - - } - public setupCoverage(): void { - // Set up Code Coverage, hooking require so that instrumented code is returned - this.instrumenter = new istanbul.Instrumenter({ coverageVariable: this.coverageVar }); - const sourceRoot = paths.join(this.testsRoot, this.options.relativeSourcePath); - - // Glob source files - const srcFiles = glob.sync("**/**.js", { - cwd: sourceRoot, - ignore: this.options.ignorePatterns, - }); - - // Create a match function - taken from the run-with-cover.js in istanbul. - const decache = require("decache"); - const fileMap = {}; - srcFiles.forEach( (file) => { - const fullPath = paths.join(sourceRoot, file); - fileMap[fullPath] = true; - - // On Windows, extension is loaded pre-test hooks and this mean we lose - // our chance to hook the Require call. In order to instrument the code - // we have to decache the JS file so on next load it gets instrumented. - // This doesn"t impact tests, but is a concern if we had some integration - // tests that relied on VSCode accessing our module since there could be - // some shared global state that we lose. - decache(fullPath); - }); - - this.matchFn = (file): boolean => fileMap[file]; - this.matchFn.files = Object.keys(fileMap); - - // Hook up to the Require function so that when this is called, if any of our source files - // are required, the instrumented version is pulled in instead. These instrumented versions - // write to a global coverage variable with hit counts whenever they are accessed - this.transformer = this.instrumenter.instrumentSync.bind(this.instrumenter); - const hookOpts = { verbose: false, extensions: [".js"]}; - istanbul.hook.hookRequire(this.matchFn, this.transformer, hookOpts); - - // initialize the global variable to stop mocha from complaining about leaks - global[this.coverageVar] = {}; - - // Hook the process exit event to handle reporting - // Only report coverage if the process is exiting successfully - process.on("exit", (code) => { - this.reportCoverage(); - }); - } - - /** - * Writes a coverage report. - * Note that as this is called in the process exit callback, all calls must be synchronous. - * - * @returns {void} - * - * @memberOf CoverageRunner - */ - public reportCoverage(): void { - istanbul.hook.unhookRequire(); - let cov: any; - if (typeof global[this.coverageVar] === "undefined" || Object.keys(global[this.coverageVar]).length === 0) { - // tslint:disable:no-console - console.error("No coverage information was collected, exit without writing coverage information"); - return; - } else { - cov = global[this.coverageVar]; - } - - // TODO consider putting this under a conditional flag - // Files that are not touched by code ran by the test runner is manually instrumented, to - // illustrate the missing coverage. - this.matchFn.files.forEach( (file) => { - if (!cov[file]) { - this.transformer(fs.readFileSync(file, "utf-8"), file); - - // When instrumenting the code, istanbul will give each FunctionDeclaration a value of 1 in - // coverState.s, presumably to compensate for function hoisting. We need to reset this, as the function - // was not hoisted, as it was never loaded. - Object.keys(this.instrumenter.coverState.s).forEach( (key) => { - this.instrumenter.coverState.s[key] = 0; - }); - - cov[file] = this.instrumenter.coverState; - } - }); - - // TODO Allow config of reporting directory with - const reportingDir = paths.join(this.testsRoot, this.options.relativeCoverageDir); - const includePid = this.options.includePid; - const pidExt = includePid ? ("-" + process.pid) : ""; - const coverageFile = paths.resolve(reportingDir, "coverage" + pidExt + ".json"); - - // yes, do this again since some test runners could clean the dir initially created - _mkDirIfExists(reportingDir); - fs.writeFileSync(coverageFile, JSON.stringify(cov), "utf8"); - - const remappedCollector = remapIstanbul.remap(cov, {warn: (warning) => { - // We expect some warnings as any JS file without a typescript mapping will cause this. - // By default, we"ll skip printing these to the console as it clutters it up - if (this.options.verbose) { - console.warn(warning); - } - }}); - - const reporter = new istanbul.Reporter(undefined, reportingDir); - const reportTypes = (this.options.reports instanceof Array) ? this.options.reports : ["lcov"]; - reporter.addAll(reportTypes); - reporter.write(remappedCollector, true, () => { - console.log(`reports written to ${reportingDir}`); +export function run(): Promise<void> { + // Create the mocha test + const mocha = new Mocha({ + ui: "tdd", + }); + + const nyc = setupNyc(); + const testsRoot = path.resolve(__dirname, "."); + + return new Promise((c, e) => { + glob("**/**.test.js", { cwd: testsRoot }, (err, files) => { + if (err) { + return e(err); + } + + // Add files to the test suite + files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); + + try { + // Run the mocha test + mocha.run((failures) => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } }); - } + } catch (err) { + e(err); + } finally { + nyc.writeCoverageFile(); + nyc.report(); + } + }); + }); } |