Permalink
Browse files

Use FooUnit.

  • Loading branch information...
1 parent 3ded30d commit 02f7074f688236049e1718468cf09895346b7de1 mde committed May 3, 2011
View
1 geddy-model/tests/browser/autogen_suite.js
@@ -0,0 +1 @@
+foounit.getSuite().addFile(':test/shared/datatypes.js');
View
79 geddy-model/tests/browser/suite_runner.html
@@ -0,0 +1,79 @@
+<html>
+
+<style type="text/css">
+body {
+ font-family: Lucida Console, Monaco, monospace;
+ font-size: 8pt;
+}
+
+.title {
+ padding-bottom: 10px;
+}
+
+.example {
+ margin-bottom: 12px;
+}
+
+.stack {
+ background-color: #FFFFD1;
+ margin-top: 5px;
+ border-radius: 5px;
+ padding: 5px;
+}
+
+.failure, .success {
+ padding: 10px;
+ border-radius: 5px;
+}
+
+.failure {
+ background-color: #E99494;
+}
+
+.success {
+ background-color: #339933;
+}
+
+.example.success {
+ color: #ccc;
+}
+
+.example.pending {
+ background-color: #E6E600;
+ padding: 2px 2px 2px 10px;
+ border-radius: 5px;
+}
+
+.progress-bar {
+ padding: 10px;
+ margin: 10px 0;
+ background-color: #ccc;
+ border-radius: 5px;
+ word-wrap: break-word;
+}
+
+.progress-bar a {
+ text-decoration: none;
+}
+
+.progress-bar .success,
+.progress-bar .failure,
+.progress-bar .pending {
+ background-color: #ccc;
+ font-size: 14px;
+ padding: 0;
+}
+
+.progress-bar .success { color: #339933; }
+.progress-bar .failure { color: #ff0000; }
+.progress-bar .pending { font-weight: bold; color: #E8E819; }
+
+.topnav, .topnav:visited { color: #0000ff; text-decoration: none; }
+.example .topnav { visibility: hidden; }
+.example:hover .topnav { visibility: visible; }
+</style>
+
+<script type="text/javascript" src="../foounit/foounit.js"></script>
+<script type="text/javascript" src="../foounit/foounit-browser.js"></script>
+<script type="text/javascript" src="../suite.js"></script>
+</html>
View
1,146 geddy-model/tests/foounit/foounit-browser.js
@@ -0,0 +1,1146 @@
+/*
+ http://www.JSON.org/json2.js
+ 2011-02-23
+
+ Public Domain.
+
+ NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
+
+ See http://www.JSON.org/js.html
+
+
+ This code should be minified before deployment.
+ See http://javascript.crockford.com/jsmin.html
+
+ USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
+ NOT CONTROL.
+
+
+ This file creates a global JSON object containing two methods: stringify
+ and parse.
+
+ JSON.stringify(value, replacer, space)
+ value any JavaScript value, usually an object or array.
+
+ replacer an optional parameter that determines how object
+ values are stringified for objects. It can be a
+ function or an array of strings.
+
+ space an optional parameter that specifies the indentation
+ of nested structures. If it is omitted, the text will
+ be packed without extra whitespace. If it is a number,
+ it will specify the number of spaces to indent at each
+ level. If it is a string (such as '\t' or '&nbsp;'),
+ it contains the characters used to indent at each level.
+
+ This method produces a JSON text from a JavaScript value.
+
+ When an object value is found, if the object contains a toJSON
+ method, its toJSON method will be called and the result will be
+ stringified. A toJSON method does not serialize: it returns the
+ value represented by the name/value pair that should be serialized,
+ or undefined if nothing should be serialized. The toJSON method
+ will be passed the key associated with the value, and this will be
+ bound to the value
+
+ For example, this would serialize Dates as ISO strings.
+
+ Date.prototype.toJSON = function (key) {
+ function f(n) {
+ // Format integers to have at least two digits.
+ return n < 10 ? '0' + n : n;
+ }
+
+ return this.getUTCFullYear() + '-' +
+ f(this.getUTCMonth() + 1) + '-' +
+ f(this.getUTCDate()) + 'T' +
+ f(this.getUTCHours()) + ':' +
+ f(this.getUTCMinutes()) + ':' +
+ f(this.getUTCSeconds()) + 'Z';
+ };
+
+ You can provide an optional replacer method. It will be passed the
+ key and value of each member, with this bound to the containing
+ object. The value that is returned from your method will be
+ serialized. If your method returns undefined, then the member will
+ be excluded from the serialization.
+
+ If the replacer parameter is an array of strings, then it will be
+ used to select the members to be serialized. It filters the results
+ such that only members with keys listed in the replacer array are
+ stringified.
+
+ Values that do not have JSON representations, such as undefined or
+ functions, will not be serialized. Such values in objects will be
+ dropped; in arrays they will be replaced with null. You can use
+ a replacer function to replace those with JSON values.
+ JSON.stringify(undefined) returns undefined.
+
+ The optional space parameter produces a stringification of the
+ value that is filled with line breaks and indentation to make it
+ easier to read.
+
+ If the space parameter is a non-empty string, then that string will
+ be used for indentation. If the space parameter is a number, then
+ the indentation will be that many spaces.
+
+ Example:
+
+ text = JSON.stringify(['e', {pluribus: 'unum'}]);
+ // text is '["e",{"pluribus":"unum"}]'
+
+
+ text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
+ // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
+
+ text = JSON.stringify([new Date()], function (key, value) {
+ return this[key] instanceof Date ?
+ 'Date(' + this[key] + ')' : value;
+ });
+ // text is '["Date(---current time---)"]'
+
+
+ JSON.parse(text, reviver)
+ This method parses a JSON text to produce an object or array.
+ It can throw a SyntaxError exception.
+
+ The optional reviver parameter is a function that can filter and
+ transform the results. It receives each of the keys and values,
+ and its return value is used instead of the original value.
+ If it returns what it received, then the structure is not modified.
+ If it returns undefined then the member is deleted.
+
+ Example:
+
+ // Parse the text. Values that look like ISO date strings will
+ // be converted to Date objects.
+
+ myData = JSON.parse(text, function (key, value) {
+ var a;
+ if (typeof value === 'string') {
+ a =
+/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
+ if (a) {
+ return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+ +a[5], +a[6]));
+ }
+ }
+ return value;
+ });
+
+ myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
+ var d;
+ if (typeof value === 'string' &&
+ value.slice(0, 5) === 'Date(' &&
+ value.slice(-1) === ')') {
+ d = new Date(value.slice(5, -1));
+ if (d) {
+ return d;
+ }
+ }
+ return value;
+ });
+
+
+ This is a reference implementation. You are free to copy, modify, or
+ redistribute.
+*/
+
+/*jslint evil: true, strict: false, regexp: false */
+
+/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
+ call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
+ getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
+ lastIndex, length, parse, prototype, push, replace, slice, stringify,
+ test, toJSON, toString, valueOf
+*/
+
+
+// Create a JSON object only if one does not already exist. We create the
+// methods in a closure to avoid creating global variables.
+
+var JSON;
+if (!JSON) {
+ JSON = {};
+}
+
+(function () {
+ "use strict";
+
+ function f(n) {
+ // Format integers to have at least two digits.
+ return n < 10 ? '0' + n : n;
+ }
+
+ if (typeof Date.prototype.toJSON !== 'function') {
+
+ Date.prototype.toJSON = function (key) {
+
+ return isFinite(this.valueOf()) ?
+ this.getUTCFullYear() + '-' +
+ f(this.getUTCMonth() + 1) + '-' +
+ f(this.getUTCDate()) + 'T' +
+ f(this.getUTCHours()) + ':' +
+ f(this.getUTCMinutes()) + ':' +
+ f(this.getUTCSeconds()) + 'Z' : null;
+ };
+
+ String.prototype.toJSON =
+ Number.prototype.toJSON =
+ Boolean.prototype.toJSON = function (key) {
+ return this.valueOf();
+ };
+ }
+
+ var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+ escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+ gap,
+ indent,
+ meta = { // table of character substitutions
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '"' : '\\"',
+ '\\': '\\\\'
+ },
+ rep;
+
+
+ function quote(string) {
+
+// If the string contains no control characters, no quote characters, and no
+// backslash characters, then we can safely slap some quotes around it.
+// Otherwise we must also replace the offending characters with safe escape
+// sequences.
+
+ escapable.lastIndex = 0;
+ return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
+ var c = meta[a];
+ return typeof c === 'string' ? c :
+ '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ }) + '"' : '"' + string + '"';
+ }
+
+
+ function str(key, holder) {
+
+// Produce a string from holder[key].
+
+ var i, // The loop counter.
+ k, // The member key.
+ v, // The member value.
+ length,
+ mind = gap,
+ partial,
+ value = holder[key];
+
+// If the value has a toJSON method, call it to obtain a replacement value.
+
+ if (value && typeof value === 'object' &&
+ typeof value.toJSON === 'function') {
+ value = value.toJSON(key);
+ }
+
+// If we were called with a replacer function, then call the replacer to
+// obtain a replacement value.
+
+ if (typeof rep === 'function') {
+ value = rep.call(holder, key, value);
+ }
+
+// What happens next depends on the value's type.
+
+ switch (typeof value) {
+ case 'string':
+ return quote(value);
+
+ case 'number':
+
+// JSON numbers must be finite. Encode non-finite numbers as null.
+
+ return isFinite(value) ? String(value) : 'null';
+
+ case 'boolean':
+ case 'null':
+
+// If the value is a boolean or null, convert it to a string. Note:
+// typeof null does not produce 'null'. The case is included here in
+// the remote chance that this gets fixed someday.
+
+ return String(value);
+
+// If the type is 'object', we might be dealing with an object or an array or
+// null.
+
+ case 'object':
+
+// Due to a specification blunder in ECMAScript, typeof null is 'object',
+// so watch out for that case.
+
+ if (!value) {
+ return 'null';
+ }
+
+// Make an array to hold the partial results of stringifying this object value.
+
+ gap += indent;
+ partial = [];
+
+// Is the value an array?
+
+ if (Object.prototype.toString.apply(value) === '[object Array]') {
+
+// The value is an array. Stringify every element. Use null as a placeholder
+// for non-JSON values.
+
+ length = value.length;
+ for (i = 0; i < length; i += 1) {
+ partial[i] = str(i, value) || 'null';
+ }
+
+// Join all of the elements together, separated with commas, and wrap them in
+// brackets.
+
+ v = partial.length === 0 ? '[]' : gap ?
+ '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' :
+ '[' + partial.join(',') + ']';
+ gap = mind;
+ return v;
+ }
+
+// If the replacer is an array, use it to select the members to be stringified.
+
+ if (rep && typeof rep === 'object') {
+ length = rep.length;
+ for (i = 0; i < length; i += 1) {
+ if (typeof rep[i] === 'string') {
+ k = rep[i];
+ v = str(k, value);
+ if (v) {
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
+ }
+ }
+ }
+ } else {
+
+// Otherwise, iterate through all of the keys in the object.
+
+ for (k in value) {
+ if (Object.prototype.hasOwnProperty.call(value, k)) {
+ v = str(k, value);
+ if (v) {
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
+ }
+ }
+ }
+ }
+
+// Join all of the member texts together, separated with commas,
+// and wrap them in braces.
+
+ v = partial.length === 0 ? '{}' : gap ?
+ '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' :
+ '{' + partial.join(',') + '}';
+ gap = mind;
+ return v;
+ }
+ }
+
+// If the JSON object does not yet have a stringify method, give it one.
+
+ if (typeof JSON.stringify !== 'function') {
+ JSON.stringify = function (value, replacer, space) {
+
+// The stringify method takes a value and an optional replacer, and an optional
+// space parameter, and returns a JSON text. The replacer can be a function
+// that can replace values, or an array of strings that will select the keys.
+// A default replacer method can be provided. Use of the space parameter can
+// produce text that is more easily readable.
+
+ var i;
+ gap = '';
+ indent = '';
+
+// If the space parameter is a number, make an indent string containing that
+// many spaces.
+
+ if (typeof space === 'number') {
+ for (i = 0; i < space; i += 1) {
+ indent += ' ';
+ }
+
+// If the space parameter is a string, it will be used as the indent string.
+
+ } else if (typeof space === 'string') {
+ indent = space;
+ }
+
+// If there is a replacer, it must be a function or an array.
+// Otherwise, throw an error.
+
+ rep = replacer;
+ if (replacer && typeof replacer !== 'function' &&
+ (typeof replacer !== 'object' ||
+ typeof replacer.length !== 'number')) {
+ throw new Error('JSON.stringify');
+ }
+
+// Make a fake root object containing our value under the key of ''.
+// Return the result of stringifying the value.
+
+ return str('', {'': value});
+ };
+ }
+
+
+// If the JSON object does not yet have a parse method, give it one.
+
+ if (typeof JSON.parse !== 'function') {
+ JSON.parse = function (text, reviver) {
+
+// The parse method takes a text and an optional reviver function, and returns
+// a JavaScript value if the text is a valid JSON text.
+
+ var j;
+
+ function walk(holder, key) {
+
+// The walk method is used to recursively walk the resulting structure so
+// that modifications can be made.
+
+ var k, v, value = holder[key];
+ if (value && typeof value === 'object') {
+ for (k in value) {
+ if (Object.prototype.hasOwnProperty.call(value, k)) {
+ v = walk(value, k);
+ if (v !== undefined) {
+ value[k] = v;
+ } else {
+ delete value[k];
+ }
+ }
+ }
+ }
+ return reviver.call(holder, key, value);
+ }
+
+
+// Parsing happens in four stages. In the first stage, we replace certain
+// Unicode characters with escape sequences. JavaScript handles many characters
+// incorrectly, either silently deleting them, or treating them as line endings.
+
+ text = String(text);
+ cx.lastIndex = 0;
+ if (cx.test(text)) {
+ text = text.replace(cx, function (a) {
+ return '\\u' +
+ ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ });
+ }
+
+// In the second stage, we run the text against regular expressions that look
+// for non-JSON patterns. We are especially concerned with '()' and 'new'
+// because they can cause invocation, and '=' because it can cause mutation.
+// But just to be safe, we want to reject all unexpected forms.
+
+// We split the second stage into 4 regexp operations in order to work around
+// crippling inefficiencies in IE's and Safari's regexp engines. First we
+// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
+// replace all simple value tokens with ']' characters. Third, we delete all
+// open brackets that follow a colon or comma or that begin the text. Finally,
+// we look to see that the remaining characters are only whitespace or ']' or
+// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
+
+ if (/^[\],:{}\s]*$/
+ .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
+ .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
+ .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
+
+// In the third stage we use the eval function to compile the text into a
+// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
+// in JavaScript: it can begin a block or an object literal. We wrap the text
+// in parens to eliminate the ambiguity.
+
+ j = eval('(' + text + ')');
+
+// In the optional fourth stage, we recursively walk the new structure, passing
+// each name/value pair to a reviver function for possible transformation.
+
+ return typeof reviver === 'function' ?
+ walk({'': j}, '') : j;
+ }
+
+// If the text is not JSON parseable, then a SyntaxError is thrown.
+
+ throw new SyntaxError('JSON.parse');
+ };
+ }
+}());
+if (typeof foounit.ui == 'undefined'){
+ foounit.ui = {};
+}
+
+(function (ui){
+ var _body, _index = 0;
+
+ var _createTitleNode = function (title, index, className){
+ var titleDiv = document.createElement('div');
+ titleDiv.className = 'example ' + className;
+ titleDiv.innerHTML = '<a name="example' + index + '" ' +
+ 'class="title">' + title + '</a> ' +
+ '&nbsp;<a class="topnav" href="#top">top &raquo;</a>';
+ return titleDiv;
+ }
+
+ /**
+ * Creates a failure node for an example
+ */
+ var _createFailureNode = function (example, index){
+ var titleDiv = _createTitleNode(example.getFullDescription(), index, 'failure');
+
+ var stackDiv = document.createElement('div');
+ stackDiv.className = 'stack';
+ stackDiv.innerHTML = '<pre>' +
+ example.getException().message + "\n\n" +
+ example.getStack() +
+ '</pre>';
+ titleDiv.appendChild(stackDiv);
+
+ return titleDiv;
+ };
+
+ /**
+ * Creates a success node for an example
+ */
+ var _createSuccessNode = function (example, index){
+ return _createTitleNode(example.getFullDescription(), index, 'success');
+ };
+
+ /**
+ * Creates a pending node for an example
+ */
+ var _createPendingNode = function (example, index){
+ return _createTitleNode(example.getFullDescription(), index, 'pending');
+ };
+
+ /**
+ * Progress bar
+ */
+ var _ProgressBar = function (body){
+ this._body = body;
+
+ this._node = document.createElement('div');
+ this._node.className = 'progress-bar';
+ this._body.appendChild(this._node);
+
+ this._progress = document.createElement('div');
+ this._progress.className = 'progress';
+ this._node.appendChild(this._progress);
+
+ this._log = document.createElement('div');
+ this._log.id = 'progress-bar-log';
+ this._log.className = 'progress-bar-log';
+ this._node.appendChild(this._log);
+
+ this._appendStatus = function (className, display, index){
+ var node = document.createElement('a');
+ node.href = '#example' + index;
+ node.className = className;
+ node.innerHTML = display;
+ this._progress.appendChild(node);
+ }
+
+ this.fail = function (index){
+ this._appendStatus('failure', 'F', index);
+ }
+
+ this.success = function (index){
+ this._appendStatus('success', '.', index);
+ }
+
+ this.pending = function (index){
+ this._appendStatus('pending', 'P', index);
+ }
+
+ this.log = function (message){
+ var node = document.createElement('div');
+ node.innerHTML = message;
+ this._log.appendChild(node);
+ }
+ }
+
+ /************ Public functions ***********/
+
+ /**
+ * Called to initialize the UI
+ */
+ ui.init = function (){
+ _body = document.getElementsByTagName('body')[0];
+
+ var topNode = document.createElement('a');
+ topNode.setAttribute('name', 'top');
+ _body.appendChild(topNode);
+
+ _progressBar = new _ProgressBar(_body, 'progress');
+ };
+
+ /**
+ * Called when an the runner runs an example that fails
+ */
+ ui.onFailure = function (example){
+ try {
+ _body.appendChild(_createFailureNode(example, _index));
+ _progressBar.fail(_index);
+ ++_index;
+ } catch (e){
+ alert('foounit.ui.onFailure: ' + e.message);
+ }
+ };
+
+ /**
+ * Called when the runner runs an example that succeeds
+ */
+ ui.onSuccess = function (example){
+ try {
+ _body.appendChild(_createSuccessNode(example, _index));
+ _progressBar.success(_index);
+ ++_index;
+ } catch (e){
+ alert('foounit.ui.onSuccess: ' + e.message);
+ }
+ };
+
+ /**
+ * Called when the runner runs a pending example
+ */
+ ui.onPending = function (example){
+ try {
+ _body.appendChild(_createPendingNode(example, _index));
+ _progressBar.pending(_index);
+ ++_index;
+ } catch (e){
+ alert('foounit.ui.onPending: ' + e.message);
+ }
+ }
+
+ /**
+ * Called when the suite has finished running
+ */
+ ui.onFinish = function (info){
+ try {
+ _progressBar.log('>> foounit summary: ' +
+ info.failCount + ' failed, ' +
+ info.passCount + ' passed, ' +
+ info.pending.length + ' pending, ' +
+ info.totalCount + ' total');
+
+ _progressBar.log('>> foounit runtime: ', info.runMillis + 'ms');
+ } catch (e){
+ alert('foounit.ui.onFinish: ' + e.message);
+ }
+ };
+
+})(foounit.ui);
+
+// Mostly pulled from node's assert.js library
+// Made a few mods to get this working in a browser
+// other than chrome. - Bob Remeika
+
+// Licensing included:
+
+// http://wiki.commonjs.org/wiki/Unit_Testing/1.0
+//
+// Originally from narwhal.js (http://narwhaljs.org)
+// Copyright (c) 2009 Thomas Robinson <280north.com>
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the 'Software'), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+// UTILITY
+assert = (function (){
+ var assert = {};
+
+ var pSlice = Array.prototype.slice;
+
+
+ // FIXME: This is super hacky
+ // AssertionError only works in v8
+ if (navigator.userAgent.match(/Chrome/)){
+ assert.AssertionError = function AssertionError(options) {
+ this.name = 'AssertionError';
+ this.message = options.message;
+ this.actual = options.actual;
+ this.expected = options.expected;
+ this.operator = options.operator;
+ var stackStartFunction = options.stackStartFunction || fail;
+
+ //if (Error.captureStackTrace) {
+ Error.captureStackTrace(this, stackStartFunction);
+ //}
+ };
+ foounit.mixin(assert.AssertionError.prototype, Error.prototype);
+
+ assert.AssertionError.prototype.toString = function() {
+ if (this.message) {
+ return [this.name + ':', this.message].join(' ');
+ } else {
+ return [this.name + ':',
+ JSON.stringify(this.expected),
+ this.operator,
+ JSON.stringify(this.actual)].join(' ');
+ }
+ };
+
+ // assert.AssertionError instanceof Error
+ assert.AssertionError.__proto__ = Error.prototype;
+ }
+
+
+ function fail(actual, expected, message, operator, stackStartFunction) {
+ // FIXME: This is super hacky
+ // AssertionError only works in V8
+ if (navigator.userAgent.match(/Chrome/)){
+ throw new assert.AssertionError({
+ message: message,
+ actual: actual,
+ expected: expected,
+ operator: operator,
+ stackStartFunction: stackStartFunction
+ });
+ } else {
+ if (message) {
+ var msg = ['AssertionError:', message].join(' ');
+ throw new Error(msg);
+ } else {
+ var formatted = ['AssertionError:',
+ JSON.stringify(expected),
+ operator,
+ JSON.stringify(actual)].join(' ');
+ throw new Error(formatted);
+ }
+ }
+ }
+
+ // EXTENSION! allows for well behaved errors defined elsewhere.
+ assert.fail = fail;
+
+ // 4. Pure assertion tests whether a value is truthy, as determined
+ // by !!guard.
+ // assert.ok(guard, message_opt);
+ // This statement is equivalent to assert.equal(true, guard,
+ // message_opt);. To test strictly for the value true, use
+ // assert.strictEqual(true, guard, message_opt);.
+
+ assert.ok = function ok(value, message) {
+ if (!!!value) fail(value, true, message, '==', assert.ok);
+ };
+
+ // 5. The equality assertion tests shallow, coercive equality with
+ // ==.
+ // assert.equal(actual, expected, message_opt);
+
+ assert.equal = function equal(actual, expected, message) {
+ if (actual != expected) fail(actual, expected, message, '==', assert.equal);
+ };
+
+ // 6. The non-equality assertion tests for whether two objects are not equal
+ // with != assert.notEqual(actual, expected, message_opt);
+
+ assert.notEqual = function notEqual(actual, expected, message) {
+ if (actual == expected) {
+ fail(actual, expected, message, '!=', assert.notEqual);
+ }
+ };
+
+ // 7. The equivalence assertion tests a deep equality relation.
+ // assert.deepEqual(actual, expected, message_opt);
+
+ assert.deepEqual = function deepEqual(actual, expected, message) {
+ if (!_deepEqual(actual, expected)) {
+ fail(actual, expected, message, 'deepEqual', assert.deepEqual);
+ }
+ };
+
+ function _deepEqual(actual, expected) {
+ // 7.1. All identical values are equivalent, as determined by ===.
+ if (actual === expected) {
+ return true;
+
+ // 7.2. If the expected value is a Date object, the actual value is
+ // equivalent if it is also a Date object that refers to the same time.
+ } else if (actual instanceof Date && expected instanceof Date) {
+ return actual.getTime() === expected.getTime();
+
+ // 7.3. Other pairs that do not both pass typeof value == 'object',
+ // equivalence is determined by ==.
+ } else if (typeof actual != 'object' && typeof expected != 'object') {
+ return actual == expected;
+
+ // 7.4. For all other Object pairs, including Array objects, equivalence is
+ // determined by having the same number of owned properties (as verified
+ // with Object.prototype.hasOwnProperty.call), the same set of keys
+ // (although not necessarily the same order), equivalent values for every
+ // corresponding key, and an identical 'prototype' property. Note: this
+ // accounts for both named and indexed properties on Arrays.
+ } else {
+ return objEquiv(actual, expected);
+ }
+ }
+
+ function isUndefinedOrNull(value) {
+ return value === null || value === undefined;
+ }
+
+ function isArguments(object) {
+ return Object.prototype.toString.call(object) == '[object Arguments]';
+ }
+
+ function getKeys(obj){
+ var keys = [];
+ try {
+ keys = Object.keys(obj);
+ } catch (e){
+ for (var p in obj){
+ keys.push(p);
+ }
+ }
+ return keys;
+ }
+
+ function objEquiv(a, b) {
+ if (isUndefinedOrNull(a) || isUndefinedOrNull(b))
+ return false;
+ // an identical 'prototype' property.
+ if (a.prototype !== b.prototype) return false;
+ //~~~I've managed to break Object.keys through screwy arguments passing.
+ // Converting to array solves the problem.
+ if (isArguments(a)) {
+ if (!isArguments(b)) {
+ return false;
+ }
+ a = pSlice.call(a);
+ b = pSlice.call(b);
+ return _deepEqual(a, b);
+ }
+ try {
+ var ka = getKeys(a),
+ kb = getKeys(b),
+ key, i;
+ } catch (e) {//happens when one is a string literal and the other isn't
+ return false;
+ }
+ // having the same number of owned properties (keys incorporates
+ // hasOwnProperty)
+ if (ka.length != kb.length)
+ return false;
+ //the same set of keys (although not necessarily the same order),
+ ka.sort();
+ kb.sort();
+ //~~~cheap key test
+ for (i = ka.length - 1; i >= 0; i--) {
+ if (ka[i] != kb[i])
+ return false;
+ }
+ //equivalent values for every corresponding key, and
+ //~~~possibly expensive deep test
+ for (i = ka.length - 1; i >= 0; i--) {
+ key = ka[i];
+ if (!_deepEqual(a[key], b[key])) return false;
+ }
+ return true;
+ }
+
+ // 8. The non-equivalence assertion tests for any deep inequality.
+ // assert.notDeepEqual(actual, expected, message_opt);
+
+ assert.notDeepEqual = function notDeepEqual(actual, expected, message) {
+ if (_deepEqual(actual, expected)) {
+ fail(actual, expected, message, 'notDeepEqual', assert.notDeepEqual);
+ }
+ };
+
+ // 9. The strict equality assertion tests strict equality, as determined by ===.
+ // assert.strictEqual(actual, expected, message_opt);
+
+ assert.strictEqual = function strictEqual(actual, expected, message) {
+ if (actual !== expected) {
+ fail(actual, expected, message, '===', assert.strictEqual);
+ }
+ };
+
+ // 10. The strict non-equality assertion tests for strict inequality, as
+ // determined by !==. assert.notStrictEqual(actual, expected, message_opt);
+
+ assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
+ if (actual === expected) {
+ fail(actual, expected, message, '!==', assert.notStrictEqual);
+ }
+ };
+
+ function expectedException(actual, expected) {
+ if (!actual || !expected) {
+ return false;
+ }
+
+ if (expected instanceof RegExp) {
+ return expected.test(actual);
+ } else if (actual instanceof expected) {
+ return true;
+ } else if (expected.call({}, actual) === true) {
+ return true;
+ }
+
+ return false;
+ }
+
+ function _throws(shouldThrow, block, expected, message) {
+ var actual;
+
+ if (typeof expected === 'string') {
+ message = expected;
+ expected = null;
+ }
+
+ try {
+ block();
+ } catch (e) {
+ actual = e;
+ }
+
+ message = (expected && expected.name ? ' (' + expected.name + ').' : '.') +
+ (message ? ' ' + message : '.');
+
+ if (shouldThrow && !actual) {
+ fail('Missing expected exception' + message);
+ }
+
+ if (!shouldThrow && expectedException(actual, expected)) {
+ fail('Got unwanted exception' + message);
+ }
+
+ var actualMessage = actual && (actual.message || actual.toString());
+ if ((shouldThrow && actual && expected &&
+ !expectedException(actualMessage, expected)) || (!shouldThrow && actual)) {
+ throw actual;
+ }
+ }
+
+ // 11. Expected to throw an error:
+ // assert.throws(block, Error_opt, message_opt);
+
+ assert.throws = function(block, /*optional*/error, /*optional*/message) {
+ _throws.apply(this, [true].concat(pSlice.call(arguments)));
+ };
+
+ // EXTENSION! This is annoying to write outside this module.
+ assert.doesNotThrow = function(block, /*optional*/error, /*optional*/message) {
+ _throws.apply(this, [false].concat(pSlice.call(arguments)));
+ };
+
+ assert.ifError = function(err) { if (err) {throw err;}};
+
+ return assert;
+})();
+// TODO: Beef this up for crappy browsers
+(function (){
+ if (typeof console === 'undefined'){ console = {}; }
+
+ var funcs = ['log', 'dir', 'info', 'debug', 'error'];
+ for (var i = 0; i < funcs.length; ++i){
+ var func = funcs[i];
+ if (!console[func]){
+ console[func] = function (){ };
+ }
+ }
+})();
+if (typeof foounit.browser === 'undefined'){
+ foounit.browser = {};
+}
+
+foounit.browser.XhrLoaderStrategy = function (){
+
+ var xhr = function (){
+ if (window.XMLHttpRequest) {
+ return new window.XMLHttpRequest;
+ } else {
+ try {
+ return new ActiveXObject("MSXML2.XMLHTTP.3.0");
+ } catch(ex) {
+ throw new Error('Could not get XmlHttpRequest object');
+ }
+ }
+ };
+
+ var get = function (uri){
+ var request = xhr();
+ request.open('GET', uri, false);
+ request.send(null);
+ if (request.status == 200){
+ return request.responseText;
+ }
+ throw new Error('Failed XHR request to: ' + uri);
+ };
+
+ var dirname = function (file){
+ var parts = file.split('/');
+ if (parts.length > 1){
+ return parts.slice(parts, parts.length - 2).join('/');
+ } else {
+ return '.';
+ }
+ };
+
+ var basename = function (file){
+ var parts = file.split('/');
+ return parts[parts.length - 1];
+ };
+
+ /**
+ * Implements lower level require responsible for syncronously getting code
+ * and loading the code in CommonJS format with functional scope.
+ */
+ this.require = function (path){
+ var code = get(path)
+ , module = { exports: {} }
+ , funcString = '(function (foounit, module, exports, __dirname, __filename){' + code + '});';
+
+ var func;
+ try {
+ // IE sucks shit.
+ if (document.all){
+ eval('func = ' + funcString);
+ } else {
+ func = eval(funcString);
+ }
+ func.call({}, foounit, module, module.exports, dirname(path), basename(path));
+ } catch (e){
+ console.error('Failed to load path: ' + path + ': ' + e.message, e);
+ }
+
+ return module.exports;
+ };
+
+ /**
+ * Implements low level require for synchronously running code in a global scope.
+ */
+ this.load = function (path){
+ var code = get(path);
+ eval(code);
+ return true;
+ };
+};
+(function (foounit){
+ if (typeof foounit.browser === 'undefined'){
+ foounit.browser = {};
+ }
+
+ var _loaded = {}
+ , _loaderStrategy;
+
+ var _loadCode = function (path, type){
+ // FIXME: Kinda hacky
+ if (path.match(/foounit$/) || path.match(/foounit-browser$/)){
+ return foounit;
+ }
+
+ path = foounit.translatePath(path);
+ if (!_loaded[path]){
+ _loaded[path] = _loaderStrategy[type](path + '.js');
+ }
+ return _loaded[path];
+ };
+
+ /**
+ * Set the strategy for synchronous dependency loading in functional scope (ala Node's require)
+ */
+ foounit.browser.setLoaderStrategy = function (strategy){
+ _loaderStrategy = strategy;
+ };
+
+ /**
+ * Load a javascript file in a functional scope
+ */
+ foounit.require = function (path){
+ return _loadCode(path, 'require');
+ };
+
+ /**
+ * Load a javascript file in the global scope
+ */
+ foounit.load = function (path){
+ return _loadCode(path, 'load');
+ };
+
+ /**
+ * Extracts the directory from a loaded script in the DOM
+ *
+ * @param pattern - A pattern used to locate the script source in the DOM
+ */
+ foounit.browser.dirname = function (pattern){
+ var getDirectoryFromPath = function (path){
+ var dir = path.split('/');
+ dir.pop();
+ return dir.join('/');
+ }
+
+ var scripts = document.getElementsByTagName('script');
+ for (var i = 0; i < scripts.length; ++i){
+ var script = scripts[i].src;
+ if (script.match(pattern)){
+ return getDirectoryFromPath(script);
+ }
+ }
+ };
+
+ /**
+ * Reports the final results of the suite
+ */
+ foounit.report = function (info){
+ foounit.ui.onFinish(info);
+ };
+
+ /**
+ * Report a single example
+ */
+ foounit.reportExample = (function (){
+ var isUiInit = false;
+
+ return function (example){
+ if (!isUiInit){
+ foounit.ui.init();
+ isUiInit = true;
+ }
+
+ try {
+ if (example.isSuccess()){
+ foounit.ui.onSuccess(example);
+ } else if (example.isFailure()){
+ foounit.ui.onFailure(example);
+ } else if (example.isPending()){
+ foounit.ui.onPending(example);
+ }
+ } catch (e){
+ alert('foounit.reportExample: ' + e.message);
+ }
+ };
+ })();
+
+})(foounit);
+
View
326 geddy-model/tests/foounit/foounit-node.js
@@ -0,0 +1,326 @@
+var fsh = (function (module){var fs = require('fs')
+ , exec = require('child_process').exec;
+
+var fsh = module.exports;
+fsh.separator = '/';
+fsh.debug = false;
+
+fsh.mkdirpSync = function (path, mode){
+ var parts = path.split(this.separator);
+ for (var i = 0, ii = parts.length; i < ii; ++i){
+ var newpath = parts[i] + this.separator;
+ if (!fsh.existsSync(newpath)){
+ fs.mkdirSync(newpath, mode);
+ }
+ }
+ return true;
+}
+
+fsh.existsSync = function (path){
+ var stat;
+ try {
+ stat = fs.statSync(path);
+ } catch (e){
+ if (e.type == 'ENOENT'){
+ return false;
+ }
+ }
+ return stat;
+}
+
+fsh.isDirectorySync = function (path){
+ return _isStatSync(path, 'isDirectory');
+}
+
+fsh.isSymbolicLinkSync = function (path){
+ return _isStatSync(path, 'isSymbolicLink');
+}
+
+var _isStatSync = function (path, func){
+ try {
+ var stat = fs.lstatSync(path);
+ return stat[func]();
+ } catch (e) {}
+ return false;
+}
+
+
+// FIXME: This fails silently... BAAAAAAAAAAAAAAD
+fsh.copyFileSync = function (source, dest){
+ var cmd = 'cp ' + source + ' ' + dest;
+ exec(cmd);
+}
+
+
+// TODO: Add option for following symlinks
+fsh.findSync = function (basedir, pattern, options){
+ var matches = []
+ , defaults = { includeDirs: false };
+
+ var _mixin = function (target, source){
+ for (var p in source){
+ if (source.hasOwnProperty(p)){
+ target[p] = source[p];
+ }
+ }
+ return target;
+ }
+
+ var _find = function (dir, options){
+ var files = fs.readdirSync(dir);
+ for (var i = 0, ii = files.length; i < ii; ++i){
+ var file = files[i]
+ , fullPath = fsh.join(dir, file);
+
+ if (!fsh.isSymbolicLinkSync(fullPath) && fsh.isDirectorySync(fullPath)){
+ if (options.includeDirs){
+ matches.push(fullPath);
+ }
+ _find(fullPath, options);
+ } else if (file.match(pattern)){
+ matches.push(fullPath);
+ }
+ }
+ }
+
+ options = _mixin(defaults, options || {});
+ _find(basedir, options);
+ return matches;
+}
+
+fsh.join = function (){
+ var parts = Array.prototype.slice.apply(arguments);
+ return parts.join(this.separator);
+}
+
+ return module.exports; })({ exports: {} });
+
+var assertPatch = (function (module){// https://github.com/ry/node/blob/23cf938e4f7272635a50f8be9a5d99d40d60e0da/lib/assert.js
+
+// These patches are "cherry-picked" from node's master (2/3/2011)
+// and fix bugs in node's assert.throws and assert.doesNotThrow in node version 0.2.6
+
+// Hack in our patches to node's assert. This works because
+// node apparently doesn't reload a file once it has been
+// loaded, so require('assert') in the matchers.js file will include
+// our patched methods.
+var assert = require('assert');
+var pSlice = Array.prototype.slice;
+
+function fail(actual, expected, message, operator, stackStartFunction) {
+ throw new assert.AssertionError({
+ message: message,
+ actual: actual,
+ expected: expected,
+ operator: operator,
+ stackStartFunction: stackStartFunction
+ });
+}
+
+
+function expectedException(actual, expected) {
+ if (!actual || !expected) {
+ return false;
+ }
+
+ if (expected instanceof RegExp) {
+ return expected.test(actual);
+ } else if (actual instanceof expected) {
+ return true;
+ } else if ( expected.call({}, actual) === true ) {
+ return true;
+ }
+
+ return false;
+}
+
+function _throws(shouldThrow, block, expected, message) {
+ var actual;
+
+ if (typeof expected === 'string') {
+ message = expected;
+ expected = null;
+ }
+
+ try {
+ block();
+ } catch (e) {
+ actual = e;
+ }
+
+ message = (expected && expected.name ? ' (' + expected.name + ').' : '.') +
+ (message ? ' ' + message : '.');
+
+ if (shouldThrow && !actual) {
+ fail('Missing expected exception' + message);
+ }
+
+ if (!shouldThrow && expectedException(actual, expected)) {
+ fail('Got unwanted exception' + message);
+ }
+
+ if ((shouldThrow && actual && expected &&
+ !expectedException(actual, expected)) || (!shouldThrow && actual)) {
+ throw actual;
+ }
+}
+
+assert.throws = function(block, /*optional*/error, /*optional*/message) {
+ _throws.apply(this, [true].concat(pSlice.call(arguments)));
+};
+
+assert.doesNotThrow = function(block, /*optional*/error, /*optional*/message) {
+ _throws.apply(this, [false].concat(pSlice.call(arguments)));
+};
+
+ return module.exports; })({ exports: {} });
+
+var colors = (function (){
+ var sys = require('sys');
+
+ var self = this;
+
+ self.putsRed = function (str){
+ sys.puts('\33[31m' + str + '\33[39m');
+ }
+
+ self.printYellow = function (str){
+ sys.print('\33[33m' + str + '\33[39m');
+ }
+
+ self.putsYellow = function (str){
+ sys.puts('\33[33m' + str + '\33[39m');
+ }
+
+ self.printGreen = function (str){
+ sys.print('\33[32m' + str + '\33[39m');
+ }
+
+ self.putsGreen = function (str){
+ sys.puts('\33[32m' + str + '\33[39m');
+ }
+
+ self.highlightSpecs = function (stack){
+ var lines = stack.split("\n");
+ for (var i = 0, ii = lines.length; i < ii; ++i){
+ var line = lines[i];
+ if (line.match(/_spec\.js/)){
+ printYellow(line + "\n");
+ } else {
+ sys.puts(line);
+ }
+ }
+ }
+
+ return self;
+})();
+var foounit = require('./foounit');
+
+// This is a little weird, but fsh will get baked into
+// the foounit-node build.
+//, fsh = require('../build/fsh');
+if (typeof fsh === 'undefined'){
+ throw new Error('Looks like there was a problem ' +
+ 'building foounit-node. ' +
+ 'fsh should have been baked in.');
+}
+
+
+
+var adapter = (function (){
+ var sys = require('sys')
+ , fs = require('fs')
+ , runInThisContext = process.binding('evals').Script.runInThisContext;
+
+ // Private variables
+ var self = {}, _specdir, _codedir;
+
+ // Private functions
+ var _translate = function(str, tvars){
+ return str.replace(/:(\w+)/g, function(match, ref){
+ return tvars[ref];
+ });
+ };
+
+ /**
+ * Override foounit.require
+ */
+ self.require = function (path){
+ path = foounit.translatePath(path);
+ return require(path);
+ }
+
+ /**
+ * Override foouint.load
+ */
+ self.load = function (path){
+ path = foounit.translatePath(path) + '.js';
+ var code = fs.readFileSync(path);
+ runInThisContext(code, path, true);
+ }
+
+ /**
+ * Default runner
+ */
+ self.run = function (specdir, codedir, pattern) {
+ _specdir = specdir;
+ _codedir = codedir;
+
+ var specs = fsh.findSync(_specdir, pattern);
+ for (var i = 0, ii = specs.length; i < ii; ++i){
+ var specFile = specs[i].replace(/\.js$/, '');
+ console.log('running spec: ', specFile);
+ var spec = require(specFile);
+ }
+ foounit.execute(foounit.build());
+ }
+
+ /*
+ * Reporting
+ */
+ self.reportExample = function (example){
+ if (example.isFailure()){
+ colors.putsRed('F');
+ colors.putsRed(example.getFullDescription());
+ sys.puts(new Array(example.getFullDescription().length+1).join('='));
+ highlightSpecs(example.getException().stack);
+ } else if (example.isSuccess()){
+ colors.printGreen('.');
+ } else if (example.isPending()){
+ colors.printYellow('P');
+ }
+ }
+
+ self.report = function (info){
+ if (info.pending.length){
+ var pending = info.pending;
+ console.log("\n");
+ for (var i = 0, ii = pending.length; i < ii; ++i){
+ colors.putsYellow('PENDING: ' + pending[i]);
+ }
+ }
+
+ if (info.failCount){
+ colors.putsRed("\n" + info.failCount + ' test(s) FAILED');
+ } else {
+ colors.putsGreen("\nAll tests passed.");
+ }
+
+ var endMessage = info.totalCount + ' total.';
+ if (info.pending.length){
+ endMessage += ' ' + info.pending.length + ' pending.';
+ }
+ endMessage += ' runtime ' + info.runMillis + 'ms';
+ console.log(endMessage);
+ }
+
+ return self;
+})();
+
+foounit.mixin(foounit, adapter);
+module.exports = foounit;
+
+// TODO: Launch if file was not required
+// TODO: Parse params from cmd-line
+//run('../', /_spec\.js$/, '../../dist');
+
View
46 geddy-model/tests/foounit/foounit-server.js
@@ -0,0 +1,46 @@
+var http = require('http');
+
+var Server = function (host, port){
+ this._host = host;
+ this._port = port;
+ this._server = null;
+ this._mounts = [];
+};
+
+Server.prototype.start = function (){
+ //console.log('>> starting foounit.Server');
+ var self = this;
+
+ this._server = http.createServer(function (request, response){
+ //console.log('>> request');
+ try {
+ var mounts = self._mounts;
+ for (var i = 0; i < mounts.length; ++i){
+ var mount = mounts[i];
+ if (request.url.match(mount.pattern)){
+ //console.log('>> match: ', mount.pattern);
+ mount.service.call(request, response);
+ return;
+ }
+ }
+ response.writeHead(404, {});
+ response.end();
+ //console.log('>> no service match');
+ } catch (e){
+ console.log('>> error foounit.Server: ', e);
+ }
+ });
+
+ this._server.listen(this._port, this._host);
+};
+
+Server.prototype.stop = function (){
+ //console.log('>> stopping foounit.Server');
+ this._server.close();
+};
+
+Server.prototype.mount = function (pattern, service){
+ this._mounts.push({ pattern: pattern, service: service });
+};
+
+module.exports.Server = Server;
View
1,196 geddy-model/tests/foounit/foounit.js
@@ -0,0 +1,1196 @@
+// Copyright Bob Remeika 2011-forever
+
+// This file consists of some host environment bootstrapping and discovery.
+foounit = typeof foounit === 'undefined' ? {} : foounit;
+
+// Appends .hostenv and .mixin to foounit
+(function (foounit){
+
+ /**
+ * Throws a bootstrapping error
+ */
+ var _throwError = function (message){
+ throw new Error('foounit: bootstrap: ' + message);
+ }
+
+ /**
+ * Requisite host environment params
+ */
+ foounit.hostenv = (function (){
+ var _type = 'unknown';
+
+ // Host environment is browser-like
+ if (typeof window !== 'undefined'){
+ _type = 'browser';
+ _global = window;
+ } else if (typeof global !== 'undefined'){
+ _type = 'node';
+ _global = global;
+ } else {
+ _throwError('Unrecognized environment');
+ }
+
+ return { type: _type, global: _global };
+ })();
+
+ /**
+ * Does a shallow copy
+ */
+ foounit.mixin = function (target, source){
+ for (var prop in source){
+ if (source.hasOwnProperty(prop)){
+ target[prop] = source[prop];
+ }
+ }
+ return target;
+ };
+
+ /**
+ * Default settings
+ */
+ foounit.defaults = {
+ waitForTimeout: 5000
+ };
+
+ // TODO: Make settings configurable
+ foounit.settings = {};
+ foounit.mixin(foounit.settings, foounit.defaults);
+
+ /**
+ * Ressettable wrappers for your pleasure
+ */
+ foounit.setInterval = function (func, interval) {
+ return foounit.hostenv.global.setInterval(func, interval);
+ };
+ foounit.clearInterval = function (handle) {
+ return foounit.hostenv.global.clearInterval(handle);
+ };
+ foounit.setTimeout = function (func, timeout) {
+ return foounit.hostenv.global.setTimeout(func, timeout)
+ };
+ foounit.clearTimeout = function (handle) {
+ return foounit.hostenv.global.clearTimeout(handle);
+ };
+ foounit.getTime = function (){ return new Date().getTime(); };
+
+ /**
+ * Returns a function bound to a scope
+ */
+ foounit.bind = function (scope, func){
+ return function (){
+ return func.apply(scope, arguments);
+ }
+ }
+
+ /**
+ * Gets the test foounit.Suite object. If the suite has
+ * not been set then it creates a new test suite
+ */
+ var _suite;
+ foounit.getSuite = function (){
+ _suite = _suite || new foounit.Suite();
+ return _suite;
+ }
+
+ /**
+ * Function used while building up the tests
+ */
+ var _buildContext;
+ foounit.setBuildContext = function (context){
+ _buildContext = context;
+ }
+
+ foounit.getBuildContext = function (){
+ _buildContext = _buildContext || new foounit.BuildContext();
+ return _buildContext;
+ }
+
+ /**
+ * Set the loader strategy
+ */
+ foounit.setLoaderStrategy = function (strategy){
+ }
+
+ /**
+ * Mounts a special path for use in foounit.require and foounit.load
+ */
+ var _mounts = {};
+ foounit.mount = function (key, path){
+ _mounts[key] = path;
+ }
+
+ /**
+ * Unmounts a special path to be used by foounit.require and foounit.load
+ */
+ foounit.unmount = function (key, path){
+ delete _mounts[key];
+ }
+
+ /**
+ * Translates mounted paths into a physical path
+ */
+ foounit.translatePath = (function (){
+ var regex = /:(\w+)/g;
+
+ return function (path){
+ var file = path.replace(regex, function (match, mount){
+ return _mounts[mount] ? _mounts[mount] : match;
+ });
+ return file;
+ }
+ })();
+
+ /**
+ * Adds groups / tests to the root level ExampleGroup
+ */
+ foounit.add = function (func){
+ var context = foounit.getBuildContext();
+ context.setCurrentGroup(context.getRoot());
+ func.call(context, foounit.keywords);
+ }
+
+ /**
+ * Builds an array of tests to be run
+ */
+ foounit.build = function (){
+ var befores = [], afters = [], descriptions = [];
+
+ var addExamples = function (group){
+ var examples = group.getExamples();
+ for (var i = 0, ii = examples.length; i < ii; ++i){
+ examples[i].setBefores(befores.concat());
+ examples[i].setAfters(afters.concat());
+ examples[i].setDescriptions(descriptions.concat());
+ runners.push(examples[i]);
+ }
+ }
+
+ var recurseGroup = function (group){
+ befores.push(group.getBefore());
+ afters.push(group.getAfter());
+ descriptions.push(group.getDescription());
+
+ addExamples(group);
+
+ var groups = group.getGroups();
+ for (var i = 0, ii = groups.length; i < ii; ++i){
+ recurseGroup(groups[i]);
+ }
+
+ befores.pop();
+ afters.pop();
+ descriptions.pop();
+ }
+
+ var runners = [];
+
+ var tic = new Date().getTime();
+ recurseGroup(foounit.getBuildContext().getRoot());
+ console.log('>> foounit build time: ', new Date().getTime() - tic);
+
+ return runners;
+ }
+
+ /**
+ * Report the results of a single example. This is called
+ * after an example has finished running and before the next
+ * example begins.
+ */
+ foounit.reportExample = function (example){
+ throw new Error('foounit.reportExample is abstract');
+ }
+
+ /**
+ * Report the results of the entire test suite.
+ */
+ foounit.report = function (info){
+ throw new Error('foounit.report is abstract');
+ }
+
+ /**
+ * Executes an array of tests
+ */
+ foounit.execute = function (examples){
+ var pending = []
+ , passCount = 0, failCount = 0
+ , queue = new foounit.WorkQueue(examples);
+
+ var tic = new Date().getTime();
+
+ queue.onTaskComplete = function (example){
+ try {
+ if (example.isSuccess()){
+ ++passCount;
+ } else if (example.isFailure()){
+ ++failCount;
+ } else if (example.isPending()){
+ pending.push(example.getFullDescription());
+ }
+
+ foounit.reportExample(example);
+ } catch (e) {
+ console.log('Error in onTaskComplete: ', e.message);
+ }
+ };
+
+ queue.onComplete = function (queue){
+ try {
+ foounit.report({
+ passCount: passCount
+ , failCount: failCount
+ , totalCount: passCount + failCount + pending.length
+ , pending: pending
+ , runMillis: new Date().getTime() - tic
+ });
+ } catch (e){
+ console.log('Error in onComplete: ', e);
+ }
+ };
+
+ queue.run();
+ }
+
+ /**
+ * foounit keyword context
+ */
+ foounit.keywords = {};
+
+ /**
+ * Adds a keyword and some definition of functionality
+ */
+ foounit.addKeyword = function (keyword, definition){
+ foounit.keywords[keyword] = definition;
+ }
+
+
+})(foounit);
+
+// If this is node then we need to mixin and include
+// the node adapter immediately.
+if (foounit.hostenv.type == 'node'){
+ module.exports = foounit;
+ global.foounit = foounit;
+}
+
+foounit.Suite = function (){
+ this._files = [];
+}
+
+foounit.mixin(foounit.Suite.prototype, {
+ addPattern: function (){}
+
+ , addFile: function (file){
+ this._files.push(file);
+ }
+
+ , run: function (){
+ var self = this;
+
+ var files = self._files;
+ for (var i = 0; i < files.length; ++i){
+ var file = files[i].replace(/\.js$/, '');
+ foounit.require(file);
+ }
+ console.log('>> foounit building...');
+ foounit.execute(foounit.build());
+ }
+
+ , getFiles: function (){
+ return this._files;
+ }
+});
+foounit.BuildContext = function (){
+ this,_currentGroup = undefined;
+ this._currentExample = undefined;
+ this._root = undefined;
+};
+
+foounit.mixin(foounit.BuildContext.prototype, {
+ getRoot: function (){
+ this._root = this._root || new foounit.ExampleGroup('root', function (){});
+ return this._root;
+ }
+
+ , setCurrentGroup: function (group){
+ this._currentGroup = group;
+ }
+
+ , getCurrentGroup: function (){
+ return this._currentGroup;
+ }
+
+ , setCurrentExample: function (example){
+ this._currentExample = example;
+ }
+
+ , getCurrentExample: function (){
+ return this._currentExample;
+ }
+});
+foounit.WorkQueue = function (tasks){
+ this._tasks = tasks ? tasks.concat() : [];
+}
+
+foounit.mixin(foounit.WorkQueue.prototype, {
+ run: function (){
+ this._runNext();
+ }
+
+ , enqueue: function (task){
+ this._tasks.push(task);
+ }
+
+ , enqueueAll: function (tasks){
+ this._tasks = this._tasks.concat(tasks);
+ }
+
+ , dequeue: function (){
+ return this._tasks.shift();
+ }
+
+ , size: function (){
+ return this._tasks.length;
+ }
+
+ , peekNext: function (){
+ return this._tasks[0];
+ }
+
+ , runTask: function (task){
+ task.onComplete = foounit.bind(this, this._onTaskComplete);
+ task.onFailure = foounit.bind(this, this._onTaskFailure);
+ foounit.setTimeout(function (){
+ task.run();
+ }, 0);
+ }
+
+ , stop: function (){
+ this._tasks = [];
+ }
+
+ // Replace function to receive event
+ , onTaskComplete: function (task){}
+
+ // Replace function to receive event
+ , onTaskFailure: function (task){}
+
+ // Replace function to receive event
+ , onComplete: function (queue){}
+
+ , _onTaskFailure: function (task){
+ this.onTaskFailure(task);
+ }
+
+ , _onTaskComplete: function (task){
+ this.onTaskComplete(task);
+ this._runNext();
+ }
+
+ , _runNext: function (){
+ var task = this.dequeue();
+ if (task){
+ this.runTask(task);
+ } else {
+ this.onComplete(this);
+ }
+ }
+});
+/**
+ * A queue designed to run blocks but also to look like a task in a queue
+ */
+
+foounit.BlockQueue = function (example){
+ this._example = example;
+ this._tasks = [];
+ this._exception = undefined;
+}
+
+foounit.mixin(foounit.BlockQueue.prototype, foounit.WorkQueue.prototype);
+
+foounit.mixin(foounit.BlockQueue.prototype, {
+ onFailure: function (blockQueue){}
+ , onComplete: function (blockQueue){}
+
+ , getException: function (){
+ return this._exception;
+ }
+
+ , _onTaskFailure: function (task){
+ this._exception = task.getException();
+ this.stop();
+ this.onFailure(this);
+ }
+});
+
+// FIXME: This is a little hacky
+(function (foounit){
+ var origRunTask = foounit.BlockQueue.prototype.runTask;
+ foounit.BlockQueue.prototype.runTask = function (task){
+ this._example.setCurrentBlockQueue(this);
+ origRunTask.apply(this, arguments);
+ };
+})(foounit);
+foounit.Block = function (func){
+ this._func = func;
+ this._exception = undefined;
+}
+
+foounit.mixin(foounit.Block.prototype, {
+ onComplete: function (block){}
+ , onFailure: function (block){}
+ , getException: function (){ return this._exception; }
+
+ , run: function (){
+ var runContext = {};
+
+ try {
+ this._func.apply(runContext, []);
+ this.onComplete(this);
+ } catch (e){
+ this._exception = e;
+ this.onFailure(this);
+ }
+ }
+});
+foounit.PollingBlock = function (func, timeout){
+ this._func = func;
+ this._tasks = [];
+ this._timeout = timeout;
+ this._exception = undefined;
+ this._pollInterval = 50;
+};
+
+foounit.mixin(foounit.PollingBlock.prototype, foounit.BlockQueue.prototype);
+foounit.mixin(foounit.PollingBlock.prototype, {
+ getTimeout: function (){ return this._timeout; }
+
+ , run: function (){
+ var self = this
+ , start = foounit.getTime()
+ , interval;
+
+ interval = foounit.setInterval(function (){
+ try {
+ self._func.apply({}, []);
+ foounit.clearInterval(interval);
+ self.onComplete(self);
+ } catch (e){
+ if (foounit.getTime() - start >= self._timeout){
+ foounit.clearInterval(interval);
+ e.message = 'waitFor timeout: ' + e.message;
+ self._exception = e;
+ self.onFailure(self);
+ }
+ }
+ }, this._pollInterval);
+ }
+});
+
+foounit.TimeoutBlock = function (func, timeout){
+ this._func = func;
+ this._tasks = [];
+ this._timeout = timeout;
+ this._exception = undefined;
+ this._pollInterval = 50;
+};
+
+foounit.mixin(foounit.TimeoutBlock.prototype, foounit.BlockQueue.prototype);
+foounit.mixin(foounit.TimeoutBlock.prototype, {
+ getTimeout: function (){ return this._timeout; }
+
+ , run: function (){
+ var self = this
+ , start = foounit.getTime()
+ , passed = true
+ , interval;
+
+ interval = foounit.setInterval(function (){
+ try {
+ self._func.apply({}, []);
+ foounit.clearInterval(interval);
+ self._exception = new Error('timeout was not reached');
+ self.onFailure(self);
+ } catch (e){
+ if (foounit.getTime() - start > self._timeout){
+ foounit.clearInterval(interval);
+ self.onComplete(self);
+ }
+ }
+ }, this._pollInterval);
+ }
+});
+
+foounit.Example = function (description, test, pending){
+ this._befores = [];
+ this._test = test;
+ this._afters = [];
+ this._description = description;
+ this._descriptions = [];
+ this._currentBlockQueue = undefined;
+
+ this._status = 0;
+ this._exception = undefined;
+
+ if (pending === true) {
+ this._status = this.PENDING;
+ }
+}
+
+foounit.mixin(foounit.Example.prototype, {
+ SUCCESS: 1
+ , FAILURE: 2
+ , PENDING: 3
+
+ , onComplete: function (example){}
+
+ // Key events in the run lifecycle
+ , onBeforesComplete: function (){}
+ , onBeforesFailure: function (failedAt){}
+ , onTestComplete: function (){}
+ , onAftersComplete: function (){}
+
+ , setCurrentBlockQueue: function (blockQueue){
+ this._currentBlockQueue = blockQueue;
+ }
+
+ , getCurrentBlockQueue: function (){
+ return this._currentBlockQueue;
+ }
+
+ , run: function (){
+ foounit.getBuildContext().setCurrentExample(this);
+
+ if (this.isPending()){
+ this.onComplete(this);
+ return;
+ }
+ this._status = this.SUCCESS;
+
+
+ var self = this;
+
+ this.onBeforesComplete = function (failedAt){
+ self._runTest();
+ };
+
+ this.onBeforesFailure = function (failedAt){
+ this._runAfters(failedAt);
+ };
+
+ this.onTestComplete = function (){
+ self._runAfters();
+ };
+
+ this.onAftersComplete = foounit.bind(this, function (){
+ foounit.getBuildContext().setCurrentExample(undefined);
+ foounit.resetMocks();
+ self.onComplete(self);
+ });
+
+ this._runBefores();
+ }
+
+ , enqueue: function (block){
+ this._currentBlockQueue.enqueue(block);
+ }
+
+ // TODO: Refactor, before, after and it should all return BlockQueues
+ , _enqueueBlocks: function (funcs){
+ for (var i = 0; i < funcs.length; ++i){
+ var func = funcs[i] || function (){}
+ , blockQueue = new foounit.BlockQueue(this)
+ , block = new foounit.Block(func);
+
+ blockQueue.enqueue(block);
+ this._queue.enqueue(blockQueue);
+ }
+ }
+
+ , _runBefores: function (){
+ var self = this
+ , index = 0;
+
+ this._queue = new foounit.WorkQueue();
+
+ this._queue.onComplete = function (){
+ self.onBeforesComplete();
+ };
+
+ // This index stuff is a little janky
+ this._queue.onTaskComplete = function (blockQueue){
+ ++index;
+ }
+
+ this._queue.onTaskFailure = function (blockQueue){
+ self._status = self.FAILURE;
+ self._exception = blockQueue.getException();
+ self._queue.stop();
+ self.onBeforesFailure(index);
+ };
+
+ this._enqueueBlocks(this._befores);
+ this._queue.run();
+ }
+
+ , _runAfters: function (fromIndex){
+ var self = this
+ , afters = this._afters;
+
+ fromIndex = fromIndex || afters.length;
+ afters.reverse();
+ afters = afters.slice(afters.length - fromIndex);
+
+ this._queue = new foounit.WorkQueue();
+
+ this._queue.onComplete = function (){
+ self.onAftersComplete();
+ };
+
+ this._queue.onTaskFailure = function (task){
+ if (self._status !== self.FAILURE){
+ self._status = self.FAILURE;
+ self._exception = task.getException();
+ }
+ task.onComplete(task);
+ }
+
+ this._enqueueBlocks(afters);
+ this._queue.run();
+ }
+
+ , _runTest: function (){
+ var self = this;
+
+ this._queue = new foounit.WorkQueue();
+
+ this._queue.onTaskFailure = function (blockQueue){
+ self._status = self.FAILURE;
+ self._exception = blockQueue.getException();
+ self.onTestComplete();
+ };
+
+ this._queue.onComplete = function (){
+ self.onTestComplete();
+ };
+
+ this._enqueueBlocks([this._test]);
+ this._queue.run();
+ }
+
+ , getStack: function (){
+ var e = this.getException();
+
+ // TODO: We need to do a lot better than this
+ return e.stack || e.stacktrace || e.sourceURL + ':' + e.line;
+ }
+
+ , isSuccess: function (){
+ return this._status === this.SUCCESS;
+ }
+
+ , isFailure: function (){
+ return this._status === this.FAILURE;
+ }
+
+ , isPending: function (){
+ return this._status === this.PENDING;
+ }
+
+ , getException: function (){
+ return this._exception;
+ }
+
+ , setBefores: function (befores){
+ this._befores = befores;
+ }
+
+ , getDescription: function (){
+ return this._description;
+ }
+
+ , getFullDescription: function (){
+ var descriptions = this._descriptions.concat();
+ descriptions.shift();
+ descriptions.push(this.getDescription());
+ return descriptions.join(' ');
+ }
+
+ , getDescriptions: function (){
+ return this._descriptions;
+ }
+
+ , setDescriptions: function (descriptions){
+ this._descriptions = descriptions;
+ }
+
+ , getBefores: function (){
+ return this._befores;
+ }
+
+ , setAfters: function (afters){
+ this._afters = afters;
+ }
+
+ , getAfters: function (){
+ return this._afters;
+ }
+
+ , getTest: function (){
+ return this._test;
+ }
+});
+foounit.ExampleGroup = function (description, builder){
+ this._description = description;
+ this._builder = builder;
+ this._before = null;
+ this._after = null;
+ this._examples = [];
+ this._groups = [];
+}
+
+foounit.mixin(foounit.ExampleGroup.prototype, {
+ build: function (){
+ this._builder();
+ }
+
+ , getExamples: function (){
+ return this._examples;
+ }
+
+ , addExample: function (example){
+ this._examples.push(example);
+ }
+
+ , addGroup: function (group){
+ this._groups.push(group);
+ }
+
+ , setAfter: function (func){
+ this._after = func;
+ }
+
+ , getAfter: function (){
+ return this._after;
+ }
+
+ , setBefore: function (func){
+ this._before = func;
+ }
+
+ , getBefore: function (){
+ return this._before;
+ }
+
+ , getGroups: function (){
+ return this._groups;
+ }
+
+ , getDescription: function (){
+ return this._description;
+ }
+});
+foounit.Expectation = function (actual){
+ this._actual = actual;
+}
+
+foounit.mixin(foounit.Expectation.prototype, {
+ to: function (matcherClass, expected){
+ var matcher = new matcherClass();
+ matcher.match(this._actual, expected);
+ }
+
+ , toNot: function (matcherClass, expected){
+ var matcher = new matcherClass();
+ matcher.notMatch(this._actual, expected);
+ }
+});
+/**
+ * Creates an example to be added to the current group in the BuildContext
+ */
+foounit.addKeyword('it', function (description, test){
+ var example = new foounit.Example(description, test);
+ foounit
+ .getBuildContext()
+ .getCurrentGroup()
+ .addExample(example);
+ return example;
+});
+
+/**