Nickolay - Test.Run-0.10

Documentation | Source
Class('Test.Run.Result', {
    
    have : {
        description : null
    }
        
})
//eof Test.Run.Result

;
Class('Test.Run.Result.Diagnostic', {
    
    isa : Test.Run.Result,
    

    methods : {
        
        toString : function () {
            return '# ' + this.description
        }
        
    }    
    
})

;
Class('Test.Run.Result.Assertion', {
    
    isa : Test.Run.Result,
    

    have : {
        passed      : null,
        
        annotation  : null,
        
        index       : null,
        
        isSkipped   : false,
        isTodo      : false
    },
    
    
    methods : {
        
        toString : function () {
            var text = (this.passed ? 'ok' : 'not ok') + ' ' + this.index + ' - ' + this.description
            
            if (this.annotation) text += '\n' + this.annotation
            
            return text
        }
        
    }
        
})

;
Role('Test.Run.Test.More', {
    
    methods : {
        
        like : function (string, regex, desc) {
            if (regex instanceof RegExp) 
                this.ok(string.match(regex), desc)
            else
                this.ok(string.indexOf(regex) != -1, desc)
        },
        
        
        unlike : function(string, regex, desc) {
            if (regex instanceof RegExp) 
                this.ok(!string.match(regex), desc)
            else
                this.ok(string.indexOf(regex) == -1, desc)
        },
        
        
        throwsOk : function () {
            this.throws_ok.apply(this, arguments)
        },
        
        
        livesOk : function () {
            this.lives_ok.apply(this, arguments)
        },
        
        
        throws_ok : function(func, expected, desc) {
            if (typeof func != 'function') throw 'throws_ok accepts a function as 1st argument'
            
            var e = this.topScope.__EXCEPTION_CATCHER__(func)
            
            if (e instanceof this.topScope.Error)
                //IE uses non-standard 'description' property for error msg
                e = e.message || e.description
            
            this.like('' + e, expected, desc + ' (got [' + e + '], expected [' + expected + '])')
        },
        
        
        lives_ok : function (func, desc) {
            if (typeof func != 'function') throw 'lives_ok accepts a function as 1st argument'
            
            var e = this.topScope.__EXCEPTION_CATCHER__(func)
            
            if (e) 
                this.fail(desc)
            else
                this.pass(desc)
        },
        
        
        isaOk : function (value, className, desc) {
            this.isa_ok(value, className, desc)
        },
        
        
        isa_ok : function (value, className, desc) {
            try {
                if (typeof className == 'string') className = eval(className)
            } catch (e) {
                this.fail("Exception [" + e + "] caught, when evaluting the class name [" + className + "]")
            }
            
            this.ok(value instanceof className, desc)
        },
        
        
        typeOf : function (object) {
            return Object.prototype.toString.call(object).replace(/^\[object /, '').replace(/\]$/, '')
        },
        
        
        countKeys : function (object) {
            var counter = 0
            
            Joose.O.eachOwn(object, function () {
                counter++
            })
            
            return counter
        },
        
        
        compareObjects : function (obj1, obj2) {
            if (obj1 == obj2) return true
            
            var type1 = this.typeOf(obj1)
            var type2 = this.typeOf(obj2)
            
            if (type1 != type2) return false
            
            if (type1 == 'Array')
                if (obj1.length != obj2.length) 
                    return false
                else {
                    for (var i = 0; i < obj1.length; i++)
                        if (!this.compareObjects(obj1[ i ], obj2[ i ])) return false
                    
                    return true
                }
            
            if (type1 == 'Object')
                if (this.countKeys(obj1) != this.countKeys(obj2)) 
                    return false
                else {
                    var res = Joose.O.eachOwn(obj1, function (value, name) {
                        
                        if (!this.compareObjects(value, obj2[ name ])) return false
                    })
                    
                    return res === false ? false : true
                }
        }, 
        
        
        isDeeply : function (obj1, obj2, desc) {
            this.ok(this.compareObjects(obj1, obj2), desc)
        },
        
        
        is_deeply : function (obj1, obj2, desc) {
            this.ok(this.compareObjects(obj1, obj2), desc)
        }
        
        
    }
        
})
//eof Test.Run.Test.More
;
Class('Test.Run.Test', {
    
    does        : [ Test.Run.Test.More ],
    
    
    have : {
        url                 : null,
        
        assertPlanned       : null,
        assertCount         : 0,
        
        results             : null,
        
        run                 : null,
        
        harness             : null,
        
        failed              : false,
        failedException     : null,
        
        startDate           : null,
        endDate             : null,
        
        topScope            : null,
        
        transparentEx       : false,
        passThroughEx       : false,
        
        isDone              : false,
        
        timeoutsCount       : 0,
        timeoutIds          : null,
        processed           : false,
        
        callback            : null
    },
    
    
    after : {
    
        initialize : function (config) {
            if (Object.prototype.toString.call(this.run) != "[object Function]") throw "The body of test absent"
            
            this.results        = []
            this.timeoutIds     = {}
        }
        
    },
    
    
    methods : {
        
        toString : function() {
            return this.url
        },
        
        
        plan : function (value) {
            if (this.assertPlanned != null) throw "Test plan can't be changed"
            
            this.assertPlanned = value
        },
        
        
        addResult : function (result) {
            if (this.isDone || this.isFinished()) throw "Adding assertions after the test has been already done"
            
            if (result instanceof Test.Run.Result.Assertion) result.index = ++this.assertCount
            
            this.results.push(result)
            
            this.harness.onTestUpdate(this, result)
        },
        
        
        diag : function (desc) {
            this.addResult(new Test.Run.Result.Diagnostic({
                description : desc
            }))
        },
        
        
        pass : function (desc) {
            this.addResult(new Test.Run.Result.Assertion({
                passed      : true,
                
                description : desc
            }))
        },
        
        
        fail : function (desc) {
            this.addResult(new Test.Run.Result.Assertion({
                passed      : false,
                
                description : desc
            }))
        },
        
        
        eachAssertion : function (func, scope) {
            scope       = scope || this
            
            var index   = 0
            
            Joose.A.each(this.results, function (result) {
                
                if (result instanceof Test.Run.Result.Assertion) func.call(scope, result, index++)
            })
        },
        
        
        ok : function (condition, desc) {
            if (condition) 
                this.pass(desc)
            else 
                this.fail(desc)
        },
        
        
        notOk : function (condition, desc) {
            this.ok(!condition, desc)
        },
        
        
        is : function (got, expected, desc) {
            this.ok(got == expected, desc)
        },

        
        isnt : function (got, expected, desc) {
            this.ok(got != expected, desc)
        },
        
        
        beginAsync : function (time) {
            var me = this
            
            // in NodeJS `setTimeout` returns an object and not a simple ID, so we try hard to store that object under unique index
            var timeoutId = this.topScope.setTimeout(function () {
                me.endAsync(timeoutId)
            }, time || 1e4)
            
            var index = this.timeoutsCount++
            
            this.timeoutIds[ index ] = timeoutId
            
            return index
        },
        
        
        endAsync : function (index) {
            var counter = 0
            
            if (index == null) Joose.O.each(this.timeoutIds, function (timeoutId, indx) {
                index = indx
                if (counter++) throw "Calls to endAsync without argument should only be performed if you have single beginAsync statement" 
            })
            
            var timeoutId = this.timeoutIds[ index ]
            
            this.topScope.clearTimeout(timeoutId)
            delete this.timeoutIds[ index ]
            
            var me = this
            
            if (this.processed)
                // to allow potential call to `done` after `endAsync`
                setTimeout(function (){
                    me.finalize()
                }, 1)
        },
        
        
        clearTimeouts : function () {
            var me = this
            
            Joose.O.each(this.timeoutIds, function (value, id) {
                me.topScope.clearTimeout(value)
            })
            
            this.timeoutIds = {}
        },
        
        
        skipIf : function (condition, why, code, howMany) {
            howMany = howMany || 1
            
            if (condition) {
                
                for (var i = 1; i <= howMany; i++) this.addResult(new Test.Run.Result.Assertion({
                    passed      : true,
                    isSkipped   : true,
                    
                    description : 'SKIPPED: ' + why
                }))    
                
            } else
                code()
        },
        
        
        skip : function (why, code, howMany) {
            this.skipIf(true, why, code, howMany)
        },
        
        
        todo : function (why, code) {
            var todo  = new Test.Run.Test.Todo({
                parent  : this,
                run     : function () {}
            })
            
            this.topScope.__EXCEPTION_CATCHER__(function(){
                code(todo)
            })
        },
        
        
        start : function (callback) {
            this.callback   = callback
            this.startDate  = new Date()
            
            this.harness.onTestStart(this)
            
            var me      = this
            var run     = this.run
            
            if (this.transparentEx)
                run(me)
            else 
                var e = this.topScope.__EXCEPTION_CATCHER__(function(){
                    run(me)
                })
            
            if (e) {
                this.failed             = true
                this.failedException    = e
                
                this.harness.onTestFail(this, e)
                
                this.finalize(true)
                
                if (this.passThroughEx) throw e
                
                return
            } 
            
            this.finalize()
        },
        
        
        finalize : function (force) {
            if (this.isFinished()) return
            
            this.processed = true
            
            if (force) this.clearTimeouts()
            
            if (!Joose.O.isEmpty(this.timeoutIds)) return
            
            this.endDate = new Date()
            
            this.harness.onTestEnd(this)
            
            this.callback && this.callback()
        },
        
        
        getSummaryMessage : function (lineBreaks) {
            var res = []
            
            var passCount       = this.getPassCount()
            var failCount       = this.getFailCount()
            var assertPlanned   = this.assertPlanned
            var total           = failCount + passCount
            
            res.push('Passed: ' + passCount)
            res.push('Failed: ' + failCount)
            
            if (!this.failed) {
                if (assertPlanned != null) {
                    if (total < assertPlanned) 
                        res.push('Looks like you planned ' + assertPlanned + ' tests, but ran only ' + total)
                        
                    if (total > assertPlanned) 
                        res.push('Looks like you planned ' + assertPlanned + ' tests, but ran ' +  (total - assertPlanned) + ' extra tests, ' + total + ' total.')
                    
                    if (total == assertPlanned && !failCount) res.push('All tests successfull')
                } else 
                    if (this.isDone && !failCount) res.push('All tests successfull')
                
            } else {
                res.push('Test suite threw an exception: ' + this.failedException)
            }
            
            return res.join(lineBreaks || '')
        },
        
        
        done : function () {
            this.isDone = true
            
            if (this.processed) this.finalize()
        },
        
        
        getPassCount : function () {
            var passCount = 0
            
            this.eachAssertion(function (assertion) {
                if (assertion.passed && !assertion.isTodo) passCount++
            })
            
            return passCount
        },
        
        
        getFailCount : function () {
            var failCount = 0
            
            this.eachAssertion(function (assertion) {
                if (!assertion.passed && !assertion.isTodo) failCount++
            })
            
            return failCount
        },
        
        
        isPassed : function () {
            var passCount       = this.getPassCount()
            var failCount       = this.getFailCount()
            var assertPlanned   = this.assertPlanned
            
            return !this.failed && !failCount && (
                (assertPlanned != null && passCount == assertPlanned)
                    ||
                (assertPlanned == null) && this.isDone
            )
        },
        
        
        isFinished : function () {
            return this.endDate != null
        },
        
        
        getTimeLength : function () {
            return this.endDate - this.startDate
        }
        
    }
        
})
//eof Test.Run.Test


/**

Name
====


Test.Run.Test - Class, representing the individual test file


SYNOPSIS
========

        t.ok(1 == 1, 'Indeed')
        t.is(2 * 2, '4', 'Indeed')
        
        t.pass('Some assertion is correct')
        
        t.done()


DESCRIPTION
===========

`Test.Run.Test` is a base testing class in Test.Run hierarchy. Its not supposed to be created manually, instead, 
the harness will create it for you.


USAGE
=====

Below is the list of methods, intended for usage in the individual tests.


### plan

> `void plan(Number tests)`

> This method setups test's plan. When used, it should be called before any assertions were checked. 

> **tests** - a number of planned assertions in this test file.


### done

> `void done()`

> This method indicates that you've done testing and all assertions have been ran.
It should be called after any assertions were checked.  



### diag

> `void diag(String text)`

> This method output the diagnostic message. The actual presentation logic of the message is delegated to harness.

> **text** - The text of diagnostic message


### pass

> `void pass(String text)`

> This method add the passed assertion into results queue. 

> **text** - The description of the assertion


### fail

> `void fail(String text)`

> This method add the failed assertion into results queue. 

> **text** - The description of the assertion


### ok

> `void ok(Boolean condition, String text)`

> This assertion passes when the supplied `condition` evalutes to `true` and fails otherwise. 

> **condition** - The boolean condition, indicating wheter assertions is passed or failed

> **text** - The description of the assertion


### notOk

> `void notOk(Boolean condition, String text)`

> This is a reverse of `ok` (test passes when condition is false) 

        
### is

> `void is(Object value1, Object value2, String text)`

> This assertion passes when comparison of 1st and 2nd arguments shows that they are equal.
Comparison is performed with '==' operator

> **value1** - The 1st value for comparison

> **value2** - The 2nd value for comparison

> **text** - The description of the assertion


### isnt

> `void isnt(Object value1, Object value2, String text)`

> This method is a reverse of `is` (passes when the operands are different).


### beginAsync

> `Number beginAsync(Number? maxTime)`

> This method starts the "asynchronous frame". The test will not finished, until the frame will not be finished with [endAsync] call.
[endAsync] will be automatically called after specified `maxtime`.

> **maxTime** - the maximum time (in ms) to wait until explicitly finalize this async frame. Default time is 10000 ms.

> *return* - The timeoutId, which can be used in [endAsync] call



### endAsync

> `void endAsync(Number timeoutId)`

> This method finalize the "asynchronous frame" started with [beginAsync].

> **timeoutId** - The timeoutId, returned by [beginAsync] call


### like

> `void like(String str, String|RegExp regex, String text)`

> This assertion passes when the passed `str` (1st argument) matches to a regular expression `regex` (2nd argument)

> **str** - The string to test

> **regex** - The regex against which to test the string, can be also a plain string

> **text** - The description of the assertion


### unlike

> `void unlike(String str, String|RegExp regex, String text)`

> This method is the opposite of 'like', it adds failed assertion, when the string matches the passed regex.

> **str** - The string to test

> **regex** - The regex against which to test the string, can be also a plain string

> **text** - The description of the assertion



### throwsOk

> `void throwsOk(Function func, String|RegExp expected, String text)`

> This assertion is passed, when the `func` function throws the exception during executing, and the 
stringified exception passes the 'like' assertion (with 'expected' parameter). This method has a synonym: throws_ok

> **func** - The function which supposed to throw an exception

> **expected** - The regex against which to test the *stringified* exception, can be also a plain string

> **text** - The description of the assertion


### livesOk

> `void livesOk(Function func, String text)`

> This assertion passes, when the supplied `func` function doesn't throw the exception during execution. 
This method has a synonym: lives_ok

> **func** - The function which supposed to not throw an exception

> **text** - The description of the assertion


### isaOk

> `void isaOk(Object value, Function/String class, String text)`

> This assertion passes, when the supplied `value` is the instance of the `class`. The check is performed with
`instanceof` operator. The `class` parameter can be supplied as class constructor or as string, representing the class
name. In this case the `class` will eval'ed to receive the class constructor.

This method has a synonym: isa_ok

> **value** - The value to check for 'isa' relationship

> **class** - The class to check for 'isa' relationship with `value`

> **text** - The description of the assertion


### skipIf

> `void skipIf(Boolean condition, String why, Function code, Number? howMany)`

> This methods check the supplied `condition` and if its *true* then *do not* executes the supplied code.
Instead, it adds `howMany` pseudo-passed assertions to the test suite. If the condition is *false*, then
it just run the `code` function.

This method is useful for skipping parts of the test suite, for example if the functionality being tested
is not supported on the current platform.

> **condition** - The boolean condition, indicating whether to run or skip the `code`

> **why** - The reason for the skip

> **code** - A function, wrapping the assertions which needs to be skipped

> **howMany** - Optional. A number of pseudo-passed assertions to add, when skipping real ones. Defaults to 1.


### skip

> `void skip(String why, Function code, Number? howMany)`

> Unconditional `skipIf` (always skips the code).


### todo

> `void todo(String why, Function code)`

> With this method you can mark part of the test suite as "todo", assuming it most probably will fail,
but its still worth to try run them. 

>The supplied `code` function will be run, it will receive a new test instance as the 1st argument, 
which *must* be used for assertions checks (not the primary test instance, received from `StartTest`). 

>Assertions, failed inside the `code` block will be treated by harness normally.
Assertions, passed inside the `code` block will be treated by harness as bonus ones and highlighted. 

> **why** - The reason/description for the todo

> **code** - A function, wrapping the "todo" assertions. This function will receive a special test class instance
which should be used for assertions checks.



SEE ALSO
========

General documentation for Joose: <http://openjsan.org/go/?l=Joose>


BUGS
====

All complex software has bugs lurking in it, and this module is no exception.

Please report any bugs through the web interface at <http://github.com/SamuraiJack/test.run/issues>



AUTHORS
=======

Nickolay Platonov [nplatonov@cpan.org](mailto:nplatonov@cpan.org)



COPYRIGHT AND LICENSE
=====================

Copyright (c) 2010, Nickolay Platonov

All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of Nickolay Platonov nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 

        
[Test.Run.Result]: Result.html
[Test.Run.Harness]: Harness.html

*/;
Class('Test.Run.Test.Todo', {
    
    isa         : Test.Run.Test,
    
    
    have : {
        parent              : null
    },
    
    
    methods : {
        
        addResult : function (result) {
            if (result instanceof Test.Run.Result.Assertion) result.isTodo = true
            
            this.parent.addResult(result)
        },
        
        
        beginAsync : function (time) {
            return this.parent.beginAsync(time)
        },
        
        
        endAsync : function (index) {
            return this.parent.endAsync(index)
        }
        
    }
        
})
//eof Test.Run.Test
;
Class('Test.Run.Harness', {
    
    my : {
        
        have : {
            title               : null,
            
            testClass           : Test.Run.Test,
            
            tests               : null,
            testsByURL          : null,
            descriptorsByURL    : null,
            scopesByURL         : null,
            
            startArgs       : null,
            
            passThroughEx   : false,
            transparentEx   : false,
            
            scopeProvider   : null,
            runCore         : 'parallel', // or 'sequential'
            maxThreads      : 4,
            
            preload         : null,
            
            verbosity       : 0,
            keepResults     : false
        },
        
        
        after : {
            
            initialize : function () {
                this.testsByURL         = {}
                this.descriptorsByURL   = {}
                this.scopesByURL        = {}
                this.tests              = []
            }
        },
        
        
        methods : {
            
            onTestUpdate : function (test, result) {
            },
            
            
            onTestFail : function (test, exception) {
            },
            
            
            onTestStart : function (test) {
            },
            
            
            onTestEnd : function (test) {
            },
            
            
            onTestSuiteStart : function () {
            },
            
            
            onTestSuiteEnd : function () {
            },
            
            
            configure : function (config) {
                Joose.O.copy(config, this)
            },
            
            
            start : function () {
                var me = this
                
                this.startArgs = arguments
                
                var descriptors = []
                
                Joose.A.each(arguments, function (desc, index) {
                    desc = me.normalizeDescriptor(desc, index)
                    
                    descriptors.push(desc)
                    
                    me.descriptorsByURL[ desc.url ] = desc
                })
                
                this.onTestSuiteStart(descriptors)
                
                this.runTestsForDescriptors(descriptors, function () {
                    me.onTestSuiteEnd()
                })
            },
            
            
            runTestsForDescriptors : function (descriptors, callback) {
                var runCoreMethod = 'runCore' + Joose.S.uppercaseFirst(this.runCore)
                
                if (typeof this[ runCoreMethod ] != 'function') throw "Invalid `runCore` specified: [" + this.runCore + "]"
                
                this[ runCoreMethod ](descriptors, callback)
            },
            
            
            runCoreParallel : function (descriptors, callback) {
                var me              = this
                var processedNum    = 0
                var count           = descriptors.length
                
                if (!count) callback()
                
                var launch  = function (descriptors) {
                    var desc = descriptors.shift()
                    
                    if (!desc) return
                    
                    me.processURL(desc, desc.index, function () {
                        processedNum++
                        
                        if (processedNum == count) 
                            callback()
                        else
                            launch(descriptors)
                    })
                }
                
                for (var i = 1; i <= this.maxThreads; i++) launch(descriptors)
            },
            
            
            runCoreSequential : function (descriptors, callback) {
                if (descriptors.length) {
                    var desc = descriptors.shift()
                    
                    var me = this
                    
                    this.processURL(desc, desc.index, function () {
                        
                        me.runCoreSequential(descriptors, callback)
                    })
                    
                } else
                    callback()
            },
            
            
            setupScope : function (desc, callback) {
                var scopeProvider       = desc.target
                
                var scopeProvideClass   = eval(scopeProvider)
                
                new scopeProvideClass().setup(callback)
            },
            
            
            cleanupScopeForURL : function (url) {
                var scopeProvider = this.scopesByURL[ url ]
                
                if (scopeProvider) {
                    scopeProvider.cleanup()
                    
                    delete this.scopesByURL[ url ]
                }
            },
            
            
            prepareScope : function (scopeProvider, desc, callback) {
                var me = this
                
                scopeProvider.runCode(
                    'StartTest = function () { __START_TEST__ = arguments };' +
                    '__EXCEPTION_CATCHER__ = function (func) { var ex; try { func() } catch (e) { ex = e; }; return ex; };',
                    
                    function () {
                        var preload         = desc.preload || me.preload || []
                        
                        preload             = preload.concat(desc.alsoPreload || [])
                        
                        me.preloadScripts(scopeProvider, preload, callback)
                    }
                )
            },
            
            
            preloadScripts : function (scopeProvider, scripts, callback) {
                var me  = this
                
                if (scripts.length) {
                    var script = scripts.shift()
                    
                    if (typeof script == 'object')
                        scopeProvider.runCode(script.text, function () {
                            me.preloadScripts(scopeProvider, scripts, callback)
                        })
                    else
                        scopeProvider.runScript(this.resolveURL(script), function () {
                            me.preloadScripts(scopeProvider, scripts, callback)
                        })
                } else
                    callback()
            },
            
            
            normalizeDescriptor : function (desc, index) {
                if (typeof desc == 'string') return {
                    url     : desc,
                    target  : this.scopeProvider,
                    index   : index
                }
                
                if (desc.target) {
                    var match 
                    
                    if (match = /^=(.+)/.exec(desc.target))
                        desc.target = match[ 1 ]
                    else 
                        desc.target = desc.target.replace(/^(Scope.Provider.)?/, 'Scope.Provider.')
                }
                
                desc.index = index
                
                return desc
            },
            
            
            resolveURL : function (url) {
                return url
            },
            
            
            processURL : function (desc, index, callback) {
                var me      = this
                var url     = desc.url
                
                this.cleanupScopeForURL(url)
                
                this.setupScope(desc, function (scopeProvider) {
                    
                    me.scopesByURL[ url ] = scopeProvider
                    
                    me.prepareScope(scopeProvider, desc,  function () {
                        
                        scopeProvider.runScript(me.resolveURL(url), function () {
                            
                            var scope           = scopeProvider.scope
                            var startTestArgs   = scope.__START_TEST__
                            var run             = startTestArgs[0]
                            var testClass       = startTestArgs[1]
                            
                            var test = new (testClass || me.testClass)({
                                url             : url,
                                harness         : me,
                                
                                run             : run,
                                topScope        : scope,
                                
                                passThroughEx   : me.passThroughEx,
                                transparentEx   : me.transparentEx
                            })
                            
                            me.addTest(test, index)
                            
                            test.start(function () {
                                if (!me.keepResults) me.cleanupScopeForURL(url)
                                
                                callback && callback()
                            })
                        })
                    })
                })   
            },
            
            
            addTest : function (test, index) {
                this.tests[ index ] = test
                
                this.testsByURL[ test.url ] = test
            },
            
            
            getTestByURL : function (url) {
                return this.testsByURL[url]
            },
            
            
            getTestAt : function (index) {
                return this.tests[ index ]
            },
            
            
            reRunTest : function (test, callback) {
                this.reRunTests([ test ], callback)
            },
            
            
            reRunTests : function (tests, callback) {
                var descriptors = []
                
                Joose.A.each(tests, function (test) {
                    descriptors.push( this.descriptorsByURL[ test.url ])
                }, this)
                
                this.runTestsForDescriptors(descriptors, callback)
            },
            
            
            reRunSuite : function () {
                this.start.apply(this, this.startArgs)
            },
            
            
            isPassed : function () {
                var res = true
                
                Joose.O.each(this.testsByURL, function (test) {
                    if (!test.isPassed()) res = false
                })
                
                return res
            },
            
            
            isRunning : function () {
                var res = false
                
                Joose.O.each(this.testsByURL, function (test) {
                    if (!test.isFinished()) res = true
                })
                
                return res
            }
            
        }
        
    }
    //eof my
})
//eof Test.Run.Harness



/**

Name
====


Test.Run.Harness - Abstract base class for test harness


SYNOPSIS
========

            Test.Run.Harness.Browser.Multi.configure({
                title : 'Module.Stub Test Suite',
                
                passThroughEx : true,
                
                preload : [
                    '/jsan/Task/Joose/Core.js',
                    "/jsan/JooseX/SimpleRequest.js",
                    '/jsan/Task/JooseX/Namespace/Depended/Web.js',
                    {
                        text : "JooseX.Namespace.Depended.Manager.my.INC = " + Ext.encode(INC)
                    }
                ]
            })
            
            
            Test.Run.Harness.Browser.Multi.start(
                '/doc/s/sa/samuraijack/Test/Run/010/lib/Task/Test/Run/010_sanity.t.js',
                '/doc/s/sa/samuraijack/Test/Run/010/lib/Task/Test/Run/020_basics.t.js'
            )
        

DESCRIPTION
===========

`Test.Run.Harness` is an abstract base harness class in Test.Run hierarchy. This class provides no UI, 
you should use one of it subclasses, for example [Test.Run.Harness.Browser.ExtJS]


USAGE
=====

Methods
-------

### configure

> `void configure(Object options)`

> This method configure the harness instance. It just copies the passed configuration option into static instance. 

> **options** - configuration options (values of attributes for this class, see below for details)


### start

> `void start(String url1, String url2, ...)`

> This method starts a whole test suite 

> **url1, url2, ...** - the variable number of test files urls


Configuration options
---------------------

### title

> `String title`

> The title of the test suite


### passThroughEx

> `Boolean passThroughEx`

> The sign whether the each tests in suite should re-throw any exceptions caught (sometimes useful 
for debugging with FireBug). Defaults to 'false'.

### transparentEx

> `Boolean transparentEx`

> The sign whether the each tests in suite shouldn't catch any exceptions at all. Exceptions will be thrown "as is".
This is also useful for debugging. Defaults to 'false'


### preload

> `Array preload`

> The array which contains the information about which files should be preloaded into each test's scope.
The folloing rules applies during processing of the array:

>1. If the string entry represent a class name (for example : `Test.Run.Test`) it is converting to the url, 
like "../lib/Test/Run/Test.js"

>2. All string entries starting with 'jsan:' are replaced with the link to corresponding JSAN module. For example:
    
                jsan:Task.Joose.Core

>3. If the entry ends with ".js", its supposed to be the url and is passing without modifications.

>4. If the entry is an Object with `text` property, then the value of that property will be evaluted in the test's 
global scope directly. In this way you can run arbitrary code for setup.


### runCore

> `String runCore`

> Either `parallel` or `sequential`. Indicates how the individual tests should be run - several at once or one-by-one.


### maxThreads

> `Number maxThreads`

> The maximum number of tests running at the same time. Only applicable for `parallel` run-core.


### testClass

> `Class testClass`

> The test class which will be used for running tests, defaults to [Test.Run.Test].



SEE ALSO
========

General documentation for Joose: <http://openjsan.org/go/?l=Joose>


BUGS
====

All complex software has bugs lurking in it, and this module is no exception.

Please report any bugs through the web interface at <http://github.com/SamuraiJack/test.run/issues>



AUTHORS
=======

Nickolay Platonov [nplatonov@cpan.org](mailto:nplatonov@cpan.org)



COPYRIGHT AND LICENSE
=====================

Copyright (c) 2010, Nickolay Platonov

All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of Nickolay Platonov nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 

        
[Test.Run.Harness.Browser.Multi]: Harness/Browser/Multi.html
[Test.Run.Test]: Test.html

*/
;
Class('Test.Run', {
    
    my : {
        
        methods : {
            
        }
        
    }
})
;
Class('Test.Run.Harness.NodeJS', {
    
    isa : Test.Run.Harness,
    
    my : {
        
        have : {
            runCore         : 'sequential',
            scopeProvider   : 'Scope.Provider.NodeJS',
            
            chdirToIndex    : true,
            
            styles          : {
                'bold'      : [1, 22],
                'italic'    : [3, 23],
                'underline' : [4, 24],
                'yellow'    : [33, 39],
                'cyan'      : [36, 39],
                'white'     : [37, 39],
                'green'     : [32, 39],
                'red'       : [31, 39],
                'grey'      : [90, 39],
                'blue'      : [34, 39],
                'magenta'   : [35, 39],
                'inverse'   : [7, 27]
            }
        },
        
        
        after : {
            
            onTestEnd : function (test) {
                this.puts( test.url + ' - ' + (test.isPassed() ? this.styled('pass', 'green') : this.styled('fail', 'red')) ) 
            },
            
            
            onTestSuiteStart : function () {
                this.runCore         = 'sequential'
                
                if (this.chdirToIndex) {
                    var indexFile = process.argv[1]
                    
                    var path = require('path')
                    
                    process.chdir(path.dirname(indexFile))
                }
            },
            
            
            onTestFail : function (test, exception) {
                var text
                
                if (exception.stack)
                    text = exception.stack
                else
                    text = exception + ''
                    
                text = this.styled(this.styled(text, 'red'), 'bold')
                
                this.puts(text)
            },
            
            
            onTestUpdate : function (test, result) {
                var text = result + ''
                
                var isAssertion = result instanceof Test.Run.Result.Assertion
                
                if (isAssertion) text = this.styled(text, result.passed ? 'green' : 'red')
                if (result instanceof Test.Run.Result.Diagnostic) text = this.styled(text, 'bold')
                
                if (this.verbosity > 0)
                    this.puts(text)
                else
                    if (isAssertion && !result.passed)
                        this.puts(text)
            }            
            
        },
        
        
        methods : {
            
            resolveURL : function (url) {
                var fs = require('fs')
                
                // ref to JSAN module
                if (/^jsan:/.test(url))
                    Joose.A.each(require.paths, function (path) {
                        
                        var libPath = path.replace(/\/?$/, '') + '/index.html' + url.replace(/^jsan:/, '').replace(/\./g, '/index.html') + '/doc/s/sa/samuraijack/Test/Run/010/lib/Task/Test/Run/.js'
                        
                        try {
                            if (fs.statSync(libPath).isFile()) {
                                url = libPath
                                
                                return false
                            }
                            
                        } catch (e) {
                        }
                    })
                
                // ref to lib in current dist (no trailing `.js`) 
                if (!/\.js$/.test(url)) {
                    url = '/doc/s/sa/samuraijack/Test/Run/010/lib/Task/Test/lib/index.html' + url.replace(/\./g, '/index.html') + '/doc/s/sa/samuraijack/Test/Run/010/lib/Task/Test/Run/.js'
                }
                
                // otherwise assumed to be a raw filename, relative or absolute
                return url
            },
            
            
            styled : function (text, style) {
                var styles = this.styles
                
                return '\033[' + styles[ style ][ 0 ] + 'm' + text + '\033[' + styles[ style ][ 1 ] + 'm'
            },
            
            
            puts : function (text) {
                require('sys').puts(text)
            },
            
            
            prepareINC : function (INC) {
                return JSON.stringify(INC)
            }
        }
        
    }
    //eof my
})
//eof Test.Run.Harness.NodeJS


;