Skip to content

Instantly share code, notes, and snippets.

@sveneisenschmidt
Last active March 8, 2018 14:53
Show Gist options
  • Save sveneisenschmidt/fcb295872416b99989975ceb91a031c0 to your computer and use it in GitHub Desktop.
Save sveneisenschmidt/fcb295872416b99989975ceb91a031c0 to your computer and use it in GitHub Desktop.
const glob = require('glob');
const path = require('path');
const fs = require('fs');
const splitFiles = (list, size) => {
let sets = [];
let chunks = list.length / size;
let i = 0;
while (i < chunks) {
sets[i] = list.splice(0, size);
i++;
}
return sets;
};
const findFiles = (pattern) => {
let files = [];
glob.sync(pattern).forEach((file) => {
files.push(path.resolve(file));
});
return files;
};
const flattenFiles = (list) => {
let pattern = list.join(',');
return pattern.indexOf(',') > -1 ? `{${pattern}}` : pattern;
};
const grepFiles = (file, grep) => {
const contents = fs.readFileSync(file);
const pattern = new RegExp(`((Scenario|Feature)\(.*${grep}.*\))`, 'g'); // <- How future proof/solid is this?
return !!pattern.exec(contents);
};
const createChunks = (config, pattern) => {
let files = findFiles(pattern).filter((file) => {
return !!config.grep ? grepFiles(file, config.grep) : true;
});
const size = Math.ceil(files.length/config.chunks);
let chunks = splitFiles(files, size);
let chunkConfig = { ...config };
delete chunkConfig.chunks;
return chunks.map((chunkFiles) => {
return { ...chunkConfig, tests: flattenFiles(chunkFiles) }
});
}
module.exports = {
createChunks
};
let config = {
tests: 'features/*_test.js',
timeout: 10000,
output: './output',
helpers: {
WebDriverIO: {
url: 'https://www.google.com',
browser: 'chrome',
host: 'selenium-hub',
}
},
include: {
I: './support/steps_file.js'
},
bootstrap: false,
mocha: {}
}
let multiple = {
my_parallel_suite: {
chunks: 2,
browsers: ['chrome']
}
};
exports.config = {...config, multiple };
const getConfig = require('./utils').getConfig;
const getTestRoot = require('./utils').getTestRoot;
const fail = require('./utils').fail;
const deepMerge = require('./utils').deepMerge;
const Codecept = require('../codecept');
const Config = require('../config');
const fork = require('child_process').fork;
const output = require('../output');
const path = require('path');
const runHook = require('../hooks');
const event = require('../event');
const suite = require('../suite');
const runner = path.join(__dirname, '/../../bin/codecept');
let config;
const childOpts = {};
const copyOptions = ['steps', 'reporter', 'verbose', 'config', 'reporter-options', 'grep', 'fgrep', 'debug'];
// codeceptjs run:multiple smoke:chrome regression:firefox - will launch smoke suite in chrome and regression in firefox
// codeceptjs run:multiple smoke:chrome regression - will launch smoke suite in chrome and regression in firefox and chrome
// codeceptjs run:multiple all - will launch all suites
// codeceptjs run:multiple smoke regression'
// Changelog:
// - added: suite.prepareSuites - creates unified suites coniguration based on preferredSuites
// - added: suite.prepareSuitesChunks - expands configuration by (n) chunks if chunks are configured
// - added: suite.prepareSuitesBrowsers - expands configuration by (n) browsers if browsers are configured
// - added: suite.filterSuitesBrowsers - filters configuration by requested browsers set via preferredSuites
// - updated: runSuite
// - browser argument is removed, it is now retrieved through a unified browser config
// - forking over multiple browsers is removed, it became obsolete as soon as we prepared
// and expanded the whole configuration beforehand, including chunks and browsers
let suiteId = 1;
let subprocessCount = 0;
let totalSubprocessCount = 0;
let processesDone;
module.exports = function (preferredSuites, options) {
// registering options globally to use in config
process.profile = options.profile;
const configFile = options.config;
let codecept;
const testRoot = getTestRoot(configFile);
config = getConfig(configFile);
// copy opts to run
Object.keys(options)
.filter(key => copyOptions.indexOf(key) > -1)
.forEach((key) => {
childOpts[key] = options[key];
});
if (!config.multiple) {
fail('Multiple suites not configured, add "multiple": { /../ } section to config');
}
preferredSuites = options.all ? Object.keys(config.multiple) : preferredSuites;
if (!preferredSuites.length) {
fail('No suites provided. Use --all option to run all configured suites');
}
const done = () => event.emit(event.multiple.before, null);
runHook(config.bootstrapAll, done, 'multiple.bootstrap');
const childProcessesPromise = new Promise((resolve, reject) => {
processesDone = resolve;
});
const forksToExecute = [];
const suites = suite.prepareSuites(preferredSuites, suites, config);
Object.entries(suites).forEach((suite) => {
let [suiteName, suiteConfig] = suite;
forksToExecute.push(runSuite(suiteName, suiteConfig));
});
// Execute all forks
totalSubprocessCount = forksToExecute.length;
forksToExecute.forEach(currentForkFunc => currentForkFunc.call(this));
return childProcessesPromise.then(() => {
// fire hook
const done = () => event.emit(event.multiple.after, null);
runHook(config.teardownAll, done, 'multiple.teardown');
});
};
function runSuite(suite, suiteConf) {
// clone config
let overriddenConfig = Object.assign({}, config);
// get configuration
const browserConfig = suiteConf.browser;
const browserName = browserConfig.browser;
for (const key in browserConfig) {
overriddenConfig.helpers = replaceValue(overriddenConfig.helpers, key, browserConfig[key]);
}
let outputDir = `${suite}_`;
if (browserConfig.outputName) {
outputDir += typeof browserConfig.outputName === 'function' ? browserConfig.outputName() : browserConfig.outputName;
} else {
outputDir += JSON.stringify(browserConfig).replace(/[^\d\w]+/g, '_');
}
outputDir += `_${suiteId}`;
// tweaking default output directories and for mochawesome
overriddenConfig = replaceValue(overriddenConfig, 'output', path.join(config.output, outputDir));
overriddenConfig = replaceValue(overriddenConfig, 'reportDir', path.join(config.output, outputDir));
overriddenConfig = replaceValue(overriddenConfig, 'mochaFile', path.join(config.output, outputDir, `${browserName}_report.xml`));
// override tests configuration
if (overriddenConfig.tests) {
overriddenConfig.tests = suiteConf.tests;
}
// override grep param and collect all params
const params = ['run',
'--child', `${suiteId++}.${suite}`,
'--override', JSON.stringify(overriddenConfig),
];
Object.keys(childOpts).forEach((key) => {
params.push(`--${key}`);
if (childOpts[key] !== true) params.push(childOpts[key]);
});
if (suiteConf.grep) {
params.push('--grep');
params.push(suiteConf.grep);
}
const onProcessEnd = (errorCode) => {
if (errorCode !== 0) {
process.exitCode = errorCode;
}
subprocessCount += 1;
if (subprocessCount === totalSubprocessCount) {
processesDone();
}
return errorCode;
};
// Return function of fork for later execution
return () => fork(runner, params, { stdio: [0, 1, 2, 'ipc'] })
.on('exit', (code) => {
return onProcessEnd(code);
})
.on('error', (err) => {
return onProcessEnd(1);
});
}
/**
* search key in object recursive and replace value in it
*/
function replaceValue(obj, key, value) {
if (!obj) return;
if (obj instanceof Array) {
for (const i in obj) {
replaceValue(obj[i], key, value);
}
}
if (obj[key]) obj[key] = value;
if (typeof obj === 'object' && obj !== null) {
const children = Object.keys(obj);
for (let childIndex = 0; childIndex < children.length; childIndex++) {
replaceValue(obj[children[childIndex]], key, value);
}
}
return obj;
}
const chunk = require('./chunk');
/**
* @param array preferredSuites Format: ['suite1', 'suite2', ...]
* @param object suites Format: {}
* @param object config
*
*
*/
const prepareSuites = (preferredSuites, suites, config) => {
// reset suites
suites = {};
preferredSuites.forEach((suite) => {
const [suiteName] = suite.split(':');
const suiteConfig = config.multiple[suiteName];
if (!suiteConfig) {
throw new Error(`Suite ${suiteName} was not configured in "multiple" section of config`);
}
suites[suiteName] = suiteConfig;
});
suites = suite.prepareSuitesChunks(preferredSuites, suites, config);
suites = suite.prepareSuitesBrowsers(preferredSuites, suites, config);
suites = suite.filterSuitesBrowsers(preferredSuites, suites, config);
return suites;
};
/**
*
* @param array preferredSuites Format: ['suite1', 'suite2', ...]
* @param object suites Format: {suite1: {}, suite2: {}, ... }
* @param object config
*
* Expands suites by their (n) via the `chunks` property to multiple suites.
*/
const prepareSuitesChunks = (preferredSuites, suites, config) => {
Object.entries(suites).forEach((suite) => {
let [suiteName, suiteConfig] = suite;
let pattern = suite.tests || config.tests;
if(!suiteConfig.chunks || !Number.isFinite(suiteConfig.chunks) || !pattern) {
return;
}
delete suites[suiteName];
chunk.createChunks(suiteConfig, pattern).forEach((suiteChunkConfig, index) => {
suites[`${suiteName}-${index+1}`] = suiteChunkConfig;
});
});
return suites;
};
/**
*
* @param array preferredSuites Format: ['suite1', 'suite2', ...]
* @param object suites Format: {suite1: {}, suite2: {}, ... }
* @param object config
*
* Expands browser declared via `browsers` property to multiple
* suites that all have one single `browser` property and omits
* the `browsers` property.
*/
const prepareSuitesBrowsers = (preferredSuites, suites, config) => {
Object.entries(suites).forEach((suite) => {
let [suiteName, suiteConfig] = suite;
delete suites[suiteName];
suiteConfig.browsers.forEach((browser) => {
let browserConfig = typeof browser === 'string' ? { browser } : browser;
let suiteBrowserConfig = { ...suiteConfig, browser: browserConfig };
delete suiteBrowserConfig.browsers;
suites[`${suiteName}-${browser}`] = suiteBrowserConfig;
});
});
return suites;
};
/**
*
* @param array preferredSuites Format: ['suite1', 'suite2', ...]
* @param object suites Format: {suite1: {}, suite2: {}, ... }
* @param object config
*
* Filters all suites by their `browser` property. The propoerty `browsers` is ignored.
* If value of property `browser` does not match the preferred `browser` in conjugation
* with the preferredSuiteName then the suite is removed from configuration.
*/
const filterSuitesBrowsers = (preferredSuites, suites, config) => {
preferredSuites.forEach((preferredSuite) => {
let [preferredSuiteName, preferredSuiteBrowserName] = preferredSuite.split(':');
if (preferredSuiteBrowserName) {
Object.entries(suites).forEach((suite) => {
let [suiteName, suiteConfig] = suite;
let suiteBrowserName = suiteConfig.browser.browser;
if (preferredSuiteName !== suiteName && suiteBrowserName !== preferredSuiteBrowserName) {
delete suites[suiteName];
}
});
}
});
return suites;
};
module.exports = {
prepareSuites
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment