cli.logger.js

var _ = require('underscore'),
    colors = require('colors'),
    httpStatus = require('http-status'),
    inflection = require('inflection'),
    url = require('url');


FIXME: make a pull request into http-status for this

httpStatus[422] = 'Unprocessable Entity';

var EVENTS = {
  scenario: [ 'start', 'error', 'end' ],
  client: [ 'request', 'error', 'response' ],
  step: [ 'start', 'skip', 'error', 'done' ]
};

module.exports = function(log4js) {

  function Logger(scenario) {

    this.scenario = scenario;

    this.logger = log4js.getLogger(scenario.name);

    _.each(EVENTS, function(events, type) {

      var prefix = type + ':',
          methodPrefix = 'on' + inflection.capitalize(type);

      _.each(events, function(event) {
        scenario.on(prefix + event, _.bind(this[methodPrefix + inflection.capitalize(event)], this));
      }, this);
    }, this);

    scenario.on('configure', _.bind(this.configure, this));
  }

  _.extend(Logger.prototype, {

    configure: function(options) {

      _.extend(this, _.pick(options, 'baseUrl', 'showRequest', 'showResponseBody', 'showFullUrl'));

      this.configureLog4js(options);

      if (options.log) {
        this.logger.setLevel(options.log);
      }

      if (!this.configured) {
        console.log();
        this.logger.info(options.name.bold);
        console.log(); // FIXME: only if debug level
      }

      if (options.log) {
        if (this.configured && this.logger.level.toString() == options.log.toUpperCase()) {
          this.logger.debug('Log level set to ' + options.log);
        }
      }

      if (_.has(options, 'baseUrl')) {
        this.logger.debug('Base URL set to ' + options.baseUrl);
      }

      this.configured = true;
    },

    configureLog4js: function(options) {

      var logPattern = '';

      if (options.showTime) {
        logPattern += '%[[%d]%] ';
      }

      logPattern += '%m';

      log4js.configure({
        appenders: [
          {
            type: 'console',
            layout: {
              type: 'pattern',
              pattern: logPattern
            }
          }
        ]
      });
    },

    onScenarioStart: function(options) {

      this.stepNumber = 1;
      this.startTime = new Date().getTime();


TODO: check logger level once abstracted

      var params = options.params;
      if (_.isEmpty(params)) {
        this.logger.debug('Runtime parameters: none');
      } else {
        this.logger.debug('Runtime parameters:');
        _.each(_.keys(params).sort(), function(key) {
          this.logger.debug('  ' + key.underline + ' = ' + this.paramDisplayValue(key, params[key]));
        }, this);
      }
    },

    onScenarioError: function(err) {

      if (err instanceof Error && err.stack && this.logger.isLevelEnabled('trace')) {
        this.logger.trace(err.stack.red);
      } else {
        this.logger.error(err.toString().red);
      }

      console.log();
      this.logger.warn('Scenario terminated'.yellow);
      console.log();
    },

    onScenarioEnd: function() {

      var duration = new Date().getTime() - this.startTime;

      console.log();
      this.logger.info(('DONE in ' + (duration / 1000).toFixed(2) + 's' + '!').green);
      console.log(); 

      delete this.configured;
    },

    onStepStart: function(description) {

      this.stepStart = new Date().getTime();

      console.log();
      this.logger.info(('STEP ' + (this.stepNumber++) + ': ' + description.name).bold);
    },

    onStepSkip: function(description, message) {
      this.logger.info(('Skipped: ' + (message || 'no reason')).grey);
    },

    onStepError: function(description, err) {

nothing to do; handled by onScenarioError

    },

    onStepDone: function(description) {
      this.logger.info('Completed in ' + (new Date().getTime() - this.stepStart) + 'ms');
    },

    onClientRequest: function(requestNumber, requestOptions, originalOptions) {

      this.logRequest(requestNumber, requestOptions.method + ' ' + this.buildDisplayUrl(requestOptions.url));

      if (this.showRequest) {
        this.logRequest(requestNumber, 'request options: ' + JSON.stringify(requestOptions).magenta);
      }
    },

    onClientError: function(requestNumber, error) {

nothing to do; handled by onScenarioError

    },

    onClientResponse: function(requestNumber, response, time) {

      var status = response.statusCode + ' ' + httpStatus[response.statusCode],
          statusColor = response.statusCode >= 200 && response.statusCode <= 399 ? 'green' : 'yellow';

      this.logRequest(requestNumber, status[statusColor] + ' in ' + time + 'ms');

      if (this.showResponseBody && response.body) {
        this.logRequest(requestNumber, 'response body: ' + (_.isString(response.body) ? response.body : JSON.stringify(response.body)).magenta);
      }
    },
    
    logRequest: function(requestNumber, message) {
      this.logger.debug(('http[' + requestNumber + ']').cyan + ' ' + message);
    },

    buildDisplayUrl: function(urlString) {
      if (!this.baseUrl || this.showFullUrl) {
        return urlString;
      }

      try {
        var parsedUrl = url.parse(urlString);
        return parsedUrl.pathname ? parsedUrl.pathname : urlString;
      } catch (e) {
        return urlString;
      }
    },

    paramDisplayValue: function(key, value) {
      try {
        return JSON.stringify(this.scenario.getParameter(key).displayValue(value));
      } catch (e) {
        return value;
      }
    }
  });

  return Logger;
};

module.exports['@singleton'] = true;
module.exports['@require'] = [ 'log4js' ];