text: var fs = require('fs');
  
  var common = require('../../tasks/util/common');
  var getMockList = require('./assets/get_mock_list');
  var getRequestOpts = require('./assets/get_image_request_options');
  var getImagePaths = require('./assets/get_image_paths');
  
  // packages inside the image server docker
  var test = require('tape');
  var request = require('request');
  var gm = require('gm');
  
  // pixel comparison tolerance
  var TOLERANCE = 1e-6;
  
  // wait time between each test batch
  var BATCH_WAIT = 500;
  
  // number of tests in each test batch
  var BATCH_SIZE = 5;
  
  // wait time between each test in test queue
  var QUEUE_WAIT = 10;
  
  /**
   *  Image pixel comparison test script.
   *
   *  Called by `tasks/test_image.sh in `npm run test-image`.
   *
   *  CLI arguments:
   *
   *  1. 'pattern' : glob determining which mock(s) are to be tested
   *  2. --queue : if sent, the image will be run in queue instead of in batch.
   *      Makes the test run significantly longer, but is recommended on weak hardware.
   *
   *  Examples:
   *
   *  Run all tests in batch:
   *
   *      npm run test-image
   *
   *  Run the 'contour_nolines' test:
   *
   *      npm run test-image -- contour_nolines
   *
   *  Run all gl3d image test in queue:
   *
   *      npm run test-image -- gl3d_* --queue
   */
  
  var pattern = process.argv[2];
  var mockList = getMockList(pattern);
  var isInQueue = (process.argv[3] === '--queue');
  var isCI = process.env.CIRCLECI;
  
  
  if(mockList.length === 0) {
      throw new Error('No mocks found with pattern ' + pattern);
  }
  
  // filter out untestable mocks if no pattern is specified
  if(!pattern) {
      console.log('Filtering out untestable mocks:');
      mockList = mockList.filter(untestableFilter);
      console.log('\n');
  }
  
  // gl2d have limited image-test support
  if(pattern === 'gl2d_*') {
  
      if(!isInQueue) {
          console.log('WARN: Running gl2d image tests in batch may lead to unwanted results\n');
      }
  
      if(isCI) {
          console.log('Filtering out multiple-subplot gl2d mocks:');
          mockList = mockList
              .filter(untestableGL2DonCIfilter)
              .sort(sortForGL2DonCI);
          console.log('\n');
      }
  }
  
  // main
  if(isInQueue) {
      runInQueue(mockList);
  }
  else {
      runInBatch(mockList);
  }
  
  /* Test cases:
   *
   * - font-wishlist
   * - all gl2d
   * - all mapbox
   *
   * don't behave consistently from run-to-run and/or
   * machine-to-machine; skip over them for now.
   *
   */
  function untestableFilter(mockName) {
      var cond = !(
          mockName === 'font-wishlist' ||
          mockName.indexOf('gl2d_') !== -1 ||
          mockName.indexOf('mapbox_') !== -1
      );
  
      if(!cond) console.log(' -', mockName);
  
      return cond;
  }
  
  /* gl2d mocks that have multiple subplots
   * can't be generated properly on CircleCI
   * at the moment.
   *
   * For more info see:
   * https://github.com/plotly/plotly.js/pull/980
   *
   */
  function untestableGL2DonCIfilter(mockName) {
      var cond = [
          'gl2d_multiple_subplots',
          'gl2d_simple_inset',
          'gl2d_stacked_coupled_subplots',
          'gl2d_stacked_subplots'
      ].indexOf(mockName) === -1;
  
      if(!cond) console.log(' -', mockName);
  
      return cond;
  }
  
  /* gl2d pointcloud mock(s) must be tested first
   * on CircleCI in order to work; sort them here.
   *
   * Pointcloud relies on gl-shader@4.2.1 whereas
   * other gl2d trace modules rely on gl-shader@4.2.0,
   * we suspect that the lone gl context on CircleCI is
   * having issues with dealing with the two different
   * gl-shader versions.
   *
   * More info here:
   * https://github.com/plotly/plotly.js/pull/1037
   */
  function sortForGL2DonCI(a, b) {
      var root = 'gl2d_pointcloud',
          ai = a.indexOf(root),
          bi = b.indexOf(root);
  
      if(ai < bi) return 1;
      if(ai > bi) return -1;
  
      return 0;
  }
  
  function runInBatch(mockList) {
      var running = 0;
  
      test('testing mocks in batch', function(t) {
          t.plan(mockList.length);
  
          for(var i = 0; i < mockList.length; i++) {
              run(mockList[i], t);
          }
      });
  
      function run(mockName, t) {
          if(running >= BATCH_SIZE) {
              setTimeout(function() {
                  run(mockName, t);
              }, BATCH_WAIT);
              return;
          }
          running++;
  
          // throttle the number of tests running concurrently
  
          comparePixels(mockName, function(isEqual, mockName) {
              running--;
              t.ok(isEqual, mockName + ' should be pixel perfect');
          });
      }
  }
  
  function runInQueue(mockList) {
      var index = 0;
  
      test('testing mocks in queue', function(t) {
          t.plan(mockList.length);
  
          run(mockList[index], t);
      });
  
      function run(mockName, t) {
          comparePixels(mockName, function(isEqual, mockName) {
              t.ok(isEqual, mockName + ' should be pixel perfect');
  
              index++;
              if(index < mockList.length) {
                  setTimeout(function() {
                      run(mockList[index], t);
                  }, QUEUE_WAIT);
              }
          });
      }
  }
  
  function comparePixels(mockName, cb) {
      var requestOpts = getRequestOpts({ mockName: mockName }),
          imagePaths = getImagePaths(mockName),
          saveImageStream = fs.createWriteStream(imagePaths.test);
  
      function log(msg) {
          process.stdout.write('Error for', mockName + ':', msg);
      }
  
      function checkImage() {
  
          // baseline image must be generated first
          if(!common.doesFileExist(imagePaths.baseline)) {
              var err = new Error('baseline image not found');
              return onEqualityCheck(err, false);
          }
  
          /*
           * N.B. The non-zero tolerance was added in
           * https://github.com/plotly/plotly.js/pull/243
           * where some legend mocks started generating different png outputs
           * on `npm run test-image` and `npm run test-image -- mock.json`.
           *
           * Note that the svg outputs for the problematic mocks were the same
           * and playing around with the batch size and timeout durations
           * did not seem to affect the results.
           *
           * With the above tolerance individual `npm run test-image` and
           * `npm run test-image -- mock.json` give the same result.
           *
           * Further investigation is needed.
           */
  
          var gmOpts = {
              file: imagePaths.diff,
              highlightColor: 'purple',
              tolerance: TOLERANCE
          };
  
          gm.compare(
              imagePaths.test,
              imagePaths.baseline,
              gmOpts,
              onEqualityCheck
          );
      }
  
      function onEqualityCheck(err, isEqual) {
          if(err) {
              common.touch(imagePaths.diff);
              log(err);
              return cb(false, mockName);
          }
          if(isEqual) {
              fs.unlinkSync(imagePaths.diff);
          }
  
          cb(isEqual, mockName);
      }
  
      // 525 means a plotly.js error
      function onResponse(response) {
          if(+response.statusCode === 525) {
              log('plotly.js error');
              return cb(false, mockName);
          }
      }
  
      // this catches connection errors
      // e.g. when the image server blows up
      function onError(err) {
          log(err);
          return cb(false, mockName);
      }
  
      request(requestOpts)
          .on('error', onError)
          .on('response', onResponse)
          .pipe(saveImageStream)
          .on('close', checkImage);
  }
  