platform-helpers/runner-helper.js

'use strict';

const Launcher = require('@wdio/cli').default;
const dotenv = require('dotenv');
const fse = require('fs-extra');
const rootRequire = require('rpcm-root-require');
const LoggerWrapper = rootRequire('/platform-helpers/log4js-wrapper');
const RunnerOptionsValidator = rootRequire('/platform-helpers/runner-options-validator');
const Utils = rootRequire('/platform-helpers/utils');
const RunnerCapabilitiesBuilder = rootRequire('/platform-helpers/runner-capabilities-builder');
const RunnerTestsListBuilder = rootRequire('platform-helpers/runner-tests-list-builder');
const DockerHelper = rootRequire('platform-helpers/docker-helper');
const ReportGenerator = rootRequire('/platform-helpers/report-generator.js');
const RetryHelper = rootRequire('/platform-helpers/retry-helper.js');
const Constants = rootRequire('/platform-helpers/constants');
const logger = LoggerWrapper.getLogger();
rootRequire('/platform-helpers/string-extensions');

const checkReadyFolder = () => {
  fse.ensureDirSync('{0}/ready'.format(rootRequire.rootPath));
};
const checkTmpFolder = () => {
  // make sure the directory to store the copy of the tests (to include in the report folder) is present
  fse.ensureDirSync('{0}/tmp/tests'.format(rootRequire.rootPath));
  // make sure the directory to store the copy of the tests (to include in the report folder) is present
  fse.ensureDirSync('{0}/tmp/tests/helpers'.format(rootRequire.rootPath));
};

/**
 * Main process
 */
class RunnerHelper {
  static run = async (opts) => {
    logger.debug('runner-helper.js --> RunnerHelper.run called with the following options: {0}{1}'.format(Utils.getNewLine(), JSON.stringify(opts, null, 2)));

    logger.trace('runner-helper.js --> processing DockerHelper.checkDockerLogFiles();');
    DockerHelper.checkDockerLogFiles();
    logger.trace('runner-helper.js --> processing checkReadyFolder();');
    checkReadyFolder();
    logger.trace('runner-helper.js --> processing checkTmpFolder();');
    checkTmpFolder();
    logger.trace('runner-helper.js --> processing dotenv.config();');
    dotenv.config();

    logger.trace('runner-helper.js --> processing RunnerOptionsValidator.validate(opts);');
    RunnerOptionsValidator.validate(opts);
    logger.trace('runner-helper.js --> processing RunnerCapabilitiesBuilder.build(opts);');
    RunnerCapabilitiesBuilder.build(opts);
    logger.trace('runner-helper.js --> processing RunnerTestsListBuilder.build(opts);');
    const testsList = RunnerTestsListBuilder.build(opts);
    logger.debug('runner-helper.js --> tests list: {0}{1}'.format(Utils.getNewLine(), JSON.stringify(testsList, null, 2)));
    logger.info('runner-helper.js --> running {0} file/browser combination(s).'.format(testsList.length));

    /**
     * saucelabs run
     */
    logger.trace('runner-helper.js --> processing the test overridable options object');
    const overridableOptions = {
      services: [['selenium-standalone', { skipSeleniumInstall: false, drivers: { chrome: true } }]]
    };
    if (opts.runCrossBrowser || opts.runRemotely || opts.runFullCrossBrowser) {
      overridableOptions.services = [['sauce']];
      overridableOptions.connectionRetryTimeout = 60000;
      overridableOptions.user = process.env.SAUCE_USERNAME;
      overridableOptions.key = process.env.SAUCE_ACCESS_KEY;
      overridableOptions.capabilities = [];
      overridableOptions.bail = 0;
      overridableOptions.coloredLogs = true;
      overridableOptions.screenshotPath = './errorShots/';
      overridableOptions.maxInstances = 1;
      overridableOptions.host = 'https://ondemand.{0}.saucelabs.com'.format(process.env.SAUCE_REGION === 'eu' ? 'eu-central-1' : 'us-west-1');
      overridableOptions.region = process.env.SAUCE_REGION;
    }
    logger.debug('runner-helper.js --> test overridable options object: {0}{1}'.format(Utils.getNewLine(), JSON.stringify(overridableOptions, null, 2)));

    logger.trace('runner-helper.js --> instantiating ReportGenerator');
    const reportGenerator = new ReportGenerator({
      reportTitle: opts.summaryReportTitle,
      reportContext: opts.reportContext,
      summaryContext: opts.summaryContext,
      showInBrowser: opts.showInBrowser,
      options: opts
    });
    logger.trace('runner-helper.js --> clearing filesystem from old report files');
    reportGenerator.clean();

    logger.trace('runner-helper.js --> stopping any running Docker containers...');
    DockerHelper.stopSync();

    logger.trace('runner-helper.js --> looping throughout the test list and processing each!');
    for (let index = 0; index < testsList.length; index++) {
      const testSpec = testsList[index];
      logger.trace('runner-helper.js --> test {0} running with specification: {1}{2}!'.format(index + 1, Utils.getNewLine(), JSON.stringify(testSpec, null, 2)));

      const capabilitiesUnref = JSON.parse(JSON.stringify(testSpec.capability));
      const retryController = new RetryHelper(capabilitiesUnref['teal:runRetries']);
      const useSauceConnect = capabilitiesUnref['teal:enableSauceConnect'];
      const sauceConnectOverrideString = opts.enableProxy === useSauceConnect
        ? ''
        : '(originally set to {0})'.format(opts.enableProxy ? Constants.ACTIVE : Constants.OFF);

      logger.info(
        ('runner-helper.js --> {1}' +
        '{15}Run {2} of {3}{1}' +
        '{15}File               : {4}{0}' +
        '{15}Browser            : {5}{1}' +
        '{15}Trace              : {6}{0}' +
        '{15}Proxy              : {7}{0}' +
        '{15}Sauce Connect      : {8} {9}{1}' +
        '{15}Remote Run         : {10}{0}' +
        '{15}Cross Browser      : {11}{0}' +
        '{15}So Many Browsers   : {12}{1}' +
        '{15}Retries Allowed    : {13}{0}' +
        '{15}Show Browser Report: {16}{0}' +
        '{0}{15}{14}{0}')
          .format(
            Utils.getNewLine(),
            Utils.getNewLine(2),
            index + 1,
            testsList.length,
            testSpec.spec,
            capabilitiesUnref.browserName,
            opts.enableTrace ? Constants.ACTIVE : Constants.OFF,
            opts.enableProxy ? Constants.ACTIVE : Constants.OFF,
            useSauceConnect ? Constants.ACTIVE : Constants.OFF,
            sauceConnectOverrideString,
            opts.runRemotely ? Constants.ACTIVE : Constants.OFF,
            opts.runCrossBrowser ? Constants.ACTIVE : Constants.OFF,
            opts.runFullCrossBrowser ? Constants.ACTIVE : Constants.OFF,
            capabilitiesUnref['teal:runRetries'],
            opts.enableProxy ? '' : '  Skipping Sauce Connect setup because Proxy is inactive.',
            Utils.getRepeatedCharacters(' ', 10),
            opts.showInBrowser ? Constants.ACTIVE : Constants.OFF)
      );

      try {
        overridableOptions.specs = [testSpec.spec];
        overridableOptions.capabilities = [capabilitiesUnref];
        overridableOptions.sauceConnect = !!testSpec.sauceConnect;

        logger.trace('runner-helper.js --> executing wdio test run with options: {0}{1}'.format(Utils.getNewLine(), JSON.stringify(overridableOptions, null, 2)));
        let testRunResult = await new Launcher('{0}/wdio.conf.js'.format(rootRequire.rootPath), overridableOptions).run();
        // 1 means there was at least one failure (likely a Sauce Labs or other infrastructure error) - retry if appropriate
        while (testRunResult !== 0 && retryController.shouldRetry()) {
          // spin down the docker images, if any - don't wait for this to complete before continuing
          if (useSauceConnect) {
            logger.trace('runner-helper.js --> RETRYING... stopping any running Docker containers...');
            DockerHelper.stopSync();
          }
          logger.trace('runner-helper.js --> RETRYING...{0}retry {1} of {2} maximum{0}'.format(Utils.getNewLine(), retryController.incrementRetryCounter(), retryController.getMaxAttempts()));
          testRunResult = await new Launcher('{0}/wdio.conf.js'.format(rootRequire.rootPath), overridableOptions).run();
        }
        logger.trace('runner-helper.js --> test run finished');
        logger.trace('runner-helper.js --> moving files between runs for report generation');
        reportGenerator.moveFilesBetweenRuns(index);
      } catch (error) {
        if (capabilitiesUnref['teal:enableSauceConnect']) {
          logger.trace('runner-helper.js --> Stopping any running Docker containers...');
          DockerHelper.stopAsync();
        }
        logger.error('{1}{1}{2}runner.js --> CRITICAL Fail running test!{3}{2}{4}{1}{0}'
          .format(Utils.getRepeatedCharacters('*', 100), Utils.getNewLine(2), Utils.getRepeatedCharacters(' ', 10), Utils.getNewLine(), error));
      }
    }

    logger.trace('runner-helper.js --> generating report');
    reportGenerator.createReport({
      config: {},
      capabilities: opts.capabilities,
      results: {},
      'teal:fullRunList': JSON.stringify(testsList)
    });

    logger.trace('runner-helper.js --> Stopping any running Docker containers...');
    DockerHelper.stopSync();

    logger.info('runner-helper.js --> Summary report successfully generated!');
    logger.debug('runner-helper.js --> RunnerHelper.run finished');
  };
}

module.exports = RunnerHelper;