commit a95c0156336086796ad795f2c9c91d1b426646e5 Author: csausdev Date: Sun Jan 17 12:08:28 2010 +1100 First commit. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d042c6 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# log4js-node + +This is a conversion of the [log4js](http://log4js.berlios.de/index.html) +framework to work with [node](http://nodejs.org). I've mainly stripped out the browser-specific code +and tidied up some of the javascript. + +## tests + +Run the tests with `node tests.js`. They use the awesome [jspec](http://visionmedia.github.com/jspec) - 3.1.3 + +## usage + +See example.js: + + var log4js = require('log4js-node'); + log4js.addAppender(consoleAppender()); + log4js.addAppender(fileAppender('logs/cheese.log'), 'cheese'); + + var logger = log4js.getLogger('cheese'); + logger.setLevel('ERROR'); + + logger.trace('Entering cheese testing'); + logger.debug('Got cheese.'); + logger.info('Cheese is Gouda.'); + logger.warn('Cheese is quite smelly.'); + logger.error('Cheese is too ripe!'); + logger.fatal('Cheese was breeding ground for listeria.'); + +Output + \[2010-01-17 11:43:37.987\] \[ERROR\] cheese - Cheese is too ripe! + \[2010-01-17 11:43:37.990\] \[FATAL\] cheese - Cheese was breeding ground for listeria. + + +## configuration + +You can either configure the appenders and log levels manually (as above), or provide a +configuration file (`log4js.configure('path/to/file.json')`). An example file can be found +in spec/fixtures/log4js.json + +## todo + +I need to make a RollingFileAppender, which will do log rotation. + +patternLayout has no tests. This is mainly because I haven't found a use for it yet, +and am not entirely sure what it was supposed to do. It is more-or-less intact from +the original log4js. + +## author (of this node version) + +Gareth Jones (csausdev - gareth.jones@sensis.com.au) + +## License + +The original log4js was distributed under the Apache 2.0 License, and so is this. I've tried to +keep the original copyright and author credits in place, except in sections that I have rewritten +extensively. diff --git a/example.js b/example.js new file mode 100644 index 0000000..b15fdfb --- /dev/null +++ b/example.js @@ -0,0 +1,13 @@ +var log4js = require('./lib/log4js-node'); +log4js.addAppender(log4js.consoleAppender()); +log4js.addAppender(log4js.fileAppender('cheese.log'), 'cheese'); + +var logger = log4js.getLogger('cheese'); +logger.setLevel('ERROR'); + +logger.trace('Entering cheese testing'); +logger.debug('Got cheese.'); +logger.info('Cheese is Gouda.'); +logger.warn('Cheese is quite smelly.'); +logger.error('Cheese is too ripe!'); +logger.fatal('Cheese was breeding ground for listeria.'); diff --git a/lib/log4js-node.js b/lib/log4js-node.js new file mode 100644 index 0000000..ac95d99 --- /dev/null +++ b/lib/log4js-node.js @@ -0,0 +1,603 @@ +var posix = require('posix'), sys = require('sys'); +var DEFAULT_CATEGORY = '[default]'; +var ALL_CATEGORIES = '[all]'; + +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*jsl:option explicit*/ + +/** + * @fileoverview log4js is a library to log in JavaScript in similar manner + * than in log4j for Java. The API should be nearly the same. + * + * This file contains all log4js code and is the only file required for logging. + * + *

Example:

+ *
+ *  var logging = require('log4js-node');
+ *  //add an appender that logs all messages to stdout.
+ *  logging.addAppender(logging.consoleAppender());
+ *  //add an appender that logs "some-category" to a file
+ *  logging.addAppender(logging.fileAppender("file.log"), "some-category");
+ *  //get a logger
+ *  var log = logging.getLogger("some-category"); 
+ *  log.setLevel(logging.levels.TRACE); //set the Level
+ * 
+ *  ...
+ * 
+ *  //call the log
+ *  log.trace("trace me" );
+ * 
+ * + * @version 1.0 + * @author Stephan Strittmatter - http://jroller.com/page/stritti + * @author Seth Chisamore - http://www.chisamore.com + * @since 2005-05-20 + * @static + * Website: http://log4js.berlios.de + */ +var log4js = { + + /** + * Current version of log4js-node. + * @static + * @final + */ + version: "0.1.0", + + /** + * Date of logger initialized. + * @static + * @final + */ + applicationStartDate: new Date(), + + /** + * Hashtable of loggers. + * @static + * @final + * @private + */ + loggers: {}, + + appenders: {}, + + /** + * Get a logger instance. Instance is cached on categoryName level. + * @param {String} categoryName name of category to log to. + * @return {Logger} instance of logger for the category + * @static + */ + getLogger: function(categoryName) { + + // Use default logger if categoryName is not specified or invalid + if (!(typeof categoryName == "string")) { + categoryName = DEFAULT_CATEGORY; + } + + var self = this, appenderList; + if (!this.loggers[categoryName]) { + // Create the logger for this name if it doesn't already exist + this.loggers[categoryName] = new Logger(categoryName); + if (this.appenders[categoryName]) { + appenderList = this.appenders[categoryName]; + appenderList.forEach(function(appender) { + self.loggers[categoryName].addListener("log", appender); + }); + } + if (this.appenders[ALL_CATEGORIES]) { + appenderList = this.appenders[ALL_CATEGORIES]; + appenderList.forEach(function(appender) { + self.loggers[categoryName].addListener("log", appender); + }); + } + } + + return this.loggers[categoryName]; + }, + + /** + * Get the default logger instance. + * @return {Logger} instance of default logger + * @static + */ + getDefaultLogger: function() { + return this.getLogger(DEFAULT_CATEGORY); + }, + + addAppender: function (appender, categoryName) { + var category = categoryName || ALL_CATEGORIES; + if (!this.appenders[category]) { + this.appenders[category] = []; + } + this.appenders[category].push(appender); + + if (category === ALL_CATEGORIES) { + for (var logger in this.loggers) { + if (this.loggers.hasOwnProperty(logger)) { + this.loggers[logger].addListener("log", appender); + } + } + } else if (this.loggers[category]) { + this.loggers[category].addListener("log", appender); + } + + }, + + clearAppenders: function() { + this.appenders = []; + for (var logger in this.loggers) { + if (this.loggers.hasOwnProperty(logger)) { + this.loggers[logger].listeners.length = 0; + } + } + }, + + configure: function(configurationFile) { + var config; + posix + .cat(configurationFile) + .addCallback( + function(contents) { config = JSON.parse(contents); } + ).wait(); + + configureAppenders(config.appenders); + configureLevels(config.levels); + }, + + levels: { + ALL: new Level(Number.MIN_VALUE, "ALL"), + TRACE: new Level(5000, "TRACE"), + DEBUG: new Level(10000, "DEBUG"), + INFO: new Level(20000, "INFO"), + WARN: new Level(30000, "WARN"), + ERROR: new Level(40000, "ERROR"), + FATAL: new Level(50000, "FATAL"), + OFF: new Level(Number.MAX_VALUE, "OFF") + } + +}; + +var appenderMakers = { + "file": function(config) { + var layout; + if (config.layout) { + layout = layoutMakers[config.layout.type](config.layout); + } + return fileAppender(config.filename, layout); + }, + "console": function(config) { + var layout; + if (config.layout) { + layout = layoutMakers[config.layout.type](config.layout); + } + return consoleAppender(layout); + } +}; + +var layoutMakers = { + "messagePassThrough": function() { return messagePassThroughLayout; }, + "basic": function() { return basicLayout; }, + "pattern": function (config) { + var pattern = config.pattern || undefined; + return patternLayout(pattern); + } +}; + +function configureAppenders(appenderList) { + log4js.clearAppenders(); + if (appenderList) { + appenderList.forEach( + function(appenderConfig) { + var appender = appenderMakers[appenderConfig.type](appenderConfig); + log4js.addAppender(appender, appenderConfig.category); + } + ); + } else { + log4js.addAppender(consoleAppender); + } +} + +function configureLevels(levels) { + if (levels) { + for (var category in levels) { + if (levels.hasOwnProperty(category)) { + log4js.getLogger(category).setLevel(levels[category]); + } + } + } +} + + +/** + * Log4js.Level Enumeration. Do not use directly. Use static objects instead. + * @constructor + * @param {Number} level number of level + * @param {String} levelString String representation of level + * @private + */ +function Level(level, levelStr) { + this.level = level; + this.levelStr = levelStr; +}; + +/** + * converts given String to corresponding Level + * @param {String} sArg String value of Level + * @param {Log4js.Level} defaultLevel default Level, if no String representation + * @return Level object + * @type Log4js.Level + */ +Level.toLevel = function(sArg, defaultLevel) { + + if (sArg === null) { + return defaultLevel; + } + + if (typeof sArg == "string") { + var s = sArg.toUpperCase(); + if (log4js.levels[s]) { + return log4js.levels[s]; + } + } + return defaultLevel; +}; + +Level.prototype.toString = function() { + return this.levelStr; +}; + +Level.prototype.isLessThanOrEqualTo = function(otherLevel) { + return this.level <= otherLevel.level; +}; + +/** + * Models a logging event. + * @constructor + * @param {String} categoryName name of category + * @param {Log4js.Level} level level of message + * @param {String} message message to log + * @param {Log4js.Logger} logger the associated logger + * @author Seth Chisamore + */ +LoggingEvent = function(categoryName, level, message, exception, logger) { + /** + * the timestamp of the Logging Event + * @type Date + * @private + */ + this.startTime = new Date(); + /** + * category of event + * @type String + * @private + */ + this.categoryName = categoryName; + /** + * the logging message + * @type String + * @private + */ + this.message = message; + /** + * the logging exception + * @type Exception + * @private + */ + this.exception = exception; + /** + * level of log + * @type Log4js.Level + * @private + */ + this.level = level; + /** + * reference to logger + * @type Log4js.Logger + * @private + */ + this.logger = logger; +}; + +/** + * Logger to log messages. + * use {@see Log4js#getLogger(String)} to get an instance. + * @constructor + * @param name name of category to log to + * @author Stephan Strittmatter + */ +Logger = function(name, level) { + this.category = name || DEFAULT_CATEGORY; + this.level = Level.toLevel(level, log4js.levels.TRACE); +}; + +process.inherits(Logger, process.EventEmitter); + +Logger.prototype.setLevel = function(level) { + this.level = Level.toLevel(level, log4js.levels.TRACE); +}; + +Logger.prototype.log = function(logLevel, message, exception) { + var loggingEvent = new LoggingEvent(this.category, logLevel, message, exception, this); + this.emit("log", loggingEvent); +}; + +Logger.prototype.isLevelEnabled = function(otherLevel) { + return this.level.isLessThanOrEqualTo(otherLevel); +}; + +['Trace','Debug','Info','Warn','Error','Fatal'].forEach( + function(levelString) { + var level = Level.toLevel(levelString); + Logger.prototype['is'+levelString+'Enabled'] = function() { + return this.isLevelEnabled(level); + }; + + Logger.prototype[levelString.toLowerCase()] = function (message, exception) { + if (this.isLevelEnabled(level)) { + this.log(level, message, exception); + } + }; + } +); + +consoleAppender = function (layout) { + layout = layout || basicLayout; + return function(loggingEvent) { + sys.puts(layout(loggingEvent)); + }; +}; + +/** + * File Appender writing the logs to a text file. + * + * @param file file log messages will be written to + * @param layout a function that takes a logevent and returns a string (defaults to basicLayout). + */ +fileAppender = function(file, layout) { + layout = layout || basicLayout; + file = file || "log4js.log"; + var logFile; + //waits are generally bad, but we need + //the file to be open before we start doing any writing. + posix + .open(file, process.O_APPEND | process.O_WRONLY | process.O_CREAT, 0644) + .addCallback(function(fileDescriptor) { logFile = fileDescriptor; }) + .addErrback(function(err) { throw new Error("Could not open logfile "+file+", error was: "+sys.inspect(err)); }) + .wait(); + + //register ourselves as listeners for shutdown + //so that we can close the file. + //not entirely sure this is necessary, but still. + process.addListener("exit", function() { posix.close(logFile); }); + + return function(loggingEvent) { + posix.write(logFile, layout(loggingEvent)+'\n', null, "utf-8"); + }; +}; + +/** + * BasicLayout is a simple layout for storing the logs. The logs are stored + * in following format: + *
+ * [startTime] [logLevel] categoryName - message\n
+ * 
+ * + * @author Stephan Strittmatter + */ +basicLayout = function(loggingEvent) { + var timestampLevelAndCategory = '[' + loggingEvent.startTime.toFormattedString() + '] '; + timestampLevelAndCategory += '[' + loggingEvent.level.toString() + '] '; + timestampLevelAndCategory += loggingEvent.categoryName + ' - '; + + var output = timestampLevelAndCategory + loggingEvent.message; + + if (loggingEvent.exception) { + output += '\n' + output += timestampLevelAndCategory; + if (loggingEvent.exception.stack) { + output += loggingEvent.exception.stack; + } else { + output += loggingEvent.exception.name + ': '+loggingEvent.exception.message; + } + } + return output; +}; + +messagePassThroughLayout = function(loggingEvent) { + return loggingEvent.message; +}; + +/** + * PatternLayout + * Takes a pattern string and returns a layout function. + * @author Stephan Strittmatter + */ +patternLayout = function(pattern) { + pattern = pattern || patternLayout.DEFAULT_CONVERSION_PATTERN; + var regex = /%(-?[0-9]+)?(\.?[0-9]+)?([cdmnpr%])(\{([^\}]+)\})?|([^%]+)/; + + return function(loggingEvent) { + var formattedString = ""; + var result; + var searchString = this.pattern; + + while ((result = regex.exec(searchString))) { + var matchedString = result[0]; + var padding = result[1]; + var truncation = result[2]; + var conversionCharacter = result[3]; + var specifier = result[5]; + var text = result[6]; + + // Check if the pattern matched was just normal text + if (text) { + formattedString += "" + text; + } else { + // Create a raw replacement string based on the conversion + // character and specifier + var replacement = ""; + switch(conversionCharacter) { + case "c": + var loggerName = loggingEvent.categoryName; + if (specifier) { + var precision = parseInt(specifier, 10); + var loggerNameBits = loggingEvent.categoryName.split("."); + if (precision >= loggerNameBits.length) { + replacement = loggerName; + } else { + replacement = loggerNameBits.slice(loggerNameBits.length - precision).join("."); + } + } else { + replacement = loggerName; + } + break; + case "d": + var dateFormat = Date.ISO8601_FORMAT; + if (specifier) { + dateFormat = specifier; + // Pick up special cases + if (dateFormat == "ISO8601") { + dateFormat = Date.ISO8601_FORMAT; + } else if (dateFormat == "ABSOLUTE") { + dateFormat = Date.ABSOLUTETIME_FORMAT; + } else if (dateFormat == "DATE") { + dateFormat = Date.DATETIME_FORMAT; + } + } + // Format the date + replacement = loggingEvent.startTime.toFormattedString(dateFormat); + break; + case "m": + replacement = loggingEvent.message; + break; + case "n": + replacement = "\n"; + break; + case "p": + replacement = loggingEvent.level.toString(); + break; + case "r": + replacement = "" + loggingEvent.startTime.toLocaleTimeString(); //TODO: .getDifference(Log4js.applicationStartDate); + break; + case "%": + replacement = "%"; + break; + default: + replacement = matchedString; + break; + } + // Format the replacement according to any padding or + // truncation specified + + var len; + + // First, truncation + if (truncation) { + len = parseInt(truncation.substr(1), 10); + replacement = replacement.substring(0, len); + } + // Next, padding + if (padding) { + if (padding.charAt(0) == "-") { + len = parseInt(padding.substr(1), 10); + // Right pad with spaces + while (replacement.length < len) { + replacement += " "; + } + } else { + len = parseInt(padding, 10); + // Left pad with spaces + while (replacement.length < len) { + replacement = " " + replacement; + } + } + } + formattedString += replacement; + } + searchString = searchString.substr(result.index + result[0].length); + } + return formattedString; + }; + +}; + +patternLayout.TTCC_CONVERSION_PATTERN = "%r %p %c - %m%n"; +patternLayout.DEFAULT_CONVERSION_PATTERN = "%m%n"; + +Date.ISO8601_FORMAT = "yyyy-MM-dd hh:mm:ss.SSS"; +Date.ISO8601_WITH_TZ_OFFSET_FORMAT = "yyyy-MM-ddThh:mm:ssO"; +Date.DATETIME_FORMAT = "dd MMM YYYY hh:mm:ss.SSS"; +Date.ABSOLUTETIME_FORMAT = "hh:mm:ss.SSS"; + +Date.prototype.toFormattedString = function(format) { + format = format || Date.ISO8601_FORMAT; + + var vDay = addZero(this.getDate()); + var vMonth = addZero(this.getMonth()+1); + var vYearLong = addZero(this.getFullYear()); + var vYearShort = addZero(this.getFullYear().toString().substring(3,4)); + var vYear = (format.indexOf("yyyy") > -1 ? vYearLong : vYearShort); + var vHour = addZero(this.getHours()); + var vMinute = addZero(this.getMinutes()); + var vSecond = addZero(this.getSeconds()); + var vMillisecond = padWithZeros(this.getMilliseconds(), 3); + var vTimeZone = offset(this); + var formatted = format + .replace(/dd/g, vDay) + .replace(/MM/g, vMonth) + .replace(/y{1,4}/g, vYear) + .replace(/hh/g, vHour) + .replace(/mm/g, vMinute) + .replace(/ss/g, vSecond) + .replace(/SSS/g, vMillisecond) + .replace(/O/g, vTimeZone); + return formatted; + + function padWithZeros(vNumber, width) { + var numAsString = vNumber + ""; + while (numAsString.length < width) { + numAsString = "0" + numAsString; + } + return numAsString; + } + + function addZero(vNumber) { + return padWithZeros(vNumber, 2); + } + + /** + * Formats the TimeOffest + * Thanks to http://www.svendtofte.com/code/date_format/ + * @private + */ + function offset(date) { + // Difference to Greenwich time (GMT) in hours + var os = Math.abs(date.getTimezoneOffset()); + var h = String(Math.floor(os/60)); + var m = String(os%60); + h.length == 1? h = "0"+h:1; + m.length == 1? m = "0"+m:1; + return date.getTimezoneOffset() < 0 ? "+"+h+m : "-"+h+m; + } +}; + +log4js.consoleAppender = consoleAppender; +log4js.fileAppender = fileAppender; +log4js.basicLayout = basicLayout; +log4js.patternLayout = patternLayout; +log4js.messagePassThroughLayout = messagePassThroughLayout; + +process.mixin(exports,log4js); diff --git a/package.json b/package.json new file mode 100644 index 0000000..d34425f --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "log4js-node", + "version": "0.1.0", + "description": "Port of Log4js to work with node.", + "keywords": [ + "logging", + "log", + "log4j", + "node" + ], + "maintainers": [ + { + "name": "Gareth Jones", + "email": "gareth.jones@sensis.com.au", + "web": "http://github.com/csausdev/" + } + ], + "contributors": [ + { + "name": "Gareth Jones", + "email": "gareth.jones@sensis.com.au", + "web": "http://github.com/csausdev/" + } + ], + "bugs": { + "web": "http://github.com/csausdev/log4js-node/issues" + }, + "licenses": [ + { + "type": "Apache-2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0" + } + ], + "repositories": [ + { + "type": "git", + "url": "http://github.com/csausdev/log4js-node.git" + } + ], + "dependencies": { + }, + "implements": [], + "engines": ["node"], + "scripts": { + "test": "test.js" + }, + "directories": { + "lib": "lib", + "test": "spec" + } +} diff --git a/spec/fixtures/log4js.json b/spec/fixtures/log4js.json new file mode 100644 index 0000000..40a06ad --- /dev/null +++ b/spec/fixtures/log4js.json @@ -0,0 +1,16 @@ +{ + appenders: [ + { + category: "tests", + type: "file", + filename: "tmp-tests.log", + layout: { + type: "messagePassThrough" + } + } + ], + + levels: { + "tests": "WARN" + } +} diff --git a/spec/lib/images/bg.png b/spec/lib/images/bg.png new file mode 100644 index 0000000..947804f Binary files /dev/null and b/spec/lib/images/bg.png differ diff --git a/spec/lib/images/hr.png b/spec/lib/images/hr.png new file mode 100644 index 0000000..4a94d12 Binary files /dev/null and b/spec/lib/images/hr.png differ diff --git a/spec/lib/images/loading.gif b/spec/lib/images/loading.gif new file mode 100644 index 0000000..c69e937 Binary files /dev/null and b/spec/lib/images/loading.gif differ diff --git a/spec/lib/images/sprites.bg.png b/spec/lib/images/sprites.bg.png new file mode 100644 index 0000000..dc8790f Binary files /dev/null and b/spec/lib/images/sprites.bg.png differ diff --git a/spec/lib/images/sprites.png b/spec/lib/images/sprites.png new file mode 100644 index 0000000..010b98e Binary files /dev/null and b/spec/lib/images/sprites.png differ diff --git a/spec/lib/images/vr.png b/spec/lib/images/vr.png new file mode 100644 index 0000000..b2e7617 Binary files /dev/null and b/spec/lib/images/vr.png differ diff --git a/spec/lib/jspec.css b/spec/lib/jspec.css new file mode 100644 index 0000000..629d41c --- /dev/null +++ b/spec/lib/jspec.css @@ -0,0 +1,149 @@ +body.jspec { + margin: 45px 0; + font: 12px "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; + background: #efefef url(images/bg.png) top left repeat-x; + text-align: center; +} +#jspec { + margin: 0 auto; + padding-top: 30px; + width: 1008px; + background: url(images/vr.png) top left repeat-y; + text-align: left; +} +#jspec-top { + position: relative; + margin: 0 auto; + width: 1008px; + height: 40px; + background: url(images/sprites.bg.png) top left no-repeat; +} +#jspec-bottom { + margin: 0 auto; + width: 1008px; + height: 15px; + background: url(images/sprites.bg.png) bottom left no-repeat; +} +#jspec .loading { + margin-top: -45px; + width: 1008px; + height: 80px; + background: url(images/loading.gif) 50% 50% no-repeat; +} +#jspec-title { + position: absolute; + top: 15px; + left: 20px; + width: 160px; + font-size: 22px; + font-weight: normal; + background: url(images/sprites.png) 0 -126px no-repeat; + text-align: center; +} +#jspec-title em { + font-size: 10px; + font-style: normal; + color: #BCC8D1; +} +#jspec-report * { + margin: 0; + padding: 0; + background: none; + border: none; +} +#jspec-report { + padding: 15px 40px; + font: 11px "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; + color: #7B8D9B; +} +#jspec-report.has-failures { + padding-bottom: 30px; +} +#jspec-report .hidden { + display: none; +} +#jspec-report .heading { + margin-bottom: 15px; +} +#jspec-report .heading span { + padding-right: 10px; +} +#jspec-report .heading .passes em { + color: #0ea0eb; +} +#jspec-report .heading .failures em { + color: #FA1616; +} +#jspec-report table { + font-size: 11px; + border-collapse: collapse; +} +#jspec-report td { + padding: 8px; + text-indent: 30px; + color: #7B8D9B; +} +#jspec-report tr.body { + display: none; +} +#jspec-report tr.body pre { + margin: 0; + padding: 0 0 5px 25px; +} +#jspec-report tr.even:hover + tr.body, +#jspec-report tr.odd:hover + tr.body { + display: block; +} +#jspec-report tr td:first-child em { + display: block; + clear: both; + font-style: normal; + font-weight: normal; + color: #7B8D9B; +} +#jspec-report tr.even:hover, +#jspec-report tr.odd:hover { + text-shadow: 1px 1px 1px #fff; + background: #F2F5F7; +} +#jspec-report td + td { + padding-right: 0; + width: 15px; +} +#jspec-report td.pass { + background: url(images/sprites.png) 3px -7px no-repeat; +} +#jspec-report td.fail { + background: url(images/sprites.png) 3px -158px no-repeat; + font-weight: bold; + color: #FC0D0D; +} +#jspec-report td.requires-implementation { + background: url(images/sprites.png) 3px -333px no-repeat; +} +#jspec-report tr.description td { + margin-top: 25px; + padding-top: 25px; + font-size: 12px; + font-weight: bold; + text-indent: 0; + color: #1a1a1a; +} +#jspec-report tr.description:first-child td { + border-top: none; +} +#jspec-report .assertion { + display: block; + float: left; + margin: 0 0 0 1px; + padding: 0; + width: 1px; + height: 5px; + background: #7B8D9B; +} +#jspec-report .assertion.failed { + background: red; +} +.jspec-sandbox { + display: none; +} \ No newline at end of file diff --git a/spec/lib/jspec.growl.js b/spec/lib/jspec.growl.js new file mode 100644 index 0000000..a150257 --- /dev/null +++ b/spec/lib/jspec.growl.js @@ -0,0 +1,115 @@ + +// JSpec - Growl - Copyright TJ Holowaychuk (MIT Licensed) + +;(function(){ + + Growl = { + + // --- Version + + version: '1.0.0', + + /** + * Execute the given _cmd_, returning an array of lines from stdout. + * + * Examples: + * + * Growl.exec('growlnotify', '-m', msg) + * + * @param {string ...} cmd + * @return {array} + * @api public + */ + + exec: function(cmd) { + var lines = [], line + with (JavaImporter(java.lang, java.io)) { + var proccess = Runtime.getRuntime().exec(Array.prototype.slice.call(arguments)) + var stream = new DataInputStream(proccess.getInputStream()) + while (line = stream.readLine()) + lines.push(line + '') + stream.close() + } + return lines + }, + + /** + * Return the extension of the given _path_ or null. + * + * @param {string} path + * @return {string} + * @api private + */ + + extname: function(path) { + return path.lastIndexOf('.') != -1 ? + path.slice(path.lastIndexOf('.') + 1, path.length) : + null + }, + + /** + * Version of the 'growlnotify' binary. + * + * @return {string} + * @api private + */ + + binVersion: function() { + try { return this.exec('growlnotify', '-v')[0].split(' ')[1] } catch (e) {} + }, + + /** + * Send growl notification _msg_ with _options_. + * + * Options: + * + * - title Notification title + * - sticky Make the notification stick (defaults to false) + * - name Application name (defaults to growlnotify) + * - image + * - path to an icon sets --iconpath + * - path to an image sets --image + * - capitalized word sets --appIcon + * - filename uses extname as --icon + * - otherwise treated as --icon + * + * Examples: + * + * Growl.notify('New email') + * Growl.notify('5 new emails', { title: 'Thunderbird' }) + * + * @param {string} msg + * @param {options} hash + * @api public + */ + + notify: function(msg, options) { + options = options || {} + var args = ['growlnotify', '-m', msg] + if (!this.binVersion()) throw new Error('growlnotify executable is required') + if (image = options.image) { + var flag, ext = this.extname(image) + flag = flag || ext == 'icns' && 'iconpath' + flag = flag || /^[A-Z]/.test(image) && 'appIcon' + flag = flag || /^png|gif|jpe?g$/.test(ext) && 'image' + flag = flag || ext && (image = ext) && 'icon' + flag = flag || 'icon' + args.push('--' + flag, image) + } + if (options.sticky) args.push('--sticky') + if (options.name) args.push('--name', options.name) + if (options.title) args.push(options.title) + this.exec.apply(this, args) + } + } + + JSpec.include({ + name: 'Growl', + reporting: function(options){ + var stats = JSpec.stats + if (stats.failures) Growl.notify('failed ' + stats.failures + ' assertions', { title: 'JSpec'}) + else Growl.notify('passed ' + stats.passes + ' assertions', { title: 'JSpec' }) + } + }) + +})() \ No newline at end of file diff --git a/spec/lib/jspec.jquery.js b/spec/lib/jspec.jquery.js new file mode 100644 index 0000000..3c1f784 --- /dev/null +++ b/spec/lib/jspec.jquery.js @@ -0,0 +1,71 @@ + +// JSpec - jQuery - Copyright TJ Holowaychuk (MIT Licensed) + +JSpec +.requires('jQuery', 'when using jspec.jquery.js') +.include({ + name: 'jQuery', + + // --- Initialize + + init : function() { + jQuery.ajaxSetup({ async: false }) + }, + + // --- Utilities + + utilities : { + element: jQuery, + elements: jQuery, + sandbox : function() { + return jQuery('
') + } + }, + + // --- Matchers + + matchers : { + have_tag : "jQuery(expected, actual).length == 1", + have_one : "alias have_tag", + have_tags : "jQuery(expected, actual).length > 1", + have_many : "alias have_tags", + have_child : "jQuery(actual).children(expected).length == 1", + have_children : "jQuery(actual).children(expected).length > 1", + have_text : "jQuery(actual).text() == expected", + have_value : "jQuery(actual).val() == expected", + be_enabled : "!jQuery(actual).attr('disabled')", + have_class : "jQuery(actual).hasClass(expected)", + + be_visible : function(actual) { + return jQuery(actual).css('display') != 'none' && + jQuery(actual).css('visibility') != 'hidden' && + jQuery(actual).attr('type') != 'hidden' + }, + + be_hidden : function(actual) { + return !JSpec.does(actual, 'be_visible') + }, + + have_classes : function(actual) { + return !JSpec.any(JSpec.toArray(arguments, 1), function(arg){ + return !JSpec.does(actual, 'have_class', arg) + }) + }, + + have_attr : function(actual, attr, value) { + return value ? jQuery(actual).attr(attr) == value: + jQuery(actual).attr(attr) + }, + + 'be disabled selected checked' : function(attr) { + return 'jQuery(actual).attr("' + attr + '")' + }, + + 'have type id title alt href src sel rev name target' : function(attr) { + return function(actual, value) { + return JSpec.does(actual, 'have_attr', attr, value) + } + } + } +}) + diff --git a/spec/lib/jspec.js b/spec/lib/jspec.js new file mode 100644 index 0000000..1e95f93 --- /dev/null +++ b/spec/lib/jspec.js @@ -0,0 +1,1773 @@ + +// JSpec - Core - Copyright TJ Holowaychuk (MIT Licensed) + +;(function(){ + + JSpec = { + version : '3.1.3', + assert : true, + cache : {}, + suites : [], + modules : [], + allSuites : [], + matchers : {}, + stubbed : [], + options : {}, + request : 'XMLHttpRequest' in this ? XMLHttpRequest : null, + stats : { specs: 0, assertions: 0, failures: 0, passes: 0, specsFinished: 0, suitesFinished: 0 }, + + /** + * Default context in which bodies are evaluated. + * + * Replace context simply by setting JSpec.context + * to your own like below: + * + * JSpec.context = { foo : 'bar' } + * + * Contexts can be changed within any body, this can be useful + * in order to provide specific helper methods to specific suites. + * + * To reset (usually in after hook) simply set to null like below: + * + * JSpec.context = null + * + */ + + defaultContext : { + + /** + * Return an object used for proxy assertions. + * This object is used to indicate that an object + * should be an instance of _object_, not the constructor + * itself. + * + * @param {function} constructor + * @return {hash} + * @api public + */ + + an_instance_of : function(constructor) { + return { an_instance_of : constructor } + }, + + /** + * Load fixture at _path_. + * + * Fixtures are resolved as: + * + * - + * - .html + * + * @param {string} path + * @return {string} + * @api public + */ + + fixture : function(path) { + if (JSpec.cache[path]) return JSpec.cache[path] + return JSpec.cache[path] = + JSpec.tryLoading(JSpec.options.fixturePath + '/' + path) || + JSpec.tryLoading(JSpec.options.fixturePath + '/' + path + '.html') + } + }, + + // --- Objects + + reporters : { + + /** + * Report to server. + * + * Options: + * - uri specific uri to report to. + * - verbose weither or not to output messages + * - failuresOnly output failure messages only + * + * @api public + */ + + Server : function(results, options) { + var uri = options.uri || 'http://' + window.location.host + '/results' + JSpec.post(uri, { + stats: JSpec.stats, + options: options, + results: map(results.allSuites, function(suite) { + if (suite.hasSpecs()) + return { + description: suite.description, + specs: map(suite.specs, function(spec) { + return { + description: spec.description, + message: !spec.passed() ? spec.failure().message : null, + status: spec.requiresImplementation() ? 'pending' : + spec.passed() ? 'pass' : + 'fail', + assertions: map(spec.assertions, function(assertion){ + return { + passed: assertion.passed + } + }) + } + }) + } + }) + }) + if ('close' in main) main.close() + }, + + /** + * Default reporter, outputting to the DOM. + * + * Options: + * - reportToId id of element to output reports to, defaults to 'jspec' + * - failuresOnly displays only suites with failing specs + * + * @api public + */ + + DOM : function(results, options) { + var id = option('reportToId') || 'jspec' + var report = document.getElementById(id) + var failuresOnly = option('failuresOnly') + var classes = results.stats.failures ? 'has-failures' : '' + if (!report) throw 'JSpec requires the element #' + id + ' to output its reports' + + function bodyContents(body) { + return JSpec. + escape(JSpec.contentsOf(body)). + replace(/^ */gm, function(a){ return (new Array(Math.round(a.length / 3))).join(' ') }). + replace(/\r\n|\r|\n/gm, '
') + } + + report.innerHTML = '
\ + Passes: ' + results.stats.passes + ' \ + Failures: ' + results.stats.failures + ' \ + Duration: ' + results.duration + ' ms \ +
' + map(results.allSuites, function(suite) { + var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran + if (displaySuite && suite.hasSpecs()) + return '' + + map(suite.specs, function(i, spec) { + return '' + + (spec.requiresImplementation() ? + '' : + (spec.passed() && !failuresOnly) ? + '' : + !spec.passed() ? + '' : + '') + + '' + }).join('') + '' + }).join('') + '
' + escape(suite.description) + '
' + escape(spec.description) + '' + escape(spec.description)+ '' + spec.assertionsGraph() + '' + escape(spec.description) + + map(spec.failures(), function(a){ return '' + escape(a.message) + '' }).join('') + + '' + spec.assertionsGraph() + '
' + bodyContents(spec.body) + '
' + }, + + /** + * Terminal reporter. + * + * @api public + */ + + Terminal : function(results, options) { + failuresOnly = option('failuresOnly') + print(color("\n Passes: ", 'bold') + color(results.stats.passes, 'green') + + color(" Failures: ", 'bold') + color(results.stats.failures, 'red') + + color(" Duration: ", 'bold') + color(results.duration, 'green') + " ms \n") + + function indent(string) { + return string.replace(/^(.)/gm, ' $1') + } + + each(results.allSuites, function(suite) { + var displaySuite = failuresOnly ? suite.ran && !suite.passed() : suite.ran + if (displaySuite && suite.hasSpecs()) { + print(color(' ' + suite.description, 'bold')) + each(suite.specs, function(spec){ + var assertionsGraph = inject(spec.assertions, '', function(graph, assertion){ + return graph + color('.', assertion.passed ? 'green' : 'red') + }) + if (spec.requiresImplementation()) + print(color(' ' + spec.description, 'blue') + assertionsGraph) + else if (spec.passed() && !failuresOnly) + print(color(' ' + spec.description, 'green') + assertionsGraph) + else if (!spec.passed()) + print(color(' ' + spec.description, 'red') + assertionsGraph + + "\n" + indent(map(spec.failures(), function(a){ return a.message }).join("\n")) + "\n") + }) + print("") + } + }) + + quit(results.stats.failures) + }, + + /** + * Console reporter. + * + * @api public + */ + + Console : function(results, options) { + console.log('') + console.log('Passes: ' + results.stats.passes + ' Failures: ' + results.stats.failures) + each(results.allSuites, function(suite) { + if (suite.ran) { + console.group(suite.description) + each(suite.specs, function(spec){ + var assertionCount = spec.assertions.length + ':' + if (spec.requiresImplementation()) + console.warn(spec.description) + else if (spec.passed()) + console.log(assertionCount + ' ' + spec.description) + else + console.error(assertionCount + ' ' + spec.description + ', ' + spec.failure().message) + }) + console.groupEnd() + } + }) + } + }, + + Assertion : function(matcher, actual, expected, negate) { + extend(this, { + message: '', + passed: false, + actual: actual, + negate: negate, + matcher: matcher, + expected: expected, + + // Report assertion results + + report : function() { + if (JSpec.assert) + this.passed ? JSpec.stats.passes++ : JSpec.stats.failures++ + return this + }, + + // Run the assertion + + run : function() { + // TODO: remove unshifting + expected.unshift(actual) + this.result = matcher.match.apply(this, expected) + this.passed = negate ? !this.result : this.result + if (!this.passed) this.message = matcher.message.call(this, actual, expected, negate, matcher.name) + return this + } + }) + }, + + ProxyAssertion : function(object, method, times, negate) { + var self = this + var old = object[method] + + // Proxy + + object[method] = function(){ + args = toArray(arguments) + result = old.apply(object, args) + self.calls.push({ args : args, result : result }) + return result + } + + // Times + + this.times = { + once : 1, + twice : 2 + }[times] || times || 1 + + extend(this, { + calls: [], + message: '', + defer: true, + passed: false, + negate: negate, + object: object, + method: method, + + // Proxy return value + + and_return : function(result) { + this.expectedResult = result + return this + }, + + // Proxy arguments passed + + with_args : function() { + this.expectedArgs = toArray(arguments) + return this + }, + + // Check if any calls have failing results + + anyResultsFail : function() { + return any(this.calls, function(call){ + return self.expectedResult.an_instance_of ? + call.result.constructor != self.expectedResult.an_instance_of: + !equal(self.expectedResult, call.result) + }) + }, + + // Check if any calls have passing results + + anyResultsPass : function() { + return any(this.calls, function(call){ + return self.expectedResult.an_instance_of ? + call.result.constructor == self.expectedResult.an_instance_of: + equal(self.expectedResult, call.result) + }) + }, + + // Return the passing result + + passingResult : function() { + return this.anyResultsPass().result + }, + + // Return the failing result + + failingResult : function() { + return this.anyResultsFail().result + }, + + // Check if any arguments fail + + anyArgsFail : function() { + return any(this.calls, function(call){ + return any(self.expectedArgs, function(i, arg){ + if (arg == null) return call.args[i] == null + return arg.an_instance_of ? + call.args[i].constructor != arg.an_instance_of: + !equal(arg, call.args[i]) + + }) + }) + }, + + // Check if any arguments pass + + anyArgsPass : function() { + return any(this.calls, function(call){ + return any(self.expectedArgs, function(i, arg){ + return arg.an_instance_of ? + call.args[i].constructor == arg.an_instance_of: + equal(arg, call.args[i]) + + }) + }) + }, + + // Return the passing args + + passingArgs : function() { + return this.anyArgsPass().args + }, + + // Return the failing args + + failingArgs : function() { + return this.anyArgsFail().args + }, + + // Report assertion results + + report : function() { + if (JSpec.assert) + this.passed ? ++JSpec.stats.passes : ++JSpec.stats.failures + return this + }, + + // Run the assertion + + run : function() { + var methodString = 'expected ' + object.toString() + '.' + method + '()' + (negate ? ' not' : '' ) + + function times(n) { + return n > 2 ? n + ' times' : { 1: 'once', 2: 'twice' }[n] + } + + if (this.expectedResult != null && (negate ? this.anyResultsPass() : this.anyResultsFail())) + this.message = methodString + ' to return ' + puts(this.expectedResult) + + ' but ' + (negate ? 'it did' : 'got ' + puts(this.failingResult())) + + if (this.expectedArgs && (negate ? !this.expectedResult && this.anyArgsPass() : this.anyArgsFail())) + this.message = methodString + ' to be called with ' + puts.apply(this, this.expectedArgs) + + ' but was' + (negate ? '' : ' called with ' + puts.apply(this, this.failingArgs())) + + if (negate ? !this.expectedResult && !this.expectedArgs && this.calls.length >= this.times : this.calls.length != this.times) + this.message = methodString + ' to be called ' + times(this.times) + + ', but ' + (this.calls.length == 0 ? ' was not called' : ' was called ' + times(this.calls.length)) + + if (!this.message.length) + this.passed = true + + return this + } + }) + }, + + /** + * Specification Suite block object. + * + * @param {string} description + * @param {function} body + * @api private + */ + + Suite : function(description, body) { + var self = this + extend(this, { + body: body, + description: description, + suites: [], + specs: [], + ran: false, + hooks: { 'before' : [], 'after' : [], 'before_each' : [], 'after_each' : [] }, + + // Add a spec to the suite + + addSpec : function(description, body) { + var spec = new JSpec.Spec(description, body) + this.specs.push(spec) + JSpec.stats.specs++ // TODO: abstract + spec.suite = this + }, + + // Add a hook to the suite + + addHook : function(hook, body) { + this.hooks[hook].push(body) + }, + + // Add a nested suite + + addSuite : function(description, body) { + var suite = new JSpec.Suite(description, body) + JSpec.allSuites.push(suite) + suite.name = suite.description + suite.description = this.description + ' ' + suite.description + this.suites.push(suite) + suite.suite = this + }, + + // Invoke a hook in context to this suite + + hook : function(hook) { + if (this.suite) this.suite.hook(hook) + each(this.hooks[hook], function(body) { + JSpec.evalBody(body, "Error in hook '" + hook + "', suite '" + self.description + "': ") + }) + }, + + // Check if nested suites are present + + hasSuites : function() { + return this.suites.length + }, + + // Check if this suite has specs + + hasSpecs : function() { + return this.specs.length + }, + + // Check if the entire suite passed + + passed : function() { + return !any(this.specs, function(spec){ + return !spec.passed() + }) + } + }) + }, + + /** + * Specification block object. + * + * @param {string} description + * @param {function} body + * @api private + */ + + Spec : function(description, body) { + extend(this, { + body: body, + description: description, + assertions: [], + + // Add passing assertion + + pass : function(message) { + this.assertions.push({ passed: true, message: message }) + if (JSpec.assert) ++JSpec.stats.passes + }, + + // Add failing assertion + + fail : function(message) { + this.assertions.push({ passed: false, message: message }) + if (JSpec.assert) ++JSpec.stats.failures + }, + + // Run deferred assertions + + runDeferredAssertions : function() { + each(this.assertions, function(assertion){ + if (assertion.defer) assertion.run().report(), hook('afterAssertion', assertion) + }) + }, + + // Find first failing assertion + + failure : function() { + return find(this.assertions, function(assertion){ + return !assertion.passed + }) + }, + + // Find all failing assertions + + failures : function() { + return select(this.assertions, function(assertion){ + return !assertion.passed + }) + }, + + // Weither or not the spec passed + + passed : function() { + return !this.failure() + }, + + // Weither or not the spec requires implementation (no assertions) + + requiresImplementation : function() { + return this.assertions.length == 0 + }, + + // Sprite based assertions graph + + assertionsGraph : function() { + return map(this.assertions, function(assertion){ + return '' + }).join('') + } + }) + }, + + Module : function(methods) { + extend(this, methods) + }, + + JSON : { + + /** + * Generic sequences. + */ + + meta : { + '\b' : '\\b', + '\t' : '\\t', + '\n' : '\\n', + '\f' : '\\f', + '\r' : '\\r', + '"' : '\\"', + '\\' : '\\\\' + }, + + /** + * Escapable sequences. + */ + + escapable : /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + + /** + * JSON encode _object_. + * + * @param {mixed} object + * @return {string} + * @api private + */ + + encode : function(object) { + var self = this + if (object == undefined || object == null) return 'null' + if (object === true) return 'true' + if (object === false) return 'false' + switch (typeof object) { + case 'number': return object + case 'string': return this.escapable.test(object) ? + '"' + object.replace(this.escapable, function (a) { + return typeof self.meta[a] === 'string' ? self.meta[a] : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4) + }) + '"' : + '"' + object + '"' + case 'object': + if (object.constructor == Array) + return '[' + map(object, function(val){ + return self.encode(val) + }).join(', ') + ']' + else if (object) + return '{' + map(object, function(key, val){ + return self.encode(key) + ':' + self.encode(val) + }).join(', ') + '}' + } + return 'null' + } + }, + + // --- DSLs + + DSLs : { + snake : { + expect : function(actual){ + return JSpec.expect(actual) + }, + + describe : function(description, body) { + return JSpec.currentSuite.addSuite(description, body) + }, + + it : function(description, body) { + return JSpec.currentSuite.addSpec(description, body) + }, + + before : function(body) { + return JSpec.currentSuite.addHook('before', body) + }, + + after : function(body) { + return JSpec.currentSuite.addHook('after', body) + }, + + before_each : function(body) { + return JSpec.currentSuite.addHook('before_each', body) + }, + + after_each : function(body) { + return JSpec.currentSuite.addHook('after_each', body) + }, + + should_behave_like : function(description) { + return JSpec.shareBehaviorsOf(description) + } + } + }, + + // --- Methods + + /** + * Check if _value_ is 'stop'. For use as a + * utility callback function. + * + * @param {mixed} value + * @return {bool} + * @api public + */ + + haveStopped : function(value) { + return value === 'stop' + }, + + /** + * Include _object_ which may be a hash or Module instance. + * + * @param {hash, Module} object + * @return {JSpec} + * @api public + */ + + include : function(object) { + var module = object.constructor == JSpec.Module ? object : new JSpec.Module(object) + this.modules.push(module) + if ('init' in module) module.init() + if ('utilities' in module) extend(this.defaultContext, module.utilities) + if ('matchers' in module) this.addMatchers(module.matchers) + if ('reporters' in module) extend(this.reporters, module.reporters) + if ('DSLs' in module) + each(module.DSLs, function(name, methods){ + JSpec.DSLs[name] = JSpec.DSLs[name] || {} + extend(JSpec.DSLs[name], methods) + }) + return this + }, + + /** + * Add a module hook _name_, which is immediately + * called per module with the _args_ given. An array of + * hook return values is returned. + * + * @param {name} string + * @param {...} args + * @return {array} + * @api private + */ + + hook : function(name, args) { + args = toArray(arguments, 1) + return inject(JSpec.modules, [], function(results, module){ + if (typeof module[name] == 'function') + results.push(JSpec.evalHook(module, name, args)) + }) + }, + + /** + * Eval _module_ hook _name_ with _args_. Evaluates in context + * to the module itself, JSpec, and JSpec.context. + * + * @param {Module} module + * @param {string} name + * @param {array} args + * @return {mixed} + * @api private + */ + + evalHook : function(module, name, args) { + hook('evaluatingHookBody', module, name) + try { return module[name].apply(module, args) } + catch(e) { error('Error in hook ' + module.name + '.' + name + ': ', e) } + }, + + /** + * Same as hook() however accepts only one _arg_ which is + * considered immutable. This function passes the arg + * to the first module, then passes the return value of the last + * module called, to the following module. + * + * @param {string} name + * @param {mixed} arg + * @return {mixed} + * @api private + */ + + hookImmutable : function(name, arg) { + return inject(JSpec.modules, arg, function(result, module){ + if (typeof module[name] == 'function') + return JSpec.evalHook(module, name, [result]) + }) + }, + + /** + * Find a suite by its description or name. + * + * @param {string} description + * @return {Suite} + * @api private + */ + + findSuite : function(description) { + return find(this.allSuites, function(suite){ + return suite.name == description || suite.description == description + }) + }, + + /** + * Share behaviors (specs) of the given suite with + * the current suite. + * + * @param {string} description + * @api public + */ + + shareBehaviorsOf : function(description) { + if (suite = this.findSuite(description)) this.copySpecs(suite, this.currentSuite) + else throw 'failed to share behaviors. ' + puts(description) + ' is not a valid Suite name' + }, + + /** + * Copy specs from one suite to another. + * + * @param {Suite} fromSuite + * @param {Suite} toSuite + * @api public + */ + + copySpecs : function(fromSuite, toSuite) { + each(fromSuite.specs, function(spec){ + spec.assertions = [] + toSuite.specs.push(spec) + }) + }, + + /** + * Convert arguments to an array. + * + * @param {object} arguments + * @param {int} offset + * @return {array} + * @api public + */ + + toArray : function(arguments, offset) { + return Array.prototype.slice.call(arguments, offset || 0) + }, + + /** + * Return ANSI-escaped colored string. + * + * @param {string} string + * @param {string} color + * @return {string} + * @api public + */ + + color : function(string, color) { + return "\u001B[" + { + bold : 1, + black : 30, + red : 31, + green : 32, + yellow : 33, + blue : 34, + magenta : 35, + cyan : 36, + white : 37 + }[color] + 'm' + string + "\u001B[0m" + }, + + /** + * Default matcher message callback. + * + * @api private + */ + + defaultMatcherMessage : function(actual, expected, negate, name) { + return 'expected ' + puts(actual) + ' to ' + + (negate ? 'not ' : '') + + name.replace(/_/g, ' ') + + ' ' + (expected.length > 1 ? + puts.apply(this, expected.slice(1)) : + '') + }, + + /** + * Normalize a matcher message. + * + * When no messge callback is present the defaultMatcherMessage + * will be assigned, will suffice for most matchers. + * + * @param {hash} matcher + * @return {hash} + * @api public + */ + + normalizeMatcherMessage : function(matcher) { + if (typeof matcher.message != 'function') + matcher.message = this.defaultMatcherMessage + return matcher + }, + + /** + * Normalize a matcher body + * + * This process allows the following conversions until + * the matcher is in its final normalized hash state. + * + * - '==' becomes 'actual == expected' + * - 'actual == expected' becomes 'return actual == expected' + * - function(actual, expected) { return actual == expected } becomes + * { match : function(actual, expected) { return actual == expected }} + * + * @param {mixed} body + * @return {hash} + * @api public + */ + + normalizeMatcherBody : function(body) { + switch (body.constructor) { + case String: + if (captures = body.match(/^alias (\w+)/)) return JSpec.matchers[last(captures)] + if (body.length < 4) body = 'actual ' + body + ' expected' + return { match: function(actual, expected) { return eval(body) }} + + case Function: + return { match: body } + + default: + return body + } + }, + + /** + * Get option value. This method first checks if + * the option key has been set via the query string, + * otherwise returning the options hash value. + * + * @param {string} key + * @return {mixed} + * @api public + */ + + option : function(key) { + return (value = query(key)) !== null ? value : + JSpec.options[key] || null + }, + + /** + * Check if object _a_, is equal to object _b_. + * + * @param {object} a + * @param {object} b + * @return {bool} + * @api private + */ + + equal: function(a, b) { + if (typeof a != typeof b) return + if (a === b) return true + if (a instanceof RegExp) + return a.toString() === b.toString() + if (a instanceof Date) + return Number(a) === Number(b) + if (typeof a != 'object') return + if (a.length !== undefined) + if (a.length !== b.length) return + else + for (var i = 0, len = a.length; i < len; ++i) + if (!equal(a[i], b[i])) + return + for (var key in a) + if (!equal(a[key], b[key])) + return + return true + }, + + /** + * Return last element of an array. + * + * @param {array} array + * @return {object} + * @api public + */ + + last : function(array) { + return array[array.length - 1] + }, + + /** + * Convert object(s) to a print-friend string. + * + * @param {...} object + * @return {string} + * @api public + */ + + puts : function(object) { + if (arguments.length > 1) + return map(toArray(arguments), function(arg){ + return puts(arg) + }).join(', ') + if (object === undefined) return 'undefined' + if (object === null) return 'null' + if (object === true) return 'true' + if (object === false) return 'false' + if (object.an_instance_of) return 'an instance of ' + object.an_instance_of.name + if (object.jquery && object.selector.length > 0) return 'selector ' + puts(object.selector) + if (object.jquery) return object.get(0).outerHTML + if (object.nodeName) return object.outerHTML + switch (object.constructor) { + case Function: return object.name || object + case String: + return '"' + object + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\t/g, '\\t') + + '"' + case Array: + return inject(object, '[', function(b, v){ + return b + ', ' + puts(v) + }).replace('[,', '[') + ' ]' + case Object: + object.__hit__ = true + return inject(object, '{', function(b, k, v) { + if (k == '__hit__') return b + return b + ', ' + k + ': ' + (v && v.__hit__ ? '' : puts(v)) + }).replace('{,', '{') + ' }' + default: + return object.toString() + } + }, + + /** + * Escape HTML. + * + * @param {string} html + * @return {string} + * @api public + */ + + escape : function(html) { + return html.toString() + .replace(/&/gmi, '&') + .replace(/"/gmi, '"') + .replace(/>/gmi, '>') + .replace(/ current) while (++current <= end) values.push(current) + else while (--current >= end) values.push(current) + return '[' + values + ']' + }, + + /** + * Report on the results. + * + * @api public + */ + + report : function() { + this.duration = Number(new Date) - this.start + hook('reporting', JSpec.options) + new (JSpec.options.reporter || JSpec.reporters.DOM)(JSpec, JSpec.options) + }, + + /** + * Run the spec suites. Options are merged + * with JSpec options when present. + * + * @param {hash} options + * @return {JSpec} + * @api public + */ + + run : function(options) { + if (any(hook('running'), haveStopped)) return this + if (options) extend(this.options, options) + this.start = Number(new Date) + each(this.suites, function(suite) { JSpec.runSuite(suite) }) + return this + }, + + /** + * Run a suite. + * + * @param {Suite} suite + * @api public + */ + + runSuite : function(suite) { + this.currentSuite = suite + this.evalBody(suite.body) + suite.ran = true + hook('beforeSuite', suite), suite.hook('before') + each(suite.specs, function(spec) { + hook('beforeSpec', spec) + suite.hook('before_each') + JSpec.runSpec(spec) + hook('afterSpec', spec) + suite.hook('after_each') + }) + if (suite.hasSuites()) { + each(suite.suites, function(suite) { + JSpec.runSuite(suite) + }) + } + hook('afterSuite', suite), suite.hook('after') + this.stats.suitesFinished++ + }, + + /** + * Report a failure for the current spec. + * + * @param {string} message + * @api public + */ + + fail : function(message) { + JSpec.currentSpec.fail(message) + }, + + /** + * Report a passing assertion for the current spec. + * + * @param {string} message + * @api public + */ + + pass : function(message) { + JSpec.currentSpec.pass(message) + }, + + /** + * Run a spec. + * + * @param {Spec} spec + * @api public + */ + + runSpec : function(spec) { + this.currentSpec = spec + try { this.evalBody(spec.body) } + catch (e) { fail(e) } + spec.runDeferredAssertions() + destub() + this.stats.specsFinished++ + this.stats.assertions += spec.assertions.length + }, + + /** + * Require a dependency, with optional message. + * + * @param {string} dependency + * @param {string} message (optional) + * @return {JSpec} + * @api public + */ + + requires : function(dependency, message) { + hook('requiring', dependency, message) + try { eval(dependency) } + catch (e) { throw 'JSpec depends on ' + dependency + ' ' + message } + return this + }, + + /** + * Query against the current query strings keys + * or the queryString specified. + * + * @param {string} key + * @param {string} queryString + * @return {string, null} + * @api private + */ + + query : function(key, queryString) { + var queryString = (queryString || (main.location ? main.location.search : null) || '').substring(1) + return inject(queryString.split('&'), null, function(value, pair){ + parts = pair.split('=') + return parts[0] == key ? parts[1].replace(/%20|\+/gmi, ' ') : value + }) + }, + + /** + * Throw a JSpec related error. + * + * @param {string} message + * @param {Exception} e + * @api public + */ + + error : function(message, e) { + throw (message ? message : '') + e.toString() + + (e.line ? ' near line ' + e.line : '') + }, + + /** + * Ad-hoc POST request for JSpec server usage. + * + * @param {string} uri + * @param {string} data + * @api private + */ + + post : function(uri, data) { + if (any(hook('posting', uri, data), haveStopped)) return + var request = this.xhr() + request.open('POST', uri, false) + request.setRequestHeader('Content-Type', 'application/json') + request.send(JSpec.JSON.encode(data)) + }, + + /** + * Instantiate an XMLHttpRequest. + * + * Here we utilize IE's lame ActiveXObjects first which + * allow IE access serve files via the file: protocol, otherwise + * we then default to XMLHttpRequest. + * + * @return {XMLHttpRequest, ActiveXObject} + * @api private + */ + + xhr : function() { + return this.ieXhr() || new JSpec.request + }, + + /** + * Return Microsoft piece of crap ActiveXObject. + * + * @return {ActiveXObject} + * @api public + */ + + ieXhr : function() { + function object(str) { + try { return new ActiveXObject(str) } catch(e) {} + } + return object('Msxml2.XMLHTTP.6.0') || + object('Msxml2.XMLHTTP.3.0') || + object('Msxml2.XMLHTTP') || + object('Microsoft.XMLHTTP') + }, + + /** + * Check for HTTP request support. + * + * @return {bool} + * @api private + */ + + hasXhr : function() { + return JSpec.request || 'ActiveXObject' in main + }, + + /** + * Try loading _file_ returning the contents + * string or null. Chain to locate / read a file. + * + * @param {string} file + * @return {string} + * @api public + */ + + tryLoading : function(file) { + try { return JSpec.load(file) } catch (e) {} + }, + + /** + * Load a _file_'s contents. + * + * @param {string} file + * @param {function} callback + * @return {string} + * @api public + */ + + load : function(file, callback) { + if (any(hook('loading', file), haveStopped)) return + if ('readFile' in main) + return readFile(file) + else if (this.hasXhr()) { + var request = this.xhr() + request.open('GET', file, false) + request.send(null) + if (request.readyState == 4 && + (request.status == 0 || + request.status.toString().charAt(0) == 2)) + return request.responseText + } + else + error("failed to load `" + file + "'") + }, + + /** + * Load, pre-process, and evaluate a file. + * + * @param {string} file + * @param {JSpec} + * @api public + */ + + exec : function(file) { + if (any(hook('executing', file), haveStopped)) return this + eval('with (JSpec){' + this.preprocess(this.load(file)) + '}') + return this + } + } + + // --- Utility functions + + var main = this + var find = JSpec.any + var utils = 'haveStopped stub hookImmutable hook destub map any last pass fail range each option inject select \ + error escape extend puts query strip color does addMatchers callIterator toArray equal'.split(/\s+/) + while (utils.length) eval('var ' + utils[0] + ' = JSpec.' + utils.shift()) + if (!main.setTimeout) main.setTimeout = function(callback){ callback() } + + // --- Matchers + + addMatchers({ + equal : "===", + eql : "equal(actual, expected)", + be : "alias equal", + be_greater_than : ">", + be_less_than : "<", + be_at_least : ">=", + be_at_most : "<=", + be_a : "actual.constructor == expected", + be_an : "alias be_a", + be_an_instance_of : "actual instanceof expected", + be_null : "actual == null", + be_true : "actual == true", + be_false : "actual == false", + be_undefined : "typeof actual == 'undefined'", + be_type : "typeof actual == expected", + match : "typeof actual == 'string' ? actual.match(expected) : false", + respond_to : "typeof actual[expected] == 'function'", + have_length : "actual.length == expected", + be_within : "actual >= expected[0] && actual <= last(expected)", + have_length_within : "actual.length >= expected[0] && actual.length <= last(expected)", + + receive : { defer : true, match : function(actual, method, times) { + proxy = new JSpec.ProxyAssertion(actual, method, times, this.negate) + JSpec.currentSpec.assertions.push(proxy) + return proxy + }}, + + be_empty : function(actual) { + if (actual.constructor == Object && actual.length == undefined) + for (var key in actual) + return false; + return !actual.length + }, + + include : function(actual) { + for (state = true, i = 1; i < arguments.length; i++) { + arg = arguments[i] + switch (actual.constructor) { + case String: + case Number: + case RegExp: + case Function: + state = actual.toString().indexOf(arg) !== -1 + break + + case Object: + state = arg in actual + break + + case Array: + state = any(actual, function(value){ return equal(value, arg) }) + break + } + if (!state) return false + } + return true + }, + + throw_error : { match : function(actual, expected, message) { + try { actual() } + catch (e) { + this.e = e + var assert = function(arg) { + switch (arg.constructor) { + case RegExp : return arg.test(e.message || e.toString()) + case String : return arg == (e.message || e.toString()) + case Function : return e instanceof arg || e.name == arg.name + } + } + return message ? assert(expected) && assert(message) : + expected ? assert(expected) : + true + } + }, message : function(actual, expected, negate) { + // TODO: refactor when actual is not in expected [0] + var message_for = function(i) { + if (expected[i] == undefined) return 'exception' + switch (expected[i].constructor) { + case RegExp : return 'exception matching ' + puts(expected[i]) + case String : return 'exception of ' + puts(expected[i]) + case Function : return expected[i].name || 'Error' + } + } + exception = message_for(1) + (expected[2] ? ' and ' + message_for(2) : '') + return 'expected ' + exception + (negate ? ' not ' : '' ) + + ' to be thrown, but ' + (this.e ? 'got ' + puts(this.e) : 'nothing was') + }}, + + have : function(actual, length, property) { + return actual[property].length == length + }, + + have_at_least : function(actual, length, property) { + return actual[property].length >= length + }, + + have_at_most :function(actual, length, property) { + return actual[property].length <= length + }, + + have_within : function(actual, range, property) { + length = actual[property].length + return length >= range.shift() && length <= range.pop() + }, + + have_prop : function(actual, property, value) { + return actual[property] == null || + actual[property] instanceof Function ? false: + value == null ? true: + does(actual[property], 'eql', value) + }, + + have_property : function(actual, property, value) { + return actual[property] == null || + actual[property] instanceof Function ? false: + value == null ? true: + value === actual[property] + } + }) + +})() \ No newline at end of file diff --git a/spec/lib/jspec.shell.js b/spec/lib/jspec.shell.js new file mode 100644 index 0000000..cb19c69 --- /dev/null +++ b/spec/lib/jspec.shell.js @@ -0,0 +1,39 @@ + +// JSpec - Shell - Copyright TJ Holowaychuk (MIT Licensed) + +;(function(){ + + var _quit = quit + + Shell = { + + // --- Global + + main: this, + + // --- Commands + + commands: { + quit: ['Terminate the shell', function(){ _quit() }], + exit: ['Terminate the shell', function(){ _quit() }], + p: ['Inspect an object', function(o){ return o.toSource() }] + }, + + /** + * Start the interactive shell. + * + * @api public + */ + + start : function() { + for (var name in this.commands) + if (this.commands.hasOwnProperty(name)) + this.commands[name][1].length ? + this.main[name] = this.commands[name][1] : + this.main.__defineGetter__(name, this.commands[name][1]) + } + } + + Shell.start() + +})() \ No newline at end of file diff --git a/spec/lib/jspec.timers.js b/spec/lib/jspec.timers.js new file mode 100644 index 0000000..c88d10b --- /dev/null +++ b/spec/lib/jspec.timers.js @@ -0,0 +1,90 @@ + +// JSpec - Mock Timers - Copyright TJ Holowaychuk (MIT Licensed) + +;(function(){ + + /** + * Version. + */ + + mockTimersVersion = '1.0.2' + + /** + * Localized timer stack. + */ + + var timers = [] + + /** + * Set mock timeout with _callback_ and timeout of _ms_. + * + * @param {function} callback + * @param {int} ms + * @return {int} + * @api public + */ + + setTimeout = function(callback, ms) { + var id + return id = setInterval(function(){ + callback() + clearInterval(id) + }, ms) + } + + /** + * Set mock interval with _callback_ and interval of _ms_. + * + * @param {function} callback + * @param {int} ms + * @return {int} + * @api public + */ + + setInterval = function(callback, ms) { + callback.step = ms, callback.current = callback.last = 0 + return timers[timers.length] = callback, timers.length + } + + /** + * Destroy timer with _id_. + * + * @param {int} id + * @return {bool} + * @api public + */ + + clearInterval = clearTimeout = function(id) { + return delete timers[--id] + } + + /** + * Reset timers. + * + * @return {array} + * @api public + */ + + resetTimers = function() { + return timers = [] + } + + /** + * Increment each timers internal clock by _ms_. + * + * @param {int} ms + * @api public + */ + + tick = function(ms) { + for (var i = 0, len = timers.length; i < len; ++i) + if (timers[i] && (timers[i].current += ms)) + if (timers[i].current - timers[i].last >= timers[i].step) { + var times = Math.floor((timers[i].current - timers[i].last) / timers[i].step) + var remainder = (timers[i].current - timers[i].last) % timers[i].step + timers[i].last = timers[i].current - remainder + while (times-- && timers[i]) timers[i]() + } + } + +})() \ No newline at end of file diff --git a/spec/lib/jspec.xhr.js b/spec/lib/jspec.xhr.js new file mode 100644 index 0000000..906e4c5 --- /dev/null +++ b/spec/lib/jspec.xhr.js @@ -0,0 +1,193 @@ + +// JSpec - XHR - Copyright TJ Holowaychuk (MIT Licensed) + +(function(){ + + // --- Original XMLHttpRequest + + var OriginalXMLHttpRequest = 'XMLHttpRequest' in this ? + XMLHttpRequest : + function(){} + var OriginalActiveXObject = 'ActiveXObject' in this ? + ActiveXObject : + undefined + + // --- MockXMLHttpRequest + + var MockXMLHttpRequest = function() { + this.requestHeaders = {} + } + + MockXMLHttpRequest.prototype = { + status: 0, + async: true, + readyState: 0, + responseText: '', + abort: function(){}, + onreadystatechange: function(){}, + + /** + * Return response headers hash. + */ + + getAllResponseHeaders : function(){ + return this.responseHeaders + }, + + /** + * Return case-insensitive value for header _name_. + */ + + getResponseHeader : function(name) { + return this.responseHeaders[name.toLowerCase()] + }, + + /** + * Set case-insensitive _value_ for header _name_. + */ + + setRequestHeader : function(name, value) { + this.requestHeaders[name.toLowerCase()] = value + }, + + /** + * Open mock request. + */ + + open : function(method, url, async, user, password) { + this.user = user + this.password = password + this.url = url + this.readyState = 1 + this.method = method.toUpperCase() + if (async != undefined) this.async = async + if (this.async) this.onreadystatechange() + }, + + /** + * Send request _data_. + */ + + send : function(data) { + var self = this + this.data = data + this.readyState = 4 + if (this.method == 'HEAD') this.responseText = null + this.responseHeaders['content-length'] = (this.responseText || '').length + if(this.async) this.onreadystatechange() + lastRequest = function(){ + return self + } + } + } + + // --- Response status codes + + JSpec.statusCodes = { + 100: 'Continue', + 101: 'Switching Protocols', + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 300: 'Multiple Choice', + 301: 'Moved Permanently', + 302: 'Found', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 307: 'Temporary Redirect', + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Request Entity Too Large', + 414: 'Request-URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Requested Range Not Satisfiable', + 417: 'Expectation Failed', + 422: 'Unprocessable Entity', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported' + } + + /** + * Mock XMLHttpRequest requests. + * + * mockRequest().and_return('some data', 'text/plain', 200, { 'X-SomeHeader' : 'somevalue' }) + * + * @return {hash} + * @api public + */ + + function mockRequest() { + return { and_return : function(body, type, status, headers) { + XMLHttpRequest = MockXMLHttpRequest + ActiveXObject = false + status = status || 200 + headers = headers || {} + headers['content-type'] = type + JSpec.extend(XMLHttpRequest.prototype, { + responseText: body, + responseHeaders: headers, + status: status, + statusText: JSpec.statusCodes[status] + }) + }} + } + + /** + * Unmock XMLHttpRequest requests. + * + * @api public + */ + + function unmockRequest() { + XMLHttpRequest = OriginalXMLHttpRequest + ActiveXObject = OriginalActiveXObject + } + + JSpec.include({ + name: 'Mock XHR', + + // --- Utilities + + utilities : { + mockRequest: mockRequest, + unmockRequest: unmockRequest + }, + + // --- Hooks + + afterSpec : function() { + unmockRequest() + }, + + // --- DSLs + + DSLs : { + snake : { + mock_request: mockRequest, + unmock_request: unmockRequest, + last_request: function(){ return lastRequest() } + } + } + + }) +})() \ No newline at end of file diff --git a/spec/spec.logging.js b/spec/spec.logging.js new file mode 100644 index 0000000..fc37abd --- /dev/null +++ b/spec/spec.logging.js @@ -0,0 +1,202 @@ +posix = require('posix'); + +waitForWriteAndThenRead = function(filename) { + //here's the tricky part - writes are asynchronous + //so I'm going to make a promise, wait a bit and then + //try to read the file. + var content, promise = new process.Promise(); + setTimeout(function() { + promise.emitSuccess(); + }, 50); + promise.addCallback(function() { + posix.cat(filename).addCallback( + function(fileContents) { content = fileContents; } + ).wait(); + }).wait(); + + return content; +} + +describe 'log4js' + before_each + log4js.clearAppenders(); + event = ''; + logger = log4js.getLogger('tests'); + logger.setLevel("TRACE"); + logger.addListener("log", function (logEvent) { event = logEvent; }); + end + + it 'should have a version' + log4js.version.should.not.be undefined + end + + describe 'getLogger' + + it 'should take a category and return a Logger' + logger.category.should.be 'tests' + logger.level.should.be log4js.levels.TRACE + logger.should.respond_to 'debug' + logger.should.respond_to 'info' + logger.should.respond_to 'warn' + logger.should.respond_to 'error' + logger.should.respond_to 'fatal' + end + + it 'should emit log events' + logger.trace("Trace event"); + + event.level.toString().should.be 'TRACE' + event.message.should.be 'Trace event' + event.startTime.should.not.be undefined + end + + it 'should not emit events of a lower level than the minimum' + logger.setLevel("DEBUG"); + event = undefined; + logger.trace("This should not generate a log message"); + event.should.be undefined + end + end + + describe 'addAppender' + before_each + log4js.clearAppenders(); + appenderEvent = undefined; + appender = function(logEvent) { appenderEvent = logEvent; }; + end + + describe 'without a category' + it 'should register the function as a listener for all loggers' + log4js.addAppender(appender); + logger.debug("This is a test"); + appenderEvent.should.be event + end + + it 'should also register as an appender for loggers if an appender for that category is defined' + var otherEvent; + log4js.addAppender(appender); + log4js.addAppender(function (evt) { otherEvent = evt; }, 'cheese'); + + var cheeseLogger = log4js.getLogger('cheese'); + cheeseLogger.addListener("log", function (logEvent) { event = logEvent; }); + + cheeseLogger.debug('This is a test'); + + appenderEvent.should.be event + otherEvent.should.be event + end + end + + describe 'with a category' + it 'should only register the function as a listener for that category' + log4js.addAppender(appender, 'tests'); + + logger.debug('this is a test'); + appenderEvent.should.be event + + appenderEvent = undefined; + log4js.getLogger('some other category').debug('Cheese'); + appenderEvent.should.be undefined + end + end + + end + + describe 'basicLayout' + it 'should take a logevent and output a formatted string' + logger.debug('this is a test'); + var output = log4js.basicLayout(event); + output.should.match /\[.*?\] \[DEBUG\] tests - this is a test/ + end + + it 'should output a stacktrace, message if the event has an error attached' + var error = new Error("Some made-up error"); + var stack = error.stack.split(/\n/); + + logger.debug('this is a test', error); + + var output = log4js.basicLayout(event); + var lines = output.split(/\n/); + lines.length.should.be stack.length+1 + lines[0].should.match /\[.*?\] \[DEBUG\] tests - this is a test/ + lines[1].should.match /\[.*?\] \[DEBUG\] tests - Error: Some made-up error/ + for (var i = 1; i < stack.length; i++) { + lines[i+1].should.eql stack[i] + } + end + + it 'should output a name and message if the event has something that pretends to be an error' + logger.debug('this is a test', { name: 'Cheese', message: 'Gorgonzola smells.' }); + var output = log4js.basicLayout(event); + var lines = output.split(/\n/); + lines.length.should.be 2 + lines[0].should.match /\[.*?\] \[DEBUG\] tests - this is a test/ + lines[1].should.match /\[.*?\] \[DEBUG\] tests - Cheese: Gorgonzola smells./ + end + end + + describe 'messagePassThroughLayout' + it 'should take a logevent and output only the message' + logger.debug('this is a test'); + log4js.messagePassThroughLayout(event).should.be 'this is a test' + end + end + + describe 'fileAppender' + before + log4js.clearAppenders(); + try { + posix.unlink('./tmp-tests.log').wait(); + } catch(e) { + print('Could not delete tmp-tests.log: '+e.message); + } + end + + it 'should write log events to a file' + log4js.addAppender(log4js.fileAppender('./tmp-tests.log', log4js.messagePassThroughLayout), 'tests'); + logger.debug('this is a test'); + + var content = waitForWriteAndThenRead('./tmp-tests.log'); + content.should.be 'this is a test\n' + end + end + + describe 'configure' + before + log4js.clearAppenders(); + try { + posix.unlink('./tmp-tests.log').wait(); + } catch(e) { + print('Could not delete tmp-tests.log: '+e.message); + } + end + + it 'should load appender configuration from a json file' + //this config file defines one file appender (to ./tmp-tests.log) + //and sets the log level for "tests" to WARN + log4js.configure('spec/fixtures/log4js.json'); + event = undefined; + + logger.info('this should not fire an event'); + event.should.be undefined + + logger.warn('this should fire an event'); + event.message.should.be 'this should fire an event' + waitForWriteAndThenRead('./tmp-tests.log').should.be 'this should fire an event\n' + end + end +end + +describe 'Date' + describe 'toFormattedString' + it 'should add a toFormattedString method to Date' + var date = new Date(); + date.should.respond_to 'toFormattedString' + end + + it 'should default to a format' + var date = new Date(2010, 0, 11, 14, 31, 30, 5); + date.toFormattedString().should.be '2010-01-11 14:31:30.005' + end + end +end diff --git a/tests.js b/tests.js new file mode 100644 index 0000000..ce335d0 --- /dev/null +++ b/tests.js @@ -0,0 +1,55 @@ +require.paths.unshift("./spec/lib", "./lib"); +require("jspec"); + +log4js = require("log4js-node"); + +var sys = require("sys"), posix = require("posix"); + +quit = process.exit +print = sys.puts + +readFile = function(path) { + var result; + try { + posix + .cat(path) + .addCallback( + function(contents){ result = contents; } + ).wait(); + } catch (e) { + throw e; + } + return result; +} + +var specsFound = false; + +if (process.ARGV[2]) { + specsFound = true; + JSpec.exec('spec/spec.' + process.ARGV[2] + '.js'); +} else { + var files; + posix + .readdir('spec/') + .addCallback( + function(dirFiles) { files = dirFiles; } + ).wait(); + + files.filter( + function (file) { + return file.indexOf('spec.') === 0; + } + ).forEach( + function(file) { + specsFound = true; + JSpec.exec('spec/'+file); + } + ); +} +if (specsFound) { + JSpec.run({ reporter: JSpec.reporters.Terminal }); + JSpec.report(); +} else { + print("No tests to run. This makes me sad."); +} +