Skip to content

Instantly share code, notes, and snippets.

@bijoutrouvaille
Created May 26, 2012 13:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bijoutrouvaille/2793871 to your computer and use it in GitHub Desktop.
Save bijoutrouvaille/2793871 to your computer and use it in GitHub Desktop.
A far from perfect hack to implement debugging of jasmine suites on NodeJs
/**
* Original Author: https://github.com/mhevery
* Mangled for debug support by: Bijou Trouvaille
* The original MIT License can be found here
* https://github.com/mhevery/jasmine-node/blob/master/LICENSE
*/
(function () {
var Path = require ( 'path' );
var _ = require ('underscore');
var util;
try {
util = require ( 'util' )
}
catch ( e ) {
util = require ( 'sys' )
}
var fs = require ( 'fs' );
var exec = require ( 'child_process' ).exec;
var spawn = require('child_process' ).spawn;
var Runner = function (opt, params) {
this.init(opt, params)
};
_.extend(Runner.prototype, {
init: function (opt, params) {
opt = opt || {};
params = params || opt;
var self = this, my = this;
this.updatedFiles = {}; // key=directory, val=path to file
this.times = {}; // key=file path, value=modified timestamp
this.runHolds = 0; // prevents test runs
this.specFolder = ''; // set in options as in the original runner
this.baseArgv = []; // a collection of arguments forwarded to child process with some additions
// decouple everything
this.process = params.process || process;
this.fs = params.fs || fs;
this.util = params.util || util;
this.exec = params.exec || exec;
this.spawn = params.spawn || spawn;
this.require = params.require || require;
this.callbacks = {}; // event=>callback
this.pathToJasmineCli = params.pathToJasmineCli
|| Path.join ( Path.dirname (require.resolve('jasmine-node')) , 'cli.js' );
this.options = opt;
this.params = params;
_.each(opt.on||[], function (v) {
this.on (v[0],v[1])
}, this)
this.createEnv (function () {
// trigger the first change manually to run and index everything
self.handleChange ( my.specFolder );
});
}, holdRun: function ( hold, silentRelease ) {
if (hold===true) {
++this.runHolds
} else if (hold===false && this.runHolds) {
--this.runHolds || silentRelease || this.run_batched_changes()
}
return this.runHolds
}, run_batched_changes : function () {
var self = this, my = this;
var names = _.reduce ( this.updatedFiles, function ( m, v ) {
return m.concat ( v )
}, [] );
if ( !names.length || this.holdRun () ) {
return;
}
var matchExpr = names.join ( '|' );
var childArgv;
(names.length==_.size(my.times))
&& (childArgv = this.baseArgv)
|| (childArgv = [].concat ( this.baseArgv, ['--match', '"' + matchExpr + '"'] ));
_.each ( this.updatedFiles, function ( v, k ) {
this[k] = []
}, this.updatedFiles );
this.holdRun ( true );
this.run_external ( childArgv, function ( code ) {
self.trigger ( 'clear', { specNames : names, argv : childArgv } );
// if there are no previous hold, which there
// probably aren't, this will re-execute, to observe any changes
// since the start of this run
self.holdRun ( false );
} );
}, watchDir: function ( dir ) {
var self = this, my = this;
dir && my.options.autotest && my.fs.watch ( dir, function () {
self.handleChange (dir)
} )
}, handleChange : function ( dir ) {
var self = this, my = this;
self.holdRun (true);
dir = dir.replace(/\/$/,'');
my.updatedFiles[dir]===undefined && self.watchDir ( dir);
// This command requires pearl, but otherwise its compatible with any POSIX implementation.
// It fetches a LF delimited list of directory files in a format
// "modified_timestamp<TAB>[d || f]<TAB>absolute_path"
var listFilesCommand =
'find '+dir+' -maxdepth 1 -exec perl -e \'print ((stat($_))[9],"\\t",((-d $_)?"d":"f"), "\\t$_\\n") for @ARGV\' {} + | sort -n'
;
this.exec ( listFilesCommand, function ( err, stout, stin ) {
//require('fs' ).writeFileSync('tmp/d_'+ dir.replace(this.process.cwd(),'./').replace(/\//g,'_' )+'.txt',listFilesCommand+"\n"+stout,'utf-8');
var updatedDirFiles=[];
var lines = (stout || '').trim ().split ( '\n' ),
i, len, line, split, file, time, matchExpr, type
;
for (i in lines) {
line = lines[i];
if (!line) continue;
split = line.split ( '\t' ); // 3 fields delimited by tabs
file = Path.normalize ( split.pop ().trim () ).replace(/\/$/,''); // path to file or directory
if (split.length < 2) {
//require('eyes').inspect(split, "split");
if (!Path.existsSync(split[1])) {
if (split[0]=='f') {
delete my.times[file];
} else if (split[0]=='d') {
delete my.updatedFiles[file];
}
}
continue;
}
time = split.shift ().trim (); // timestamp
type = split.shift ().trim (); // d or f for file or directory
if (type=='d' && my.updatedFiles[file]===undefined) { // only recur if this is an un-indexed directory
file==dir || self.handleChange (file);
} else if ((/\.spec\.js$/i).test(file)) { // only relevant files
// has this file been indexed since it last changed or at all?
if ( !my.times[file] || my.times[file] != time ) {
updatedDirFiles.push ( self.specName ( file ) )
}
my.times[file] = time;
}
}
my.updatedFiles[dir] = updatedDirFiles;
self.holdRun (false); // will trigger the run if there are no more holds
} )
}, specName: function (file) {
var match = Path.basename(file, Path.extname(file)) + ".*";
match = match.replace(new RegExp("spec", "i"), "");
return match;
}, run_external: function (args, callback) {
var self = this, my = this;
/*var child = this.spawn(this.process.execPath, args);
var self = this, my = this;
child.stdout.on('data', function(data) {
my.process.stdout.write(data);
});
child.stderr.on('data', function(data) {
my.process.stderr.write(data);
});
if(typeof callback == 'function') {
child.on('exit', callback);
}*/
var regExpSpec;
var i = args.indexOf('--match');
var fileE = ~i && ('('+args[i+1].replace(/"/g,'')+')') || '';
regExpSpec = new RegExp ( fileE + (this.options.matchAll ? "" : "spec\\.") + "(" + 'js' + ")$", 'i' )
this.jasmine || this.loadJasmineModules();
this.jasmine.executeSpecsInFolder ( this.specFolder,
function (runner, log) {
self.clearModuleCache ( );
self.loadJasmineModules ( );
callback();
},
my.options.isVerbose,
my.options.showColors,
my.options.teamcity,
my.options.useRequireJs,
regExpSpec,
my.options.junitreport );
}, loadJasmineModules: function ( ) {
this.jasmine = require('jasmine-node');
for ( var key in this.jasmine ) {
global[key] = this.jasmine[key];
}
}, clearModuleCache: function () {
for ( var i in this.require.cache ) {
if ( !~i.indexOf('underscore')) delete this.require.cache[i]
}
}, makeForwardingArgs: function () {
var forwardingArgs = [];
var filename = Path.basename(__filename);
var process = this.process;
var arg;
for ( var i = 0; i < process.argv.length; i++ ) {
arg = process.argv[i];
if (
arg !== '--autotest' // not --autotest
&& /^--/.test(arg[0]) // only options
&& !~arg.indexOf('--debug')
) {
forwardingArgs.push ( arg );
}
}
return forwardingArgs
}, createEnv: function (cb) {
var self = this, my = this;
this.parseScriptArgv();
my.baseArgv = self.makeForwardingArgs ();
my.baseArgv.splice(0,0,this.pathToJasmineCli, my.specFolder);
my.baseArgv.push('--forceexit')
my.debugPort && my.baseArgv.splice ( 0, 0, '--debug-brk=' + my.debugPort );
cb && cb()
}, parseScriptArgv: function () {
var args = this.process.argv.slice(1);
var isVerbose = false;
var showColors = true;
var teamcity = this.process.env.TEAMCITY_PROJECT_NAME || false;
var useRequireJs = false;
var match = '.';
var matchall = false;
var useHelpers = true;
var forceExit = false;
var extension = "js";
var junitreport = {
report: false,
savePath : "./reports/",
useDotNotation: true,
consolidate: true
}
while ( args.length ) {
var arg = args.shift ();
switch ( arg ) {
case '--color':
showColors = true;
break;
case '--autotest':
this.options.autotest = true;
break;
case '--noColor':
case '--nocolor':
showColors = false;
break;
case '--verbose':
isVerbose = true;
break;
case '--coffee':
require ( 'coffee-script' );
extension = "js|coffee";
break;
case '-m':
case '--match':
match = args.shift ();
break;
case '--matchall':
matchall = true;
break;
case '--junitreport':
junitreport.report = true;
break;
case '--output':
junitreport.savePath = args.shift ();
break;
case '--teamcity':
teamcity = true;
break;
case '--selftest':
break;
case '--runWithRequireJs':
useRequireJs = true;
break;
case '--nohelpers':
useHelpers = false;
break;
case '--test-dir':
var dir = args.shift ();
if ( !Path.existsSync ( dir ) ) {
throw new Error ( "Test root path '" + dir + "' doesn't exist!" );
}
this.specFolder = dir; // NOTE: Does not look from current working directory.
break;
case '--forceexit':
forceExit = true;
break;
case '-h':
help ();
break;
default:
if ( arg.indexOf ( '--debug-port=' ) == 0) {
this.debugPort = arg.replace ( '--debug-port=', '' )
}
else if ( arg.match ( /^--/ ) ) {
help ();
}
else if ( arg.match ( /^\/.*/ ) ) {
this.specFolder = arg;
} else {
this.specFolder = Path.join ( this.process.cwd (), arg );
}
break;
}
}
this.options.isVerbose = this.options.isVerbose || isVerbose;
this.options.showColors = this.options.showColors || showColors;
this.options.teamcity = this.options.teamcity || teamcity;
this.options.useRequireJs = this.options.useRequireJs || useRequireJs;
this.options.junitreport = this.options.junitreport || junitreport ;
this.options.matchAll = this.options.junitreport || matchall;
},on: function (event, callback) {
this.callbacks[event] = this.callbacks[event] || [];
this.callbacks[event].push(callback);
},trigger: function (event, data) {
_.each (this.callbacks[event]||[], function(cb) {
cb(data)
} )
}, off: function (event, fn) {
if (!fn) {
delete this.callbacks[event]
} else {
_.each ( this.callbacks || [], function ( cb, i ) {
cb === fn && this.splice ( i, 1 )
}, this.callbacks )
}
}
});
var Public = function (opt,params) {
var runner = new Runner ( opt, params );
this.on = function ( event, cb ) {
runner.on ( event, cb )
};
this.off = function ( event, fn ) {
runner.off ( event, fn )
}
};
module.exports = Public;
if (!~process.argv.indexOf('--self-test')) {
(new Public())
}
function help() {
util.print([
'USAGE: jasmine-node [--color|--noColor] [--verbose] [--coffee] directory'
, ''
, 'Options:'
, ' --autotest - rerun automatically the specs when a file changes'
, ' --color - use color coding for output'
, ' --noColor - do not use color coding for output'
, ' -m, --match REGEXP - load only specs containing "REGEXPspec"'
, ' --matchall - relax requirement of "spec" in spec file names'
, ' --verbose - print extra information per each test run'
, ' --coffee - load coffee-script which allows execution .coffee files'
, ' --junitreport - export tests results as junitreport xml format'
, ' --output - defines the output folder for junitreport files'
, ' --teamcity - converts all console output to teamcity custom test runner commands. (Normally auto detected.)'
, ' --runWithRequireJs - loads all specs using requirejs instead of node\'s native require method'
, ' --test-dir - the absolute root directory path where tests are located'
, ' --nohelpers - does not load helpers.'
, ' --forceexit - force exit once tests complete.'
, ' -h, --help - display this help and exit'
, ''
].join("\n"));
process.exit(-1);
}
} () );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment