// // Jala Project [http://opensvn.csie.org/traccgi/jala] // // Copyright 2004 ORF Online und Teletext GmbH // // 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. // // $Revision$ // $LastChangedBy$ // $LastChangedDate$ // $HeadURL$ // /** * @fileoverview Fields and methods of the jala.Test class. */ // Define the global namespace for Jala modules if (!global.jala) { global.jala = {}; } /** * HelmaLib dependencies */ app.addRepository("modules/core/String.js"); app.addRepository("modules/helma/Http.js"); /** * Jala dependencies */ app.addRepository(getProperty("jala.dir", "modules/jala") + "/code/Database.js"); /** * Constructs a new Test instance. * @class Provides various methods for automated tests. * This is essentially a port of JSUnit (http://www.edwardh.com/jsunit/) * suitable for testing Helma applications. * @param {Number} capacity The capacity of the cache * @constructor */ jala.Test = function() { /** * Contains the number of tests that were executed * @type Number */ this.testsRun = 0; /** * Contains the number of tests that failed * @type Boolean */ this.testsFailed = 0; /** * Contains the number of test functions that passed * @type Number */ this.functionsPassed = 0; /** * Contains the number of test functions that failed * @type Number */ this.functionsFailed = 0; /** * An Array containing the results of this Test instance. * @type Array */ this.results = []; return this; }; /************************************************************* ***** S T A T I C F I E L D S A N D M E T H O D S ***** *************************************************************/ /** * Constant indicating "passed" status * @type String * @final */ jala.Test.PASSED = "passed"; /** * Constant indicating "failed" status * @type String * @final */ jala.Test.FAILED = "failed"; /** * Helper method useable for displaying a value * @param {Object} The value to render * @returns The value rendered as string * @type String */ jala.Test.valueToString = function(val) { res.push(); if (val === null) { res.write("null"); } else if (val === undefined) { res.write("undefined"); } else { if (typeof(val) == "function") { // functions can be either JS methods or Java classes // the latter throws an exception when trying to access a property try { res.write(val.name || val); } catch (e) { res.write(val); } } else { if (val.constructor && val.constructor == String) { res.write('"' + encode(val.head(200)) + '"'); } else { res.write(val.toString()); } res.write(" ("); if (val.constructor && val.constructor.name != null) { res.write(val.constructor.name); } else { res.write(typeof(val)); } res.write(")"); } } return res.pop(); }; /** * Returns the directory containing the test files. * The location of the directory is either defined by the * application property "jala.testDir" or expected to be one level * above the application directory (and named "tests") * @returns The directory containing the test files * @type helma.File */ jala.Test.getTestsDirectory = function() { var dir; if (getProperty("jala.testDir") != null) { dir = new helma.File(getProperty("jala.testDir")); } if (!dir || !dir.exists()) { var appDir = new helma.File(app.dir); dir = new helma.File(appDir.getParent(), "tests"); if (!dir.exists()) return null; } return dir; }; /** * Returns an array containing the test files located * in the directory. * @returns An array containing the names of all test files * @type Array */ jala.Test.getTestFiles = function() { var dir; if ((dir = jala.Test.getTestsDirectory()) != null) { return dir.list(/.*\.js$/).sort(); } return null; }; /** * Returns the testfile with the given name * @param {String} fileName The name of the test file * @returns The test file * @type helma.File */ jala.Test.getTestFile = function(fileName) { var dir = jala.Test.getTestsDirectory(); if (dir != null) { return new helma.File(dir, fileName); } return null; }; /** * @param {Number} nr The number of arguments to be expected * @param {Object} args The arguments array. * @returns True in case the number of arguments matches * the expected amount, false otherwise. * @type Boolean */ jala.Test.evalArguments = function(args, argsExpected) { if (!(args.length == argsExpected || (args.length == argsExpected + 1 && typeof(args[0]) == "string"))) { throw new jala.Test.ArgumentsException("Insufficient arguments passed to assertion function"); } return; }; /** * Returns true if the arguments array passed as argument * contains an additional comment. * @param {Array} args The arguments array to check for an existing comment. * @param {Number} argsExpected The number of arguments expected by the * assertion function. * @returns True if the arguments contain a comment, false otherwise. * @type Boolean */ jala.Test.argsContainComment = function(args, argsExpected) { return !(args.length <= argsExpected || (args.length == argsExpected + 1 && typeof(args[0]) != "string")) }; /** * Cuts out the comment from the arguments array passed * as argument and returns it. CAUTION: this actually modifies * the arguments array! * @param {Array} args The arguments array. * @returns The comment, if existing. Null otherwise. * @type String */ jala.Test.getComment = function(args, argsExpected) { if (jala.Test.argsContainComment(args, argsExpected)) return args[0]; return null; }; /** * Returns the argument on the index position in the array * passed as arguments. This method respects an optional comment * at the beginning of the arguments array. * @param {Array} args The arguments to retrieve the non-comment * value from. * @param {Number} idx The index position on which the value to * retrieve is to be expected if no comment is existing. * @returns The non-comment value, or null. * @type Object */ jala.Test.getValue = function(args, argsExpected, idx) { return jala.Test.argsContainComment(args, argsExpected) ? args[idx+1] : args[idx]; }; /** * Creates a stack trace and parses it for display. * @param {java.lang.StackTraceElement} trace The trace to parse. If not given * a stacktrace will be generated * @returns The parsed stack trace * @type String */ jala.Test.getStackTrace = function(trace) { /** * Private method for filtering out only JS parts of the stack trace * @param {Object} name */ var accept = function(name) { return name.endsWith(".js") || name.endsWith(".hac") || name.endsWith(".hsp"); }; // create exception and fill in stack trace if (!trace) { var ex = new Packages.org.mozilla.javascript.EvaluatorException(""); ex.fillInStackTrace(); trace = ex.getStackTrace(); } var stack = []; var el, fileName, lineNumber; // parse the stack trace and keep only the js elements var inTrace = false; for (var i=trace.length; i>0; i--) { el = trace[i-1]; fileName = el.getFileName(); lineNumber = el.getLineNumber(); if (fileName != null && lineNumber > -1 && accept(fileName)) { if (fileName.endsWith(res.meta.currentTest)) { inTrace = true; } if (inTrace == true) { // ignore all trace lines that refer to jala.Test if (fileName.endsWith("jala.Test.js")) { break; } stack.push("at " + fileName + ":" + lineNumber); } } } return stack.reverse().join("\n"); }; /** * Adds all assertion methods, the http client, test database manager and * smpt server to the per-thread global object. * @private */ jala.Test.prepareTestScope = function() { // define global assertion functions for (var i in jala.Test.prototype) { if (i.indexOf("assert") == 0) { global[i] = jala.Test.prototype[i]; } } // add global include method global.include = function(file) { jala.Test.include(global, file); return; }; // instantiate a global HttpClient global.httpClient = new jala.Test.HttpClient(); // instantiate the test database manager global.testDatabases = new jala.Test.DatabaseMgr(); // instantiate the smtp server global.smtpServer = new jala.Test.SmtpServer(); return; }; /** * Evaluates a javascript file in the global test scope. This method can be used * to include generic testing code in test files. This method is available as * global method "include" for all test methods * @param {Object} scope The scope in which the file should be evaluated * @param {String} fileName The name of the file to include, including the path */ jala.Test.include = function(scope, file) { var file = new helma.File(file); var fileName = file.getName(); if (file.canRead() && file.exists()) { var cx = Packages.org.mozilla.javascript.Context.enter(); var code = new java.lang.String(file.readAll() || ""); cx.evaluateString(scope, code, fileName, 1, null); } return; }; /******************************* ***** E X C E P T I O N S ***** *******************************/ /** * Creates a new Exception instance * @class Base exception class * @returns A newly created Exception instance * @constructor */ jala.Test.Exception = function Exception() { return this; }; /** @ignore */ jala.Test.Exception.prototype.toString = function() { return "[jala.Test.Exception: " + this.message + "]"; }; /** * Creates a new TestException instance * @class Instances of this exception are thrown whenever an * assertion function fails * @param {String} comment An optional comment * @param {String} message The failure message * @returns A newly created TestException instance * @constructor */ jala.Test.TestException = function TestException(comment, message) { this.functionName = null; this.comment = comment; this.message = message; this.stackTrace = jala.Test.getStackTrace(); return this; }; jala.Test.TestException.prototype = new jala.Test.Exception(); /** @ignore */ jala.Test.TestException.prototype.toString = function() { return "[jala.Test.TestException in " + this.functionName + ": " + this.message + "]"; }; /** * Creates a new ArgumentsException instance * @class Instances of this exception are thrown whenever an assertion * function is called with incorrect or insufficient arguments * @param {String} message The failure message * @returns A newly created ArgumentsException instance * @constructor */ jala.Test.ArgumentsException = function ArgumentsException(message) { this.functionName = null; this.message = message; this.stackTrace = jala.Test.getStackTrace(); return this; }; jala.Test.ArgumentsException.prototype = new jala.Test.Exception(); /** @ignore */ jala.Test.ArgumentsException.prototype.toString = function() { return "[jala.Test.ArgumentsException in " + this.functionName + ": " + this.message + "]"; }; /** * Creates a new EvaluatorException instance * @class Instances of this exception are thrown when attempt * to evaluate the test code fails. * @param {String} message The failure message, or an Error previously * thrown. * @param {String} exception An optional nested Error * @returns A newly created EvaluatorException instance * @constructor */ jala.Test.EvaluatorException = function EvaluatorException(message, exception) { this.functionName = null; this.message = null; this.stackTrace = null; this.fileName = null; this.lineNumber = null; if (arguments.length == 1 && arguments[0] instanceof Error) { this.message = ""; exception = arguments[0]; } else { this.message = message; } if (exception != null) { this.name = exception.name; if (exception.rhinoException != null) { var e = exception.rhinoException; this.message += e.details(); this.stackTrace = jala.Test.getStackTrace(e.getStackTrace()); } else if (exception instanceof Error) { this.message = exception.message; } if (!this.stackTrace) { // got no stack trace, so add at least filename and line number this.fileName = exception.fileName || null; this.lineNumber = exception.lineNumber || null; } } return this; }; jala.Test.EvaluatorException.prototype = new jala.Test.Exception(); /** @ignore */ jala.Test.EvaluatorException.prototype.toString = function() { return "[jala.Test.EvaluatorException: " + this.message + "]"; }; /************************************************* ***** R E S U L T C O N S T R U C T O R S ***** *************************************************/ /** * Constructs a new TestResult instance * @class Instances of this class represent the result of the execution * of a single test file * @param {String} testFileName The name of the excecuted test file * @returns A new TestResult instance * @constructor */ jala.Test.TestResult = function(testFileName) { this.name = testFileName; this.elapsed = 0; this.status = jala.Test.PASSED; this.log = []; return this; }; /** * Constructs a new TestFunctionResult instance * @class Instances of this class represent the result of the successful * execution of a single test function (failed executions will be represented * as Exceptions in the log of a test result). * @param {String} functionName The name of the excecuted test function * @param {Date} startTime A Date instance marking the begin of the test * @returns A new TestFunctionResult instance * @constructor */ jala.Test.TestFunctionResult = function(functionName, startTime) { this.functionName = functionName; this.elapsed = (new Date()) - startTime; return this; }; /************************************************* ***** P R O T O T Y P E F U N C T I O N S ***** *************************************************/ /** * Executes a single test file * @param {helma.File} testFile The file containing the test to run */ jala.Test.prototype.executeTest = function(testFile) { var testFileName = testFile.getName(); // store the name of the current test in res.meta.currentTest // as this is needed in getStackTrace res.meta.currentTest = testFileName; var cx = Packages.org.mozilla.javascript.Context.enter(); var code = new java.lang.String(testFile.readAll() || ""); var testResult = new jala.Test.TestResult(testFileName); global.testFunctionIdents = []; try { // prepare the test scope jala.Test.prepareTestScope(); // evaluate the test file in the per-thread which is garbage // collected at the end of the test run cx.evaluateString(global, code, testFileName, 1, null); for (var ident in global) { if (ident.startsWith("test") && (global[ident] instanceof Function)) { testFunctionIdents.push(ident); } } var start = new Date(); // run all test methods defined in the array "tests" var functionName; for (var i=0;i 0) { for (var i=0;i 0) { var fileName, skinParam; for (var i=0;i= 301 && result.code <= 303 && result.location != null) { // received a redirect location, so follow it result = this.executeRequest("GET", result.location); } return result; }; /** * Convenience method for requesting the url passed as argument * using method GET * @param {String} url The url to request * @param {Object} param A parameter object to use with this request * @return An object containing response values * @see helma.Http.prototype.getUrl */ jala.Test.HttpClient.prototype.getUrl = function(url, param) { return this.executeRequest("GET", url, param); }; /** * Convenience method for submitting a form. * @param {String} url The url to request * @param {Object} param A parameter object to use with this request * @return An object containing response values * @see helma.Http.prototype.getUrl */ jala.Test.HttpClient.prototype.submitForm = function(url, param) { return this.executeRequest("POST", url, param); }; /** @ignore */ jala.Test.HttpClient.prototype.toString = function() { return "[jala.Test.HttpClient]"; }; /***************************************************** ***** T E S T D A T A B A S E M A N A G E R ***** *****************************************************/ /** * Returns a newly created DatabaseMgr instance * @class Instances of this class allow managing test databases * and switching a running application to an in-memory test * database to use within a unit test. * @returns A newly created instance of DatabaseMgr * @constructor */ jala.Test.DatabaseMgr = function() { /** * Map containing all test databases */ this.databases = {}; /** * Map containing the original datasource * properties that were temporarily deactivated * by activating a test database */ this.dbSourceProperties = {}; return this; }; jala.Test.DatabaseMgr.prototype.toString = function() { return "[jala.Test DatabaseMgr]"; }; /** * Returns a newly initialized in-memory test database with the given name * @param {String} name The name of the test database * @returns The newly initialized test database * @type jala.db.RamDatabase */ jala.Test.DatabaseMgr.prototype.initDatabase = function(name) { return new jala.db.RamDatabase(name); }; /** * Switches the application to the test database passed as argument. * In addition this method clears the application cache and invalidates * the root object. * @param {jala.db.RamDatabase} testDb The test database to switch to. * @param {String} dbSourceName Optional name of the application's database * source that will be replaced by the test database. */ jala.Test.DatabaseMgr.prototype.switchToDatabase = function(testDb, dbSourceName) { var dbName = dbSourceName || testDb.getName(); // switch the datasource to the test database var dbSource = app.getDbSource(dbName); var oldProps = dbSource.switchProperties(testDb.getProperties()); // store the old db properties in this manager for use in stopAll() this.databases[dbName] = testDb; this.dbSourceProperties[dbName] = oldProps; // clear the application cache and invalidate root app.clearCache(); root.invalidate(); return; }; /** * Switches the application datasource with the given name * to a newly created in-memory database. In addition this method * also clears the application cache and invalidates the root * object to ensure that everything is re-retrieved from the * test database. * This method can be called with a second boolean argument indicating * that tables used by the application should be created in the in-memory * database (excluding any data). * @param {String} dbSourceName The name of the application database * source as defined in db.properties * @param {Boolean} copyTables If true this method also copies all * tables in the source database to the test database (excluding * indexes). * @param {Array} tables An optional array containing table names that * should be created in the test database. If not specified this method * collects all tables that are mapped in the application. * @returns The test database * @type jala.db.RamDatabase */ jala.Test.DatabaseMgr.prototype.startDatabase = function(dbSourceName, copyTables, tables) { try { var testDb = this.initDatabase(dbSourceName); // switch the datasource to the test database var dbSource = app.getDbSource(dbSourceName); if (copyTables === true) { if (tables === null || tables === undefined) { // collect the table names of all relational prototypes tables = []; var protos = app.getPrototypes(); for each (var proto in protos) { var dbMap = proto.getDbMapping(); if (dbMap.isRelational()) { tables.push(dbMap.getTableName()); } } } testDb.copyTables(dbSource, tables); } this.switchToDatabase(testDb); return testDb; } catch (e) { throw new jala.Test.EvaluatorException("Unable to switch to test database because of ", e); } }; /** * Stops all registered test databases and and reverts the application * to its original datasource(s) as defined in db.properties file. * In addition this method rolls back all pending changes within the request, * clears the application cache and invalidates the root object * to ensure no traces of the test database are left behind. */ jala.Test.DatabaseMgr.prototype.stopAll = function() { // throw away all pending transactions res.rollback(); try { // loop over all registered databases and revert the appropriate // datasource back to the original database var testDb, dbSource; for (var dbSourceName in this.databases) { testDb = this.databases[dbSourceName]; dbSource = app.getDbSource(dbSourceName); dbSource.switchProperties(this.dbSourceProperties[dbSourceName]); testDb.shutdown(); } // clear the application cache and invalidate root app.clearCache(); root.invalidate(); } catch (e) { throw new jala.Test.EvaluatorException("Unable to stop test databases because of ", e); } return; }; /********************************* ***** S M T P S E R V E R ***** *********************************/ /** * Creates a new SmtpServer instance * @class Instances of this class represent an SMTP server listening on * localhost. By default jala.Test will create a global variable called * "smtpServer" that contains an instance of this class. To use the server call * {@link #start} in a test method (eg. in the basic setup method) and * {@link #stop} in the cleanup method. * @param {Number} port Optional port to listen on (defaults to 25) * @returns A newly created SmtpServer instance * @constructor */ jala.Test.SmtpServer = function(port) { var server = null; var oldSmtpServer = null; /** * Starts the SMTP server. Note that this method switches the SMTP server as * defined in app.properties of the tested application or server.properties * to "localhost" to ensure that all mails sent during tests are received * by this server. The SMTP server definition is switched back to the * original when {@link #stop} is called. */ this.start = function() { server = new Packages.org.subethamail.wiser.Wiser() // listen only on localhost server.setHostname("localhost"); if (port != null && !isNaN(port)) { server.setPort(port); } // switch smtp property of tested application oldSmtpServer = getProperty("smtp"); app.__app__.getProperties().put("smtp", "localhost"); server.start(); return; }; /** * Stops the SMTP server and switches the "smtp" property of the tested * application back to the value defined in app or server.properties */ this.stop = function() { server.stop(); server = null; // switch back to original SMTP server address var props = app.__app__.getProperties(); if (oldSmtpServer != null) { props.put("smtp", oldSmtpServer); } else { props.remove("smtp"); } return; }; /** * Returns an array containing all mails received by the server, * where each one is an instance of {@link jala.Test.SmtpServer.Message} * @returns An array with all messages * @type Array */ this.getMessages = function() { var it = server.getMessages().listIterator(); var result = []; while (it.hasNext()) { result.push(new jala.Test.SmtpServer.Message(it.next())); } return result; }; return this; }; /** @ignore */ jala.Test.SmtpServer.prototype.toString = function() { return "[Jala Test SmtpServer]"; }; /** * Creates a new Mail instance * @class Instances of this class represent a mail message received * by the SMTP server * @param {org.subethamail.wiser.WiserMessage} message The message * as received by the SMTP server * @returns A newly created Mail instance * @constructor */ jala.Test.SmtpServer.Message = function(message) { /** * The wrapped message as MimeMessage instance * @type javax.mail.internet.MimeMessage * @private */ var mimeMessage = message.getMimeMessage(); /** * Returns the wrapped message * @type org.subethamail.wiser.WiserMessage */ this.getMessage = function() { return message; }; /** * Returns the wrapped message as MimeMessage * @type javax.mail.internet.MimeMessage */ this.getMimeMessage = function() { return mimeMessage; }; return this; }; /** @ignore */ jala.Test.SmtpServer.Message.prototype.toString = function() { return "[Jala Test Mail]"; }; /** * Returns an array containing all senders of this mail * @returns An array with all senders of this mail * @type Array */ jala.Test.SmtpServer.Message.prototype.getFrom = function() { var result = []; this.getMimeMessage().getFrom().forEach(function(addr) { result.push(addr.toString()) }); return result; }; /** * Returns an array containing all recipients of this mail * @returns An array with all recipients of this mail * @type Array */ jala.Test.SmtpServer.Message.prototype.getTo = function() { var type = Packages.javax.mail.internet.MimeMessage.RecipientType.TO; var result = []; this.getMimeMessage().getRecipients(type).forEach(function(addr) { result.push(addr.toString()) }); return result; }; /** * Returns an array containing all CC recipients of this mail * @returns An array with all CC recipients of this mail * @type Array */ jala.Test.SmtpServer.Message.prototype.getCc = function() { var type = Packages.javax.mail.internet.MimeMessage.RecipientType.CC; var result = []; this.getMimeMessage().getRecipients(type).forEach(function(addr) { result.push(addr.toString()) }); return result; }; /** * Returns an array with all reply-to addresses of this mail * @returns An array with all reply-to addresses of this mail * @type Array */ jala.Test.SmtpServer.Message.prototype.getReplyTo = function() { var result = []; this.getMimeMessage().getReplyTo().forEach(function(addr) { result.push(addr.toString()) }); return result; }; /** * Returns the encoding of this mail as defined in the "Content-Transfer-Encoding" * header field * @returns The encoding of this mail * @type String */ jala.Test.SmtpServer.Message.prototype.getEncoding = function() { return this.getMimeMessage().getEncoding(); }; /** * Returns the subject of this mail * @returns The subject of this mail * @type String */ jala.Test.SmtpServer.Message.prototype.getSubject = function() { return this.getMimeMessage().getSubject(); }; /** * Returns the content of this mail * @returns The content of this mail */ jala.Test.SmtpServer.Message.prototype.getContent = function() { return this.getMimeMessage().getContent(); }; /** * Returns the content type of this mail as defined in the "Content-Type" * header field * @returns The content type of this mail * @type String */ jala.Test.SmtpServer.Message.prototype.getContentType = function() { return this.getMimeMessage().getContentType(); };