UNPKG

coffee-coverage

Version:

Istanbul and JSCoverage-style instrumentation for CoffeeScript files.

439 lines (374 loc) 17.5 kB
// Generated by CoffeeScript 2.3.2 (function() { //### CoffeeCoverage // JSCoverage-style instrumentation for CoffeeScript files. // By Jason Walton, Benbria var CoverageError, EXTENSIONS, INSTRUMENTORS, NodeWrapper, SkipVisitor, _, assert, coffeeScript, events, excludeFile, factoryDefaults, fs, getInstrumentorClass, mkdirs, path, statFile, util; assert = require('assert'); events = require('events'); fs = require('fs'); util = require('util'); path = require('path'); coffeeScript = require('coffeescript'); _ = require('lodash'); NodeWrapper = require('./NodeWrapper'); ({mkdirs, statFile, excludeFile} = require('./utils/helpers')); ({EXTENSIONS} = require('./constants')); SkipVisitor = require('./SkipVisitor'); exports.INSTRUMENTORS = INSTRUMENTORS = { jscoverage: require('./instrumentors/JSCoverage'), istanbul: require('./instrumentors/Istanbul') }; CoverageError = class CoverageError extends Error { constructor(message) { super(); this.message = message; this.name = "CoverageError"; Error.captureStackTrace(this, CoverageError); } }; // Default options. factoryDefaults = { exclude: [], recursive: true, bare: false, instrumentor: 'jscoverage' }; exports.getInstrumentorClass = getInstrumentorClass = function(instrumentorName) { var instrumentor; instrumentor = INSTRUMENTORS[instrumentorName]; if (!instrumentor) { throw new Error(`Invalid instrumentor ${instrumentorName}. Valid options are: ${Object.keys(INSTRUMENTORS).join(', ')}`); } return instrumentor; }; exports.CoverageInstrumentor = (function() { var getEffectiveOptions, validateSrcDest, writeToFile; //### CoverageInstrumentor // Instruments .coffee files to provide code-coverage data. class CoverageInstrumentor extends events.EventEmitter { //### Create a new CoverageInstrumentor // For a list of available options see `@instrument`. constructor(options = {}) { super(); this.defaultOptions = _.defaults({}, options, factoryDefaults); _.defaults(this.defaultOptions, getInstrumentorClass(this.defaultOptions.instrumentor).getDefaultOptions()); } //### Instrument a file or directory. // This calls @instrumentFile or @instrumentDirectory, depending on whether "source" is // a file or directory respectively. // * `options.coverageVar` gives the name of the global variable to use to store // coverage data in. This defaults to '_$jscoverage' to be compatible with // JSCoverage. // * `options.recursive` controls whether or not this will descend recursively into // subdirectories. This defaults to true. // * `options.exclude` is an array of files to ignore. instrumentDirectory will // not instrument a file if it is in this list, nor will it recursively traverse // into a directory if it is in this list. This defaults to []. // Note that this field is case sensitive! // * `options.basePath` if provided, then all excludes will be evaluated relative // to this base path. For example, if `options.exclude` is `['a/b']`, and // `options.basePath` is "/Users/jwalton/myproject", then this will prevent // coffeeCoverage from traversing "/Users/jwalton/myproject/a/b". `basePath` // will also be stripped from the front of any files when generating names. // * `options.initFileStream` is a stream to which all global initialization will be // written to via `initFileStream.write(data)`. // * `options.log` should be a `{debug(), info(), warn(), error()}` object, where each is a function // that takes multiple parameters and logs them (similar to `console.log()`.) // * `options.instrumentor` is the name of the instrumentor to use (see `INSTURMENTORS`.) // All options passed in will be passed along to the instrumentor implementation, so // instrumentor-specific options may be added to `options` as well. // Throws CoverageError if there is a problem with the `source` or `out` parameters. instrument(source, out, options = {}) { var sourceStat; ({sourceStat} = validateSrcDest(source, out)); if (sourceStat.isFile()) { return this.instrumentFile(source, out, options); } else if (sourceStat.isDirectory()) { return this.instrumentDirectory(source, out, options); } else { throw new CoverageError(`Can't instrument ${source}.`); } } // Return the output file name for a given input file name. // e.g. `getOutputFileName('foo.coffee') # => 'foo.js'` getOutputFileName(fileName) { var coffee_extension, ext, outFile; if (fileName == null) { return null; } outFile = fileName; for (coffee_extension in EXTENSIONS) { ext = EXTENSIONS[coffee_extension]; if (_.endsWith(fileName.toLowerCase(), coffee_extension)) { outFile = fileName.slice(0, +(-(coffee_extension.length + 1)) + 1 || 9e9) + ext.js_extension; break; } } return outFile; } //### Instrument a directory. // This finds all .coffee files in the specified `sourceDirectory`, and writes instrumented // files into `outDirectory`. `outDirectory` will be created if it does not already exist. // For a list of available options see `@instrument`. // Emits an "instrumentingDirectory" event before doing any work, with the names of the source // and out directories. The directory names are guaranteed to end in path.sep. Emits a // "skip" event for any files which are skipped because they are in the `options.exclude` list. // Throws CoverageError if there is a problem with the `sourceDirectory` or `outDirectory` // parameters. // Returns an object consisting of: // - `lines` - the total number of instrumented lines. // Returns null if `sourceDirectory` does not exist. instrumentDirectory(sourceDirectory, outDirectory, options = {}) { var answer, coffee_extension, effectiveOptions, file, inst, j, len, outDirectoryStat, outFile, outputDirectoryExists, processed, ref, ref1, ref2, sourceDirectoryMode, sourceDirectoryStat, sourceFile, sourceStat; // Turn the source directory into an absolute path sourceDirectory = path.resolve(sourceDirectory); sourceDirectoryStat = statFile(sourceDirectory); if (!sourceDirectoryStat) { return null; } this.emit("instrumentingDirectory", sourceDirectory, outDirectory); effectiveOptions = getEffectiveOptions(options, this.defaultOptions); effectiveOptions.basePath = effectiveOptions.basePath ? path.resolve(effectiveOptions.basePath) : sourceDirectory; answer = { lines: 0 }; validateSrcDest(sourceDirectory, outDirectory); if (!_.endsWith(sourceDirectory, path.sep)) { sourceDirectory += path.sep; } sourceDirectoryMode = sourceDirectoryStat.mode; if (outDirectory) { if (!_.endsWith(outDirectory, path.sep)) { outDirectory += path.sep; } // Check to see if the output directory exists outDirectoryStat = statFile(outDirectory); outputDirectoryExists = !!outDirectoryStat; } ref = fs.readdirSync(sourceDirectory); // Instrument every file in the directory for (j = 0, len = ref.length; j < len; j++) { file = ref[j]; sourceFile = sourceDirectory + file; if (excludeFile(sourceFile, effectiveOptions)) { this.emit("skip", sourceDirectory + file); continue; } sourceStat = statFile(sourceFile); if (!sourceStat) { // Can happen if file or folder is deleted while we're instrumenting. // Also, see https://github.com/benbria/coffee-coverage/issues/54. continue; } outFile = outDirectory ? outDirectory + file : null; if (effectiveOptions.recursive && sourceStat.isDirectory()) { inst = this.instrumentDirectory(sourceFile, outFile, effectiveOptions); answer.lines += (ref1 = inst != null ? inst.lines : void 0) != null ? ref1 : 0; } else { processed = false; for (coffee_extension in EXTENSIONS) { // TODO: Make this work for streamline files. if (coffee_extension === '._coffee') { continue; } if (_.endsWith(file.toLowerCase(), coffee_extension) && sourceStat.isFile()) { // lazy-create the output directory. if ((outDirectory != null) && !outputDirectoryExists) { mkdirs(outDirectory, sourceDirectoryMode); outputDirectoryExists = true; } // Replace the ".(lit)coffee(.md)" extension with a ".js" extension outFile = this.getOutputFileName(outFile); inst = this.instrumentFile(sourceFile, outFile, effectiveOptions); answer.lines += (ref2 = inst != null ? inst.lines : void 0) != null ? ref2 : 0; processed = true; break; } } } } return answer; } //### Instrument a .coffee file. // Same as `@instrumentCoffee` but takes a file name instead of file data. // Emits an "instrumentingFile" event with the name of the input and output file. // * `outFile` is optional; if present then the compiled JavaScript will be written out to this // file. // * `options.fileName` is the fileName to use in the generated instrumentation. // For other options, see `@instrumentCoffee` and `@instrument`. // Throws CoverageError if there is a problem with the `sourceFile` or `outFile` parameters. instrumentFile(sourceFile, outFile = null, options = {}) { var answer, data, effectiveOptions; this.emit("instrumentingFile", sourceFile, outFile); effectiveOptions = getEffectiveOptions(options, this.defaultOptions); validateSrcDest(sourceFile, outFile); data = fs.readFileSync(sourceFile, 'utf8'); answer = this.instrumentCoffee(path.resolve(sourceFile), data, effectiveOptions); if (outFile) { writeToFile(outFile, answer.init + answer.js); } return answer; } //### Instrument a .coffee file. // Parameters: // * `fileName` is the name of the file. This should be an absolute path. // * `source` is the contents of the coffee file. // * `options.fileName` - if rpresent, this will be the filename passed to the instrumentor. // Otherwise the absolute path will be passed. // * If `options.usedFileNameMap` is present, it must be an object. This method will add a // mapping from the absolute file path to the short filename in usedFileNameMap. If the name // of the file is already in usedFileNameMap then this method will generate a unique name. // If `options.usedFileNames` is present, it must be an array - this is the deprecated version // of `usedFileNameMap`. // * If `options.initFileStream` is present, then all global initialization will be written // to `initFileStream.write()`, in addition to being returned. // Returns an object consisting of: // * `init` - the intialization JavaScript code. // * `js` - the compiled JavaScript, instrumented to collect coverage data. // * `lines` - the total number of instrumented lines. instrumentCoffee(fileName, source, options = {}) { var effectiveOptions, instrumentor, instrumentorConstructor, ref, ref1, result; effectiveOptions = getEffectiveOptions(options, this.defaultOptions); if ((ref = effectiveOptions.log) != null) { ref.info(`Instrumenting ${fileName}`); } instrumentorConstructor = getInstrumentorClass(effectiveOptions.instrumentor); instrumentor = new instrumentorConstructor(fileName, source, effectiveOptions); result = exports._runInstrumentor(instrumentor, fileName, source, effectiveOptions); if ((ref1 = effectiveOptions.initFileStream) != null) { ref1.write(result.init); } return result; } }; // Write a string to a file. writeToFile = function(outFile, content) { return fs.writeFileSync(outFile, content); }; // Some basic valication of source and out files. validateSrcDest = function(source, out) { var outStat, sourceStat; sourceStat = statFile(source); outStat = out ? statFile(out) : null; if (!sourceStat) { throw new CoverageError(`Source file ${source} does not exist.`); } if (outStat) { if (sourceStat.isFile() && outStat.isDirectory()) { throw new CoverageError(`Refusing to overwrite directory ${out} with file.`); } if (sourceStat.isDirectory() && outStat.isFile()) { throw new CoverageError(`Refusing to overwrite file ${out} with directory.`); } } return {sourceStat, outStat}; }; getEffectiveOptions = function(options = {}, defaultOptions) { return _.defaults({}, options, defaultOptions); }; return CoverageInstrumentor; }).call(this); // Runs an instrumentor on some source code. // * `instrumentor` an instance of an instrumentor class to run on. // * `fileName` the absolute path of the source file. // * `source` a string containing the sourcecode the instrument. // * `options.bare` true if we should compile bare coffeescript (no enclosing function). // * `options.log` log object. exports._runInstrumentor = function(instrumentor, fileName, source, options = {}) { var answer, ast, coffeeOptions, err, init, js, ref, ref1, runVisitor, token, tokens, wrappedAST; assert(instrumentor, "instrumentor"); try { // Compile coffee to nodes. if ((ref = options.log) != null) { if (typeof ref.debug === "function") { ref.debug(`Instrumenting ${fileName}`); } } coffeeOptions = { bare: (ref1 = options.bare) != null ? ref1 : false, literate: /\.(litcoffee|coffee\.md)$/.test(fileName) }; tokens = coffeeScript.tokens(source, coffeeOptions); // collect referenced variables coffeeOptions.referencedVars = _.uniq((function() { var j, len, results; results = []; for (j = 0, len = tokens.length; j < len; j++) { token = tokens[j]; if (token[0] === 'IDENTIFIER') { results.push(token[1]); } } return results; })()); // convert tokens to ast ast = coffeeScript.nodes(tokens); } catch (error) { err = error; throw new CoverageError(`Could not parse ${fileName}: ${err.stack}`); } runVisitor = function(visitor, nodeWrapper) { var __, i, indent, j, len, name, ref2, ref3, ref4; // Ignore code that we generated. if ((ref2 = nodeWrapper.node.coffeeCoverage) != null ? ref2.generated : void 0) { return; } if (((ref3 = options.log) != null ? ref3.debug : void 0) != null) { indent = ((function() { var j, ref4, results; results = []; for (i = j = 0, ref4 = nodeWrapper.depth; (0 <= ref4 ? j < ref4 : j > ref4); i = 0 <= ref4 ? ++j : --j) { results.push(" "); } return results; })()).join(''); options.log.debug(`${indent}Examining ${nodeWrapper.toString()}`); } if (nodeWrapper.node.comments) { if (typeof visitor["visitComment"] === "function") { visitor["visitComment"](nodeWrapper); } } if (nodeWrapper.isStatement) { if (typeof visitor["visitStatement"] === "function") { visitor["visitStatement"](nodeWrapper); } } if (typeof visitor[name = `visit${nodeWrapper.type}`] === "function") { visitor[name](nodeWrapper); } if (nodeWrapper.isSwitchCases) { ref4 = nodeWrapper.node; for (i = j = 0, len = ref4.length; j < len; i = ++j) { __ = ref4[i]; nodeWrapper.forEachChildOfType(i, function(child) { return runVisitor(visitor, child); }); } } // Recurse into child nodes return nodeWrapper.forEachChild(function(child) { return runVisitor(visitor, child); }); }; wrappedAST = new NodeWrapper(ast); runVisitor(new SkipVisitor(fileName), wrappedAST); runVisitor(instrumentor, wrappedAST); init = instrumentor.getInitString(); try { // Compile the instrumented CoffeeScript and write it to the JS file. js = ast.compile(coffeeOptions); } catch (error) { err = error; /* !pragma coverage-skip-block */ throw new CoverageError(`Could not compile ${fileName} after instrumenting: ${err.stack}`); } answer = { init: init, js: js, lines: instrumentor.getInstrumentedLineCount() }; return answer; }; }).call(this);