index.js 8.18 KB
'use strict';

function _chalk() {
  const data = _interopRequireDefault(require('chalk'));

  _chalk = function () {
    return data;
  };

  return data;
}

function _emittery() {
  const data = _interopRequireDefault(require('emittery'));

  _emittery = function () {
    return data;
  };

  return data;
}

function _exit() {
  const data = _interopRequireDefault(require('exit'));

  _exit = function () {
    return data;
  };

  return data;
}

function _throat() {
  const data = _interopRequireDefault(require('throat'));

  _throat = function () {
    return data;
  };

  return data;
}

function _jestUtil() {
  const data = require('jest-util');

  _jestUtil = function () {
    return data;
  };

  return data;
}

function _jestWorker() {
  const data = _interopRequireDefault(require('jest-worker'));

  _jestWorker = function () {
    return data;
  };

  return data;
}

var _runTest = _interopRequireDefault(require('./runTest'));

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : {default: obj};
}

function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }
  return obj;
}

const TEST_WORKER_PATH = require.resolve('./testWorker');

class TestRunner {
  constructor(globalConfig, context) {
    _defineProperty(this, '_globalConfig', void 0);

    _defineProperty(this, '_context', void 0);

    _defineProperty(this, 'eventEmitter', new (_emittery().default.Typed)());

    _defineProperty(
      this,
      '__PRIVATE_UNSTABLE_API_supportsEventEmitters__',
      true
    );

    _defineProperty(this, 'isSerial', void 0);

    _defineProperty(this, 'on', this.eventEmitter.on.bind(this.eventEmitter));

    this._globalConfig = globalConfig;
    this._context = context || {};
  }

  async runTests(tests, watcher, onStart, onResult, onFailure, options) {
    return await (options.serial
      ? this._createInBandTestRun(tests, watcher, onStart, onResult, onFailure)
      : this._createParallelTestRun(
          tests,
          watcher,
          onStart,
          onResult,
          onFailure
        ));
  }

  async _createInBandTestRun(tests, watcher, onStart, onResult, onFailure) {
    process.env.JEST_WORKER_ID = '1';
    const mutex = (0, _throat().default)(1);
    return tests.reduce(
      (promise, test) =>
        mutex(() =>
          promise
            .then(async () => {
              if (watcher.isInterrupted()) {
                throw new CancelRun();
              }

              let sendMessageToJest; // Remove `if(onStart)` in Jest 27

              if (onStart) {
                await onStart(test);
                return (0, _runTest.default)(
                  test.path,
                  this._globalConfig,
                  test.context.config,
                  test.context.resolver,
                  this._context,
                  undefined
                );
              } else {
                // `deepCyclicCopy` used here to avoid mem-leak
                sendMessageToJest = (eventName, args) =>
                  this.eventEmitter.emit(
                    eventName,
                    (0, _jestUtil().deepCyclicCopy)(args, {
                      keepPrototype: false
                    })
                  );

                await this.eventEmitter.emit('test-file-start', [test]);
                return (0, _runTest.default)(
                  test.path,
                  this._globalConfig,
                  test.context.config,
                  test.context.resolver,
                  this._context,
                  sendMessageToJest
                );
              }
            })
            .then(result => {
              if (onResult) {
                return onResult(test, result);
              } else {
                return this.eventEmitter.emit('test-file-success', [
                  test,
                  result
                ]);
              }
            })
            .catch(err => {
              if (onFailure) {
                return onFailure(test, err);
              } else {
                return this.eventEmitter.emit('test-file-failure', [test, err]);
              }
            })
        ),
      Promise.resolve()
    );
  }

  async _createParallelTestRun(tests, watcher, onStart, onResult, onFailure) {
    const resolvers = new Map();

    for (const test of tests) {
      if (!resolvers.has(test.context.config.name)) {
        resolvers.set(test.context.config.name, {
          config: test.context.config,
          serializableModuleMap: test.context.moduleMap.toJSON()
        });
      }
    }

    const worker = new (_jestWorker().default)(TEST_WORKER_PATH, {
      exposedMethods: ['worker'],
      forkOptions: {
        stdio: 'pipe'
      },
      maxRetries: 3,
      numWorkers: this._globalConfig.maxWorkers,
      setupArgs: [
        {
          serializableResolvers: Array.from(resolvers.values())
        }
      ]
    });
    if (worker.getStdout()) worker.getStdout().pipe(process.stdout);
    if (worker.getStderr()) worker.getStderr().pipe(process.stderr);
    const mutex = (0, _throat().default)(this._globalConfig.maxWorkers); // Send test suites to workers continuously instead of all at once to track
    // the start time of individual tests.

    const runTestInWorker = test =>
      mutex(async () => {
        if (watcher.isInterrupted()) {
          return Promise.reject();
        } // Remove `if(onStart)` in Jest 27

        if (onStart) {
          await onStart(test);
        } else {
          await this.eventEmitter.emit('test-file-start', [test]);
        }

        const promise = worker.worker({
          config: test.context.config,
          context: {
            ...this._context,
            changedFiles:
              this._context.changedFiles &&
              Array.from(this._context.changedFiles),
            sourcesRelatedToTestsInChangedFiles:
              this._context.sourcesRelatedToTestsInChangedFiles &&
              Array.from(this._context.sourcesRelatedToTestsInChangedFiles)
          },
          globalConfig: this._globalConfig,
          path: test.path
        });

        if (promise.UNSTABLE_onCustomMessage) {
          // TODO: Get appropriate type for `onCustomMessage`
          promise.UNSTABLE_onCustomMessage(([event, payload]) => {
            this.eventEmitter.emit(event, payload);
          });
        }

        return promise;
      });

    const onError = async (err, test) => {
      // Remove `if(onFailure)` in Jest 27
      if (onFailure) {
        await onFailure(test, err);
      } else {
        await this.eventEmitter.emit('test-file-failure', [test, err]);
      }

      if (err.type === 'ProcessTerminatedError') {
        console.error(
          'A worker process has quit unexpectedly! ' +
            'Most likely this is an initialization error.'
        );
        (0, _exit().default)(1);
      }
    };

    const onInterrupt = new Promise((_, reject) => {
      watcher.on('change', state => {
        if (state.interrupted) {
          reject(new CancelRun());
        }
      });
    });
    const runAllTests = Promise.all(
      tests.map(test =>
        runTestInWorker(test)
          .then(result => {
            if (onResult) {
              return onResult(test, result);
            } else {
              return this.eventEmitter.emit('test-file-success', [
                test,
                result
              ]);
            }
          })
          .catch(error => onError(error, test))
      )
    );

    const cleanup = async () => {
      const {forceExited} = await worker.end();

      if (forceExited) {
        console.error(
          _chalk().default.yellow(
            'A worker process has failed to exit gracefully and has been force exited. ' +
              'This is likely caused by tests leaking due to improper teardown. ' +
              'Try running with --detectOpenHandles to find leaks.'
          )
        );
      }
    };

    return Promise.race([runAllTests, onInterrupt]).then(cleanup, cleanup);
  }
}

class CancelRun extends Error {
  constructor(message) {
    super(message);
    this.name = 'CancelRun';
  }
}

module.exports = TestRunner;