<commit_before>// run-time control channel
var agent = require('./agent');
var async = require('async');
var cluster = require('cluster');
var debug = require('./debug')('runctl');
var fs = require('fs');
var master = require('strong-cluster-control');
var npmls = require('strong-npm-ls');
var targetctl = require('./targetctl');
var util = require('util');

exports.start = start;
exports.onRequest = onRequest; // For testing

// Expose runctl using local domain server
var server = require('strong-control-channel/server').create(onRequest);

// Expose runctl using node IPC only in the master/supervisor (workers
// attach targetctl to the master).

if (cluster.isMaster && process.send) {
  var ipcctl = require('strong-control-channel/process').attach(onRequest);
  var agentVersion = require('strong-agent/package.json').version;

  // Must not be done synchronously in the require, we don't know the app name
  // until after run has found the app, and change to its directory.
  process.nextTick(function() {
    ipcctl.notify({
      cmd: 'started',
      pid: process.pid,
      appName: agent().config.appName,
      agentVersion: agentVersion,
    });
  });

  cluster.on('listening', function(worker, address) {
    debug('listening');

    ipcctl.notify({
      cmd: 'listening',
      id: worker.id,
      pid: worker.process.pid,
      address: address,
    });
  });

  exports.parentCtl = ipcctl;

  cluster.on('fork', function(worker) {
    debug('fork');

    ipcctl.notify({
      cmd: 'fork',
      id: worker.id,
      pid: worker.process.pid,
    });
    notifyStatus();
  });

  cluster.on('exit', function(worker, code, signal) {
    ipcctl.notify({
      cmd: 'exit',
      id: worker.id,
      pid: worker.process.pid,
      suicide: worker.suicide,
      reason: signal || code,
    });
    notifyStatus();
  });

  function notifyStatus() {
    var status = master.status();
    status.cmd = 'status';

    ipcctl.notify(status);
  }
}

function start(options) {
  var logger = options.logger;

  // XXX(sam) I don't like this 'last one wins' approach, but its impossible to
  // prevent the channel outliving sl-run under all conditions, this is the only
  // robust way I've found.
  try {
    fs.unlinkSync(options.channel);
  } catch (er) {
    // Didn't exist
  }

  server.listen(options.channel);

  server.on('error', function(er) {
    logger.error('control channel failed to listen on `%s`: %s',
                 options.channel, er);
    throw er;
  });

  master.on('stop', function() {
    server.close();
  });
  return server;
}

function onRequest(req, callback) {
  /* eslint no-redeclare:0 */
  debug('request %s', debug.json(req));

  var cmd = req.cmd;
  var rsp = {
  };

  if (cmd === 'status') {
    rsp = master.status();
  } else if (cmd === 'npm-ls') {
    rsp = null;
    npmls.read('.', function(er, data) {
      return callback(er ? {error: er.message} : data);
    });
  } else if (cmd === 'set-size') {
    try {
      master.setSize(req.size);
    } catch (er) {
      rsp.error = er.message;
    }

  } else if (cmd === 'stop') {
    try {
      master.stop();
    } catch (er) {
      rsp.error = er.message;
    }

  } else if (cmd === 'restart') {
    try {
      master.restart();
    } catch (er) {
      rsp.error = er.message;
    }

  } else if (cmd === 'disconnect') {
    cluster.disconnect();

  } else if (cmd === 'fork') {
    var child = cluster.fork();
    rsp.workerID = child.workerID;
    rsp.processID = child.process.pid;

  } else if (cmd === 'env-set') {
    for (var k in req.env) {
      process.env[k] = req.env[k];
    }
    rsp = requestAllTargets(req, callback);

  } else if (cmd === 'env-unset') {
    for (var k in req.env) {
      delete process.env[req.env[k]];
    }
    rsp = requestAllTargets(req, callback);

  } else {
    // Pass any others off to the target
    rsp = requestOfTarget(req, rsp, callback);
  }

  if (callback && rsp) {
    process.nextTick(callback.bind(null, rsp));
  }
}

function requestAllTargets(req, callback) {
  console.log('forwarding to all workers:', req);
  var wIds = [];
  for (var w in cluster.workers) {
    wIds.push(cluster.workers[w].id);
  }
  async.map(wIds, forwardToWorker, makeResponse);
  return null;

  function forwardToWorker(wid, next) {
    var wreq = JSON.parse(JSON.stringify(req));
    wreq.target = wid;
    console.log('forwarding %j', wreq);
    requestOfTarget(wreq, {}, function(rsp) {
      next(null, rsp);
    });
  }

  function makeResponse(_, res) {
    callback(res);
  }
}

function requestOfTarget(req, rsp, callback) {
  if (targetctl.request(+req.target, req, wrapCallback)) {
    rsp = null;
  } else {
    rsp.error = util.format('target %s not found', req.target);
  }

  return rsp;

  function wrapCallback(rsp) {
    if (ipcctl && rsp.notify) {
      ipcctl.notify(rsp.notify);
    }
    delete rsp.notify;

    callback(rsp);
  }
}
<commit_msg>runctl: always send a cluster status<commit_after>// run-time control channel
var agent = require('./agent');
var async = require('async');
var cluster = require('cluster');
var debug = require('./debug')('runctl');
var fs = require('fs');
var master = require('strong-cluster-control');
var npmls = require('strong-npm-ls');
var targetctl = require('./targetctl');
var util = require('util');

exports.start = start;
exports.onRequest = onRequest; // For testing

// Expose runctl using local domain server
var server = require('strong-control-channel/server').create(onRequest);

// Expose runctl using node IPC only in the master/supervisor (workers
// attach targetctl to the master).

if (cluster.isMaster && process.send) {
  var ipcctl = require('strong-control-channel/process').attach(onRequest);
  var agentVersion = require('strong-agent/package.json').version;

  // Must not be done synchronously in the require, we don't know the app name
  // until after run has found the app, and change to its directory.
  process.nextTick(function() {
    ipcctl.notify({
      cmd: 'started',
      pid: process.pid,
      appName: agent().config.appName,
      agentVersion: agentVersion,
    });
    // Status notifications usually come after every fork and exit, but if there
    // are no forks, we still want at least one status notification sent. Note
    // that size may be undefined, which isn't > or < than 1.
    if (!(master.options.size >= 1)) {
      notifyStatus();
    }
  });

  cluster.on('listening', function(worker, address) {
    debug('listening');

    ipcctl.notify({
      cmd: 'listening',
      id: worker.id,
      pid: worker.process.pid,
      address: address,
    });
  });

  exports.parentCtl = ipcctl;

  cluster.on('fork', function(worker) {
    debug('fork');

    ipcctl.notify({
      cmd: 'fork',
      id: worker.id,
      pid: worker.process.pid,
    });
    notifyStatus();
  });

  cluster.on('exit', function(worker, code, signal) {
    ipcctl.notify({
      cmd: 'exit',
      id: worker.id,
      pid: worker.process.pid,
      suicide: worker.suicide,
      reason: signal || code,
    });
    notifyStatus();
  });

  function notifyStatus() {
    var status = master.status();
    status.cmd = 'status';

    ipcctl.notify(status);
  }
}

function start(options) {
  var logger = options.logger;

  // XXX(sam) I don't like this 'last one wins' approach, but its impossible to
  // prevent the channel outliving sl-run under all conditions, this is the only
  // robust way I've found.
  try {
    fs.unlinkSync(options.channel);
  } catch (er) {
    // Didn't exist
  }

  server.listen(options.channel);

  server.on('error', function(er) {
    logger.error('control channel failed to listen on `%s`: %s',
                 options.channel, er);
    throw er;
  });

  master.on('stop', function() {
    server.close();
  });
  return server;
}

function onRequest(req, callback) {
  /* eslint no-redeclare:0 */
  debug('request %s', debug.json(req));

  var cmd = req.cmd;
  var rsp = {
  };

  if (cmd === 'status') {
    rsp = master.status();
  } else if (cmd === 'npm-ls') {
    rsp = null;
    npmls.read('.', function(er, data) {
      return callback(er ? {error: er.message} : data);
    });
  } else if (cmd === 'set-size') {
    try {
      master.setSize(req.size);
    } catch (er) {
      rsp.error = er.message;
    }

  } else if (cmd === 'stop') {
    try {
      master.stop();
    } catch (er) {
      rsp.error = er.message;
    }

  } else if (cmd === 'restart') {
    try {
      master.restart();
    } catch (er) {
      rsp.error = er.message;
    }

  } else if (cmd === 'disconnect') {
    cluster.disconnect();

  } else if (cmd === 'fork') {
    var child = cluster.fork();
    rsp.workerID = child.workerID;
    rsp.processID = child.process.pid;

  } else if (cmd === 'env-set') {
    for (var k in req.env) {
      process.env[k] = req.env[k];
    }
    rsp = requestAllTargets(req, callback);

  } else if (cmd === 'env-unset') {
    for (var k in req.env) {
      delete process.env[req.env[k]];
    }
    rsp = requestAllTargets(req, callback);

  } else {
    // Pass any others off to the target
    rsp = requestOfTarget(req, rsp, callback);
  }

  if (callback && rsp) {
    process.nextTick(callback.bind(null, rsp));
  }
}

function requestAllTargets(req, callback) {
  console.log('forwarding to all workers:', req);
  var wIds = [];
  for (var w in cluster.workers) {
    wIds.push(cluster.workers[w].id);
  }
  async.map(wIds, forwardToWorker, makeResponse);
  return null;

  function forwardToWorker(wid, next) {
    var wreq = JSON.parse(JSON.stringify(req));
    wreq.target = wid;
    console.log('forwarding %j', wreq);
    requestOfTarget(wreq, {}, function(rsp) {
      next(null, rsp);
    });
  }

  function makeResponse(_, res) {
    callback(res);
  }
}

function requestOfTarget(req, rsp, callback) {
  if (targetctl.request(+req.target, req, wrapCallback)) {
    rsp = null;
  } else {
    rsp.error = util.format('target %s not found', req.target);
  }

  return rsp;

  function wrapCallback(rsp) {
    if (ipcctl && rsp.notify) {
      ipcctl.notify(rsp.notify);
    }
    delete rsp.notify;

    callback(rsp);
  }
}
<|endoftext|>


<commit_before>var util = require('util'),
    Q = require('q'),
    utils = require('./utils'),
    streams = require('./stream'),
    EventEmitter = require('events').EventEmitter;

var maxMemory        = 80000000,
    maxSizeProgram   = 65536,
    maxExecutionTime = 10000,
    stoppedExitCode  = 137;

function Runner(client, language, code) {
    EventEmitter.call(this);

    this.client = client;
    this.container = null;
}

util.inherits(Runner, EventEmitter);

Runner.prototype.create = function (args) {
    var deferred = Q.defer(),
        opts = {
            Image: utils.formatImage(this.client.repository, args.language),
            Cmd: utils.formatCmd(args.code),
            Tty: false,
            Memory: maxMemory
        },
        self = this;

    this.client.createContainer(opts, function (err, container) {
        if (err) return deferred.reject(err);

        self.container = container;
        deferred.resolve(container);
    });
    return deferred.promise;
}

Runner.prototype.get = function (info) {
    var deferred = Q.defer();

    this.container.inspect(function (err, data) {
        if (err) return deferred.reject(err);

        switch (info) {
            case 'Memory':
            case 'Image':
            case 'Cmd':
                deferred.resolve(data.Config[info]);
                break;
            case 'ExitCode':
            case 'Running':
                deferred.resolve(data.State[info]);
                break;
        }
    });
    return deferred.promise;
}

Runner.prototype.attach = function() {
    var deferred = Q.defer(),
        opts = { stream: true, stdout: true, stderr: true },
        self = this;

    this.container.attach(opts, function(err, stream) {
        if (err) return deferred.reject(err);

        var output = {
            stdout: new streams.WritableStream(),
            stderr: new streams.WritableStream()
        };
        self.container.modem.demuxStream(stream,
                                         output.stdout,
                                         output.stderr);

        ['stdout', 'stderr'].forEach(function (stream) {
            output[stream].on('data', function (data) {
                self.addOutput(stream, data);
            });
        });

        deferred.resolve(stream);
    });
    return deferred.promise;
}

Runner.prototype.start = function() {
    var deferred = Q.defer(),
        self = this;

    this.container.start(function(err, data) {
        if (err) return deferred.reject(err);

        self.emit('start');

        var id = self.container.id;

        Q.delay(maxExecutionTime)
        .then(function() {
            if (self.hasContainer(id)) {
                self.emit('timeout', maxExecutionTime);
                self.stop();
            }
        });
        deferred.resolve(data);
    });
    return deferred.promise;
}

Runner.prototype.remove = function() {
    var deferred = Q.defer(),
        self = this;

    this.container.remove(function(err, data) {
        if (err) return deferred.reject(err);

        self.container = null;
        deferred.resolve(data);
    });
    return deferred.promise;
}

Runner.prototype.wait = function() {
    var deferred = Q.defer();

    this.container.wait(function(err, data) {
        if (err) return deferred.reject(err);

        deferred.resolve(data);
    });
    return deferred.promise;
}

Runner.prototype.run = function() {
    var self = this;

    return self.attach()
    .then(function() {
        return self.start();
    })
    .then(function() {
        return self.wait();
    })
    .then(function() {
        return self.get('ExitCode');
    })
    .then(function (exitCode) {
        if (exitCode !== stoppedExitCode)
            self.emit('status', utils.formatStatus(exitCode));
        return self.remove();
    });
}

// We are not checking for stop errors, it should happend only if
// the container is already stopped.
Runner.prototype.stop = function() {
    this.container !== null && this.container.stop(function() {});
}

Runner.prototype.hasContainer = function(id) {
    return this.container && id === this.container.id;
}

Runner.prototype.addOutput = function(stream, chunk) {
    this.emit('output', { stream: stream, chunk: chunk });
};

module.exports = Runner;
<commit_msg>runner now has private methods<commit_after>var util = require('util'),
    Q = require('q'),
    utils = require('./utils'),
    streams = require('./stream'),
    EventEmitter = require('events').EventEmitter;

var maxMemory        = 80000000,
    maxSizeProgram   = 65536,
    maxExecutionTime = 10000,
    stoppedExitCode  = 137;

function Runner(client, language, code) {
    EventEmitter.call(this);

    this.client = client;
    this.container = null;
}

util.inherits(Runner, EventEmitter);

Runner.prototype.create = function (args) {
    var deferred = Q.defer(),
        opts = {
            Image: utils.formatImage(this.client.repository, args.language),
            Cmd: utils.formatCmd(args.code),
            Tty: false,
            Memory: maxMemory
        },
        self = this;

    this.client.createContainer(opts, function (err, container) {
        if (err) return deferred.reject(err);

        self.container = container;
        deferred.resolve(container);
    });
    return deferred.promise;
}

Runner.prototype.get = function (info) {
    var deferred = Q.defer();

    this.container.inspect(function (err, data) {
        if (err) return deferred.reject(err);

        switch (info) {
            case 'Memory':
            case 'Image':
            case 'Cmd':
                deferred.resolve(data.Config[info]);
                break;
            case 'ExitCode':
            case 'Running':
                deferred.resolve(data.State[info]);
                break;
        }
    });
    return deferred.promise;
}

Runner.prototype._attach = function() {
    var deferred = Q.defer(),
        opts = { stream: true, stdout: true, stderr: true },
        self = this;

    this.container.attach(opts, function(err, stream) {
        if (err) return deferred.reject(err);

        var output = {
            stdout: new streams.WritableStream(),
            stderr: new streams.WritableStream()
        };
        self.container.modem.demuxStream(stream,
                                         output.stdout,
                                         output.stderr);

        ['stdout', 'stderr'].forEach(function (stream) {
            output[stream].on('data', function (data) {
                self.addOutput(stream, data);
            });
        });

        deferred.resolve(stream);
    });
    return deferred.promise;
}

Runner.prototype._start = function() {
    var deferred = Q.defer(),
        self = this;

    this.container.start(function(err, data) {
        if (err) return deferred.reject(err);

        self.emit('start');

        var id = self.container.id;

        Q.delay(maxExecutionTime)
        .then(function() {
            if (self.hasContainer(id)) {
                self.emit('timeout', maxExecutionTime);
                self.stop();
            }
        });
        deferred.resolve(data);
    });
    return deferred.promise;
}

Runner.prototype._remove = function() {
    var deferred = Q.defer(),
        self = this;

    this.container.remove(function(err, data) {
        if (err) return deferred.reject(err);

        self.container = null;
        deferred.resolve(data);
    });
    return deferred.promise;
}

Runner.prototype._wait = function() {
    var deferred = Q.defer();

    this.container.wait(function(err, data) {
        if (err) return deferred.reject(err);

        deferred.resolve(data);
    });
    return deferred.promise;
}

Runner.prototype.run = function() {
    var self = this;

    return self._attach()
    .then(function() {
        return self._start();
    })
    .then(function() {
        return self._wait();
    })
    .then(function() {
        return self.get('ExitCode');
    })
    .then(function (exitCode) {
        if (exitCode !== stoppedExitCode)
            self.emit('status', utils.formatStatus(exitCode));
        return self._remove();
    });
}

// We are not checking for stop errors, it should happend only if
// the container is already stopped.
Runner.prototype.stop = function() {
    this.container !== null && this.container.stop(function() {});
}

Runner.prototype.hasContainer = function(id) {
    return this.container && id === this.container.id;
}

Runner.prototype.addOutput = function(stream, chunk) {
    this.emit('output', { stream: stream, chunk: chunk });
};

module.exports = Runner;
<|endoftext|>


<commit_before>var
	_               = require('lodash'),
	assert          = require('assert'),
	fs              = require('fs'),
	yaml            = require('js-yaml'),
	FiveBeansWorker = require('./worker')
	;

var FiveBeansRunner = function(id, configpath)
{
	assert(id);
	assert(configpath);

	this.id = id;
	if (configpath[0] !== '/')
		configpath = process.cwd() + '/' + configpath;
	this.configpath = configpath;

	if (!fs.existsSync(configpath))
		throw(new Error(configpath + ' does not exist'));

	this.worker = null;
	return this;
};

FiveBeansRunner.prototype.go = function()
{
	var self = this;

	this.worker = this.createWorker();

	process.on('SIGINT', this.handleStop.bind(this));
	process.on('SIGQUIT', this.handleStop.bind(this));
	process.on('SIGHUP', this.handleStop.bind(this));

	process.on('SIGUSR2', function()
	{
		self.worker.on('stopped', function()
		{
			self.worker = self.createWorker();
		});
		self.worker.logInfo('received SIGUSR2; stopping & reloading configuration');
		self.worker.stop();
	});

	return self;
};

FiveBeansRunner.prototype.readConfiguration = function()
{
	var fname = this.configpath;
	var config = yaml.load(fs.readFileSync(fname, 'utf8'));
	var dirprefix = process.cwd() + '/';
	var h;

	var handlers = {};
	for (var i = 0, len = config.handlers.length; i < len; i++)
	{
		h = require(dirprefix + config.handlers[i])();
		handlers[h.type] = h;
	}
	config.handlers = handlers;

	return config;
};

FiveBeansRunner.prototype.createWorker = function()
{
	var config = _.extend({}, this.readConfiguration());
	var options =
	{
		id: this.id,
		host: config.beanstalkd.host,
		port: config.beanstalkd.port,
		handlers: config.handlers,
		ignoreDefault: config.ignoreDefault
	};

	var worker = new FiveBeansWorker(options);
	var logLevel = config.log;
	if (logLevel === 'info')
	{
		worker.on('info', console.log);
		logLevel = 'warning';
	}
	if (logLevel === 'warning')
	{
		worker.on('warning', console.warn);
		logLevel = 'error';
	}
	if (logLevel === 'error')
	{
		worker.on('error', console.error);
	}
	worker.start(config.watch);
	return worker;
};

FiveBeansRunner.prototype.handleStop = function()
{
	this.worker.on('stopped', function()
	{
		process.exit(0);
	});
	this.worker.stop();
};

module.exports = FiveBeansRunner;
<commit_msg>log -> logLevel<commit_after>var
	_               = require('lodash'),
	assert          = require('assert'),
	fs              = require('fs'),
	yaml            = require('js-yaml'),
	FiveBeansWorker = require('./worker')
	;

var FiveBeansRunner = function(id, configpath)
{
	assert(id);
	assert(configpath);

	this.id = id;
	if (configpath[0] !== '/')
		configpath = process.cwd() + '/' + configpath;
	this.configpath = configpath;

	if (!fs.existsSync(configpath))
		throw(new Error(configpath + ' does not exist'));

	this.worker = null;
	return this;
};

FiveBeansRunner.prototype.go = function()
{
	var self = this;

	this.worker = this.createWorker();

	process.on('SIGINT', this.handleStop.bind(this));
	process.on('SIGQUIT', this.handleStop.bind(this));
	process.on('SIGHUP', this.handleStop.bind(this));

	process.on('SIGUSR2', function()
	{
		self.worker.on('stopped', function()
		{
			self.worker = self.createWorker();
		});
		self.worker.logInfo('received SIGUSR2; stopping & reloading configuration');
		self.worker.stop();
	});

	return self;
};

FiveBeansRunner.prototype.readConfiguration = function()
{
	var fname = this.configpath;
	var config = yaml.load(fs.readFileSync(fname, 'utf8'));
	var dirprefix = process.cwd() + '/';
	var h;

	var handlers = {};
	for (var i = 0, len = config.handlers.length; i < len; i++)
	{
		h = require(dirprefix + config.handlers[i])();
		handlers[h.type] = h;
	}
	config.handlers = handlers;

	return config;
};

FiveBeansRunner.prototype.createWorker = function()
{
	var config = _.extend({}, this.readConfiguration());
	var options =
	{
		id: this.id,
		host: config.beanstalkd.host,
		port: config.beanstalkd.port,
		handlers: config.handlers,
		ignoreDefault: config.ignoreDefault
	};

	var worker = new FiveBeansWorker(options);
	var logLevel = config.logLevel;
	if (logLevel === 'info')
	{
		worker.on('info', console.log);
		logLevel = 'warning';
	}
	if (logLevel === 'warning')
	{
		worker.on('warning', console.warn);
		logLevel = 'error';
	}
	if (logLevel === 'error')
	{
		worker.on('error', console.error);
	}
	worker.start(config.watch);
	return worker;
};

FiveBeansRunner.prototype.handleStop = function()
{
	this.worker.on('stopped', function()
	{
		process.exit(0);
	});
	this.worker.stop();
};

module.exports = FiveBeansRunner;
<|endoftext|>


<commit_before>const fs = require("fs");
const path = require("path");
const forker = require('child_process');

const configuration_file_name = "double-check.json";

let globToRegExp =  require("./utils/glob-to-regexp");

let defaultConfig = {
    fileExt: ".js",                         // test file supported by extension
    matchDirs: ["*"],
    testsDir: process.cwd(),                // path to the root tests location
    ignore: [".git"],
    reports: {
        basePath: process.cwd(),            // path where the reports will be saved
        prefix: "Report-",                  // prefix for report files, filename pattern: [prefix]-{timestamp}{ext}
        ext: ".txt"                         // report file extension
    }
};

const TAG = "[TEST_RUNNER]";
const MAX_WORKERS = process.env['DOUBLE_CHECK_POOL_SIZE'] || 10;
const RUNNER_VERBOSE = process.env['DOUBLE_CHECK_RUNNER_VERBOSE'] || true;

const WORKER_PROCESS_STATES = {
    READY: 'ready',
    RUNNING: 'running',
    FINISHED: 'finished'
};

function TestRunner(){

    // Session object
    function initializeSession() {
        return {
            testCount: 0,
            currentTestIndex: 0,
            processedTestCount: 0,
            workers: {
                running: 0,
                terminated: 0
            }
        };
    };

    // Template structure for test reports.
    let reportFileStructure = {
        count: 0,
        suites: {},
        passed: [],
        failed: []
    };

    let config;

    function init(cfg){

        // no matter how and when runner is exiting first of all do a report print
        let process_exit = process.exit;
        process.exit = (...args)=>{
            process.exit = process_exit;
            doReports();
            process.exit(...args);
        };

        process.on("SIGINT", process.exit);
        //--------------------------------------------------------------------------------------

        config = extend(defaultConfig, cfg);
        debug("Starting config", config);

        //the testTree holds the tree of directories and files descovered
        this.testTree = {};
        //sorted list of all test files discovered
        this.testList = [];

        this.session = initializeSession();

        // create reports directory if not exist
        if (!fs.existsSync(config.reports.basePath)){
            fs.mkdirSync(config.reports.basePath);
        }
    }

    function getDefaultNodeStructure() {
        return  {
            __meta: {
                conf: null,
                parent: null,
                isDirectory: false
            },
            data: {
                name: null,
                path: null,
            },
            result: {
                state: WORKER_PROCESS_STATES.READY, // ready | running | terminated | timeout
                pass: null,
                executionTime: 0,
                runs: 0,
                asserts: [],
                messages: []
            },
            items: null
        };
    }

    function discoverTestFiles(dir, parentConf) {
        const stat = fs.statSync(dir);
        if(!stat.isDirectory()){
            throw new Error(dir + " is not a directory!");
        }

        let currentConf = parentConf;

        let currentNode = getDefaultNodeStructure();
        currentNode.__meta.parent = path.dirname(dir);
        currentNode.__meta.isDirectory = true;

        let files = fs.readdirSync(dir);
        // first look for conf file
        if(files.indexOf(configuration_file_name) !== -1) {
            let fd = path.join(dir, configuration_file_name);
            let conf = readConf(fd);
            if(conf) {
                currentNode.__meta.conf = conf;
                currentConf = extend(currentConf, conf);
                //currentConf = conf;
            }
        }

        currentNode.data.name = path.basename(dir);
        currentNode.data.path = dir;
        currentNode.items = [];

        for(let i = 0, len = files.length; i < len; i++) {
            let item = files[i];

            let fd = path.join(dir, item);
            let stat = fs.statSync(fd);
            let isDir = stat.isDirectory();
            let isTestDir = validateAsTestDir(fd);

            if(isDir && !isTestDir) {
                continue; // ignore dirs that does not follow the naming rule for test dirs
            }

            if(!isDir && item.match(configuration_file_name)){
                continue; // already processed
            }

            // exclude files based on glob patterns
            if(currentConf) {
                // currentConf['ignore'] - array of regExp
                if(currentConf['ignore']) {
                    const isMatch = isAnyMatch(currentConf['ignore'], item);
                    if(isMatch) {continue;}
                }
            }

            let childNode = getDefaultNodeStructure();
            childNode.__meta.conf = {};
            childNode.__meta.isDirectory = isDir;
            childNode.__meta.parent = path.dirname(fd);

            if (isDir) {
                let tempChildNode = discoverTestFiles(fd, currentConf);
                childNode = Object.assign(childNode, tempChildNode);
                currentNode.items.push(childNode);
            }
            else if(path.extname(fd) ===  config.fileExt){
                childNode.__meta.conf.runs = currentConf['runs'] || 1;
                childNode.__meta.conf.silent = currentConf['silent'];

                childNode.data.name = item;
                childNode.data.path = fd;

                currentNode.items.push(childNode);
            }
        }

        return currentNode;
    }

    function readConf(confPath) {
        var config = {};
        try{
            config = require(confPath);
        } catch(error) {
            console.error(error);
        }

        return config;
    }

    function validateAsTestDir(dir) {
        if(!config || !config.matchDirs) {
            throw new Error(`matchDirs is not defined on config ${JSON.stringify(config)} does not exist!`);
        }

       let isTestDir = isAnyMatch(config.matchDirs, dir);

        return isTestDir;
    }

    function isAnyMatch(globExpArray, str) {
        const hasMatch = function(globExp) {
            const regex = globToRegExp(globExp);
            return regex.test(str);
        };

        return globExpArray.some(hasMatch);
    }

    let launchTests = () => {

        this.session.testCount = this.testList.length;

        console.log(`Start launching tests (${this.session.testCount})...`);

        reportFileStructure.startDate = new Date().getTime();
        reportFileStructure.count = this.session.testCount;
        if(this.session.testCount > 0) {
            setInterval(scheduleWork, 100);
        } else {
            doReports();
        }
    };

    let scheduleWork = () => {
        //launching tests for each workers available
        while(this.session.workers.running < MAX_WORKERS && this.session.currentTestIndex < this.session.testCount){
            let test = this.testList[this.session.currentTestIndex];
            launchTest(test);
        }
    };

    let launchTest = (test) => {
        this.session.workers.running++;

        test.result.state = WORKER_PROCESS_STATES.RUNNING;

        let env = process.env;

        const cwd = test.__meta.parent;
        let worker = forker.fork(test.data.path, [], {
                'cwd': cwd,
                'env': env,
                stdio: ["inherit", "pipe", 'pipe', 'ipc'],
                silent: false
            });
        worker.on("exit", onExitEventHandlerWrapper(test));
        worker.on("message", onMessageEventHandlerWrapper(test));
        worker.on("error", onErrorEventHandlerWrapper(test));

        debug(`Launching test ${test.data.name}, on worker pid[${worker.pid}] `);
        console.log(`Progress: ${this.session.currentTestIndex+1} of ${this.session.testCount}`);

        this.session.currentTestIndex++;

        worker.stdout.on('data', function (chunk) {
            let content = new Buffer(chunk).toString('utf8');
            if (test.__meta.conf.silent) {
                console.log(content);
            }
            test.result.messages.push(content);
        }.bind(this));

        var self = this;

        function onMessageEventHandlerWrapper(test) {
            return function (log) {
                if (log.type === 'assert') {
                    test.result.asserts.push(log);
                } else {
                    test.result.messages.push(log);
                }
            };
        }

        function onExitEventHandlerWrapper(test) {
            return function (code, signal) {
                //clearTimeout(worker.timerVar);
                debug(`Test ${test.data.name} exit with code ${code}, signal ${signal} `);

                test.result.state = WORKER_PROCESS_STATES.FINISHED;
                self.session.processedTestCount++;
                if (code === 0 && test.result.pass === null) {
                    test.result.pass = true;
                    reportFileStructure.passed.push(test);
                } else {
                    test.result.pass = false;
                    reportFileStructure.failed.push(test);
                    test.result.messages.push({
                        message: "Process finished with errors!",
                        "Exit code": code,
                        "Signal": signal
                    });
                }

                self.session.workers.running--;
                self.session.workers.terminated++;

                //scheduleWork();
                checkWorkersStatus();
            };
        }

        // this handler can be triggered when:
        // 1. The process could not be spawned, or
        // 2. The process could not be killed, or
        // 3. Sending a message to the child process failed.
        // IMPORTANT: The 'exit' event may or may not fire after an error has occurred!
        function onErrorEventHandlerWrapper(test) {
            return function (error) {
                debug(`Worker ${worker.pid} - error event.`, test);
                debug(error);

                test.result.pass = false;
                test.result.messages.push(error);

                reportFileStructure.failed = test;

                self.session.workers.running--;
                self.session.workers.terminated++;

                test.result.messages.push(error);
            };
        }
    }

    let checkWorkersStatus = ()=>{
        let remaining = this.session.testCount - this.session.processedTestCount;
        if(this.session.workers.running === 0 && remaining === 0) {
            doReports();
        }else{
            console.log(`Testing still in progress... ${this.session.workers.running} workers busy and ${remaining} tests are waiting to finish.`);
        }
    };

    let reportsAllReadyPrinted = false;
    let doReports = () => {
        if(reportsAllReadyPrinted){
           return;
        }
        reportsAllReadyPrinted = true;
        //doing reports :D
        //on console and html report please!
        reportFileStructure.endDate = new Date().getTime();
        reportFileStructure.runned = this.session.processedTestCount;
        console.log("\n\nResults\n=============");
        console.log(`Finish running ${this.session.processedTestCount} tests from a total of ${this.session.testCount}.`);
        console.log(`${reportFileStructure.passed.length} passed tests and ${reportFileStructure.failed.length} failed tests.`);
        for(let i=0; i<this.session.processedTestCount; i++){
            let test = this.testList[i];
            console.log(`   ${test.result.pass ? 'V' : 'X'}   ${test.data.name}`);
        }
        doHTMLReport();
        //this.callback(null, this.session);
    };

    let doHTMLReport = () => {
        var folderName = path.resolve(__dirname);
        fs.readFile(path.join(folderName,'/utils/report.html'), 'utf8', (err, res) => {
            if (err) {
                debug('An error occurred while reading the html report template file, with the following error: ' + JSON.stringify(err));
                throw err;
            }
            let destination = path.join(process.cwd(), "testReport.html");
            let content = res.replace("</html>", `<script>\nprint(${JSON.stringify(reportFileStructure)});\n</script>\n</html>`);
            fs.writeFile(destination, content, 'utf8', (err) => {
                if (err) {
                    debug('An error occurred while writing the html report file, with the following error: ' + JSON.stringify(err));
                    throw err;
                }

                debug(`Finished writing report to ${destination}`);
                this.callback();
            });
        });
    };

    function debug(...args){
        if(!RUNNER_VERBOSE){
            return;
        }

        console.log(...args);
    }

    function extend(first, second) {
        for (const key in second) {
            if (!first.hasOwnProperty(key)) {
                first[key] = second[key];
            } else {
                let val = second[key];
                if(typeof first[key] === 'object') {
                    val = extend(first[key], second[key]);
                }
                first[key] = val;
            }
        }

        return first;
    }

    function testTreeToList(rootNode) {
        var testList = [];

        traverse(rootNode);

        function traverse(node) {
            if(!node.__meta.isDirectory || !node.items) {
                return;
            }

            for(let i = 0, len = node.items.length; i < len; i++) {
                const item = node.items[i];
                if(item.__meta.isDirectory) {
                    traverse(item);
                } else {
                    testList.push(item);
                }
            }
        }

        return testList;
    }

    /**
     * Main entry point. It will start the flow runner flow.
     * @param cfg {Object} - object containing settings such as conf file name, test dir.
     * @param callback {Function} - handler(error, result) invoked when an error occurred or the runner has completed all jobs.
     */
    this.start = function(cfg, callback){

        this.callback = function(err, result) {
            if(err) {
                debug("Sending error to the callback", err);
            }

            if(callback) {
                return callback(err, result);
            }
        };

        init.call(this, cfg);

        console.log("Start discovering tests ...");
        let testTree = discoverTestFiles(config.testsDir, config);
        this.testList = testTreeToList(testTree);

        launchTests();
    }
}

exports.init = function(sf) {
    sf.testRunner = new TestRunner();
}<commit_msg>Sorted test results to show failed tests first<commit_after>const fs = require("fs");
const path = require("path");
const forker = require('child_process');

const configuration_file_name = "double-check.json";

let globToRegExp =  require("./utils/glob-to-regexp");

let defaultConfig = {
    fileExt: ".js",                         // test file supported by extension
    matchDirs: ["*"],
    testsDir: process.cwd(),                // path to the root tests location
    ignore: [".git"],
    reports: {
        basePath: process.cwd(),            // path where the reports will be saved
        prefix: "Report-",                  // prefix for report files, filename pattern: [prefix]-{timestamp}{ext}
        ext: ".txt"                         // report file extension
    }
};

const TAG = "[TEST_RUNNER]";
const MAX_WORKERS = process.env['DOUBLE_CHECK_POOL_SIZE'] || 10;
const RUNNER_VERBOSE = process.env['DOUBLE_CHECK_RUNNER_VERBOSE'] || true;

const WORKER_PROCESS_STATES = {
    READY: 'ready',
    RUNNING: 'running',
    FINISHED: 'finished'
};

function TestRunner(){

    // Session object
    function initializeSession() {
        return {
            testCount: 0,
            currentTestIndex: 0,
            processedTestCount: 0,
            workers: {
                running: 0,
                terminated: 0
            }
        };
    };

    // Template structure for test reports.
    let reportFileStructure = {
        count: 0,
        suites: {},
        passed: [],
        failed: []
    };

    let config;

    function init(cfg){

        // no matter how and when runner is exiting first of all do a report print
        let process_exit = process.exit;
        process.exit = (...args)=>{
            process.exit = process_exit;
            doReports();
            process.exit(...args);
        };

        process.on("SIGINT", process.exit);
        //--------------------------------------------------------------------------------------

        config = extend(defaultConfig, cfg);
        debug("Starting config", config);

        //the testTree holds the tree of directories and files descovered
        this.testTree = {};
        //sorted list of all test files discovered
        this.testList = [];

        this.session = initializeSession();

        // create reports directory if not exist
        if (!fs.existsSync(config.reports.basePath)){
            fs.mkdirSync(config.reports.basePath);
        }
    }

    function getDefaultNodeStructure() {
        return  {
            __meta: {
                conf: null,
                parent: null,
                isDirectory: false
            },
            data: {
                name: null,
                path: null,
            },
            result: {
                state: WORKER_PROCESS_STATES.READY, // ready | running | terminated | timeout
                pass: null,
                executionTime: 0,
                runs: 0,
                asserts: [],
                messages: []
            },
            items: null
        };
    }

    function discoverTestFiles(dir, parentConf) {
        const stat = fs.statSync(dir);
        if(!stat.isDirectory()){
            throw new Error(dir + " is not a directory!");
        }

        let currentConf = parentConf;

        let currentNode = getDefaultNodeStructure();
        currentNode.__meta.parent = path.dirname(dir);
        currentNode.__meta.isDirectory = true;

        let files = fs.readdirSync(dir);
        // first look for conf file
        if(files.indexOf(configuration_file_name) !== -1) {
            let fd = path.join(dir, configuration_file_name);
            let conf = readConf(fd);
            if(conf) {
                currentNode.__meta.conf = conf;
                currentConf = extend(currentConf, conf);
                //currentConf = conf;
            }
        }

        currentNode.data.name = path.basename(dir);
        currentNode.data.path = dir;
        currentNode.items = [];

        for(let i = 0, len = files.length; i < len; i++) {
            let item = files[i];

            let fd = path.join(dir, item);
            let stat = fs.statSync(fd);
            let isDir = stat.isDirectory();
            let isTestDir = validateAsTestDir(fd);

            if(isDir && !isTestDir) {
                continue; // ignore dirs that does not follow the naming rule for test dirs
            }

            if(!isDir && item.match(configuration_file_name)){
                continue; // already processed
            }

            // exclude files based on glob patterns
            if(currentConf) {
                // currentConf['ignore'] - array of regExp
                if(currentConf['ignore']) {
                    const isMatch = isAnyMatch(currentConf['ignore'], item);
                    if(isMatch) {continue;}
                }
            }

            let childNode = getDefaultNodeStructure();
            childNode.__meta.conf = {};
            childNode.__meta.isDirectory = isDir;
            childNode.__meta.parent = path.dirname(fd);

            if (isDir) {
                let tempChildNode = discoverTestFiles(fd, currentConf);
                childNode = Object.assign(childNode, tempChildNode);
                currentNode.items.push(childNode);
            }
            else if(path.extname(fd) ===  config.fileExt){
                childNode.__meta.conf.runs = currentConf['runs'] || 1;
                childNode.__meta.conf.silent = currentConf['silent'];

                childNode.data.name = item;
                childNode.data.path = fd;

                currentNode.items.push(childNode);
            }
        }

        return currentNode;
    }

    function readConf(confPath) {
        var config = {};
        try{
            config = require(confPath);
        } catch(error) {
            console.error(error);
        }

        return config;
    }

    function validateAsTestDir(dir) {
        if(!config || !config.matchDirs) {
            throw new Error(`matchDirs is not defined on config ${JSON.stringify(config)} does not exist!`);
        }

       let isTestDir = isAnyMatch(config.matchDirs, dir);

        return isTestDir;
    }

    function isAnyMatch(globExpArray, str) {
        const hasMatch = function(globExp) {
            const regex = globToRegExp(globExp);
            return regex.test(str);
        };

        return globExpArray.some(hasMatch);
    }

    let launchTests = () => {

        this.session.testCount = this.testList.length;

        console.log(`Start launching tests (${this.session.testCount})...`);

        reportFileStructure.startDate = new Date().getTime();
        reportFileStructure.count = this.session.testCount;
        if(this.session.testCount > 0) {
            setInterval(scheduleWork, 100);
        } else {
            doReports();
        }
    };

    let scheduleWork = () => {
        //launching tests for each workers available
        while(this.session.workers.running < MAX_WORKERS && this.session.currentTestIndex < this.session.testCount){
            let test = this.testList[this.session.currentTestIndex];
            launchTest(test);
        }
    };

    let launchTest = (test) => {
        this.session.workers.running++;

        test.result.state = WORKER_PROCESS_STATES.RUNNING;

        let env = process.env;

        const cwd = test.__meta.parent;
        let worker = forker.fork(test.data.path, [], {
                'cwd': cwd,
                'env': env,
                stdio: ["inherit", "pipe", 'pipe', 'ipc'],
                silent: false
            });
        worker.on("exit", onExitEventHandlerWrapper(test));
        worker.on("message", onMessageEventHandlerWrapper(test));
        worker.on("error", onErrorEventHandlerWrapper(test));

        debug(`Launching test ${test.data.name}, on worker pid[${worker.pid}] `);
        console.log(`Progress: ${this.session.currentTestIndex+1} of ${this.session.testCount}`);

        this.session.currentTestIndex++;

        worker.stdout.on('data', function (chunk) {
            let content = new Buffer(chunk).toString('utf8');
            if (test.__meta.conf.silent) {
                console.log(content);
            }
            test.result.messages.push(content);
        }.bind(this));

        var self = this;

        function onMessageEventHandlerWrapper(test) {
            return function (log) {
                if (log.type === 'assert') {
                    test.result.asserts.push(log);
                } else {
                    test.result.messages.push(log);
                }
            };
        }

        function onExitEventHandlerWrapper(test) {
            return function (code, signal) {
                //clearTimeout(worker.timerVar);
                debug(`Test ${test.data.name} exit with code ${code}, signal ${signal} `);

                test.result.state = WORKER_PROCESS_STATES.FINISHED;
                self.session.processedTestCount++;
                if (code === 0 && test.result.pass === null) {
                    test.result.pass = true;
                    reportFileStructure.passed.push(test);
                } else {
                    test.result.pass = false;
                    reportFileStructure.failed.push(test);
                    test.result.messages.push({
                        message: "Process finished with errors!",
                        "Exit code": code,
                        "Signal": signal
                    });
                }

                self.session.workers.running--;
                self.session.workers.terminated++;

                //scheduleWork();
                checkWorkersStatus();
            };
        }

        // this handler can be triggered when:
        // 1. The process could not be spawned, or
        // 2. The process could not be killed, or
        // 3. Sending a message to the child process failed.
        // IMPORTANT: The 'exit' event may or may not fire after an error has occurred!
        function onErrorEventHandlerWrapper(test) {
            return function (error) {
                debug(`Worker ${worker.pid} - error event.`, test);
                debug(error);

                test.result.pass = false;
                test.result.messages.push(error);

                reportFileStructure.failed = test;

                self.session.workers.running--;
                self.session.workers.terminated++;

                test.result.messages.push(error);
            };
        }
    }

    let checkWorkersStatus = ()=>{
        let remaining = this.session.testCount - this.session.processedTestCount;
        if(this.session.workers.running === 0 && remaining === 0) {
            doReports();
        }else{
            console.log(`Testing still in progress... ${this.session.workers.running} workers busy and ${remaining} tests are waiting to finish.`);
        }
    };

    let reportsAllReadyPrinted = false;
    let doReports = () => {
        if(reportsAllReadyPrinted){
           return;
        }
        reportsAllReadyPrinted = true;
        //doing reports :D
        //on console and html report please!
        reportFileStructure.endDate = new Date().getTime();
        reportFileStructure.runned = this.session.processedTestCount;
        console.log("\n\nResults\n=============");
        console.log(`Finish running ${this.session.processedTestCount} tests from a total of ${this.session.testCount}.`);
        console.log(`${reportFileStructure.passed.length} passed tests and ${reportFileStructure.failed.length} failed tests.`);

        const sortedTestResults = [...reportFileStructure.failed, ...reportFileStructure.passed];
        const greenCheckbox = '\x1b[32m \u2714 \x1b[0m';
        const redCross = '\x1b[31m \u274C \x1b[0m';

        for (const test of sortedTestResults) {
            const passed = test.result.pass;

            console.log(` ${passed ? greenCheckbox : redCross} ${test.data.name}`)

        }

        doHTMLReport();
        //this.callback(null, this.session);
    };

    let doHTMLReport = () => {
        var folderName = path.resolve(__dirname);
        fs.readFile(path.join(folderName,'/utils/report.html'), 'utf8', (err, res) => {
            if (err) {
                debug('An error occurred while reading the html report template file, with the following error: ' + JSON.stringify(err));
                throw err;
            }
            let destination = path.join(process.cwd(), "testReport.html");
            let content = res.replace("</html>", `<script>\nprint(${JSON.stringify(reportFileStructure)});\n</script>\n</html>`);
            fs.writeFile(destination, content, 'utf8', (err) => {
                if (err) {
                    debug('An error occurred while writing the html report file, with the following error: ' + JSON.stringify(err));
                    throw err;
                }

                debug(`Finished writing report to ${destination}`);
                this.callback();
            });
        });
    };

    function debug(...args){
        if(!RUNNER_VERBOSE){
            return;
        }

        console.log(...args);
    }

    function extend(first, second) {
        for (const key in second) {
            if (!first.hasOwnProperty(key)) {
                first[key] = second[key];
            } else {
                let val = second[key];
                if(typeof first[key] === 'object') {
                    val = extend(first[key], second[key]);
                }
                first[key] = val;
            }
        }

        return first;
    }

    function testTreeToList(rootNode) {
        var testList = [];

        traverse(rootNode);

        function traverse(node) {
            if(!node.__meta.isDirectory || !node.items) {
                return;
            }

            for(let i = 0, len = node.items.length; i < len; i++) {
                const item = node.items[i];
                if(item.__meta.isDirectory) {
                    traverse(item);
                } else {
                    testList.push(item);
                }
            }
        }

        return testList;
    }

    /**
     * Main entry point. It will start the flow runner flow.
     * @param cfg {Object} - object containing settings such as conf file name, test dir.
     * @param callback {Function} - handler(error, result) invoked when an error occurred or the runner has completed all jobs.
     */
    this.start = function(cfg, callback){

        this.callback = function(err, result) {
            if(err) {
                debug("Sending error to the callback", err);
            }

            if(callback) {
                return callback(err, result);
            }
        };

        init.call(this, cfg);

        console.log("Start discovering tests ...");
        let testTree = discoverTestFiles(config.testsDir, config);
        this.testList = testTreeToList(testTree);

        launchTests();
    }
}

exports.init = function(sf) {
    sf.testRunner = new TestRunner();
}
<|endoftext|>


<commit_before>"use strict";
/*eslint max-params: [2, 4]*/

var exec = require("child_process").exec;
var _ = require("lodash");
var async = require("async");
var chalk = require("chalk");
var log = require("./log");
var Tracker = require("./utils/tracker");

/**
 * Run a single task.
 *
 * @param {String}    cmd       Shell command
 * @param {Object}    shOpts    Shell options
 * @param {Object}    opts      Runner options
 * @param {Function}  callback  Callback `(err)`
 * @returns {Object}            Child process object
 */
var run = function (cmd, shOpts, opts, callback) {
  // Check if buffered output or piped.
  var buffer = opts.buffer;

  log.info("proc:start", cmd);
  var proc = exec(cmd, shOpts, function (err, stdout, stderr) {
    var code = err ? err.code || 1 : 0;
    var level = code === 0 ? "info" : "warn";

    // Write out buffered output.
    if (buffer) {
      process.stdout.write(stdout);
      process.stderr.write(stderr);
    }

    log[level]("proc:end:" + code, cmd);
    callback(err);
  });

  // Concurrent / "whenever" output.
  if (!buffer) {
    proc.stdout.pipe(process.stdout, { end: false });
    proc.stderr.pipe(process.stderr, { end: false });
  }

  return proc;
};

/**
 * Run with retries.
 *
 * @param {String}    cmd       Shell command
 * @param {Object}    shOpts    Shell options
 * @param {Object}    opts      Runner options
 * @param {Function}  callback  Callback `(err)`
 * @returns {void}
 */
var retry = function (cmd, shOpts, opts, callback) {
  // Expand options.
  var tracker = opts.tracker;
  var taskEnv = opts.taskEnv;

  // State.
  var tries = opts.tries;
  var success = false;
  var error;

  // Iterate and retry!
  async.whilst(
    function () {
      return !success && tries > 0;
    },
    function (cb) {
      var proc = run(cmd, shOpts, opts, function (err) {
        // Manage, update state.
        tries--;
        error = err;
        success = !error;

        // Check tries.
        if (error && tries > 0) {
          log.warn("proc:retry", chalk.red(tries) + " tries left, Command: " + chalk.gray(cmd) +
            (taskEnv ? ", Environment: " + chalk.magenta(JSON.stringify(taskEnv)) : ""));
        }

        // Execute without error.
        cb();
      });

      if (tracker) {
        tracker.add(proc);
      }
    },
    function (err) {
      error = error || err;
      if (error) {
        var code = error.code || 1;
        log.error("proc:error", "Code: " + code + ", Command: " + cmd);
      }

      callback(error);
    }
  );
};

/**
 * Add and invoke a setup task if present in options.
 *
 * @param {String}    setup     Setup task
 * @param {Object}    shOpts    Shell options
 * @returns {Object}            Process object or `null`.
 */
var addSetup = function (setup, shOpts) {
  if (!setup) { return null; }

  var done = _.once(function (code) {
    var level = code === 0 ? "info" : "error";
    log[level]("setup:end", "Setup command ended with code: " + code);
  });

  log.info("setup:start", "Starting setup task: " + setup);
  var proc = run("builder run " + setup, shOpts, {}, done);
  proc.on("exit", done);

  return proc;
};

/**
 * Wrap callback with setup termination behavior.
 *
 * - Create and invoke setup.
 * - Early termination on setup error.
 * - Ensure callback only called once.
 *
 * @param {Object}    shOpts    Shell options
 * @param {Object}    opts      Runner options
 * @param {Object}    tracker   Process tracker
 * @param {Function}  callback  Callback `(err)`
 * @returns {Function}          Wrapped callback
 */
var createFinish = function (shOpts, opts, tracker, callback) {
  // Wrap callback
  var finish = _.once(function (err) {
    tracker.kill(function () {
      callback(err);
    });
  });

  // Add, invoke, and hook to final callback if setup dies early.
  var setup = tracker.add(addSetup(opts.setup, shOpts));
  if (setup) {
    // If setup exit happens before normal termination, kill everything.
    setup.on("exit", function (code) {
      finish(new Error("Setup exited with code: " + code));
    });
  }

  return finish;
};

/**
 * Task runner.
 */
module.exports = {
  /**
   * Run a single task.
   *
   * @param {String}    cmd       Shell command
   * @param {Object}    shOpts    Shell options
   * @param {Object}    opts      Runner options
   * @param {Function}  callback  Callback `(err)`
   * @returns {Object}            Child process object
   */
  run: function (cmd, shOpts, opts, callback) {
    // Add + invoke setup (if any), bind tracker cleanup, and wrap callback.
    var tracker = new Tracker();
    var finish = createFinish(shOpts, opts, tracker, callback);

    return retry(cmd, shOpts, opts, finish);
  },

  /**
   * Run multiple tasks in parallel.
   *
   * @param {Array}     cmds      List of shell commands
   * @param {Object}    shOpts    Shell options
   * @param {Object}    opts      Runner options
   * @param {Function}  callback  Callback `(err)`
   * @returns {void}
   */
  concurrent: function (cmds, shOpts, opts, callback) {
    // Add + invoke setup (if any), bind tracker cleanup, and wrap callback.
    var tracker = new Tracker();
    var finish = createFinish(shOpts, opts, tracker, callback);

    // Get mapper (queue vs. non-queue)
    var queue = opts.queue;
    var map = queue ?
      async.mapLimit.bind(async, cmds, queue) :
      async.map.bind(async, cmds);

    log.info("concurrent", "Starting with queue size: " + chalk.magenta(queue || "unlimited"));
    map(function (cmd, cb) {
      retry(cmd, shOpts, _.extend({ tracker: tracker }, opts), cb);
    }, finish);
  },

  /**
   * Run a single task with multiple environment variables in parallel.
   *
   * @param {String}    cmd       Shell command
   * @param {Object}    shOpts    Shell options
   * @param {Object}    opts      Runner options
   * @param {Function}  callback  Callback `(err)`
   * @returns {void}
   */
  envs: function (cmd, shOpts, opts, callback) {
    // Add + invoke setup (if any), bind tracker cleanup, and wrap callback.
    var tracker = new Tracker();
    var finish = createFinish(shOpts, opts, tracker, callback);

    // Get mapper (queue vs. non-queue)
    var queue = opts.queue;
    var taskEnvs = opts._envs;
    var map = queue ?
      async.mapLimit.bind(async, taskEnvs, queue) :
      async.map.bind(async, taskEnvs);

    log.info("envs", "Starting with queue size: " + chalk.magenta(queue || "unlimited"));
    map(function (taskEnv, cb) {
      // Add each specific set of environment variables.
      var taskShOpts = _.merge({}, shOpts, { env: taskEnv });
      var taskOpts = _.extend({ tracker: tracker, taskEnv: taskEnv }, opts);

      log.info("envs", "Starting environment " + chalk.magenta(JSON.stringify(taskEnv)) +
        " run for command: " + chalk.gray(cmd));
      retry(cmd, taskShOpts, taskOpts, cb);
    }, finish);
  }
};
<commit_msg>Fix merging of process.env in Node 4+<commit_after>"use strict";
/*eslint max-params: [2, 4]*/

var exec = require("child_process").exec;
var _ = require("lodash");
var async = require("async");
var chalk = require("chalk");
var log = require("./log");
var Tracker = require("./utils/tracker");

/**
 * Run a single task.
 *
 * @param {String}    cmd       Shell command
 * @param {Object}    shOpts    Shell options
 * @param {Object}    opts      Runner options
 * @param {Function}  callback  Callback `(err)`
 * @returns {Object}            Child process object
 */
var run = function (cmd, shOpts, opts, callback) {
  // Check if buffered output or piped.
  var buffer = opts.buffer;

  log.info("proc:start", cmd);
  var proc = exec(cmd, shOpts, function (err, stdout, stderr) {
    var code = err ? err.code || 1 : 0;
    var level = code === 0 ? "info" : "warn";

    // Write out buffered output.
    if (buffer) {
      process.stdout.write(stdout);
      process.stderr.write(stderr);
    }

    log[level]("proc:end:" + code, cmd);
    callback(err);
  });

  // Concurrent / "whenever" output.
  if (!buffer) {
    proc.stdout.pipe(process.stdout, { end: false });
    proc.stderr.pipe(process.stderr, { end: false });
  }

  return proc;
};

/**
 * Run with retries.
 *
 * @param {String}    cmd       Shell command
 * @param {Object}    shOpts    Shell options
 * @param {Object}    opts      Runner options
 * @param {Function}  callback  Callback `(err)`
 * @returns {void}
 */
var retry = function (cmd, shOpts, opts, callback) {
  // Expand options.
  var tracker = opts.tracker;
  var taskEnv = opts.taskEnv;

  // State.
  var tries = opts.tries;
  var success = false;
  var error;

  // Iterate and retry!
  async.whilst(
    function () {
      return !success && tries > 0;
    },
    function (cb) {
      var proc = run(cmd, shOpts, opts, function (err) {
        // Manage, update state.
        tries--;
        error = err;
        success = !error;

        // Check tries.
        if (error && tries > 0) {
          log.warn("proc:retry", chalk.red(tries) + " tries left, Command: " + chalk.gray(cmd) +
            (taskEnv ? ", Environment: " + chalk.magenta(JSON.stringify(taskEnv)) : ""));
        }

        // Execute without error.
        cb();
      });

      if (tracker) {
        tracker.add(proc);
      }
    },
    function (err) {
      error = error || err;
      if (error) {
        var code = error.code || 1;
        log.error("proc:error", "Code: " + code + ", Command: " + cmd);
      }

      callback(error);
    }
  );
};

/**
 * Add and invoke a setup task if present in options.
 *
 * @param {String}    setup     Setup task
 * @param {Object}    shOpts    Shell options
 * @returns {Object}            Process object or `null`.
 */
var addSetup = function (setup, shOpts) {
  if (!setup) { return null; }

  var done = _.once(function (code) {
    var level = code === 0 ? "info" : "error";
    log[level]("setup:end", "Setup command ended with code: " + code);
  });

  log.info("setup:start", "Starting setup task: " + setup);
  var proc = run("builder run " + setup, shOpts, {}, done);
  proc.on("exit", done);

  return proc;
};

/**
 * Wrap callback with setup termination behavior.
 *
 * - Create and invoke setup.
 * - Early termination on setup error.
 * - Ensure callback only called once.
 *
 * @param {Object}    shOpts    Shell options
 * @param {Object}    opts      Runner options
 * @param {Object}    tracker   Process tracker
 * @param {Function}  callback  Callback `(err)`
 * @returns {Function}          Wrapped callback
 */
var createFinish = function (shOpts, opts, tracker, callback) {
  // Wrap callback
  var finish = _.once(function (err) {
    tracker.kill(function () {
      callback(err);
    });
  });

  // Add, invoke, and hook to final callback if setup dies early.
  var setup = tracker.add(addSetup(opts.setup, shOpts));
  if (setup) {
    // If setup exit happens before normal termination, kill everything.
    setup.on("exit", function (code) {
      finish(new Error("Setup exited with code: " + code));
    });
  }

  return finish;
};

/**
 * Task runner.
 */
module.exports = {
  /**
   * Run a single task.
   *
   * @param {String}    cmd       Shell command
   * @param {Object}    shOpts    Shell options
   * @param {Object}    opts      Runner options
   * @param {Function}  callback  Callback `(err)`
   * @returns {Object}            Child process object
   */
  run: function (cmd, shOpts, opts, callback) {
    // Add + invoke setup (if any), bind tracker cleanup, and wrap callback.
    var tracker = new Tracker();
    var finish = createFinish(shOpts, opts, tracker, callback);

    return retry(cmd, shOpts, opts, finish);
  },

  /**
   * Run multiple tasks in parallel.
   *
   * @param {Array}     cmds      List of shell commands
   * @param {Object}    shOpts    Shell options
   * @param {Object}    opts      Runner options
   * @param {Function}  callback  Callback `(err)`
   * @returns {void}
   */
  concurrent: function (cmds, shOpts, opts, callback) {
    // Add + invoke setup (if any), bind tracker cleanup, and wrap callback.
    var tracker = new Tracker();
    var finish = createFinish(shOpts, opts, tracker, callback);

    // Get mapper (queue vs. non-queue)
    var queue = opts.queue;
    var map = queue ?
      async.mapLimit.bind(async, cmds, queue) :
      async.map.bind(async, cmds);

    log.info("concurrent", "Starting with queue size: " + chalk.magenta(queue || "unlimited"));
    map(function (cmd, cb) {
      retry(cmd, shOpts, _.extend({ tracker: tracker }, opts), cb);
    }, finish);
  },

  /**
   * Run a single task with multiple environment variables in parallel.
   *
   * @param {String}    cmd       Shell command
   * @param {Object}    shOpts    Shell options
   * @param {Object}    opts      Runner options
   * @param {Function}  callback  Callback `(err)`
   * @returns {void}
   */
  envs: function (cmd, shOpts, opts, callback) {
    // Add + invoke setup (if any), bind tracker cleanup, and wrap callback.
    var tracker = new Tracker();
    var finish = createFinish(shOpts, opts, tracker, callback);

    // Get mapper (queue vs. non-queue)
    var queue = opts.queue;
    var taskEnvs = opts._envs;
    var map = queue ?
      async.mapLimit.bind(async, taskEnvs, queue) :
      async.map.bind(async, taskEnvs);

    log.info("envs", "Starting with queue size: " + chalk.magenta(queue || "unlimited"));
    map(function (taskEnv, cb) {
      // Add each specific set of environment variables.
      // Clone `shOpts` to turn `env` into a plain object: in Node 4+
      // `process.env` is a special object which changes merge behavior.
      var taskShOpts = _.merge(_.cloneDeep(shOpts), { env: taskEnv });
      var taskOpts = _.extend({ tracker: tracker, taskEnv: taskEnv }, opts);

      log.info("envs", "Starting environment " + chalk.magenta(JSON.stringify(taskEnv)) +
        " run for command: " + chalk.gray(cmd));
      retry(cmd, taskShOpts, taskOpts, cb);
    }, finish);
  }
};
<|endoftext|>


<commit_before>'use strict';

// Load modules

const Domain = require('domain');
const Items = require('items');
const Hoek = require('Hoek');
const Reporters = require('./reporters');
const Coverage = require('./coverage');
const Linters = require('./lint');
const Leaks = require('./leaks');
const Utils = require('./utils');

// prevent libraries like Sinon from clobbering global time functions

const Date = global.Date;
const setTimeout = global.setTimeout;
const clearTimeout = global.clearTimeout;
const setImmediate = global.setImmediate;

// Declare internals

const internals = {};

Error.stackTraceLimit = Infinity;                   // Set Error stack size

internals.defaults = {

    // assert: { incomplete(), count() },
    coverage: false,

    // coveragePath: process.cwd(),
    // coverageExclude: ['node_modules', 'test'],
    colors: null,                                   // true, false, null (based on tty)
    dry: false,
    debug: false,
    environment: 'test',

    // flat: false,
    grep: null,
    ids: [],
    globals: null,
    leaks: true,
    timeout: 2000,
    output: false,                                  // Stream.Writable or string (filename)
    parallel: false,
    progress: 1,
    reporter: 'console',

    // schedule: true,
    threshold: 0,
    'lint-errors-threshold': 0,
    'lint-warnings-threshold': 0
};

exports.report = function (scripts, options, callback) {

    const settings = Utils.mergeOptions(internals.defaults, options);
    const reporter = Reporters.generate(settings);

    const executeScripts = function (next) {

        exports.execute(scripts, settings, reporter, (ignoreErr, result) => {

            if (settings.leaks) {
                result.leaks = Leaks.detect(settings.globals);
            }

            if (settings.coverage) {
                result.coverage = Coverage.analyze(settings);
            }

            return next(null, result);
        });
    };

    const executeLint = function (next) {

        if (!settings.lint) {
            return next();
        }

        Linters.lint(settings, next);
    };

    Items.parallel.execute({ notebook: executeScripts, lint: executeLint }, (ignoreErr, results) => {

        const notebook = results.notebook;
        notebook.lint = results.lint;

        if (options.assert) {
            notebook.assertions = options.assert.count && options.assert.count();
            const incompletes = options.assert.incomplete && options.assert.incomplete();
            if (incompletes) {
                for (let i = 0; i < incompletes.length; ++i) {
                    const error = new Error('Incomplete assertion at ' + incompletes[i]);
                    error.stack = undefined;
                    notebook.errors.push(error);
                }
            }
        }

        return reporter.finalize(notebook, callback);
    });
};

exports.execute = function (scripts, options, reporter, callback) {

    const settings = Utils.mergeOptions(internals.defaults, options);

    scripts = [].concat(scripts);

    if (settings.shuffle) {
        internals.shuffle(scripts);
    }

    const experiments = scripts.map((script) => {

        script._executed = true;
        return script._root;
    });

    const onlyNodes = Hoek.flatten(scripts.map((script) => script._onlyNodes));
    if (onlyNodes.length) {
        internals.skipEverythingExceptOnlyPath(scripts, onlyNodes[0]);
    }

    reporter = reporter || { test: function () { }, start: function () { } };

    if (settings.environment) {
        process.env.NODE_ENV = settings.environment;
    }

    const filters = {
        ids: settings.ids,
        grep: settings.grep ? new RegExp(settings.grep) : null
    };

    const count = internals.count(experiments, { filters: filters });        // Sets test.id
    reporter.start({ count: count });

    const startTime = Date.now();
    const state = {
        report: {
            tests: [],
            failures: 0,
            errors: []
        },
        reporter: reporter,
        filters: filters,
        options: settings,
        only: onlyNodes[0]
    };

    internals.executeExperiments(experiments, state, settings.dry, () => {

        const notebook = {
            ms: Date.now() - startTime,
            tests: state.report.tests,
            failures: state.report.failures,
            errors: state.report.errors
        };

        return callback(null, notebook);
    });
};

internals.skipEverythingExceptOnlyPath = (scripts, onlyNode) => {

    let currentExperiment = onlyNode.experiment;
    if (onlyNode.test) {
        currentExperiment.tests
            .filter(internals.not(onlyNode.test))
            .forEach(internals.enableSkip);
        currentExperiment.experiments
            .forEach(internals.enableSkip);
    }

    while (currentExperiment.parent) {
        currentExperiment.parent.tests
            .forEach(internals.enableSkip);
        currentExperiment.parent.experiments
            .filter(internals.not(currentExperiment))
            .forEach(internals.enableSkip);
        currentExperiment = currentExperiment.parent;
    }

    scripts.forEach((script) => {

        if (script._onlyNodes.indexOf(onlyNode) === -1) {
            internals.enableSkip(script._root);
        }
    });
};

internals.not = (excludedElement) => {

    return (element) => element !== excludedElement;
};

internals.enableSkip = (element) => {

    element.options.skip = true;
};

internals.shuffle = function (scripts) {

    const last = scripts.length - 1;
    for (let i = 0; i < scripts.length; ++i) {
        const rand = i + Math.floor(Math.random() * (last - i + 1));
        const temp = scripts[i];
        scripts[i] = scripts[rand];
        scripts[rand] = temp;
    }
};

internals.executeExperiments = function (experiments, state, skip, callback) {

    Items.serial(experiments, (experiment, nextExperiment) => {

        // Create a new domains context for this level of experiments, keep the old ones to restore them when finishing
        const previousDomains = state.domains;
        state.domains = [];

        const skipExperiment = skip || experiment.options.skip;
        const steps = [
            function (next) {

                // Before

                if (skipExperiment) {
                    return next();
                }

                internals.executeDeps(experiment.befores, state, (err) => {

                    if (err) {
                        internals.fail([experiment], state, skip, '\'before\' action failed');
                    }

                    return next(err);
                });
            },
            function (next) {

                // Tests

                internals.executeTests(experiment, state, skipExperiment, next);
            },
            function (next) {

                // Sub-experiments

                internals.executeExperiments(experiment.experiments, state, skipExperiment, next);
            },
            function (next) {

                // After

                if (skipExperiment) {
                    return next();
                }

                internals.executeDeps(experiment.afters, state, next);
            }
        ];

        Items.serial(steps, (item, next) => {

            item(next);
        },
        (err, results) => {

            // Restore the domains we had before
            state.domains = previousDomains;

            if (err) {
                state.report.errors.push(err);
            }

            nextExperiment();
        });
    },
    (err) => {

        callback(err);
    });
};

internals.executeDeps = function (deps, state, callback) {

    if (!deps) {
        return callback();
    }

    Items.serial(deps, (dep, next) => {

        dep.options.timeout = dep.options.timeout || state.options['context-timeout'];
        internals.protect(dep, state, next);
    }, callback);
};

internals.executeTests = function (experiment, state, skip, callback) {

    if (!experiment.tests.length) {
        return callback();
    }

    // Collect beforeEach and afterEach from parents

    const befores = skip ? [] : internals.collectDeps(experiment, 'beforeEaches');
    const afters = skip ? [] : internals.collectDeps(experiment, 'afterEaches');

    // Separate serial and parallel execution tests

    const serial = [];
    const parallel = [];

    experiment.tests.forEach((test) => {

        if (test.options.parallel ||
            (test.options.parallel === undefined && state.options.parallel)) {

            parallel.push(test);
        }
        else {
            serial.push(test);
        }
    });

    // Execute tests

    const execute = function (test, nextTest) {

        // TODO: I would remove this and use the skip mechanism for it
        if ((state.filters.ids.length && state.filters.ids.indexOf(test.id) === -1) ||
            (state.filters.grep && !state.filters.grep.test(test.title))) {

            return nextTest();
        }

        const skipTest = skip || test.options.skip;

        const steps = [
            function (next) {

                if (skipTest) {
                    return next();
                }

                // Before each

                internals.executeDeps(befores, state, (err) => {

                    if (err) {
                        internals.failTest(test, state, skip, '\'before each\' action failed');
                    }

                    return next(err);
                });
            },
            function (next) {

                // Unit

                if (!test.fn ||
                    skipTest) {

                    test[test.fn ? 'skipped' : 'todo'] = true;
                    test.duration = 0;
                    state.report.tests.push(test);
                    state.reporter.test(test);
                    return setImmediate(next);
                }

                const start = Date.now();
                internals.protect(test, state, (err) => {

                    if (err) {
                        state.report.failures++;
                        test.err = err;
                        test.timeout = err.timeout;
                    }

                    test.duration = Date.now() - start;

                    state.report.tests.push(test);
                    state.reporter.test(test);
                    return next();
                });
            },
            function (next) {

                if (skipTest) {
                    return next();
                }

                // After each

                return internals.executeDeps(afters, state, next);
            }
        ];

        Items.serial(steps, (item, next) => {

            item(next);
        },
        (err, results) => {

            if (err) {
                state.report.errors.push(err);
            }

            return nextTest();
        });
    };

    Items.serial(serial, execute, (err) => {

        Items.parallel(parallel, execute, () => {

            return callback(err);
        });
    });
};

internals.collectDeps = function (experiment, key) {

    const set = [];

    // if we are looking at afterEaches, we want to run our parent's blocks before ours (unshift onto front)
    const arrayAddFn = key === 'afterEaches' ? Array.prototype.unshift : Array.prototype.push;

    if (experiment.parent) {
        arrayAddFn.apply(set, internals.collectDeps(experiment.parent, key));
    }

    arrayAddFn.apply(set, experiment[key] || []);
    return set;
};

internals.protect = function (item, state, callback) {

    let isFirst = true;
    let timeoutId;

    const activeDomain = Domain.active;

    // We need to keep a reference to the list of domains at the time of the call since those will change with nested
    // experiments.
    const domains = state.domains;

    const finish = function (err, cause) {

        if (err &&
            err instanceof Error === false) {

            const data = err;
            err = new Error('Non Error object received or caught');
            err.data = data;
        }

        if (!isFirst) {
            const message = 'Multiple callbacks or thrown errors received in test "' + item.title + '" (' + cause + ')';

            if (err && !/^Multiple callbacks/.test(err.message)) {
                err.message = message + ': ' + err.message;
            }
            else {
                err = new Error(message);
            }

            state.report.errors.push(err);
            return;
        }

        isFirst = false;
        clearTimeout(timeoutId);

        const immed = setImmediate(() => {

            return callback(err);
        });

        /* $lab:coverage:off$ */
        if (activeDomain) {

            // The previously active domain need to be used here in case the callback throws.
            // This is of course only valid if lab itself is ran in a domain, which is the case for its own tests.
            activeDomain.add(immed);
        }
        /* $lab:coverage:on$ */
    };

    const ms = item.options.timeout !== undefined ? item.options.timeout : state.options.timeout;
    if (ms) {
        timeoutId = setTimeout(() => {

            const error = new Error('Timed out (' + ms + 'ms) - ' + item.title);
            error.timeout = true;
            finish(error, 'timeout');
        }, ms);
    }

    const domain = Domain.createDomain();

    const onError = function (err, isForward) {

        // 1. Do not forward what's already a forward.
        // 2. Only errors that reach before*/after* are worth forwarding, otherwise we know where they came from.
        if (!isForward && item.id === undefined) {
            internals.forwardError(err, domain, domains);
        }

        if (state.options.debug) {
            state.report.errors.push(err);
        }

        finish(err, 'error');
    };

    domain.title = item.title;
    domain.on('error', onError);
    domains.push(domain);

    setImmediate(() => {

        domain.enter();
        item.fn.call(null, (err) => {

            finish(err, 'done');
        });
        domain.exit();
    });
};

internals.forwardError = function (err, sourceDomain, targetDomains) {

    for (let i = 0; i < targetDomains.length; ++i) {
        const d = targetDomains[i];
        if (d !== sourceDomain) {
            d.emit('error', err, true); // Add true to mark this as a forward.
        }
    }
};

internals.count = function (experiments, state) {

    state.count = state.count || 0;
    state.seq = state.seq || 0;

    for (let i = 0; i < experiments.length; ++i) {
        const experiment = experiments[i];

        for (let j = 0; j < experiment.tests.length; ++j) {
            const test = experiment.tests[j];
            test.id = ++state.seq;
            state.count += (state.filters.ids.length && state.filters.ids.indexOf(test.id) === -1) || (state.filters.grep && !state.filters.grep.test(test.title)) ? 0 : 1;
        }

        internals.count(experiment.experiments, state);
    }

    return state.count;
};

internals.fail = function (experiments, state, skip, err) {

    for (let i = 0; i < experiments.length; ++i) {
        const experiment = experiments[i];

        for (let j = 0; j < experiment.tests.length; ++j) {
            internals.failTest(experiment.tests[j], state, skip, err);
        }

        internals.fail(experiment.experiments, state, skip || experiment.options.skip);
    }
};

internals.failTest = function (test, state, skip, err) {

    if (!test.fn ||
        skip ||
        test.options.skip) {

        test[test.fn ? 'skipped' : 'todo'] = true;
    }
    else {
        state.report.failures++;
        test.err = err;
    }

    test.duration = 0;
    state.report.tests.push(test);
    state.reporter.test(test);
};
<commit_msg>Shorten name of "skip everything" function<commit_after>'use strict';

// Load modules

const Domain = require('domain');
const Items = require('items');
const Hoek = require('Hoek');
const Reporters = require('./reporters');
const Coverage = require('./coverage');
const Linters = require('./lint');
const Leaks = require('./leaks');
const Utils = require('./utils');

// prevent libraries like Sinon from clobbering global time functions

const Date = global.Date;
const setTimeout = global.setTimeout;
const clearTimeout = global.clearTimeout;
const setImmediate = global.setImmediate;

// Declare internals

const internals = {};

Error.stackTraceLimit = Infinity;                   // Set Error stack size

internals.defaults = {

    // assert: { incomplete(), count() },
    coverage: false,

    // coveragePath: process.cwd(),
    // coverageExclude: ['node_modules', 'test'],
    colors: null,                                   // true, false, null (based on tty)
    dry: false,
    debug: false,
    environment: 'test',

    // flat: false,
    grep: null,
    ids: [],
    globals: null,
    leaks: true,
    timeout: 2000,
    output: false,                                  // Stream.Writable or string (filename)
    parallel: false,
    progress: 1,
    reporter: 'console',

    // schedule: true,
    threshold: 0,
    'lint-errors-threshold': 0,
    'lint-warnings-threshold': 0
};

exports.report = function (scripts, options, callback) {

    const settings = Utils.mergeOptions(internals.defaults, options);
    const reporter = Reporters.generate(settings);

    const executeScripts = function (next) {

        exports.execute(scripts, settings, reporter, (ignoreErr, result) => {

            if (settings.leaks) {
                result.leaks = Leaks.detect(settings.globals);
            }

            if (settings.coverage) {
                result.coverage = Coverage.analyze(settings);
            }

            return next(null, result);
        });
    };

    const executeLint = function (next) {

        if (!settings.lint) {
            return next();
        }

        Linters.lint(settings, next);
    };

    Items.parallel.execute({ notebook: executeScripts, lint: executeLint }, (ignoreErr, results) => {

        const notebook = results.notebook;
        notebook.lint = results.lint;

        if (options.assert) {
            notebook.assertions = options.assert.count && options.assert.count();
            const incompletes = options.assert.incomplete && options.assert.incomplete();
            if (incompletes) {
                for (let i = 0; i < incompletes.length; ++i) {
                    const error = new Error('Incomplete assertion at ' + incompletes[i]);
                    error.stack = undefined;
                    notebook.errors.push(error);
                }
            }
        }

        return reporter.finalize(notebook, callback);
    });
};

exports.execute = function (scripts, options, reporter, callback) {

    const settings = Utils.mergeOptions(internals.defaults, options);

    scripts = [].concat(scripts);

    if (settings.shuffle) {
        internals.shuffle(scripts);
    }

    const experiments = scripts.map((script) => {

        script._executed = true;
        return script._root;
    });

    const onlyNodes = Hoek.flatten(scripts.map((script) => script._onlyNodes));
    if (onlyNodes.length) {
        internals.skipAllButOnly(scripts, onlyNode);
    }

    reporter = reporter || { test: function () { }, start: function () { } };

    if (settings.environment) {
        process.env.NODE_ENV = settings.environment;
    }

    const filters = {
        ids: settings.ids,
        grep: settings.grep ? new RegExp(settings.grep) : null
    };

    const count = internals.count(experiments, { filters: filters });        // Sets test.id
    reporter.start({ count: count });

    const startTime = Date.now();
    const state = {
        report: {
            tests: [],
            failures: 0,
            errors: []
        },
        reporter: reporter,
        filters: filters,
        options: settings,
        only: onlyNodes[0]
    };

    internals.executeExperiments(experiments, state, settings.dry, () => {

        const notebook = {
            ms: Date.now() - startTime,
            tests: state.report.tests,
            failures: state.report.failures,
            errors: state.report.errors
        };

        return callback(null, notebook);
    });
};

internals.skipAllButOnly = (scripts, onlyNode) => {

    let currentExperiment = onlyNode.experiment;
    if (onlyNode.test) {
        currentExperiment.tests
            .filter(internals.not(onlyNode.test))
            .forEach(internals.enableSkip);
        currentExperiment.experiments
            .forEach(internals.enableSkip);
    }

    while (currentExperiment.parent) {
        currentExperiment.parent.tests
            .forEach(internals.enableSkip);
        currentExperiment.parent.experiments
            .filter(internals.not(currentExperiment))
            .forEach(internals.enableSkip);
        currentExperiment = currentExperiment.parent;
    }

    scripts.forEach((script) => {

        if (script._onlyNodes.indexOf(onlyNode) === -1) {
            internals.enableSkip(script._root);
        }
    });
};

internals.not = (excludedElement) => {

    return (element) => element !== excludedElement;
};

internals.enableSkip = (element) => {

    element.options.skip = true;
};

internals.shuffle = function (scripts) {

    const last = scripts.length - 1;
    for (let i = 0; i < scripts.length; ++i) {
        const rand = i + Math.floor(Math.random() * (last - i + 1));
        const temp = scripts[i];
        scripts[i] = scripts[rand];
        scripts[rand] = temp;
    }
};

internals.executeExperiments = function (experiments, state, skip, callback) {

    Items.serial(experiments, (experiment, nextExperiment) => {

        // Create a new domains context for this level of experiments, keep the old ones to restore them when finishing
        const previousDomains = state.domains;
        state.domains = [];

        const skipExperiment = skip || experiment.options.skip;
        const steps = [
            function (next) {

                // Before

                if (skipExperiment) {
                    return next();
                }

                internals.executeDeps(experiment.befores, state, (err) => {

                    if (err) {
                        internals.fail([experiment], state, skip, '\'before\' action failed');
                    }

                    return next(err);
                });
            },
            function (next) {

                // Tests

                internals.executeTests(experiment, state, skipExperiment, next);
            },
            function (next) {

                // Sub-experiments

                internals.executeExperiments(experiment.experiments, state, skipExperiment, next);
            },
            function (next) {

                // After

                if (skipExperiment) {
                    return next();
                }

                internals.executeDeps(experiment.afters, state, next);
            }
        ];

        Items.serial(steps, (item, next) => {

            item(next);
        },
        (err, results) => {

            // Restore the domains we had before
            state.domains = previousDomains;

            if (err) {
                state.report.errors.push(err);
            }

            nextExperiment();
        });
    },
    (err) => {

        callback(err);
    });
};

internals.executeDeps = function (deps, state, callback) {

    if (!deps) {
        return callback();
    }

    Items.serial(deps, (dep, next) => {

        dep.options.timeout = dep.options.timeout || state.options['context-timeout'];
        internals.protect(dep, state, next);
    }, callback);
};

internals.executeTests = function (experiment, state, skip, callback) {

    if (!experiment.tests.length) {
        return callback();
    }

    // Collect beforeEach and afterEach from parents

    const befores = skip ? [] : internals.collectDeps(experiment, 'beforeEaches');
    const afters = skip ? [] : internals.collectDeps(experiment, 'afterEaches');

    // Separate serial and parallel execution tests

    const serial = [];
    const parallel = [];

    experiment.tests.forEach((test) => {

        if (test.options.parallel ||
            (test.options.parallel === undefined && state.options.parallel)) {

            parallel.push(test);
        }
        else {
            serial.push(test);
        }
    });

    // Execute tests

    const execute = function (test, nextTest) {

        // TODO: I would remove this and use the skip mechanism for it
        if ((state.filters.ids.length && state.filters.ids.indexOf(test.id) === -1) ||
            (state.filters.grep && !state.filters.grep.test(test.title))) {

            return nextTest();
        }

        const skipTest = skip || test.options.skip;

        const steps = [
            function (next) {

                if (skipTest) {
                    return next();
                }

                // Before each

                internals.executeDeps(befores, state, (err) => {

                    if (err) {
                        internals.failTest(test, state, skip, '\'before each\' action failed');
                    }

                    return next(err);
                });
            },
            function (next) {

                // Unit

                if (!test.fn ||
                    skipTest) {

                    test[test.fn ? 'skipped' : 'todo'] = true;
                    test.duration = 0;
                    state.report.tests.push(test);
                    state.reporter.test(test);
                    return setImmediate(next);
                }

                const start = Date.now();
                internals.protect(test, state, (err) => {

                    if (err) {
                        state.report.failures++;
                        test.err = err;
                        test.timeout = err.timeout;
                    }

                    test.duration = Date.now() - start;

                    state.report.tests.push(test);
                    state.reporter.test(test);
                    return next();
                });
            },
            function (next) {

                if (skipTest) {
                    return next();
                }

                // After each

                return internals.executeDeps(afters, state, next);
            }
        ];

        Items.serial(steps, (item, next) => {

            item(next);
        },
        (err, results) => {

            if (err) {
                state.report.errors.push(err);
            }

            return nextTest();
        });
    };

    Items.serial(serial, execute, (err) => {

        Items.parallel(parallel, execute, () => {

            return callback(err);
        });
    });
};

internals.collectDeps = function (experiment, key) {

    const set = [];

    // if we are looking at afterEaches, we want to run our parent's blocks before ours (unshift onto front)
    const arrayAddFn = key === 'afterEaches' ? Array.prototype.unshift : Array.prototype.push;

    if (experiment.parent) {
        arrayAddFn.apply(set, internals.collectDeps(experiment.parent, key));
    }

    arrayAddFn.apply(set, experiment[key] || []);
    return set;
};

internals.protect = function (item, state, callback) {

    let isFirst = true;
    let timeoutId;

    const activeDomain = Domain.active;

    // We need to keep a reference to the list of domains at the time of the call since those will change with nested
    // experiments.
    const domains = state.domains;

    const finish = function (err, cause) {

        if (err &&
            err instanceof Error === false) {

            const data = err;
            err = new Error('Non Error object received or caught');
            err.data = data;
        }

        if (!isFirst) {
            const message = 'Multiple callbacks or thrown errors received in test "' + item.title + '" (' + cause + ')';

            if (err && !/^Multiple callbacks/.test(err.message)) {
                err.message = message + ': ' + err.message;
            }
            else {
                err = new Error(message);
            }

            state.report.errors.push(err);
            return;
        }

        isFirst = false;
        clearTimeout(timeoutId);

        const immed = setImmediate(() => {

            return callback(err);
        });

        /* $lab:coverage:off$ */
        if (activeDomain) {

            // The previously active domain need to be used here in case the callback throws.
            // This is of course only valid if lab itself is ran in a domain, which is the case for its own tests.
            activeDomain.add(immed);
        }
        /* $lab:coverage:on$ */
    };

    const ms = item.options.timeout !== undefined ? item.options.timeout : state.options.timeout;
    if (ms) {
        timeoutId = setTimeout(() => {

            const error = new Error('Timed out (' + ms + 'ms) - ' + item.title);
            error.timeout = true;
            finish(error, 'timeout');
        }, ms);
    }

    const domain = Domain.createDomain();

    const onError = function (err, isForward) {

        // 1. Do not forward what's already a forward.
        // 2. Only errors that reach before*/after* are worth forwarding, otherwise we know where they came from.
        if (!isForward && item.id === undefined) {
            internals.forwardError(err, domain, domains);
        }

        if (state.options.debug) {
            state.report.errors.push(err);
        }

        finish(err, 'error');
    };

    domain.title = item.title;
    domain.on('error', onError);
    domains.push(domain);

    setImmediate(() => {

        domain.enter();
        item.fn.call(null, (err) => {

            finish(err, 'done');
        });
        domain.exit();
    });
};

internals.forwardError = function (err, sourceDomain, targetDomains) {

    for (let i = 0; i < targetDomains.length; ++i) {
        const d = targetDomains[i];
        if (d !== sourceDomain) {
            d.emit('error', err, true); // Add true to mark this as a forward.
        }
    }
};

internals.count = function (experiments, state) {

    state.count = state.count || 0;
    state.seq = state.seq || 0;

    for (let i = 0; i < experiments.length; ++i) {
        const experiment = experiments[i];

        for (let j = 0; j < experiment.tests.length; ++j) {
            const test = experiment.tests[j];
            test.id = ++state.seq;
            state.count += (state.filters.ids.length && state.filters.ids.indexOf(test.id) === -1) || (state.filters.grep && !state.filters.grep.test(test.title)) ? 0 : 1;
        }

        internals.count(experiment.experiments, state);
    }

    return state.count;
};

internals.fail = function (experiments, state, skip, err) {

    for (let i = 0; i < experiments.length; ++i) {
        const experiment = experiments[i];

        for (let j = 0; j < experiment.tests.length; ++j) {
            internals.failTest(experiment.tests[j], state, skip, err);
        }

        internals.fail(experiment.experiments, state, skip || experiment.options.skip);
    }
};

internals.failTest = function (test, state, skip, err) {

    if (!test.fn ||
        skip ||
        test.options.skip) {

        test[test.fn ? 'skipped' : 'todo'] = true;
    }
    else {
        state.report.failures++;
        test.err = err;
    }

    test.duration = 0;
    state.report.tests.push(test);
    state.reporter.test(test);
};
<|endoftext|>