text: var path = require('path');
  var cp = require('child_process');
  var fs = require('fs');
  
  var chai = require('chai');
  var tmp = require('tmp');
  var wrench = require('wrench');
  
  var fixtures = path.join(__dirname, 'integration', 'fixtures');
  var tmpDir = 'tmp';
  
  
  /**
   * Spawn a Grunt process.
   * @param {string} dir Directory with gruntfile.js.
   * @param {function(Error, Process)} done Callback.
   */
  function spawnGrunt(dir, done) {
    var gruntfile = path.join(dir, 'gruntfile.js');
    if (!fs.existsSync(gruntfile)) {
      done(new Error('Cannot find gruntfile.js: ' + gruntfile));
    } else {
      var node = process.argv[0];
      var grunt = process.argv[1]; // assumes grunt drives these tests
      var child = cp.spawn(node, [grunt, '--verbose', '--stack'], {cwd: dir});
      done(null, child);
    }
  }
  
  
  /**
   * Set up before running tests.
   * @param {string} name Fixture name.
   * @param {function} done Callback.
   */
  function cloneFixture(name, done) {
    var fixture = path.join(fixtures, name);
    if (!fs.existsSync(tmpDir)) {
      fs.mkdirSync(tmpDir);
    }
  
    tmp.dir({dir: tmpDir}, function(error, dir) {
      if (error) {
        return done(error);
      }
      var scratch = path.join(dir, name);
      wrench.copyDirRecursive(fixture, scratch, function(error) {
        done(error, scratch);
      });
    });
  }
  
  
  /**
   * Clone a fixture and run the default Grunt task in it.
   * @param {string} name Fixture name.
   * @param {function(Error, scratch)} done Called with an error if the task
   *     fails.  Called with the cloned fixture directory if the task succeeds.
   */
  exports.buildFixture = function(name, done) {
    cloneFixture(name, function(error, scratch) {
      if (error) {
        return done(error);
      }
      spawnGrunt(scratch, function(error, child) {
        if (error) {
          return done(error);
        }
        var messages = [];
        child.stderr.on('data', function(chunk) {
          messages.push(chunk.toString());
        });
        child.stdout.on('data', function(chunk) {
          messages.push(chunk.toString());
        });
        child.on('close', function(code) {
          if (code !== 0) {
            done(new Error('Task failed: ' + messages.join('')));
          } else {
            done(null, scratch);
          }
        });
      });
    });
  };
  
  
  /**
   * Clean up after running tests.
   * @param {string} scratch Path to scratch directory.
   * @param {function} done Callback.
   */
  exports.afterFixture = function(scratch, done) {
    var error;
    if (scratch) {
      try {
        wrench.rmdirSyncRecursive(scratch, false);
        wrench.rmdirSyncRecursive(tmpDir, false);
      } catch (err) {
        error = err;
      }
    }
    done(error);
  };
  
  
  /** @type {boolean} */
  chai.config.includeStack = true;
  
  
  /**
   * Chai's assert function configured to include stacks on failure.
   * @type {function}
   */
  exports.assert = chai.assert;
  