Last active
March 8, 2018 14:53
-
-
Save sveneisenschmidt/fcb295872416b99989975ceb91a031c0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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