/** * This task instruments our source code with istanbul, runs the test suite * on the instrumented source and collects the coverage data. It then creates * test coverage reports. * * TODO This can be improved in style. We should possibly rewrite it and use * async.waterfall. */ var fs = require('fs'); var istanbul = require('istanbul'); var wrench = require('wrench'); var path = require('path'); var glob = require('glob'); var runTestsuite = require('./test').runTests; // setup some pathes var dir = path.join(__dirname, '../src'); var backupDir = path.join(__dirname, '../src-backup'); var instrumentedDir = path.join(__dirname, '../src-instrumented'); var coverageDir = path.join(__dirname, '../coverage'); // The main players in the coverage generation via istanbul var instrumenter = new istanbul.Instrumenter(); var reporter = new istanbul.Reporter(false, coverageDir); var collector = new istanbul.Collector(); // General options used for the resource shuffling / directory copying var copyOpts = { // Whether to overwrite existing directory or not forceDelete: true, // Whether to copy hidden Unix files or not (preceding .) excludeHiddenUnix: false, // If we're overwriting something and the file already exists, keep the // existing preserveFiles: false, // Preserve the mtime and atime when copying files preserveTimestamps: true, // Whether to follow symlinks or not when copying files inflateSymlinks: false }; /** * A small utility method printing out log messages. */ var log = function(msg){ process.stdout.write(msg + '\n'); }; /** * A utility method to recursively delete a non-empty folder. * * See http://www.geedew.com/remove-a-directory-that-is-not-empty-in-nodejs/ * adjusted to use path.join */ var deleteFolderRecursive = function(p) { if( fs.existsSync(p) ) { fs.readdirSync(p).forEach(function(file,index){ var curPath = path.join(p, file); if(fs.lstatSync(curPath).isDirectory()) { // recurse deleteFolderRecursive(curPath); } else { // delete file fs.unlinkSync(curPath); } }); fs.rmdirSync(p); } }; /** * Creates folders for backup and instrumentation and copies the contents of the * current src folder into them. */ var setupBackupAndInstrumentationDir = function(){ if (!fs.existsSync(backupDir)) { log('• create directory for backup of src: ' + backupDir); fs.mkdirSync(backupDir); } if (!fs.existsSync(instrumentedDir)) { log('• create directory for instrumented src: ' + instrumentedDir); fs.mkdirSync(instrumentedDir); } log('• copy src files to backup folder'); wrench.copyDirSyncRecursive(dir, backupDir, copyOpts); log('• copy src files to instrumentation folder'); wrench.copyDirSyncRecursive(dir, instrumentedDir, copyOpts); }; /** * Reverts the changes done in setupBackupAndInstrumentationDir, copies the * backup over the src directory and removes the instrumentation and backup * directory. */ var revertBackupAndInstrumentationDir = function(){ log('• copy original src back to src folder'); wrench.copyDirSyncRecursive(backupDir, dir, copyOpts); log('• delete backup directory'); deleteFolderRecursive(backupDir); log('• delete instrumentation directory'); deleteFolderRecursive(instrumentedDir); }; /** * Callback for when runTestsuite() has finished. */ var collectAndWriteCoverageData = function(code) { log('• collect data from coverage.json'); var coverageFile = path.join(__dirname,'../coverage/coverage.json'); var coverageJson = JSON.parse(fs.readFileSync(coverageFile, 'utf8')); collector.add(coverageJson); reporter.addAll(['lcovonly','html']); revertBackupAndInstrumentationDir(); log('• write report from collected data'); reporter.write(collector, true, function () { process.exit(0); }); }; /** * Will instrument all JavaScript files that are passed as second parameter. * This is the callback to the glob call. */ var foundAllJavaScriptSourceFiles = function(err, files) { if (err) { process.stderr.write(err.message + '\n'); process.exit(1); } log('• instrumenting every src file'); var cnt = 0; files.forEach(function(file) { cnt++; var content = fs.readFileSync(file, 'utf-8'); // derive output file name from input file name, by replacing the *last* // occurence of /src/ by /src-instrumented/ var re = new RegExp('/src/', 'g'); var m, match; while ((m = re.exec(file)) !== null) { match = m; } var outfile = file.substr(0, match.index) + '/src-instrumented/' + file.substr(match.index + '/src/'.length); var instrumented = instrumenter.instrumentSync(content, file); fs.writeFileSync(outfile, instrumented); if (cnt % 10 === 0) { log(' • instrumented ' + cnt + ' files'); } }); log(' • done. ' + cnt + ' files instrumented'); log('• copy instrumented src back to src folder'); wrench.copyDirSyncRecursive(instrumentedDir, dir, copyOpts); log('• run test suite on instrumented code'); runTestsuite(true, collectAndWriteCoverageData); }; /** * Our main method, first it sets up certain directory, and then it starts the * coverage process by gathering all JavaScript files and then instrumenting * them. */ var main = function(){ setupBackupAndInstrumentationDir(); glob(dir + '/**/*.js', {}, foundAllJavaScriptSourceFiles); }; if (require.main === module) { main(); } module.exports = main;