silverbucket/teste

View on GitHub
lib/jaribu.js

Summary

Maintainability
F
5 days
Test Coverage
if (typeof define !== 'function') {
  var define = require('amdefine')(module);
}

define([ 'jaribu/colors', 'jaribu/display', 'jaribu/tools/Env',
         'jaribu/Scaffolding', 'jaribu/Test', 'jaribu/Suite' ],
function (c, display, Env, Scaffolding, Test, Suite) {
  "use strict";
  var suites = [];
  var err_msg = '';
  var pub = {};
  var _ = {};
  _.onComplete = function () {};

  var runningInConsole = true;
  var runningInBrowser = false;
  if (typeof window === 'object') {
    runningInBrowser = true;
    runningInConsole = false;
  }

  function buildTestObj(env, s, t) {
    if (! t.desc ) {
      err_msg = s.name + ": test '" + t.name +
              "'' requires a 'desc' property";
      return false;
    } else if (typeof t.run !== 'function') {
      err_msg = s.name + ": test '" + t.name +
              "'' requires a 'run' function";
      return false;
    }

    var test = new Test();
    //test.name = t.name;
    test.desc = t.desc;
    test.setup = new Scaffolding();
    test.actual = new Scaffolding();
    test.takedown = new Scaffolding();

    // even though before/afterEach are defined
    // at the suite level, we need a separate object
    // for each test to run so that it's environment
    // is preserved and there's no bleed.
    test.beforeEach = new Scaffolding();
    test.afterEach = new Scaffolding();
    test.beforeEach.env = env;
    test.afterEach.env = env;
    if (typeof s.beforeEach === 'function') {
      test.beforeEach.run = s.beforeEach;
    }
    if (typeof s.afterEach === 'function') {
      test.afterEach.run = s.afterEach;
    }

    test.setup.env = env;
    test.actual.env = env;
    test.takedown.env = env;
    test.actual.run = t.run;
    test.actual.willFail = undefined;
    if (typeof t.setup === 'function') {
      test.setup.run = t.setup;
    }
    if (typeof t.takedown === 'function') {
      test.takedown.run = t.takedown;
    }
    // figureout if there is a timeout to override default
    if (typeof t.timeout === 'number') {
      test.actual.timeout = t.timeout;
      test.setup.timeout = t.timeout;
      test.takedown.timeout = t.timeout;
    }
    if (typeof t.willFail === 'boolean') {
      // if true, a failing test passes, and passing test fails
      test.actual.willFail = t.willFail;
    }

    //console.log(test);
    return test;
  }

  function buildSuiteObj(suite, env, s) {
    suite.name = s.name;
    suite.desc = s.desc;
    suite.setup = new Scaffolding();
    suite.takedown = new Scaffolding();
    suite.setup.env = env;
    suite.takedown.env = env;
    if (typeof s.setup === 'function') {
      suite.setup.run = s.setup;
    }
    if (typeof s.takedown === 'function') {
      suite.takedown.run = s.takedown;
    }
    if (typeof s.abortOnFail === 'boolean') {
      // if true, abort execution
      suite.abortOnFail = s.abortOnFail;
    }
    return suite;
  }

  /**
   * load a single suite json object into the library
   *
   * @param  {object}   s   suite object from test file
   * @return {boolean}      success of loading
   */
  pub.loadSuite = function (s) {
    if (! s.desc ) {
      err_msg = '... suite requires a \'desc\' property';
      return false;
    } else if (! s.tests ) {
      err_msg = '... suite requires a \'tests\' array';
      return false;
    } else if ((runningInBrowser) &&
               ((typeof s.runInBrowser === 'boolean') && (! s.runInBrowser))) {
      err_msg = '... suite should not run in the browser. skipping.';
      return false;
    } else if ((runningInConsole) &&
               ((typeof s.runInConsole === 'boolean') && (! s.runInConsole))) {
      err_msg = '... suite should not run in the browser. skipping.';
      return false;
    }

    /*
     * Create all the test objects from the JSON data
     */
    var tests = [];
    var suite = new Suite(); // we define this early so we can assign it
                             // as parent to test objects
    var env = new Env();
    if (typeof s.timeout === 'number') {
      // override test timeouts with Suite timeout
      Test.prototype.timeout = s.timeout;
      // override test timeouts with Suite timeout
      Scaffolding.prototype.timeout = s.timeout;
    }

    for (var i = 0, len = s.tests.length; i < len; i++) {
      var test = buildTestObj(env, s, s.tests[i]);
      // set position related attributes to test object
      test.position = i;
      if (i !== 0) {
        test.prev = tests[i - 1];
        tests[i - 1].next = test;
      }
      test.parent = suite;
      tests.push(test);
    }

    /*
     * Create the suite object
     */
    suite = buildSuiteObj(suite, env, s);
    suite.tests = tests;

    // set position related attributes to suite object
    suite.position = suites.length;
    if (suite.position !== 0) {
      suite.prev = suites[suite.position - 1];
      suites[suite.position - 1].next = suite;
    }
    suites.push(suite);
    return true;
  };


  /**
   *  shall reset the jaribu enviroment to load new tests
   **/
  pub.reset = function() {
    suites = [];
  };


  /**
   * returns the error message
   * @return {string} error message
   */
  pub.getErrorMessage = function () {
    return err_msg;
  };


  /**
   * returns the number of suites loaded
   * @return {number} number of suites loaded
   */
  pub.getNumSuites = function () {
    return suites.length;
  };


  /**
   * returns the total number of tests in all the suites combined.
   * @return {number} total number of tests
   */
  pub.getNumTests = function () {
    var total_tests = 0, i = 0;
    var num_suites = pub.getNumSuites();
    for (i = 0; i < num_suites; i += 1) {
      total_tests = total_tests + suites[i].tests.length;
    }
    return total_tests;
  };


  /**
   * begins the test cyle, by activating the first suite
   * @param  {function} onComplete    function to call when all tests are
   *                                  complete
   * @return {}
   */
  pub.begin = function (onComplete) {
    _.onComplete = onComplete;

    function errFunction (error) {
      if ((typeof(_.current) === 'object') &&
          (! _.current._genertingStackTrace) &&
          (! _.current.willThrow)) {
        _.current.result(false, "\n" + error, error.stack);
      } else if (((typeof(_.current) !== 'object') ||
                  (( !_.current.willThrow)) &&
                   ( !_.current.genertingStackTrace))) {
        console.error("Uncaught exception without test context: ", "\n", error, error.stack);
        process.exit(-1);
      }
    }

    if (typeof process !== 'undefined') {
      process.on('uncaughtException', errFunction);
    } else {
      window.addEventListener('error', errFunction);
    }

    var num_suites = pub.getNumSuites();
    var total_tests = pub.getNumTests();
    display.begin(num_suites, total_tests);

    if (suites[0]) {
      run('setup', 0);
    }
  };


  /**
   * test object is passed here when it passes the test, setup or takedown.
   * handles printing the result, and updating the objects status.
   *
   * @param  {string}  part       - indicates the portion of test just run
   * @param  {number}  suiteIndex - the index number of the test run
   * @param  {number}  testIndex  - the index number of the test run [optional]
   */
  function pass(part, suiteIndex, testIndex) {
    var o;
    var isSuite = false;

    if (typeof testIndex === 'number') {
      o = suites[suiteIndex].tests[testIndex];
    } else {
      isSuite = true;
      o = suites[suiteIndex];
    }

    display.pass(part, o);

    if ( isSuite ) {  // Suite ----------------------------
      if (part === 'setup') {  // setup completed
        o.setup.status = true;
        // run the next test in suite
        if (!testIndex) {
          testIndex = 0;
        } else {
          testIndex = testIndex + 1;
        }

        if (typeof o.tests[testIndex] === 'object') {
          run('beforeEach', suiteIndex, testIndex);
        } else {
          run('takedown', suiteIndex);
        }
      } else if (part === 'takedown') {  // takedown completed
        o.takedown.status = true;
        if (o.next) {
          // move on to the next suite
          run('setup', suiteIndex + 1);
        } else {
          // finished, show summary results
          showSummary();
        }
      }
    } else {  // Test ----------------------------------------------
      if (part === 'beforeEach') {  // beforeEach completed
        o.beforeEach.status = true;
        // run the test setup
        run('setup', suiteIndex, testIndex);
      } else if (part === 'setup') {  // setup completed
        o.setup.status = true;
        // run the test
        run('actual', suiteIndex, testIndex);
      } else if (part === 'takedown') {  // takedown completed
        o.takedown.status = true;
        // call afterEach
        run('afterEach', suiteIndex, testIndex);
      } else if (part === 'afterEach') {  // afterEach completed
        o.afterEach.status = true;
        if (typeof o.parent.tests[testIndex + 1] === 'object') {
          o.parent.testIndex = testIndex + 1;
          run('beforeEach', suiteIndex, testIndex + 1);
        } else {
          // run suites takedown
          run('takedown', suiteIndex);
        }
      } else {
        // test is complete
        o.status = true;
        run('takedown', suiteIndex, testIndex);
      }
    }
  }


  /**
   * test object is passed here when it fails the test, setup or takedown.
   * handles printing the result, and updating the objects status.
   *
   * @param  {string}  part       - indicates the portion of test just run
   * @param  {number}  suiteIndex - the index number of the test run
   * @param  {number}  testIndex  - the index number of the test run [optional]
   * @param  {string}  msg        - any special failure message [optional]
   */
  function fail(part, suiteIndex, testIndex, msg) {
    if (typeof testIndex === 'string') {
      msg = testIndex;
      testIndex = undefined;
    }

    var o;
    var isSuite = false;

    if (typeof testIndex === 'number') {
      o = suites[suiteIndex].tests[testIndex];
      if (typeof msg === 'string') {
        suites[suiteIndex].tests[testIndex][part].failmsg = msg;
      }
      display.details('test', o);
    } else {
      isSuite = true;
      o = suites[suiteIndex];
      if (typeof msg === 'string') {
        suites[suiteIndex][part].failmsg = msg;
      }
      display.details('suite', o);
    }

    display.fail(part, o);

    // if we've failed, we always perform the takedown.
    if (part === 'takedown') {
      // takedown has been done
      o.takedown.status = false;
      if (( isSuite ) && (! o.abortOnFail)) {
        // run next suite
        run('setup', o.next);
      } else {
        // run afterEach for this test
        run('afterEach', suiteIndex, testIndex);
      }
    } else if (part === 'afterEach') {
      o.afterEach.status = false;
      if (typeof o.parent.tests[o.parent.testIndex] === 'object') {
        if (! o.parent.abortOnFail) {
          var ti = o.parent.testIndex;
          o.parent.testIndex = o.parent.testIndex + 1;
          run('beforeEach', suiteIndex, ti);
        }
      } else {
        // run suites takedown
        run('takedown', suiteIndex);
      }
    } else if (part === 'beforeEach') {
      o.beforeEach.status = false;
      run('afterEach', suiteIndex, testIndex);
    } else if (part === 'setup') {
      o.setup.status = false;
      run('takedown', suiteIndex, testIndex);
    } else if (part === 'actual') {
      // the actual test
      o.status = false;
      if (o.parent.abortOnFail) {
        display.print('test failed with abortOnFail set... aborting.');
        showSummary();
      }
      run('takedown', suiteIndex, testIndex);
    } else {
      throw new Error('no part specified in run()');
    }
  }


  /**
   * generically handles each aspect of a suite/test setup/run/takedown
   * using the commonalities in each of the objects methods, and the
   * chaining references (o.next).
   *
   * @param  {string}  part        - the portion of test to be run.
   *                                  (setup, beforeEach, etc.) if undefined
   *                                  assumes 'actual' test
   * @param  {number}  suiteIndex - the index number of the test to run
   * @param  {number}  testIndex   - the index number of the test to run [optional]
   *
   */
  function run(part, suiteIndex, testIndex) {
    var local;
    var o;
    var isSuite = true;

    if (typeof testIndex === 'number') {
      isSuite = false;
      o = suites[suiteIndex].tests[testIndex];
    } else {
      o = suites[suiteIndex];
    }

    if ( part === 'setup' ) {
      if ( isSuite ) {
        display.suiteBorder();
        display.details('suite', o);
        // display.setup('suite');
      } else {
        // display.linebreak();
        //display.details('test', o);
        // display.setup('test');
      }
      local = o.setup;
    } else if ( part === 'beforeEach' ) {
      //display.beforeEach();
      local = o.beforeEach;
    } else if ( part === 'afterEach' ) {
      //display.afterEach();
      local = o.afterEach;
    } else if ( part === 'takedown' ) {
      // if ( isSuite ) {
      //   display.takedown('suite');
      // } else {
      //   display.takedown('test');
      // }
      local = o.takedown;
    } else {
      // must be a test
      local = o.actual;
    }

    executeTest(part, local, suiteIndex, testIndex);
  }


  function executeTest(part, local, suiteIndex, testIndex) {
    //
    // we run the test in the next tick so that the function returns and
    // we don't build up the call stack
    //
    setTimeout(function () {
      // save reference to current test, so the 'uncaughtException' handler can fail
      // the right test.
      _.current = local;

      try {
        //
        // some objects should be given the current test to avoid requiring the
        // tester to pass in the object to apply the results against.
        //
        if (local.WebSocketClient.prototype) {// FIXME no proper test for existence
          local.WebSocketClient.prototype.setTest(local);
        }

        // set running flag
        local._running = true;
        var ret = local.run(local.env.get(), local);
        if (ret && typeof(ret.then) === 'function') {
          ret.then(
            function () {
              local.result(true, 'promise fulfilled');
            },
            function (err) {
              var stack;
              if (err.hasOwnProperty('stack')) {
                stack = err.stack;
              } else {
                stack = new Error('').stack;
              }
              local.result(false, 'promise failed ' + err, stack);
            }
          );
        }

        // if the promise library has a fail function, we can catch unexpected errors
        if (ret && typeof(ret.fail) === 'function') {
          ret.fail(function (err) {
            var stack;
            if (err.hasOwnProperty('stack')) {
              stack = err.stack;
            } else {
              stack = new Error('').stack;
            }
            local.result(false, 'promise failed with an exception ' + err, stack);
          });
        }
      } catch (err) {
        //console.log('LOCAL: ',local);
        if (err.hasOwnProperty('stack')) {
          local.result(false, "\n"+err, err.stack);
        } else {
          local.result(false, "\n"+err);
        }
      }

      waitResult(part, suiteIndex, testIndex, local);
    }, 0);
  }

  function waitResult(part, suiteIndex, testIndex, local) {
    // this is function calls itself after a set interval, checking the
    // status of the test via. the result property.
    // result is initialized as undefined, and the test is not complete
    // until it is 'true' or 'false'.
    var processed = false;
    var waitCount = 0;
    var waitInterval = 50;

    (function _waitResult() {
      if (processed) {
        console.log('test processed, aborting waitResult');
        return;
      } else if (local.result() === undefined)  {
        if (waitCount < local.timeout) {
          waitCount = waitCount + waitInterval;
          setTimeout(_waitResult, waitInterval);
          return;
        } else {
          if (local.willFail === true) {
            display.print('!');
            pass(part, suiteIndex, testIndex);
          } else {
            fail(part, suiteIndex, testIndex, 'timeout');
          }
        }
      } else if (local.result() === false) {
        if (local.willFail === true) {
          display.print('!');
          pass(part, suiteIndex, testIndex);
        } else {
          fail(part, suiteIndex, testIndex);
        }
      } else if (local.result() === true) {
        if ((typeof local.willFail === 'boolean') &&
            (local.willFail === true)) {
          display.print('!');
          fail(part, suiteIndex, testIndex);
        } else {
          pass(part, suiteIndex, testIndex);
        }
      } else {
        display.print(c.red + 'ERROR GETTING RESULT' + c.reset);
        fail(part, suiteIndex, testIndex);
      }
      processed = true;
    }());
  }

  function getTestSummary(summary, suite) {
    // iterate through tests for this suite, populating the summary
    for (var i = 0, len = suite.tests.length; i < len; i++) {
      var t = suite.tests[i];
      var types = [ 'setup', 'takedown', 'beforeEach', 'afterEach' ];
      summary.scaffolding.total = summary.scaffolding.total + 2;

      for (var j = 0, jlen = types.length; j < jlen; j++) {
        if (typeof t[types[j]].status === 'undefined') {
          summary.scaffolding.skipped =
                      summary.scaffolding.skipped + 1;
        } else if (!t[types[j]].status) {
          summary.scaffolding.failed = summary.scaffolding.failed + 1;
          summary.scaffolding.failObjs.push({
            name: 'test',
            type: types[j],
            obj: t
          });
        } else {
          summary.scaffolding.passed = summary.scaffolding.passed + 1;
        }
      }

      summary.tests.total = summary.tests.total + 1;
      if (typeof t.status === 'undefined') {
        summary.tests.skipped = summary.tests.skipped + 1;
      } else if (!t.status) {
        summary.tests.failed = summary.tests.failed + 1;
        summary.tests.failObjs.push({
          name: 'test',
          type: 'actual',
          obj: t
        });
      } else {
        summary.tests.passed = summary.tests.passed + 1;
      }
    }
  }

  function getSuiteSummary(summary, suite) {
    // suite setup & takedown summarization
    var types = ['setup', 'takedown'];
    for (var i = 0, len = types.length; i < len; i++) {
      if (typeof suite[types[i]].status === 'undefined') {
        summary.scaffolding.skipped = summary.scaffolding.skipped + 1;
      } else if (!suite[types[i]].status) {
        summary.scaffolding.failed = summary.scaffolding.failed + 1;
        summary.scaffolding.failObjs.push({
          name: 'suite',
          type: types[i],
          obj: suite
        });
      } else {
        summary.scaffolding.passed = summary.scaffolding.passed + 1;
      }
    }
  }

  function getSummary() {
    var summary = {
      'scaffolding': {
        'total': 0,
        'failed': 0,
        'passed': 0,
        'skipped': 0,
        'failObjs': []
      },
      'tests': {
        'total': 0,
        'failed': 0,
        'passed': 0,
        'skipped': 0,
        'failObjs': []
      }
    };

    for (var i = 0, num_suites = pub.getNumSuites(); i < num_suites; i++) {
      summary.scaffolding.total = summary.scaffolding.total + 2;
      getSuiteSummary(summary, suites[i]);
      getTestSummary(summary, suites[i]);
    }
    return summary;
  }

  function showSummary() {
    var num_suites = pub.getNumSuites();
    var summary = getSummary();

    display.summary(num_suites, summary);

    // call specified onComplete function
    _.onComplete();

    if (((summary.tests.failed > 0) || (summary.tests.failed > 0)) ||
        ((summary.scaffolding.failed > 0) || (summary.scaffolding.failed > 0))) {
      display.printn(c.redbg +   ' FAIL' + c.reset + c.red +
              ' some tests failed!' + c.reset);
      if (typeof process !== 'undefined') {
        process.exit(1);
      }
    } else {
      display.printn(c.greenbg + '  OK ' + c.reset + c.green +
              ' all tests passed!' + c.reset);
      if (typeof process !== 'undefined') {
        process.exit(0);
      }
    }
  }

  pub.display = display;
  return pub;
});