diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc4b151..0d67b2c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,4 +2,9 @@ The code style and data model are closely connected. Perturb breaks down projects into a series of nested lists and maps over them with it's core functions. As such, the source code of perturb is largely functional, and the functions are designed with mapping and partial application in mind. -First, there is the list of pairs of source files and test files. For each pair, there is the list of AST nodes in the source file. For each node there is a list of zero or more mutators. +Perturb applies a series of steps to transform a project into a list of "alive" mutations. + +Step 1: Generate a list of source file paths and a list of test file paths. +Step 2: Combine the source and test file paths into a single list of source:test pairs (matchFiles) +Step 3: For each pair, traverse the source AST to generate a list of mutation descriptors (handleMatch) +Step 4: Filter mutation descriptors into "alive" mutations by executing them with a test runner (runMutation) \ No newline at end of file diff --git a/Makefile b/Makefile index fc6ec15..3acfb97 100644 --- a/Makefile +++ b/Makefile @@ -8,10 +8,13 @@ test-example: ./node_modules/.bin/_mocha ./example/test/**/*-test.js example: - ./bin/perturb -r ./example + ./bin/perturb -r ./examples/toy-lib example-i: - ./bin/perturb -r ./example -i + ./bin/perturb -r ./examples/toy-lib -i + +events: + ./bin/perturb -r ./examples/event-emitter dogfood: NODE_ENV=testing ./bin/perturb -r ./ -c 'make test' diff --git a/TODO.md b/TODO.md index fa07918..cac3cf0 100644 --- a/TODO.md +++ b/TODO.md @@ -13,4 +13,4 @@ - Example plugging in additional runner - Example plugging in alternate AST parser - +- Dogfooding is great for development, but should implement an example on a node core library (events?) diff --git a/example/.DS_Store b/example/.DS_Store deleted file mode 100644 index 7df42d5..0000000 Binary files a/example/.DS_Store and /dev/null differ diff --git a/examples/event-emitter/.pertrubrc b/examples/event-emitter/.pertrubrc new file mode 100644 index 0000000..1666193 --- /dev/null +++ b/examples/event-emitter/.pertrubrc @@ -0,0 +1,3 @@ +exports.matcher = function (sourceFile, testFile) { + return true; +}; diff --git a/examples/event-emitter/README.md b/examples/event-emitter/README.md new file mode 100644 index 0000000..41a91d3 --- /dev/null +++ b/examples/event-emitter/README.md @@ -0,0 +1,3 @@ +# Node/io.js `EventEmitter` + +This example contains the Node/io.js `events` module and associated tests. \ No newline at end of file diff --git a/examples/event-emitter/common.js b/examples/event-emitter/common.js new file mode 100644 index 0000000..1a56449 --- /dev/null +++ b/examples/event-emitter/common.js @@ -0,0 +1,455 @@ +'use strict'; +var path = require('path'); +var fs = require('fs'); +var assert = require('assert'); +var os = require('os'); +var child_process = require('child_process'); + +exports.testDir = path.dirname(__filename); +exports.fixturesDir = path.join(exports.testDir, 'fixtures'); +exports.libDir = path.join(exports.testDir, '../lib'); +exports.tmpDirName = 'tmp'; +exports.PORT = +process.env.NODE_COMMON_PORT || 12346; +exports.isWindows = process.platform === 'win32'; + +function rimrafSync(p) { + try { + var st = fs.lstatSync(p); + } catch (e) { + if (e.code === 'ENOENT') + return; + } + + try { + if (st && st.isDirectory()) + rmdirSync(p, null); + else + fs.unlinkSync(p); + } catch (e) { + if (e.code === 'ENOENT') + return; + if (e.code === 'EPERM') + return rmdirSync(p, er); + if (e.code !== 'EISDIR') + throw e; + rmdirSync(p, e); + } +} + +function rmdirSync(p, originalEr) { + try { + fs.rmdirSync(p); + } catch (e) { + if (e.code === 'ENOTDIR') + throw originalEr; + if (e.code === 'ENOTEMPTY' || e.code === 'EEXIST' || e.code === 'EPERM') { + fs.readdirSync(p).forEach(function(f) { + rimrafSync(path.join(p, f)); + }); + fs.rmdirSync(p); + } + } +} + +exports.refreshTmpDir = function() { + rimrafSync(exports.tmpDir); + fs.mkdirSync(exports.tmpDir); +}; + +if (process.env.TEST_THREAD_ID) { + // Distribute ports in parallel tests + if (!process.env.NODE_COMMON_PORT) + exports.PORT += +process.env.TEST_THREAD_ID * 100; + + exports.tmpDirName += '.' + process.env.TEST_THREAD_ID; +} +exports.tmpDir = path.join(exports.testDir, exports.tmpDirName); + +var opensslCli = null; +var inFreeBSDJail = null; +var localhostIPv4 = null; + +Object.defineProperty(exports, 'inFreeBSDJail', { + get: function() { + if (inFreeBSDJail !== null) return inFreeBSDJail; + + if (process.platform === 'freebsd' && + child_process.execSync('sysctl -n security.jail.jailed').toString() === + '1\n') { + inFreeBSDJail = true; + } else { + inFreeBSDJail = false; + } + return inFreeBSDJail; + } +}); + +Object.defineProperty(exports, 'localhostIPv4', { + get: function() { + if (localhostIPv4 !== null) return localhostIPv4; + + if (exports.inFreeBSDJail) { + // Jailed network interfaces are a bit special - since we need to jump + // through loops, as well as this being an exception case, assume the + // user will provide this instead. + if (process.env.LOCALHOST) { + localhostIPv4 = process.env.LOCALHOST; + } else { + console.error('Looks like we\'re in a FreeBSD Jail. ' + + 'Please provide your default interface address ' + + 'as LOCALHOST or expect some tests to fail.'); + } + } + + if (localhostIPv4 === null) localhostIPv4 = '127.0.0.1'; + + return localhostIPv4; + } +}); + +// opensslCli defined lazily to reduce overhead of spawnSync +Object.defineProperty(exports, 'opensslCli', {get: function() { + if (opensslCli !== null) return opensslCli; + + if (process.config.variables.node_shared_openssl) { + // use external command + opensslCli = 'openssl'; + } else { + // use command built from sources included in io.js repository + opensslCli = path.join(path.dirname(process.execPath), 'openssl-cli'); + } + + if (process.platform === 'win32') opensslCli += '.exe'; + + var openssl_cmd = child_process.spawnSync(opensslCli, ['version']); + if (openssl_cmd.status !== 0 || openssl_cmd.error !== undefined) { + // openssl command cannot be executed + opensslCli = false; + } + return opensslCli; +}, enumerable: true }); + +Object.defineProperty(exports, 'hasCrypto', {get: function() { + return process.versions.openssl ? true : false; +}}); + +if (process.platform === 'win32') { + exports.PIPE = '\\\\.\\pipe\\libuv-test'; +} else { + exports.PIPE = exports.tmpDir + '/test.sock'; +} + +if (process.env.NODE_COMMON_PIPE) { + exports.PIPE = process.env.NODE_COMMON_PIPE; + // Remove manually, the test runner won't do it + // for us like it does for files in test/tmp. + try { + fs.unlinkSync(exports.PIPE); + } catch (e) { + // Ignore. + } +} + +if (process.platform === 'win32') { + exports.faketimeCli = false; +} else { + exports.faketimeCli = path.join(__dirname, '..', 'tools', 'faketime', 'src', + 'faketime'); +} + +var ifaces = os.networkInterfaces(); +exports.hasIPv6 = Object.keys(ifaces).some(function(name) { + return /lo/.test(name) && ifaces[name].some(function(info) { + return info.family === 'IPv6'; + }); +}); + +var util = require('util'); +for (var i in util) exports[i] = util[i]; +//for (var i in exports) global[i] = exports[i]; + +function protoCtrChain(o) { + var result = []; + for (; o; o = o.__proto__) { result.push(o.constructor); } + return result.join(); +} + +exports.indirectInstanceOf = function(obj, cls) { + if (obj instanceof cls) { return true; } + var clsChain = protoCtrChain(cls.prototype); + var objChain = protoCtrChain(obj); + return objChain.slice(-clsChain.length) === clsChain; +}; + + +exports.ddCommand = function(filename, kilobytes) { + if (process.platform === 'win32') { + var p = path.resolve(exports.fixturesDir, 'create-file.js'); + return '"' + process.argv[0] + '" "' + p + '" "' + + filename + '" ' + (kilobytes * 1024); + } else { + return 'dd if=/dev/zero of="' + filename + '" bs=1024 count=' + kilobytes; + } +}; + + +exports.spawnCat = function(options) { + var spawn = require('child_process').spawn; + + if (process.platform === 'win32') { + return spawn('more', [], options); + } else { + return spawn('cat', [], options); + } +}; + + +exports.spawnSyncCat = function(options) { + var spawnSync = require('child_process').spawnSync; + + if (process.platform === 'win32') { + return spawnSync('more', [], options); + } else { + return spawnSync('cat', [], options); + } +}; + + +exports.spawnPwd = function(options) { + var spawn = require('child_process').spawn; + + if (process.platform === 'win32') { + return spawn('cmd.exe', ['/c', 'cd'], options); + } else { + return spawn('pwd', [], options); + } +}; + +exports.platformTimeout = function(ms) { + if (process.arch !== 'arm') + return ms; + + if (process.config.variables.arm_version === '6') + return 7 * ms; // ARMv6 + + return 2 * ms; // ARMv7 and up. +}; + +var knownGlobals = [setTimeout, + setInterval, + setImmediate, + clearTimeout, + clearInterval, + clearImmediate, + console, + constructor, // Enumerable in V8 3.21. + Buffer, + process, + global]; + +if (global.gc) { + knownGlobals.push(gc); +} + +if (global.DTRACE_HTTP_SERVER_RESPONSE) { + knownGlobals.push(DTRACE_HTTP_SERVER_RESPONSE); + knownGlobals.push(DTRACE_HTTP_SERVER_REQUEST); + knownGlobals.push(DTRACE_HTTP_CLIENT_RESPONSE); + knownGlobals.push(DTRACE_HTTP_CLIENT_REQUEST); + knownGlobals.push(DTRACE_NET_STREAM_END); + knownGlobals.push(DTRACE_NET_SERVER_CONNECTION); +} + +if (global.COUNTER_NET_SERVER_CONNECTION) { + knownGlobals.push(COUNTER_NET_SERVER_CONNECTION); + knownGlobals.push(COUNTER_NET_SERVER_CONNECTION_CLOSE); + knownGlobals.push(COUNTER_HTTP_SERVER_REQUEST); + knownGlobals.push(COUNTER_HTTP_SERVER_RESPONSE); + knownGlobals.push(COUNTER_HTTP_CLIENT_REQUEST); + knownGlobals.push(COUNTER_HTTP_CLIENT_RESPONSE); +} + +if (global.LTTNG_HTTP_SERVER_RESPONSE) { + knownGlobals.push(LTTNG_HTTP_SERVER_RESPONSE); + knownGlobals.push(LTTNG_HTTP_SERVER_REQUEST); + knownGlobals.push(LTTNG_HTTP_CLIENT_RESPONSE); + knownGlobals.push(LTTNG_HTTP_CLIENT_REQUEST); + knownGlobals.push(LTTNG_NET_STREAM_END); + knownGlobals.push(LTTNG_NET_SERVER_CONNECTION); +} + +if (global.ArrayBuffer) { + knownGlobals.push(ArrayBuffer); + knownGlobals.push(Int8Array); + knownGlobals.push(Uint8Array); + knownGlobals.push(Uint8ClampedArray); + knownGlobals.push(Int16Array); + knownGlobals.push(Uint16Array); + knownGlobals.push(Int32Array); + knownGlobals.push(Uint32Array); + knownGlobals.push(Float32Array); + knownGlobals.push(Float64Array); + knownGlobals.push(DataView); +} + +// Harmony features. +if (global.Proxy) { + knownGlobals.push(Proxy); +} + +if (global.Symbol) { + knownGlobals.push(Symbol); +} + +function leakedGlobals() { + var leaked = []; + + for (var val in global) + if (-1 === knownGlobals.indexOf(global[val])) + leaked.push(val); + + return leaked; +}; +exports.leakedGlobals = leakedGlobals; + +// Turn this off if the test should not check for global leaks. +exports.globalCheck = true; + +process.on('exit', function() { + if (!exports.globalCheck) return; + var leaked = leakedGlobals(); + if (leaked.length > 0) { + console.error('Unknown globals: %s', leaked); + assert.ok(false, 'Unknown global found'); + } +}); + + +var mustCallChecks = []; + + +function runCallChecks(exitCode) { + if (exitCode !== 0) return; + + var failed = mustCallChecks.filter(function(context) { + return context.actual !== context.expected; + }); + + failed.forEach(function(context) { + console.log('Mismatched %s function calls. Expected %d, actual %d.', + context.name, + context.expected, + context.actual); + console.log(context.stack.split('\n').slice(2).join('\n')); + }); + + if (failed.length) process.exit(1); +} + + +exports.mustCall = function(fn, expected) { + if (typeof expected !== 'number') expected = 1; + + var context = { + expected: expected, + actual: 0, + stack: (new Error()).stack, + name: fn.name || '' + }; + + // add the exit listener only once to avoid listener leak warnings + if (mustCallChecks.length === 0) process.on('exit', runCallChecks); + + mustCallChecks.push(context); + + return function() { + context.actual++; + return fn.apply(this, arguments); + }; +}; + +exports.checkSpawnSyncRet = function(ret) { + assert.strictEqual(ret.status, 0); + assert.strictEqual(ret.error, undefined); +}; + +var etcServicesFileName = path.join('/etc', 'services'); +if (process.platform === 'win32') { + etcServicesFileName = path.join(process.env.SystemRoot, 'System32', 'drivers', + 'etc', 'services'); +} + +/* + * Returns a string that represents the service name associated + * to the service bound to port "port" and using protocol "protocol". + * + * If the service is not defined in the services file, it returns + * the port number as a string. + * + * Returns undefined if /etc/services (or its equivalent on non-UNIX + * platforms) can't be read. + */ +exports.getServiceName = function getServiceName(port, protocol) { + if (port == null) { + throw new Error('Missing port number'); + } + + if (typeof protocol !== 'string') { + throw new Error('Protocol must be a string'); + } + + /* + * By default, if a service can't be found in /etc/services, + * its name is considered to be its port number. + */ + var serviceName = port.toString(); + + try { + /* + * I'm not a big fan of readFileSync, but reading /etc/services + * asynchronously here would require implementing a simple line parser, + * which seems overkill for a simple utility function that is not running + * concurrently with any other one. + */ + var servicesContent = fs.readFileSync(etcServicesFileName, + { encoding: 'utf8'}); + var regexp = util.format('^(\\w+)\\s+\\s%d/%s\\s', port, protocol); + var re = new RegExp(regexp, 'm'); + + var matches = re.exec(servicesContent); + if (matches && matches.length > 1) { + serviceName = matches[1]; + } + } catch(e) { + console.error('Cannot read file: ', etcServicesFileName); + return undefined; + } + + return serviceName; +}; + +exports.hasMultiLocalhost = function hasMultiLocalhost() { + var TCP = process.binding('tcp_wrap').TCP; + var t = new TCP(); + var ret = t.bind('127.0.0.2', exports.PORT); + t.close(); + return ret === 0; +}; + +exports.isValidHostname = function(str) { + // See http://stackoverflow.com/a/3824105 + var re = new RegExp( + '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])' + + '(\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]))*$'); + + return !!str.match(re) && str.length <= 255; +}; + +exports.fileExists = function(pathname) { + try { + fs.accessSync(pathname); + return true; + } catch (err) { + return false; + } +}; diff --git a/examples/event-emitter/lib/events.js b/examples/event-emitter/lib/events.js new file mode 100644 index 0000000..3ea798b --- /dev/null +++ b/examples/event-emitter/lib/events.js @@ -0,0 +1,425 @@ +'use strict'; + +var domain; + +function EventEmitter() { + EventEmitter.init.call(this); +} +module.exports = EventEmitter; + +// Backwards-compat with node 0.10.x +EventEmitter.EventEmitter = EventEmitter; + +EventEmitter.usingDomains = false; + +EventEmitter.prototype.domain = undefined; +EventEmitter.prototype._events = undefined; +EventEmitter.prototype._maxListeners = undefined; + +// By default EventEmitters will print a warning if more than 10 listeners are +// added to it. This is a useful default which helps finding memory leaks. +EventEmitter.defaultMaxListeners = 10; + +EventEmitter.init = function() { + this.domain = null; + if (EventEmitter.usingDomains) { + // if there is an active domain, then attach to it. + domain = domain || require('domain'); + if (domain.active && !(this instanceof domain.Domain)) { + this.domain = domain.active; + } + } + + if (!this._events || this._events === Object.getPrototypeOf(this)._events) { + this._events = {}; + this._eventsCount = 0; + } + + this._maxListeners = this._maxListeners || undefined; +}; + +// Obviously not all Emitters should be limited to 10. This function allows +// that to be increased. Set to zero for unlimited. +EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { + if (typeof n !== 'number' || n < 0 || isNaN(n)) + throw new TypeError('n must be a positive number'); + this._maxListeners = n; + return this; +}; + +function $getMaxListeners(that) { + if (that._maxListeners === undefined) + return EventEmitter.defaultMaxListeners; + return that._maxListeners; +} + +EventEmitter.prototype.getMaxListeners = function getMaxListeners() { + return $getMaxListeners(this); +}; + +// These standalone emit* functions are used to optimize calling of event +// handlers for fast cases because emit() itself often has a variable number of +// arguments and can be deoptimized because of that. These functions always have +// the same number of arguments and thus do not get deoptimized, so the code +// inside them can execute faster. +function emitNone(handler, isFn, self) { + if (isFn) + handler.call(self); + else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + listeners[i].call(self); + } +} +function emitOne(handler, isFn, self, arg1) { + if (isFn) + handler.call(self, arg1); + else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + listeners[i].call(self, arg1); + } +} +function emitTwo(handler, isFn, self, arg1, arg2) { + if (isFn) + handler.call(self, arg1, arg2); + else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + listeners[i].call(self, arg1, arg2); + } +} +function emitThree(handler, isFn, self, arg1, arg2, arg3) { + if (isFn) + handler.call(self, arg1, arg2, arg3); + else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + listeners[i].call(self, arg1, arg2, arg3); + } +} + +function emitMany(handler, isFn, self, args) { + if (isFn) + handler.apply(self, args); + else { + var len = handler.length; + var listeners = arrayClone(handler, len); + for (var i = 0; i < len; ++i) + listeners[i].apply(self, args); + } +} + +EventEmitter.prototype.emit = function emit(type) { + var er, handler, len, args, i, events, domain; + var needDomainExit = false; + var doError = (type === 'error'); + + events = this._events; + if (events) + doError = (doError && events.error == null); + else if (!doError) + return false; + + domain = this.domain; + + // If there is no 'error' event listener then throw. + if (doError) { + er = arguments[1]; + if (domain) { + if (!er) + er = new Error('Uncaught, unspecified "error" event.'); + er.domainEmitter = this; + er.domain = domain; + er.domainThrown = false; + domain.emit('error', er); + } else if (er instanceof Error) { + throw er; // Unhandled 'error' event + } else { + // At least give some kind of context to the user + var err = new Error('Uncaught, unspecified "error" event. (' + er + ')'); + err.context = er; + throw err; + } + return false; + } + + handler = events[type]; + + if (!handler) + return false; + + if (domain && this !== process) { + domain.enter(); + needDomainExit = true; + } + + var isFn = typeof handler === 'function'; + len = arguments.length; + switch (len) { + // fast cases + case 1: + emitNone(handler, isFn, this); + break; + case 2: + emitOne(handler, isFn, this, arguments[1]); + break; + case 3: + emitTwo(handler, isFn, this, arguments[1], arguments[2]); + break; + case 4: + emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); + break; + // slower + default: + args = new Array(len - 1); + for (i = 1; i < len; i++) + args[i - 1] = arguments[i]; + emitMany(handler, isFn, this, args); + } + + if (needDomainExit) + domain.exit(); + + return true; +}; + +EventEmitter.prototype.addListener = function addListener(type, listener) { + var m; + var events; + var existing; + + if (typeof listener !== 'function') + throw new TypeError('listener must be a function'); + + events = this._events; + if (!events) { + events = this._events = {}; + this._eventsCount = 0; + } else { + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (events.newListener) { + this.emit('newListener', type, + listener.listener ? listener.listener : listener); + + // Re-assign `events` because a newListener handler could have caused the + // this._events to be assigned to a new object + events = this._events; + } + existing = events[type]; + } + + if (!existing) { + // Optimize the case of one listener. Don't need the extra array object. + existing = events[type] = listener; + ++this._eventsCount; + } else { + if (typeof existing === 'function') { + // Adding the second element, need to change to array. + existing = events[type] = [existing, listener]; + } else { + // If we've already got an array, just append. + existing.push(listener); + } + + // Check for listener leak + if (!existing.warned) { + m = $getMaxListeners(this); + if (m && m > 0 && existing.length > m) { + existing.warned = true; + console.error('(node) warning: possible EventEmitter memory ' + + 'leak detected. %d %s listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.', + existing.length, type); + console.trace(); + } + } + } + + return this; +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.once = function once(type, listener) { + if (typeof listener !== 'function') + throw new TypeError('listener must be a function'); + + var fired = false; + + function g() { + this.removeListener(type, g); + + if (!fired) { + fired = true; + listener.apply(this, arguments); + } + } + + g.listener = listener; + this.on(type, g); + + return this; +}; + +// emits a 'removeListener' event iff the listener was removed +EventEmitter.prototype.removeListener = + function removeListener(type, listener) { + var list, events, position, i; + + if (typeof listener !== 'function') + throw new TypeError('listener must be a function'); + + events = this._events; + if (!events) + return this; + + list = events[type]; + if (!list) + return this; + + if (list === listener || (list.listener && list.listener === listener)) { + if (--this._eventsCount === 0) + this._events = {}; + else { + delete events[type]; + if (events.removeListener) + this.emit('removeListener', type, listener); + } + } else if (typeof list !== 'function') { + position = -1; + + for (i = list.length; i-- > 0;) { + if (list[i] === listener || + (list[i].listener && list[i].listener === listener)) { + position = i; + break; + } + } + + if (position < 0) + return this; + + if (list.length === 1) { + list[0] = undefined; + if (--this._eventsCount === 0) { + this._events = {}; + return this; + } else { + delete events[type]; + } + } else { + spliceOne(list, position); + } + + if (events.removeListener) + this.emit('removeListener', type, listener); + } + + return this; + }; + +EventEmitter.prototype.removeAllListeners = + function removeAllListeners(type) { + var listeners, events; + + events = this._events; + if (!events) + return this; + + // not listening for removeListener, no need to emit + if (!events.removeListener) { + if (arguments.length === 0) { + this._events = {}; + this._eventsCount = 0; + } else if (events[type]) { + if (--this._eventsCount === 0) + this._events = {}; + else + delete events[type]; + } + return this; + } + + // emit removeListener for all listeners on all events + if (arguments.length === 0) { + var keys = Object.keys(events); + for (var i = 0, key; i < keys.length; ++i) { + key = keys[i]; + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = {}; + this._eventsCount = 0; + return this; + } + + listeners = events[type]; + + if (typeof listeners === 'function') { + this.removeListener(type, listeners); + } else if (listeners) { + // LIFO order + do { + this.removeListener(type, listeners[listeners.length - 1]); + } while (listeners[0]); + } + + return this; + }; + +EventEmitter.prototype.listeners = function listeners(type) { + var evlistener; + var ret; + var events = this._events; + + if (!events) + ret = []; + else { + evlistener = events[type]; + if (!evlistener) + ret = []; + else if (typeof evlistener === 'function') + ret = [evlistener]; + else + ret = arrayClone(evlistener, evlistener.length); + } + + return ret; +}; + +EventEmitter.listenerCount = function(emitter, type) { + var evlistener; + var ret = 0; + var events = emitter._events; + + if (events) { + evlistener = events[type]; + if (typeof evlistener === 'function') + ret = 1; + else if (evlistener) + ret = evlistener.length; + } + + return ret; +}; + +// About 1.5x faster than the two-arg version of Array#splice(). +function spliceOne(list, index) { + for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) + list[i] = list[k]; + list.pop(); +} + +function arrayClone(arr, i) { + var copy = new Array(i); + while (i--) + copy[i] = arr[i]; + return copy; +} diff --git a/examples/event-emitter/test.js b/examples/event-emitter/test.js new file mode 100644 index 0000000..4c16098 --- /dev/null +++ b/examples/event-emitter/test.js @@ -0,0 +1,16 @@ +require("./test/test-event-emitter-add-listeners.js"); +require("./test/test-event-emitter-check-listener-leaks.js"); +require("./test/test-event-emitter-errors.js"); +require("./test/test-event-emitter-get-max-listeners.js"); +require("./test/test-event-emitter-listeners-side-effects.js"); +require("./test/test-event-emitter-listeners.js"); +require("./test/test-event-emitter-max-listeners.js"); +require("./test/test-event-emitter-method-names.js"); +require("./test/test-event-emitter-modify-in-emit.js"); +require("./test/test-event-emitter-no-error-provided-to-error-event.js"); +require("./test/test-event-emitter-num-args.js"); +require("./test/test-event-emitter-once.js"); +require("./test/test-event-emitter-remove-all-listeners.js"); +require("./test/test-event-emitter-remove-listeners.js"); +require("./test/test-event-emitter-set-max-listeners-side-effects.js"); +require("./test/test-event-emitter-subclass.js"); diff --git a/examples/event-emitter/test/test-event-emitter-add-listeners.js b/examples/event-emitter/test/test-event-emitter-add-listeners.js new file mode 100644 index 0000000..08717e2 --- /dev/null +++ b/examples/event-emitter/test/test-event-emitter-add-listeners.js @@ -0,0 +1,68 @@ +'use strict'; +var common = require('../common'); +var assert = require('assert'); +var events = require('../lib/events'); + +var e = new events.EventEmitter(); + +var events_new_listener_emited = []; +var listeners_new_listener_emited = []; +var times_hello_emited = 0; + +// sanity check +assert.equal(e.addListener, e.on); + +e.on('newListener', function(event, listener) { + if (event === 'newListener') + return; // Don't track our adding of newListener listeners. + console.log('newListener: ' + event); + events_new_listener_emited.push(event); + listeners_new_listener_emited.push(listener); +}); + +function hello(a, b) { + console.log('hello'); + times_hello_emited += 1; + assert.equal('a', a); + assert.equal('b', b); +} +e.once('newListener', function(name, listener) { + assert.equal(name, 'hello'); + assert.equal(listener, hello); + assert.deepEqual(this.listeners('hello'), []); +}); +e.on('hello', hello); + +var foo = function() {}; +e.once('foo', foo); + +console.log('start'); + +e.emit('hello', 'a', 'b'); + + +// just make sure that this doesn't throw: +var f = new events.EventEmitter(); +f.setMaxListeners(0); + + +process.on('exit', function() { + assert.deepEqual(['hello', 'foo'], events_new_listener_emited); + assert.deepEqual([hello, foo], listeners_new_listener_emited); + assert.equal(1, times_hello_emited); +}); + +var listen1 = function listen1() {}; +var listen2 = function listen2() {}; +var e1 = new events.EventEmitter(); +e1.once('newListener', function() { + assert.deepEqual(e1.listeners('hello'), []); + e1.once('newListener', function() { + assert.deepEqual(e1.listeners('hello'), []); + }); + e1.on('hello', listen2); +}); +e1.on('hello', listen1); +// The order of listeners on an event is not always the order in which the +// listeners were added. +assert.deepEqual(e1.listeners('hello'), [listen2, listen1]); diff --git a/examples/event-emitter/test/test-event-emitter-check-listener-leaks.js b/examples/event-emitter/test/test-event-emitter-check-listener-leaks.js new file mode 100644 index 0000000..abbc501 --- /dev/null +++ b/examples/event-emitter/test/test-event-emitter-check-listener-leaks.js @@ -0,0 +1,67 @@ +'use strict'; +var common = require('../common'); +var assert = require('assert'); +var events = require('../lib/events'); + +var e = new events.EventEmitter(); + +// default +for (var i = 0; i < 10; i++) { + e.on('default', function() {}); +} +assert.ok(!e._events['default'].hasOwnProperty('warned')); +e.on('default', function() {}); +assert.ok(e._events['default'].warned); + +// specific +e.setMaxListeners(5); +for (var i = 0; i < 5; i++) { + e.on('specific', function() {}); +} +assert.ok(!e._events['specific'].hasOwnProperty('warned')); +e.on('specific', function() {}); +assert.ok(e._events['specific'].warned); + +// only one +e.setMaxListeners(1); +e.on('only one', function() {}); +assert.ok(!e._events['only one'].hasOwnProperty('warned')); +e.on('only one', function() {}); +assert.ok(e._events['only one'].hasOwnProperty('warned')); + +// unlimited +e.setMaxListeners(0); +for (var i = 0; i < 1000; i++) { + e.on('unlimited', function() {}); +} +assert.ok(!e._events['unlimited'].hasOwnProperty('warned')); + +// process-wide +events.EventEmitter.defaultMaxListeners = 42; +e = new events.EventEmitter(); + +for (var i = 0; i < 42; ++i) { + e.on('fortytwo', function() {}); +} +assert.ok(!e._events['fortytwo'].hasOwnProperty('warned')); +e.on('fortytwo', function() {}); +assert.ok(e._events['fortytwo'].hasOwnProperty('warned')); +delete e._events['fortytwo'].warned; + +events.EventEmitter.defaultMaxListeners = 44; +e.on('fortytwo', function() {}); +assert.ok(!e._events['fortytwo'].hasOwnProperty('warned')); +e.on('fortytwo', function() {}); +assert.ok(e._events['fortytwo'].hasOwnProperty('warned')); + +// but _maxListeners still has precedence over defaultMaxListeners +events.EventEmitter.defaultMaxListeners = 42; +e = new events.EventEmitter(); +e.setMaxListeners(1); +e.on('uno', function() {}); +assert.ok(!e._events['uno'].hasOwnProperty('warned')); +e.on('uno', function() {}); +assert.ok(e._events['uno'].hasOwnProperty('warned')); + +// chainable +assert.strictEqual(e, e.setMaxListeners(1)); diff --git a/examples/event-emitter/test/test-event-emitter-errors.js b/examples/event-emitter/test/test-event-emitter-errors.js new file mode 100644 index 0000000..e2c92c5 --- /dev/null +++ b/examples/event-emitter/test/test-event-emitter-errors.js @@ -0,0 +1,9 @@ +'use strict'; +var EventEmitter = require('../lib/events'); +var assert = require('assert'); + +var EE = new EventEmitter(); + +assert.throws(function() { + EE.emit('error', 'Accepts a string'); +}, /Accepts a string/); diff --git a/examples/event-emitter/test/test-event-emitter-get-max-listeners.js b/examples/event-emitter/test/test-event-emitter-get-max-listeners.js new file mode 100644 index 0000000..fa5bc75 --- /dev/null +++ b/examples/event-emitter/test/test-event-emitter-get-max-listeners.js @@ -0,0 +1,19 @@ +'use strict'; +var common = require('../common'); +var assert = require('assert'); +var EventEmitter = require('../lib/events'); + +var emitter = new EventEmitter(); + +assert.strictEqual(emitter.getMaxListeners(), EventEmitter.defaultMaxListeners); + +emitter.setMaxListeners(0); +assert.strictEqual(emitter.getMaxListeners(), 0); + +emitter.setMaxListeners(3); +assert.strictEqual(emitter.getMaxListeners(), 3); + +// https://github.com/nodejs/io.js/issues/523 - second call should not throw. +var recv = {}; +EventEmitter.prototype.on.call(recv, 'event', function() {}); +EventEmitter.prototype.on.call(recv, 'event', function() {}); diff --git a/examples/event-emitter/test/test-event-emitter-listeners-side-effects.js b/examples/event-emitter/test/test-event-emitter-listeners-side-effects.js new file mode 100644 index 0000000..e0de6d8 --- /dev/null +++ b/examples/event-emitter/test/test-event-emitter-listeners-side-effects.js @@ -0,0 +1,41 @@ +'use strict'; + +var common = require('../common'); +var assert = require('assert'); +var events = require('../lib/events'); + +var EventEmitter = require('events').EventEmitter; +var assert = require('assert'); + +var e = new EventEmitter(); +var fl; // foo listeners + +fl = e.listeners('foo'); +assert(Array.isArray(fl)); +assert(fl.length === 0); +assert.deepEqual(e._events, {}); + +e.on('foo', assert.fail); +fl = e.listeners('foo'); +assert(e._events.foo === assert.fail); +assert(Array.isArray(fl)); +assert(fl.length === 1); +assert(fl[0] === assert.fail); + +e.listeners('bar'); +assert(!e._events.hasOwnProperty('bar')); + +e.on('foo', assert.ok); +fl = e.listeners('foo'); + +assert(Array.isArray(e._events.foo)); +assert(e._events.foo.length === 2); +assert(e._events.foo[0] === assert.fail); +assert(e._events.foo[1] === assert.ok); + +assert(Array.isArray(fl)); +assert(fl.length === 2); +assert(fl[0] === assert.fail); +assert(fl[1] === assert.ok); + +console.log('ok'); diff --git a/examples/event-emitter/test/test-event-emitter-listeners.js b/examples/event-emitter/test/test-event-emitter-listeners.js new file mode 100644 index 0000000..07af24b --- /dev/null +++ b/examples/event-emitter/test/test-event-emitter-listeners.js @@ -0,0 +1,32 @@ +'use strict'; + +var common = require('../common'); +var assert = require('assert'); +var events = require('../lib/events'); + +function listener() {} +function listener2() {} + +var e1 = new events.EventEmitter(); +e1.on('foo', listener); +var fooListeners = e1.listeners('foo'); +assert.deepEqual(e1.listeners('foo'), [listener]); +e1.removeAllListeners('foo'); +assert.deepEqual(e1.listeners('foo'), []); +assert.deepEqual(fooListeners, [listener]); + +var e2 = new events.EventEmitter(); +e2.on('foo', listener); +var e2ListenersCopy = e2.listeners('foo'); +assert.deepEqual(e2ListenersCopy, [listener]); +assert.deepEqual(e2.listeners('foo'), [listener]); +e2ListenersCopy.push(listener2); +assert.deepEqual(e2.listeners('foo'), [listener]); +assert.deepEqual(e2ListenersCopy, [listener, listener2]); + +var e3 = new events.EventEmitter(); +e3.on('foo', listener); +var e3ListenersCopy = e3.listeners('foo'); +e3.on('foo', listener2); +assert.deepEqual(e3.listeners('foo'), [listener, listener2]); +assert.deepEqual(e3ListenersCopy, [listener]); diff --git a/examples/event-emitter/test/test-event-emitter-max-listeners.js b/examples/event-emitter/test/test-event-emitter-max-listeners.js new file mode 100644 index 0000000..3cada21 --- /dev/null +++ b/examples/event-emitter/test/test-event-emitter-max-listeners.js @@ -0,0 +1,33 @@ +'use strict'; +var common = require('../common'); +var assert = require('assert'); +var events = require('../lib/events'); + +var gotEvent = false; + +process.on('exit', function() { + assert(gotEvent); +}); + +var e = new events.EventEmitter(); + +e.on('maxListeners', function() { + gotEvent = true; +}); + +// Should not corrupt the 'maxListeners' queue. +e.setMaxListeners(42); + +assert.throws(function() { + e.setMaxListeners(NaN); +}); + +assert.throws(function() { + e.setMaxListeners(-1); +}); + +assert.throws(function() { + e.setMaxListeners('and even this'); +}); + +e.emit('maxListeners'); diff --git a/examples/event-emitter/test/test-event-emitter-method-names.js b/examples/event-emitter/test/test-event-emitter-method-names.js new file mode 100644 index 0000000..c59f5fa --- /dev/null +++ b/examples/event-emitter/test/test-event-emitter-method-names.js @@ -0,0 +1,13 @@ +'use strict'; +var common = require('../common'); +var assert = require('assert'); +var events = require('../lib/events'); + +var E = events.EventEmitter.prototype; +assert.equal(E.constructor.name, 'EventEmitter'); +assert.equal(E.on, E.addListener); // Same method. +Object.getOwnPropertyNames(E).forEach(function(name) { + if (name === 'constructor' || name === 'on') return; + if (typeof E[name] !== 'function') return; + assert.equal(E[name].name, name); +}); diff --git a/examples/event-emitter/test/test-event-emitter-modify-in-emit.js b/examples/event-emitter/test/test-event-emitter-modify-in-emit.js new file mode 100644 index 0000000..0fa0756 --- /dev/null +++ b/examples/event-emitter/test/test-event-emitter-modify-in-emit.js @@ -0,0 +1,57 @@ +'use strict'; +var common = require('../common'); +var assert = require('assert'); +var events = require('../lib/events'); + +var callbacks_called = []; + +var e = new events.EventEmitter(); + +function callback1() { + callbacks_called.push('callback1'); + e.on('foo', callback2); + e.on('foo', callback3); + e.removeListener('foo', callback1); +} + +function callback2() { + callbacks_called.push('callback2'); + e.removeListener('foo', callback2); +} + +function callback3() { + callbacks_called.push('callback3'); + e.removeListener('foo', callback3); +} + +e.on('foo', callback1); +assert.equal(1, e.listeners('foo').length); + +e.emit('foo'); +assert.equal(2, e.listeners('foo').length); +assert.deepEqual(['callback1'], callbacks_called); + +e.emit('foo'); +assert.equal(0, e.listeners('foo').length); +assert.deepEqual(['callback1', 'callback2', 'callback3'], callbacks_called); + +e.emit('foo'); +assert.equal(0, e.listeners('foo').length); +assert.deepEqual(['callback1', 'callback2', 'callback3'], callbacks_called); + +e.on('foo', callback1); +e.on('foo', callback2); +assert.equal(2, e.listeners('foo').length); +e.removeAllListeners('foo'); +assert.equal(0, e.listeners('foo').length); + +// Verify that removing callbacks while in emit allows emits to propagate to +// all listeners +callbacks_called = []; + +e.on('foo', callback2); +e.on('foo', callback3); +assert.equal(2, e.listeners('foo').length); +e.emit('foo'); +assert.deepEqual(['callback2', 'callback3'], callbacks_called); +assert.equal(0, e.listeners('foo').length); diff --git a/examples/event-emitter/test/test-event-emitter-no-error-provided-to-error-event.js b/examples/event-emitter/test/test-event-emitter-no-error-provided-to-error-event.js new file mode 100644 index 0000000..c50d7cd --- /dev/null +++ b/examples/event-emitter/test/test-event-emitter-no-error-provided-to-error-event.js @@ -0,0 +1,22 @@ +'use strict'; +var common = require('../common'); +var assert = require('assert'); +var events = require('../lib/events'); +var domain = require('domain'); + +var errorCatched = false; + +var e = new events.EventEmitter(); + +var d = domain.create(); +d.add(e); +d.on('error', function(er) { + assert(er instanceof Error, 'error created'); + errorCatched = true; +}); + +e.emit('error'); + +process.on('exit', function() { + assert(errorCatched, 'error got caught'); +}); diff --git a/examples/event-emitter/test/test-event-emitter-num-args.js b/examples/event-emitter/test/test-event-emitter-num-args.js new file mode 100644 index 0000000..8de695a --- /dev/null +++ b/examples/event-emitter/test/test-event-emitter-num-args.js @@ -0,0 +1,26 @@ +'use strict'; +var common = require('../common'); +var assert = require('assert'); +var events = require('../lib/events'); + +var e = new events.EventEmitter(), + num_args_emited = []; + +e.on('numArgs', function() { + var numArgs = arguments.length; + console.log('numArgs: ' + numArgs); + num_args_emited.push(numArgs); +}); + +console.log('start'); + +e.emit('numArgs'); +e.emit('numArgs', null); +e.emit('numArgs', null, null); +e.emit('numArgs', null, null, null); +e.emit('numArgs', null, null, null, null); +e.emit('numArgs', null, null, null, null, null); + +process.on('exit', function() { + assert.deepEqual([0, 1, 2, 3, 4, 5], num_args_emited); +}); diff --git a/examples/event-emitter/test/test-event-emitter-once.js b/examples/event-emitter/test/test-event-emitter-once.js new file mode 100644 index 0000000..63458d6 --- /dev/null +++ b/examples/event-emitter/test/test-event-emitter-once.js @@ -0,0 +1,45 @@ +'use strict'; +var common = require('../common'); +var assert = require('assert'); +var events = require('../lib/events'); + +var e = new events.EventEmitter(); +var times_hello_emited = 0; + +e.once('hello', function(a, b) { + times_hello_emited++; +}); + +e.emit('hello', 'a', 'b'); +e.emit('hello', 'a', 'b'); +e.emit('hello', 'a', 'b'); +e.emit('hello', 'a', 'b'); + +var remove = function() { + assert.fail(1, 0, 'once->foo should not be emitted', '!'); +}; + +e.once('foo', remove); +e.removeListener('foo', remove); +e.emit('foo'); + +process.on('exit', function() { + assert.equal(1, times_hello_emited); +}); + +var times_recurse_emitted = 0; + +e.once('e', function() { + e.emit('e'); + times_recurse_emitted++; +}); + +e.once('e', function() { + times_recurse_emitted++; +}); + +e.emit('e'); + +process.on('exit', function() { + assert.equal(2, times_recurse_emitted); +}); diff --git a/examples/event-emitter/test/test-event-emitter-remove-all-listeners.js b/examples/event-emitter/test/test-event-emitter-remove-all-listeners.js new file mode 100644 index 0000000..c31effe --- /dev/null +++ b/examples/event-emitter/test/test-event-emitter-remove-all-listeners.js @@ -0,0 +1,72 @@ +'use strict'; +var common = require('../common'); +var assert = require('assert'); +var events = require('../lib/events'); + + +function expect(expected) { + var actual = []; + process.on('exit', function() { + assert.deepEqual(actual.sort(), expected.sort()); + }); + function listener(name) { + actual.push(name); + } + return common.mustCall(listener, expected.length); +} + +function listener() {} + +var e1 = new events.EventEmitter(); +e1.on('foo', listener); +e1.on('bar', listener); +e1.on('baz', listener); +e1.on('baz', listener); +var fooListeners = e1.listeners('foo'); +var barListeners = e1.listeners('bar'); +var bazListeners = e1.listeners('baz'); +e1.on('removeListener', expect(['bar', 'baz', 'baz'])); +e1.removeAllListeners('bar'); +e1.removeAllListeners('baz'); +assert.deepEqual(e1.listeners('foo'), [listener]); +assert.deepEqual(e1.listeners('bar'), []); +assert.deepEqual(e1.listeners('baz'), []); +// after calling removeAllListeners, +// the old listeners array should stay unchanged +assert.deepEqual(fooListeners, [listener]); +assert.deepEqual(barListeners, [listener]); +assert.deepEqual(bazListeners, [listener, listener]); +// after calling removeAllListeners, +// new listeners arrays are different from the old +assert.notEqual(e1.listeners('bar'), barListeners); +assert.notEqual(e1.listeners('baz'), bazListeners); + +var e2 = new events.EventEmitter(); +e2.on('foo', listener); +e2.on('bar', listener); +// expect LIFO order +e2.on('removeListener', expect(['foo', 'bar', 'removeListener'])); +e2.on('removeListener', expect(['foo', 'bar'])); +e2.removeAllListeners(); +console.error(e2); +assert.deepEqual([], e2.listeners('foo')); +assert.deepEqual([], e2.listeners('bar')); + +var e3 = new events.EventEmitter(); +e3.on('removeListener', listener); +// check for regression where removeAllListeners throws when +// there exists a removeListener listener, but there exists +// no listeners for the provided event type +assert.doesNotThrow(e3.removeAllListeners.bind(e3, 'foo')); + +var e4 = new events.EventEmitter(); +var expectLength = 2; +e4.on('removeListener', function(name, listener) { + assert.equal(expectLength--, this.listeners('baz').length); +}); +e4.on('baz', function() {}); +e4.on('baz', function() {}); +e4.on('baz', function() {}); +assert.equal(e4.listeners('baz').length, expectLength + 1); +e4.removeAllListeners('baz'); +assert.equal(e4.listeners('baz').length, 0); diff --git a/examples/event-emitter/test/test-event-emitter-remove-listeners.js b/examples/event-emitter/test/test-event-emitter-remove-listeners.js new file mode 100644 index 0000000..23f41c1 --- /dev/null +++ b/examples/event-emitter/test/test-event-emitter-remove-listeners.js @@ -0,0 +1,90 @@ +'use strict'; +var common = require('../common'); +var assert = require('assert'); +var events = require('../lib/events'); + +var count = 0; + +function listener1() { + console.log('listener1'); + count++; +} + +function listener2() { + console.log('listener2'); + count++; +} + +function listener3() { + console.log('listener3'); + count++; +} + +function remove1() { + assert(0); +} + +function remove2() { + assert(0); +} + +var e1 = new events.EventEmitter(); +e1.on('hello', listener1); +e1.on('removeListener', common.mustCall(function(name, cb) { + assert.equal(name, 'hello'); + assert.equal(cb, listener1); +})); +e1.removeListener('hello', listener1); +assert.deepEqual([], e1.listeners('hello')); + +var e2 = new events.EventEmitter(); +e2.on('hello', listener1); +e2.on('removeListener', assert.fail); +e2.removeListener('hello', listener2); +assert.deepEqual([listener1], e2.listeners('hello')); + +var e3 = new events.EventEmitter(); +e3.on('hello', listener1); +e3.on('hello', listener2); +e3.once('removeListener', common.mustCall(function(name, cb) { + assert.equal(name, 'hello'); + assert.equal(cb, listener1); + assert.deepEqual([listener2], e3.listeners('hello')); +})); +e3.removeListener('hello', listener1); +assert.deepEqual([listener2], e3.listeners('hello')); +e3.once('removeListener', common.mustCall(function(name, cb) { + assert.equal(name, 'hello'); + assert.equal(cb, listener2); + assert.deepEqual([], e3.listeners('hello')); +})); +e3.removeListener('hello', listener2); +assert.deepEqual([], e3.listeners('hello')); + +var e4 = new events.EventEmitter(); +e4.on('removeListener', common.mustCall(function(name, cb) { + if (cb !== remove1) return; + this.removeListener('quux', remove2); + this.emit('quux'); +}, 2)); +e4.on('quux', remove1); +e4.on('quux', remove2); +e4.removeListener('quux', remove1); + +var e5 = new events.EventEmitter(); +e5.on('hello', listener1); +e5.on('hello', listener2); +e5.once('removeListener', common.mustCall(function(name, cb) { + assert.equal(name, 'hello'); + assert.equal(cb, listener1); + assert.deepEqual([listener2], e5.listeners('hello')); + e5.once('removeListener', common.mustCall(function(name, cb) { + assert.equal(name, 'hello'); + assert.equal(cb, listener2); + assert.deepEqual([], e5.listeners('hello')); + })); + e5.removeListener('hello', listener2); + assert.deepEqual([], e5.listeners('hello')); +})); +e5.removeListener('hello', listener1); +assert.deepEqual([], e5.listeners('hello')); diff --git a/examples/event-emitter/test/test-event-emitter-set-max-listeners-side-effects.js b/examples/event-emitter/test/test-event-emitter-set-max-listeners-side-effects.js new file mode 100644 index 0000000..262f622 --- /dev/null +++ b/examples/event-emitter/test/test-event-emitter-set-max-listeners-side-effects.js @@ -0,0 +1,10 @@ +'use strict'; +var common = require('../common'); +var assert = require('assert'); +var events = require('../lib/events'); + +var e = new events.EventEmitter(); + +assert.deepEqual(e._events, {}); +e.setMaxListeners(5); +assert.deepEqual(e._events, {}); diff --git a/examples/event-emitter/test/test-event-emitter-subclass.js b/examples/event-emitter/test/test-event-emitter-subclass.js new file mode 100644 index 0000000..a5b9065 --- /dev/null +++ b/examples/event-emitter/test/test-event-emitter-subclass.js @@ -0,0 +1,49 @@ +'use strict'; +var common = require('../common'); +var assert = require('assert'); +var EventEmitter = require('../lib/events').EventEmitter; +var util = require('util'); + +util.inherits(MyEE, EventEmitter); + +function MyEE(cb) { + this.once(1, cb); + this.emit(1); + this.removeAllListeners(); + EventEmitter.call(this); +} + +var called = false; +var myee = new MyEE(function() { + called = true; +}); + + +util.inherits(ErrorEE, EventEmitter); +function ErrorEE() { + this.emit('error', new Error('blerg')); +} + +assert.throws(function() { + new ErrorEE(); +}, /blerg/); + +process.on('exit', function() { + assert(called); + assert.deepEqual(myee._events, {}); + console.log('ok'); +}); + + +function MyEE2() { + EventEmitter.call(this); +} + +MyEE2.prototype = new EventEmitter(); + +var ee1 = new MyEE2(); +var ee2 = new MyEE2(); + +ee1.on('x', function() {}); + +assert.equal(EventEmitter.listenerCount(ee2, 'x'), 0); diff --git a/example/index.js b/examples/toy-lib/index.js similarity index 100% rename from example/index.js rename to examples/toy-lib/index.js diff --git a/example/lib/models/car.js b/examples/toy-lib/lib/models/car.js similarity index 100% rename from example/lib/models/car.js rename to examples/toy-lib/lib/models/car.js diff --git a/example/lib/models/vehicle.js b/examples/toy-lib/lib/models/vehicle.js similarity index 100% rename from example/lib/models/vehicle.js rename to examples/toy-lib/lib/models/vehicle.js diff --git a/example/lib/util/map.js b/examples/toy-lib/lib/util/map.js similarity index 100% rename from example/lib/util/map.js rename to examples/toy-lib/lib/util/map.js diff --git a/example/package.json b/examples/toy-lib/package.json similarity index 100% rename from example/package.json rename to examples/toy-lib/package.json diff --git a/example/test/models/vehicle-test--off.js b/examples/toy-lib/test/models/vehicle-test--off.js similarity index 100% rename from example/test/models/vehicle-test--off.js rename to examples/toy-lib/test/models/vehicle-test--off.js diff --git a/example/test/util/map-test.js b/examples/toy-lib/test/util/map-test.js similarity index 100% rename from example/test/util/map-test.js rename to examples/toy-lib/test/util/map-test.js diff --git a/lib/.DS_Store b/lib/.DS_Store index c79d2bb..39307a3 100644 Binary files a/lib/.DS_Store and b/lib/.DS_Store differ diff --git a/lib/index.js b/lib/index.js index 8a4793e..fc2ccc5 100644 --- a/lib/index.js +++ b/lib/index.js @@ -46,14 +46,14 @@ function main (settings, cb) { }); } -function perturb (settings, cb) { +function perturb (settings, done) { if (typeof settings === JS_TYPES.str) { settings = {rootDir: settings}; } if (typeof settings === JS_TYPES.func) { - cb = settings; + done = settings; settings = null; } @@ -93,7 +93,6 @@ function perturb (settings, cb) { var matches = matchFiles(perturbSourceFiles, perturbTestFiles, config.matcher.bind(config)); - if (matches.length === 0) return cb(new Error(ERRORS.NoMatches)); meta.matchesProcessed = matches.length; @@ -113,12 +112,20 @@ function perturb (settings, cb) { matches: matches, }); + // only this should call done! + function cleanup () { + fs.removeSync(config.perturbDir); + done.apply(null, arguments); + } + + if (matches.length === 0) return cleanup(new Error(ERRORS.NoMatches)); + if (config.verbose) config.configReporter(config); var start = Date.now(); async.mapSeries(matches, handleMatch, function (err, matches) { - if (err) return cb(err); + if (err) return cleanup(err); meta.duration = Date.now() - start; meta.errored = !!err; @@ -126,15 +133,13 @@ function perturb (settings, cb) { meta.killedMutants = pkgUtil.countDeadMutants(matches); meta.killRate = meta.killedMutants / meta.mutationCount; - fs.removeSync(config.perturbDir); - var report = { meta: meta, config: config, matches: matches, }; - cb(null, report); + cleanup(null, report); }); } @@ -148,7 +153,6 @@ function defaultConfig () { sourceGlob: DEFAULT_GLOB, testGlob: DEFAULT_GLOB, matcher: function defaultMatcher (sourceFile, testFile) { - console.log("default", sourceFile, testFile); var sourceName = sourceFile.split(path.join(PERTURB_DIR, this.sourceDir)).pop(); var testName = testFile.split(path.join(PERTURB_DIR, this.testDir)).pop(); return sourceName === testName; diff --git a/lib/mutators_redux.js b/lib/mutators_redux.js index e248dee..8d221e9 100644 --- a/lib/mutators_redux.js +++ b/lib/mutators_redux.js @@ -94,5 +94,5 @@ module.exports = { // return mutators; // } - mutators: mutators -}; \ No newline at end of file + mutators: mutators, +}; diff --git a/lib/runners.js b/lib/runners.js index 37dd4fb..2d5179c 100644 --- a/lib/runners.js +++ b/lib/runners.js @@ -34,19 +34,21 @@ function generateDiff (mutation) { // (infinite loops caused by mutations, for example). // ... however, running them in-process is EXTREMELY FAST relative to using a child -// process. Anecdotal dogfooding on mutators.js (before breaking it into separate -// modules) ran 25-33x faster in-process (observed times between 97-135s for child vs. -// consistent 4s in-process) +// process. Anecdotal dogfooding on mutators.js (before breaking it into separate +// modules) ran 25-35x faster in-process (observed times between 97-135s for child vs. +// consistent 3.5-4s in-process) // child processes have better guarantees around some conditions, but a speed difference // of that degree means it is essential to at least explore running mutations in-process // (perhaps with reduced accuracy/environmental purity guarantees) // TODO explore how to parallelize this. It's unclear how much of the child process -// start up time is just wasted. If we have a simple way to dictate parallelism we +// start up time is just wasted. If we have a simple way to dictate parallelism we // can see experimentally how close we can get child process running to in-process. // (... with the obvious caveat that we could parallelize in-process execution) +// should provide APIs for configuring both in-process and child process test runners + module.exports = { mocha: function (mutation, mutantReporter, done) { @@ -78,7 +80,7 @@ module.exports = { var cmd = "mocha -b " + mutation.testFile; var opts = {timeout: 10000}; console.log("MOCHA CMD", cmd); - var child = exec(cmd, function (err, stdout, stderr) { + var child = exec(cmd, opts, function (err, stdout, stderr) { if (err) { // TODO can we differentiate legitimate test-runner errors from broken stuff // within this library? Running in-process makes it pretty easy (it blows up @@ -89,7 +91,11 @@ module.exports = { done(formatMutation(mutation), mutantReporter); }); - } + + child.on("error", function (err) { + + }); + }; } };