UNPKG

testarmada-magellan

Version:

Massively parallel automated testing

475 lines (408 loc) 16.1 kB
#!/usr/bin/env node "use strict"; /*eslint-disable no-magic-numbers*/ /*eslint-disable global-require*/ /*eslint-disable complexity*/ // Note: this script assumes you run this from the same directory as where // the package.json that contains magellan resides. In addition // configuration must either be explicitly specified relative to that directory // or absolutely (or exist implicitly in the default location) const async = require("async"); const path = require("path"); const _ = require("lodash"); const clc = require("cli-color"); const analytics = require("./global_analytics"); const TestRunner = require("./test_runner"); const getTests = require("./get_tests"); const testFilters = require("./test_filter"); const WorkerAllocator = require("./worker_allocator"); const settings = require("./settings"); const profiles = require("./profiles"); const loadRelativeModule = require("./util/load_relative_module"); const processCleanup = require("./util/process_cleanup"); const constants = require("./constants"); const logger = require("./logger"); const BailStrategy = require("./strategies/bail"); const ResourceStrategy = require("./strategies/resource"); module.exports = { initialize() { }, version() { const project = require("../package.json"); logger.log(`Node Version: ${clc.greenBright(process.version)}`); logger.log(`Magellan Version: ${clc.greenBright(project.version)}`); logger.log("Use --help to list out all command options"); }, help(opts) { // Show help logger.log("Printing magellan command line arguments:"); require("./cli_help").help(opts); // exit process with exit code 0 const e = new Error("end of help"); e.code = constants.ERROR_CODE.HELP; return Promise.reject(e); }, loadFramework(opts) { if (opts.mockFramework) { settings.framework = opts.mockFramework; } // // Initialize Framework Plugins // ============================ // We translate old names like "mocha" to the new module names for the // respective plugins that provide support for those frameworks. Officially, // moving forward, we should specify our framework (in magellan.json) const legacyFrameworkNameTranslations = { "rowdy-mocha": "testarmada-magellan-mocha-plugin", "vanilla-mocha": "testarmada-magellan-mocha-plugin", "nightwatch": "testarmada-magellan-nightwatch-plugin" }; if (legacyFrameworkNameTranslations[settings.framework]) { settings.framework = legacyFrameworkNameTranslations[settings.framework]; } return new Promise((resolve, reject) => { let frameworkLoadException; try { // // HELP WANTED: If someone knows how to do this more gracefully, please contribute! // const frameworkModulePath = "./node_modules/" + settings.framework + "/index"; settings.testFramework = require(path.resolve(frameworkModulePath)); } catch (e) { frameworkLoadException = e; } let frameworkInitializationException; try { const pkg = require(path.join(process.cwd(), "package.json")); settings.pluginOptions = null; if (settings.testFramework && settings.testFramework.getPluginOptions && _.isFunction(settings.testFramework.getPluginOptions)) { // backward support settings.pluginOptions = settings.testFramework.getPluginOptions( { rootPackage: pkg, rootWorkingDirectory: process.cwd() }); } if (settings.framework === "testarmada-magellan-nightwatch-plugin") { if (opts.argv.debug) { // if the user has SET the debug flag, that means they want ALL the logs and NOT // filter out anything. we must let the util/ChildProcess.js know this information, // we will use the ENV to relay this information process.env.DEBUG = true; // this tells childProcess.js not to filter out any logs } // turn on nightwatch verbose logs so we can capture the nightwatch errors and warns // inside util/childProcess we filter out the verbose info logs of nightwatch opts.argv.debug = true; // this turns on nightwatch verbose logging } settings.testFramework.initialize(opts.argv, settings.pluginOptions); } catch (e) { frameworkInitializationException = e; } if (!settings.testFramework || frameworkLoadException || frameworkInitializationException) { logger.err("Could not start Magellan."); if (frameworkLoadException) { logger.err("Could not load the testing framework plugin:" + ` ${settings.framework}`); logger.err("Check and make sure your package.json includes module:" + ` ${settings.framework}`); logger.err(frameworkLoadException); } else /* istanbul ignore else */ if (frameworkInitializationException) { logger.err("Could not initialize the testing framework plugin:" + ` ${settings.framework}`); logger.err("This plugin was found and loaded, but an error" + " occurred during initialization:"); logger.err(frameworkInitializationException); } return reject("Couldn't start Magellan"); } logger.log("Loaded test framework from magellan.json: "); logger.log(` ${clc.greenBright(settings.framework)}`); return resolve(); }); }, loadExecutors(opts) { // Initialize Executor // ============================ let formalExecutors = ["testarmada-magellan-local-executor"]; // executors is as array from magellan.json by default if (opts.argv.executors) { if (_.isArray(opts.argv.executors)) { formalExecutors = opts.argv.executors; } else if (_.isString(opts.argv.executors)) { formalExecutors = [opts.argv.executors]; } else { logger.err("Executors only accepts string and array"); logger.warn("Setting executor to [testarmada-magellan-local-executor] by default"); } } else { logger.warn("No executor is configured"); logger.warn("Setting executor to [testarmada-magellan-local-executor] by default"); } settings.executors = formalExecutors; return new Promise((resolve, reject) => { // load executor const executorLoadExceptions = []; settings.testExecutors = {}; logger.log("Loaded test executors from magellan.json: "); _.forEach(settings.executors, (executor) => { try { const targetExecutor = require(executor); logger.log(" " + targetExecutor.name); // targetExecutor.validateConfig(opts.argv); settings.testExecutors[targetExecutor.shortName] = targetExecutor; } catch (e) { executorLoadExceptions.push(e); } }); if (executorLoadExceptions.length > 0) { // error happens while loading executor logger.err("There are errors in loading executors"); _.forEach(executorLoadExceptions, (exception) => { logger.err(exception.toString()); }); return reject("Couldn't start Magellan"); } return resolve(); }); }, loadStrategies(opts) { // // Initialize Strategy // ==================== if (!settings.strategies) { settings.strategies = {}; } return new Promise((resolve, reject) => { // Strategy - bail -------------------- try { settings.strategies.bail = new BailStrategy(opts.argv); if (settings.strategies.bail.MAX_TEST_ATTEMPTS) { // bail strategy can define its own test attempts settings.MAX_TEST_ATTEMPTS = settings.strategies.bail.MAX_TEST_ATTEMPTS; } logger.log("Enabled bail strategy: "); logger.log(` ${clc.greenBright(settings.strategies.bail.name)}:`); logger.log(` -> ${settings.strategies.bail.getDescription()}`); } catch (err) { logger.err(`Cannot load bail strategy due to ${err}`); logger.err("Please npm install and configure it in magellan.json"); return reject("Couldn't start Magellan"); } // Strategy - resource -------------------- try { settings.strategies.resource = new ResourceStrategy(opts.argv); logger.log("Enabled resource strategy: "); logger.log(` ${clc.greenBright(settings.strategies.resource.name)}:`); logger.log(` -> ${settings.strategies.resource.getDescription()}`); } catch (err) { logger.err(`Cannot load resource strategy due to ${err}`); logger.err("Please npm install and configure in magellan.json"); return reject("Couldn't start Magellan"); } return resolve(settings.strategies); }); }, loadListeners(opts) { // // Initialize Listeners // ==================== // // All listener/reporter types are optional and either activated through the existence // of configuration (i.e environment vars), CLI switches, or magellan.json config. let listeners = []; // // Setup / Teardown // ================ // // This is merely a listener like any other reporter, but with a developer-friendly name. if (opts.argv.setup_teardown) { // NOTE: loadRelativeModule can throw an error here if the setup module doesn't exist // FIXME: handle this error nicely instead of printing an ugly stack trace listeners.push(loadRelativeModule(opts.argv.setup_teardown)); } // // Load reporters from magellan.json // ================================= // // Reporters that conform to the reporter API and inherit from src/reporter // can be loaded in magellan.json through a reporters[] list. These can refer to // either npm modules defined in package.json or to paths relative to the current // working directory of the calling script or shell. if (opts.argv.reporters && _.isArray(opts.argv.reporters)) { // NOTE: loadRelativeModule can throw an error here if any of the reporter modules don't exist // FIXME: handle this error nicely instead of printing an ugly stack trace listeners = listeners.concat( opts.argv.reporters.map((reporterModule) => loadRelativeModule(reporterModule)) ); } // optional_reporters are modules we want to load only if found. If not found, we // still continue initializing Magellan and don't throw any errors or warnings if (opts.argv.optional_reporters && _.isArray(opts.argv.optional_reporters)) { listeners = listeners.concat( opts.argv.optional_reporters.map((reporterModule) => loadRelativeModule(reporterModule, true)) ); } // // Serial Mode Reporter (enabled with --serial) // if (opts.argv.serial) { const SerialReporter = require("./reporters/stdout/reporter"); listeners.push(new SerialReporter()); } // intiialize listeners return new Promise((resolve, reject) => { async.each(listeners, (listener, done) => { listener.initialize({ analytics, workerAmount: settings.MAX_WORKERS }) .then(() => done()) .catch((err) => done(err)); }, (err) => { if (err) { return reject(err); } else { return resolve(listeners); } }); }); }, loadTests(opts) { // // Find Tests, Start Worker Allocator // logger.log("Searching for tests..."); const tests = getTests(testFilters.detectFromCLI(opts.argv)); const testAmount = tests.length > 0 ? clc.greenBright(tests.length) : clc.yellowBright(tests.length); logger.log(`Total tests found: ${testAmount}`); if (_.isEmpty(tests)) { return Promise.reject(new Error("No tests found, please make sure" + " test filter is set correctly," + " or test path is configured correctly in nightwatch.json")); } // print out test amount and each test name _.map(tests, (t) => logger.log(` -> ${t.filename}`)); return Promise.resolve(tests); }, detectProfiles(opts) { return profiles.detectFromCLI({ argv: opts.argv, settings: opts.settings }); }, enableExecutors(opts) { // this is to allow magellan to double check profile that // is retrieved by --profile or --profiles const enabledExecutors = {}; return new Promise((resolve, reject) => { try { _.forEach( _.uniq(_.map(opts.profiles, (profile) => profile.executor)), (shortname) => { if (settings.testExecutors[shortname]) { settings.testExecutors[shortname].validateConfig({ isEnabled: true }); enabledExecutors[shortname] = settings.testExecutors[shortname]; } }); // for logging purpose if (!_.isEmpty(enabledExecutors)) { logger.log("Enabled executors:"); _.forEach(enabledExecutors, (sn) => logger.log(` ${clc.greenBright(sn.name)}`)); } return resolve(enabledExecutors); } catch (err) { return reject(err); } }); }, startTestSuite(opts) { return new Promise((resolve, reject) => { const workerAllocator = new WorkerAllocator(settings.MAX_WORKERS); Promise .all(_.map( opts.executors, (executor) => executor.setupRunner()) ) .then(() => opts.strategies.resource.holdSuiteResources({ workers: settings.MAX_WORKERS, profiles: opts.profiles, tests: opts.tests })) .then( () => workerAllocator.setup(), // if resource strategy decline the suite due to resource limit, // we fail test run (err) => reject(err) ) .then(() => new Promise((innerResolve, innerReject) => new TestRunner(opts.tests, { profiles: opts.profiles, executors: opts.executors, listeners: opts.listeners, strategies: opts.strategies, allocator: workerAllocator, onFinish: (failedTests) => { if (failedTests.length > 0) { const e = new Error("Test suite failed due to test failure"); e.code = constants.ERROR_CODE.TEST_FAILURE; return innerReject(e); } return innerResolve(); } }).run() ) ) // resource.releaseSuiteResources is guaranteed to execute .then( () => opts.strategies.resource.releaseSuiteResources({ workers: settings.MAX_WORKERS, profiles: opts.profiles, tests: opts.tests }), (err) => opts.strategies.resource.releaseSuiteResources({ workers: settings.MAX_WORKERS, profiles: opts.profiles, tests: opts.tests }).then(() => Promise.reject(err)) ) // workerAllocator.teardown is guaranteed to execute .then( () => workerAllocator.teardown(), (err) => workerAllocator.teardown(err) ) // executor.teardownRunner is guaranteed to execute .then( () => Promise .all(_.map(opts.executors, (executor) => executor.teardownRunner())), (err) => Promise .all(_.map(opts.executors, (executor) => executor.teardownRunner())) .then(() => Promise.reject(err)) /*eslint no-unused-vars: 0 */ .catch((otherErr) => Promise.reject(err)) ) // processCleanup is guaranteed to execute .then( () => processCleanup(), (err) => processCleanup(err) ) .then(() => resolve()) .catch((err) => reject(err)); }); } };