Source: org/terraswarm/accessor/accessors/web/hosts/node/test/testNodeAuto.js

/**
 * Run the tests in accessors/web/test/auto.
 * To run this test, do:
 *   sudo npm install -g mocha
 *   (cd mocha; mocha testNodeAllAuto.js)
 * or
 *   (cd ../../../; ant tests.mocha.composites)
 * @module @accessors-hosts/node/test/testNodeAuto
 * @author Christopher Brooks, Beth Osyk
 * @version: $$Id$$
 */

var nodeHost = require('../nodeHost.js');
var fs = require('fs');

/** Run all the .js tests in a directory using mocha.
 *  It is expected that the .js file define composite accessors.
 *  @param auto The directory that contains the .js files.
 */
exports.testNodeAuto = function(auto) {
    console.log('testNodeAuto.js: testNodeAuto(' + auto + ')');
    var accetssors;
    try {
        // If run in accessors/web/hosts/node/test/mocha/
        accessors = fs.readdirSync('../../../../' + auto);
    } catch (e) {
        // If run in accessors/web/
        accessors = fs.readdirSync(auto);
    }
    
    var mochaListener;
    
    var mochaTimeout = 20000;
    // describe() is a mocha function.
    // IMPORTANT: Don't change 'NodeHost', the Accessor Status page uses it.
    // See https://www.icyphy.org/accessors/wiki/Notes/Status
    describe('NodeHost' , function() {
        this.timeout(mochaTimeout); // Increase default timeout.  Originally 2000ms.
        
        before(function() {        
            // Remove the mocha listener (restore later) and 
            // use our own handlers.
            mochaListener = process.listeners('uncaughtException').pop();
            
            process.removeAllListeners('exit');
            process.removeAllListeners('uncaughtException');
        });
        
        accessors.forEach(function(accessor) {

            if (accessor.length > 3 && accessor.indexOf('.') > 0 && 
                accessor.substring(accessor.length - 3, accessor.length) === ".js" &&
                accessor.indexOf('~') == -1 &&
                accessor.substring(0,4) != '.svn' &&
                accessor.substring(0,4) != '.log') {
                // mocha-junit-reporter creates a "classname" attribute 
                // with the value of the test name.  Unfortunately, Jenkins
                // uses any classname as the top level in the display
                // hierarchy.  So, we insert a "describe" with the test name
                // It would be better not to insert a classname attribute. 
                // Unfortunately, there is no option to omit it.  
                // Alternatively, we could post-process the file, modify
                // mocha-junit-reporter or try a different reporter.  
                // Tried mocha-jenkins-reporter, but it does not seem to 
                // generate a results file when passed a file path.
                // it() is a mocha function.
                
                var testAccessorName = auto +'/' + accessor;
                
                // Remove the .js from the name
                if (testAccessorName.substring(testAccessorName.length - 3, testAccessorName.length) === ".js") {
                    testAccessorName = testAccessorName.substring(0, testAccessorName.length -3);
                }
                // Remove ./ because the last . is all that matters for Jenkins.  Also replace // with /
                var dotlessTestAccessorName = testAccessorName.replace(/\.\//g, '/').replace(/\/\//g, '/');
                it ('NodeHost./accessors/web' + dotlessTestAccessorName, function (done) {

                    var replicationMessage = '\n\tTo replicate: (cd hosts/node; node nodeHostInvoke ' + auto + '/' + accessor + ')';
                    console.log(replicationMessage);
                    var testAccessorName = auto +'/' + accessor;
                    
                    // Remove the .js from the name
                    if (testAccessorName.substring(testAccessorName.length - 3, testAccessorName.length) === ".js") {
                        testAccessorName = testAccessorName.substring(0, testAccessorName.length -3);
                    }
                    var exception = null;
                    var exceptionHandler, exitHandler;
                    var isDone = false;
                    
                    // Treat exceptions and calls to 'exit' as failures.
                    // Multiple exceptions might occur.  Catch them all, but
                    // only call 'done' once.
                    process.on('uncaughtException', exceptionHandler = function(error) { 
                        error += replicationMessage;
                        exception = error;
                        if (!isDone) {
                            isDone = true;
                            if (error !== null && typeof error !== 'undefined') {
                                if (error instanceof Error) {
                                    done(error);
                                } else {
                                    done(new Error(error.toString()));
                                }
                            } else {
                                done(new Error('Error: Uncaught exception.'));
                            }
                        }
                    });
                    
                    process.once('exit', exitHandler = function(error) { 
                        exception = error;
                        if (!isDone) {
                            isDone = true;
                            if (error !== null && typeof error !== 'undefined') {
                                if (error instanceof Error) {
                                    done(error);
                                } else {
                                    done(new Error(error.toString()));
                                }
                            } else {
                                done(new Error('Error: Exit.'));
                            }
                        }
                    });
                    
                    var testAccessor = 
                        nodeHost.instantiateTopLevel(nodeHost.uniqueName(testAccessorName),
                                                     testAccessorName);
                    
                    var oldWrapup = testAccessor.wrapup.bind(testAccessor);
                    testAccessor.wrapup = function() {
                        oldWrapup();
                        // Wait to see if any exceptions occur in wrapup.
                        // and remove the listeners.
                        setTimeout(function() {
                            process.removeListener('uncaughtException', exceptionHandler);
                            process.removeListener('exit', exitHandler);
                            done();
                        }, 500);
                    }
                    
                    testAccessor.initialize();

                    if (testAccessor.stopAtTime === undefined) {
                        console.log('testNodeAuto.js: testAccessor.stopAtTime is undefined.  ' +
                                    'Perhaps the composite accessor does not invoke "this.stopAt(xxxx)"' +
                                    'where xxxx is the stop time in ms.?  Defaulting to 5123.');
                        testAccessor.stopAtTime = 5123;
                    } else if (testAccessor.stopAtTime >= (mochaTimeout - 500)) {
                        // The problem we were seeing is that
                        // GeoCoderWeather and WebServerTimeout were
                        // failing with:

                        // Error: timeout of 20000ms exceeded. Ensure the done() callback is being called in this test.                        

                        console.log('testNodeAuto.js: WARNING: testAccessor.stopAtTime is ' +
                                    testAccessor.stopAtTime +
                                    ', which is greater than the (mochaTimeout - 500) of ' +
                                    (mochaTimeout - 500) +
                                    ', which is likely to cause problems.');
                        
                        // FIXME: Calling stopAt with an earlier time does not seem to help.
                        // testAccessor.stopAt(mochaTimeout - 1000);

                    } 

                    testAccessor.setTimeoutDeterministic(function(){
                        // A test is considered successful if no errors 
                        // occur within the timeout used with stopAt().
                        // TODO:  Any way to listen for a stop?
                        // Remove listeners at the end of a successful test
                        // to avoid having a potentially infinite number of 
                        // listeners.

                        if (exception === null) {
                            // Call wrapup() on the accessor.  
                            // If the accessor contains a TrainableTest
                            // that has not fired, the TrainableTest will
                            // (correctly) throw an exception. 

                            // It should be OK to call wrapup() again, or
                            // more to the point, it is a bug if there is
                            // a problem with calling wrapup more than once.
                            
                            testAccessor.wrapup();

                            // Wait to see if any exceptions occur in wrapup.
                            // and remove the listeners.
                            setTimeout(function() {
                                process.removeListener('uncaughtException', exceptionHandler);
                                process.removeListener('exit', exitHandler);
                                done();
                            }, 500);
                        }
                    }, // Set the timeout of this callback that
                       // removes the listeners so that it runs after
                       // the model stops.  Note that here if we are
                       // using the native Node setTimeout(), rather than
                       // what the model is using, a deterministic
                       // setTimeout().  If we don't invoke this
                       // callback after the model is done, then
                       // RampJSTest will non-deterministical have
                       // different results under Node.
                               testAccessor.stopAtTime + 1000);
                });
            }
        });
        
        after(function() {
            process.listeners('uncaughtException').push(mochaListener);
        });
        
    });
};