Created
May 26, 2012 13:05
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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