<commit_before>#!/usr/bin/env node
'use strict';

/** Environment shortcut */
var env = process.env;

if (isFinite(env.TRAVIS_PULL_REQUEST)) {
  console.log('Skipping Sauce Labs jobs for pull requests');
  process.exit(0);
}

/** Load Node.js modules */
var EventEmitter = require('events').EventEmitter,
    http = require('http'),
    path = require('path'),
    url = require('url'),
    util = require('util');

/** Load other modules */
var _ = require('../lodash.js'),
    chalk = require('chalk'),
    ecstatic = require('ecstatic'),
    request = require('request'),
    SauceTunnel = require('sauce-tunnel-sc3-1');

/** Used for Sauce Labs credentials */
var accessKey = env.SAUCE_ACCESS_KEY,
    username = env.SAUCE_USERNAME;

/** Used as the default maximum number of times to retry a job and tunnel */
var maxJobRetries = 3,
    maxTunnelRetries = 3;

/** Used as the static file server middleware */
var mount = ecstatic({
  'cache': false,
  'root': process.cwd()
});

/** Used as the list of ports supported by Sauce Connect */
var ports = [
  80, 443, 888, 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 3030, 3210,
  3333, 4000, 4001, 4040, 4321, 4502, 4503, 4567, 5000, 5001, 5050, 5555, 5432,
  6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, 7777, 8000, 8001, 8003, 8031,
  8080, 8081, 8765, 8777, 8888, 9000, 9001, 9080, 9090, 9876, 9877, 9999, 49221,
  55001
];

/** Used by `logInline` to clear previously logged messages */
var prevLine = '';

/** Method shortcut */
var push = Array.prototype.push;

/** Used to detect error messages */
var reError = /\berror\b/i;

/** Used to display the wait throbber */
var throbberDelay = 500,
    waitCount = -1;

/** Used as Sauce Labs config values */
var advisor = getOption('advisor', true),
    build = getOption('build', (env.TRAVIS_COMMIT || '').slice(0, 10)),
    compatMode = getOption('compatMode', null),
    customData = Function('return {' + getOption('customData', '').replace(/^\{|}$/g, '') + '}')(),
    framework = getOption('framework', 'qunit'),
    idleTimeout = getOption('idleTimeout', 180),
    jobName = getOption('name', 'unit tests'),
    maxDuration = getOption('maxDuration', 360),
    port = ports[Math.min(_.sortedIndex(ports, getOption('port', 9001)), ports.length - 1)],
    publicAccess = getOption('public', true),
    queueTimeout = getOption('queueTimeout', 360),
    recordVideo = getOption('recordVideo', true),
    recordScreenshots = getOption('recordScreenshots', false),
    runner = getOption('runner', 'test/index.html').replace(/^\W+/, ''),
    runnerUrl = getOption('runnerUrl', 'http://localhost:' + port + '/' + runner),
    statusInterval = getOption('statusInterval', 5000),
    tags = getOption('tags', []),
    throttled = getOption('throttled', 10),
    tunneled = getOption('tunneled', true),
    tunnelId = getOption('tunnelId', 'tunnel_' + (env.TRAVIS_JOB_NUMBER || 0)),
    tunnelTimeout = getOption('tunnelTimeout', 10000),
    videoUploadOnPass = getOption('videoUploadOnPass', false);

/** Used to convert Sauce Labs browser identifiers to their formal names */
var browserNameMap = {
  'googlechrome': 'Chrome',
  'iehta': 'Internet Explorer',
  'ipad': 'iPad',
  'iphone': 'iPhone'
};

/** List of platforms to load the runner on */
var platforms = [
  ['Linux', 'android', '4.3'],
  ['Linux', 'android', '4.0'],
  ['Windows 8.1', 'firefox', '28'],
  ['Windows 8.1', 'firefox', '27'],
  ['Windows 8.1', 'firefox', '20'],
  ['Windows 8.1', 'firefox', '3.0'],
  ['Windows 8.1', 'chrome', '34'],
  ['Windows 8.1', 'chrome', '33'],
  ['Windows 8.1', 'internet explorer', '11'],
  ['Windows 8', 'internet explorer', '10'],
  ['Windows 7', 'internet explorer', '9'],
  ['Windows 7', 'internet explorer', '8'],
  ['Windows XP', 'internet explorer', '7'],
  ['Windows XP', 'internet explorer', '6'],
  ['Windows 7', 'opera', '12'],
  ['Windows 7', 'opera', '11'],
  ['OS X 10.9', 'ipad', '7.1'],
  ['OS X 10.9', 'safari', '7'],
  ['OS X 10.8', 'safari', '6'],
  ['OS X 10.6', 'safari', '5']
];

/** Used to tailor the `platforms` array */
var runnerQuery = url.parse(runner, true).query,
    isBackbone = /\bbackbone\b/i.test(runner),
    isModern = /\bmodern\b/i.test(runnerQuery.build);

// platforms to test IE compat mode
if (compatMode) {
  platforms = [
    ['Windows 8.1', 'internet explorer', '11'],
    ['Windows 8', 'internet explorer', '10'],
    ['Windows 7', 'internet explorer', '9'],
    ['Windows 7', 'internet explorer', '8']
  ];
}
// platforms for AMD tests
if (_.contains(tags, 'amd')) {
  platforms = _.filter(platforms, function(platform) {
    var browser = browserName(platform[1]),
        version = +platform[2];

    if (browser == 'Opera') {
      return version >= 10;
    }
    return true;
  });
}
// platforms for Backbone tests
if (isBackbone) {
  platforms = _.filter(platforms, function(platform) {
    var browser = browserName(platform[1]),
        version = +platform[2];

    switch (browser) {
      case 'Firefox': return version >= 4;
      case 'Opera': return version >= 12;
    }
    return true;
  });
}
// platforms for modern builds
if (isModern) {
  platforms = _.filter(platforms, function(platform) {
    var browser = browserName(platform[1]),
        version = +platform[2];

    switch (browser) {
      case 'Android': return version >= 4.1;
      case 'Firefox': return version >= 10;
      case 'Internet Explorer': return version >= 9;
      case 'Opera': return version >= 12;
      case 'Safari': return version >= 6;
    }
    return true;
  });
}

/** Used as the default `Job` options object */
var jobOptions = {
  'build': build,
  'custom-data': customData,
  'framework': framework,
  'idle-timeout': idleTimeout,
  'max-duration': maxDuration,
  'name': jobName,
  'public': publicAccess,
  'platforms': platforms,
  'record-screenshots': recordScreenshots,
  'record-video': recordVideo,
  'sauce-advisor': advisor,
  'tags': tags,
  'url': runnerUrl,
  'video-upload-on-pass': videoUploadOnPass
};

if (publicAccess === true) {
  jobOptions['public'] = 'public';
}
if (tunneled) {
  jobOptions['tunnel-identifier'] = tunnelId;
}

/*----------------------------------------------------------------------------*/

/**
 * Resolves the formal browser name for a given Sauce Labs browser identifier.
 *
 * @private
 * @param {string} identifier The browser identifier.
 * @returns {string} Returns the formal browser name.
 */
function browserName(identifier) {
  return browserNameMap[identifier] || capitalizeWords(identifier);
}

/**
 * Capitalizes the first character of each word in `string`.
 *
 * @private
 * @param {string} string The string to augment.
 * @returns {string} Returns the augmented string.
 */
function capitalizeWords(string) {
  return _.map(string.split(' '), _.capitalize).join(' ');
}

/**
 * Gets the value for the given option name. If no value is available the
 * `defaultValue` is returned.
 *
 * @private
 * @param {string} name The name of the option.
 * @param {*} defaultValue The default option value.
 * @returns {*} Returns the option value.
 */
function getOption(name, defaultValue) {
  var isArr = _.isArray(defaultValue);
  return _.reduce(process.argv, function(result, value) {
    if (isArr) {
      value = optionToArray(name, value);
      return _.isEmpty(value) ? result : value;
    }
    value = optionToValue(name, value);

    return value == null ? result : value;
  }, defaultValue);
}

/**
 * Writes an inline message to standard output.
 *
 * @private
 * @param {string} [text=''] The text to log.
 */
function logInline(text) {
  var blankLine = _.repeat(' ', _.size(prevLine));
  prevLine = text = _.truncate(text, 40);
  process.stdout.write(text + blankLine.slice(text.length) + '\r');
}

/**
 * Writes the wait throbber to standard output.
 *
 * @private
 */
function logThrobber() {
  logInline('Please wait' + _.repeat('.', (++waitCount % 3) + 1));
}

/**
 * Converts a comma separated option value into an array.
 *
 * @private
 * @param {string} name The name of the option to inspect.
 * @param {string} string The options string.
 * @returns {Array} Returns the new converted array.
 */
function optionToArray(name, string) {
  return _.compact(_.invoke((optionToValue(name, string) || '').split(/, */), 'trim'));
}

/**
 * Extracts the option value from an option string.
 *
 * @private
 * @param {string} name The name of the option to inspect.
 * @param {string} string The options string.
 * @returns {string|undefined} Returns the option value, else `undefined`.
 */
function optionToValue(name, string) {
  var result = (result = string.match(RegExp('^' + name + '(?:=([\\s\\S]+))?$'))) && (result[1] ? result[1].trim() : true);
  if (result === 'false') {
    return false;
  }
  return result || undefined;
}

/*----------------------------------------------------------------------------*/

/**
 * The `request.del` callback used by `Jobs#remove`.
 *
 * @private
 */
function onJobRemove(error, res, body) {
  this.id = this.testId = this.url = null;
  this.removing = false;
  this.emit('remove');
}

/**
 * The `request.post` callback used by `Jobs#start`.
 *
 * @private
 * @param {Object} [error] The error object.
 * @param {Object} res The response data object.
 * @param {Object} body The response body JSON object.
 */
function onJobStart(error, res, body) {
  var statusCode = _.result(res, 'statusCode'),
      testId = _.first(_.result(body, 'js tests')),
      tunnel = this.tunnel;

  this.starting = false;
  if (this.stopping) {
    return;
  }
  if (error || !testId || statusCode != 200) {
    if (this.attempts < this.retries) {
      this.restart();
      return;
    }
    logInline();
    console.error('Failed to start job; status: %d, body:\n%s', statusCode, JSON.stringify(body));
    if (error) {
      console.error(error);
    }
    this.failed = true;
    this.emit('complete');
    return;
  }
  this.running = true;
  this.testId = testId;
  this.timestamp = _.now();
  this.emit('start');
  this.status();
}

/**
 * The `request.post` callback used by `Job#status`.
 *
 * @private
 * @param {Object} [error] The error object.
 * @param {Object} res The response data object.
 * @param {Object} body The response body JSON object.
 */
function onJobStatus(error, res, body) {
  var completed = _.result(body, 'completed'),
      data = _.first(_.result(body, 'js tests')),
      jobResult = _.result(data, 'result'),
      jobStatus = _.result(data, 'status'),
      jobUrl = _.result(data, 'url'),
      options = this.options,
      platform = options.platforms[0],
      description = browserName(platform[1]) + ' ' + platform[2] + ' on ' + capitalizeWords(platform[0]),
      elapsed = (_.now() - this.timestamp) / 1000,
      expired = (jobStatus != 'test session in progress' && elapsed >= queueTimeout),
      failures = _.result(jobResult, 'failed'),
      label = options.name + ':',
      tunnel = this.tunnel;

  this.checking = false;
  if (!this.running || this.stopping) {
    return;
  }
  this.emit('status', jobStatus);
  if (jobUrl) {
    this.id = _.last(url.parse(jobUrl).pathname.split('/'));
    this.url = jobUrl;
  }
  if (!completed && !expired) {
    this._timerId = setTimeout(_.bind(this.status, this), this.statusInterval);
    return;
  }
  this.result = jobResult;
  if (!jobResult || failures || reError.test(jobResult.message)) {
    if (this.attempts < this.retries) {
      this.restart();
      return;
    }
    var details = 'See ' + url + ' for details.';
    this.failed = true;

    logInline();
    if (failures) {
      console.error(label + ' %s ' + chalk.red('failed') + ' %d test' + (failures > 1 ? 's' : '') + '. %s', description, failures, details);
    }
    else if (tunnel.attempts < tunnel.retries) {
      tunnel.restart();
      return;
    }
    else {
      var message = _.result(jobResult, 'message', 'no results available. ' + details);
      console.error(label, description, chalk.red('failed') + ';', message);
    }
  } else {
    console.log(label, description, chalk.green('passed'));<commit_msg>Fix `jobUrl` typo in saucelabs.js.<commit_after>#!/usr/bin/env node
'use strict';

/** Environment shortcut */
var env = process.env;

if (isFinite(env.TRAVIS_PULL_REQUEST)) {
  console.log('Skipping Sauce Labs jobs for pull requests');
  process.exit(0);
}

/** Load Node.js modules */
var EventEmitter = require('events').EventEmitter,
    http = require('http'),
    path = require('path'),
    url = require('url'),
    util = require('util');

/** Load other modules */
var _ = require('../lodash.js'),
    chalk = require('chalk'),
    ecstatic = require('ecstatic'),
    request = require('request'),
    SauceTunnel = require('sauce-tunnel-sc3-1');

/** Used for Sauce Labs credentials */
var accessKey = env.SAUCE_ACCESS_KEY,
    username = env.SAUCE_USERNAME;

/** Used as the default maximum number of times to retry a job and tunnel */
var maxJobRetries = 3,
    maxTunnelRetries = 3;

/** Used as the static file server middleware */
var mount = ecstatic({
  'cache': false,
  'root': process.cwd()
});

/** Used as the list of ports supported by Sauce Connect */
var ports = [
  80, 443, 888, 2000, 2001, 2020, 2109, 2222, 2310, 3000, 3001, 3030, 3210,
  3333, 4000, 4001, 4040, 4321, 4502, 4503, 4567, 5000, 5001, 5050, 5555, 5432,
  6000, 6001, 6060, 6666, 6543, 7000, 7070, 7774, 7777, 8000, 8001, 8003, 8031,
  8080, 8081, 8765, 8777, 8888, 9000, 9001, 9080, 9090, 9876, 9877, 9999, 49221,
  55001
];

/** Used by `logInline` to clear previously logged messages */
var prevLine = '';

/** Method shortcut */
var push = Array.prototype.push;

/** Used to detect error messages */
var reError = /\berror\b/i;

/** Used to display the wait throbber */
var throbberDelay = 500,
    waitCount = -1;

/** Used as Sauce Labs config values */
var advisor = getOption('advisor', true),
    build = getOption('build', (env.TRAVIS_COMMIT || '').slice(0, 10)),
    compatMode = getOption('compatMode', null),
    customData = Function('return {' + getOption('customData', '').replace(/^\{|}$/g, '') + '}')(),
    framework = getOption('framework', 'qunit'),
    idleTimeout = getOption('idleTimeout', 180),
    jobName = getOption('name', 'unit tests'),
    maxDuration = getOption('maxDuration', 360),
    port = ports[Math.min(_.sortedIndex(ports, getOption('port', 9001)), ports.length - 1)],
    publicAccess = getOption('public', true),
    queueTimeout = getOption('queueTimeout', 360),
    recordVideo = getOption('recordVideo', true),
    recordScreenshots = getOption('recordScreenshots', false),
    runner = getOption('runner', 'test/index.html').replace(/^\W+/, ''),
    runnerUrl = getOption('runnerUrl', 'http://localhost:' + port + '/' + runner),
    statusInterval = getOption('statusInterval', 5000),
    tags = getOption('tags', []),
    throttled = getOption('throttled', 10),
    tunneled = getOption('tunneled', true),
    tunnelId = getOption('tunnelId', 'tunnel_' + (env.TRAVIS_JOB_NUMBER || 0)),
    tunnelTimeout = getOption('tunnelTimeout', 10000),
    videoUploadOnPass = getOption('videoUploadOnPass', false);

/** Used to convert Sauce Labs browser identifiers to their formal names */
var browserNameMap = {
  'googlechrome': 'Chrome',
  'iehta': 'Internet Explorer',
  'ipad': 'iPad',
  'iphone': 'iPhone'
};

/** List of platforms to load the runner on */
var platforms = [
  ['Linux', 'android', '4.3'],
  ['Linux', 'android', '4.0'],
  ['Windows 8.1', 'firefox', '28'],
  ['Windows 8.1', 'firefox', '27'],
  ['Windows 8.1', 'firefox', '20'],
  ['Windows 8.1', 'firefox', '3.0'],
  ['Windows 8.1', 'chrome', '34'],
  ['Windows 8.1', 'chrome', '33'],
  ['Windows 8.1', 'internet explorer', '11'],
  ['Windows 8', 'internet explorer', '10'],
  ['Windows 7', 'internet explorer', '9'],
  ['Windows 7', 'internet explorer', '8'],
  ['Windows XP', 'internet explorer', '7'],
  ['Windows XP', 'internet explorer', '6'],
  ['Windows 7', 'opera', '12'],
  ['Windows 7', 'opera', '11'],
  ['OS X 10.9', 'ipad', '7.1'],
  ['OS X 10.9', 'safari', '7'],
  ['OS X 10.8', 'safari', '6'],
  ['OS X 10.6', 'safari', '5']
];

/** Used to tailor the `platforms` array */
var runnerQuery = url.parse(runner, true).query,
    isBackbone = /\bbackbone\b/i.test(runner),
    isModern = /\bmodern\b/i.test(runnerQuery.build);

// platforms to test IE compat mode
if (compatMode) {
  platforms = [
    ['Windows 8.1', 'internet explorer', '11'],
    ['Windows 8', 'internet explorer', '10'],
    ['Windows 7', 'internet explorer', '9'],
    ['Windows 7', 'internet explorer', '8']
  ];
}
// platforms for AMD tests
if (_.contains(tags, 'amd')) {
  platforms = _.filter(platforms, function(platform) {
    var browser = browserName(platform[1]),
        version = +platform[2];

    if (browser == 'Opera') {
      return version >= 10;
    }
    return true;
  });
}
// platforms for Backbone tests
if (isBackbone) {
  platforms = _.filter(platforms, function(platform) {
    var browser = browserName(platform[1]),
        version = +platform[2];

    switch (browser) {
      case 'Firefox': return version >= 4;
      case 'Opera': return version >= 12;
    }
    return true;
  });
}
// platforms for modern builds
if (isModern) {
  platforms = _.filter(platforms, function(platform) {
    var browser = browserName(platform[1]),
        version = +platform[2];

    switch (browser) {
      case 'Android': return version >= 4.1;
      case 'Firefox': return version >= 10;
      case 'Internet Explorer': return version >= 9;
      case 'Opera': return version >= 12;
      case 'Safari': return version >= 6;
    }
    return true;
  });
}

/** Used as the default `Job` options object */
var jobOptions = {
  'build': build,
  'custom-data': customData,
  'framework': framework,
  'idle-timeout': idleTimeout,
  'max-duration': maxDuration,
  'name': jobName,
  'public': publicAccess,
  'platforms': platforms,
  'record-screenshots': recordScreenshots,
  'record-video': recordVideo,
  'sauce-advisor': advisor,
  'tags': tags,
  'url': runnerUrl,
  'video-upload-on-pass': videoUploadOnPass
};

if (publicAccess === true) {
  jobOptions['public'] = 'public';
}
if (tunneled) {
  jobOptions['tunnel-identifier'] = tunnelId;
}

/*----------------------------------------------------------------------------*/

/**
 * Resolves the formal browser name for a given Sauce Labs browser identifier.
 *
 * @private
 * @param {string} identifier The browser identifier.
 * @returns {string} Returns the formal browser name.
 */
function browserName(identifier) {
  return browserNameMap[identifier] || capitalizeWords(identifier);
}

/**
 * Capitalizes the first character of each word in `string`.
 *
 * @private
 * @param {string} string The string to augment.
 * @returns {string} Returns the augmented string.
 */
function capitalizeWords(string) {
  return _.map(string.split(' '), _.capitalize).join(' ');
}

/**
 * Gets the value for the given option name. If no value is available the
 * `defaultValue` is returned.
 *
 * @private
 * @param {string} name The name of the option.
 * @param {*} defaultValue The default option value.
 * @returns {*} Returns the option value.
 */
function getOption(name, defaultValue) {
  var isArr = _.isArray(defaultValue);
  return _.reduce(process.argv, function(result, value) {
    if (isArr) {
      value = optionToArray(name, value);
      return _.isEmpty(value) ? result : value;
    }
    value = optionToValue(name, value);

    return value == null ? result : value;
  }, defaultValue);
}

/**
 * Writes an inline message to standard output.
 *
 * @private
 * @param {string} [text=''] The text to log.
 */
function logInline(text) {
  var blankLine = _.repeat(' ', _.size(prevLine));
  prevLine = text = _.truncate(text, 40);
  process.stdout.write(text + blankLine.slice(text.length) + '\r');
}

/**
 * Writes the wait throbber to standard output.
 *
 * @private
 */
function logThrobber() {
  logInline('Please wait' + _.repeat('.', (++waitCount % 3) + 1));
}

/**
 * Converts a comma separated option value into an array.
 *
 * @private
 * @param {string} name The name of the option to inspect.
 * @param {string} string The options string.
 * @returns {Array} Returns the new converted array.
 */
function optionToArray(name, string) {
  return _.compact(_.invoke((optionToValue(name, string) || '').split(/, */), 'trim'));
}

/**
 * Extracts the option value from an option string.
 *
 * @private
 * @param {string} name The name of the option to inspect.
 * @param {string} string The options string.
 * @returns {string|undefined} Returns the option value, else `undefined`.
 */
function optionToValue(name, string) {
  var result = (result = string.match(RegExp('^' + name + '(?:=([\\s\\S]+))?$'))) && (result[1] ? result[1].trim() : true);
  if (result === 'false') {
    return false;
  }
  return result || undefined;
}

/*----------------------------------------------------------------------------*/

/**
 * The `request.del` callback used by `Jobs#remove`.
 *
 * @private
 */
function onJobRemove(error, res, body) {
  this.id = this.testId = this.url = null;
  this.removing = false;
  this.emit('remove');
}

/**
 * The `request.post` callback used by `Jobs#start`.
 *
 * @private
 * @param {Object} [error] The error object.
 * @param {Object} res The response data object.
 * @param {Object} body The response body JSON object.
 */
function onJobStart(error, res, body) {
  var statusCode = _.result(res, 'statusCode'),
      testId = _.first(_.result(body, 'js tests')),
      tunnel = this.tunnel;

  this.starting = false;
  if (this.stopping) {
    return;
  }
  if (error || !testId || statusCode != 200) {
    if (this.attempts < this.retries) {
      this.restart();
      return;
    }
    logInline();
    console.error('Failed to start job; status: %d, body:\n%s', statusCode, JSON.stringify(body));
    if (error) {
      console.error(error);
    }
    this.failed = true;
    this.emit('complete');
    return;
  }
  this.running = true;
  this.testId = testId;
  this.timestamp = _.now();
  this.emit('start');
  this.status();
}

/**
 * The `request.post` callback used by `Job#status`.
 *
 * @private
 * @param {Object} [error] The error object.
 * @param {Object} res The response data object.
 * @param {Object} body The response body JSON object.
 */
function onJobStatus(error, res, body) {
  var completed = _.result(body, 'completed'),
      data = _.first(_.result(body, 'js tests')),
      jobResult = _.result(data, 'result'),
      jobStatus = _.result(data, 'status'),
      jobUrl = _.result(data, 'url'),
      options = this.options,
      platform = options.platforms[0],
      description = browserName(platform[1]) + ' ' + platform[2] + ' on ' + capitalizeWords(platform[0]),
      elapsed = (_.now() - this.timestamp) / 1000,
      expired = (jobStatus != 'test session in progress' && elapsed >= queueTimeout),
      failures = _.result(jobResult, 'failed'),
      label = options.name + ':',
      tunnel = this.tunnel;

  this.checking = false;
  if (!this.running || this.stopping) {
    return;
  }
  this.emit('status', jobStatus);
  if (jobUrl) {
    this.id = _.last(url.parse(jobUrl).pathname.split('/'));
    this.url = jobUrl;
  }
  if (!completed && !expired) {
    this._timerId = setTimeout(_.bind(this.status, this), this.statusInterval);
    return;
  }
  this.result = jobResult;
  if (!jobResult || failures || reError.test(jobResult.message)) {
    if (this.attempts < this.retries) {
      this.restart();
      return;
    }
    var details = 'See ' + jobUrl + ' for details.';
    this.failed = true;

    logInline();
    if (failures) {
      console.error(label + ' %s ' + chalk.red('failed') + ' %d test' + (failures > 1 ? 's' : '') + '. %s', description, failures, details);
    }
    else if (tunnel.attempts < tunnel.retries) {
      tunnel.restart();
      return;
    }
    else {
      var message = _.result(jobResult, 'message', 'no results available. ' + details);
      console.error(label, description, chalk.red('failed') + ';', message);
    }
  } else {
    console.log(label, description, chalk.green('passed'));
{'fraction_non_alphanumeric': 0.1838223106248964, 'fraction_numerical': 0.029835902536051714, 'mean_word_length': 3.33034272384712, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 23, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2083051', 'n_tokens_mistral': 8342, 'n_tokens_neox': 7710, 'n_words': 2189}