//
// 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();
};