Last active
March 9, 2017 00:06
-
-
Save janpaul123/6bcb1f0f7c017f656b4d195d85f1c35d 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
#!/usr/bin/env node | |
const commandLineArgs = require('command-line-args'); | |
const { existsSync, writeFileSync } = require('fs'); | |
const { execSync } = require('child_process'); | |
const jmerge = require('junit-merge/lib'); | |
const path = require('path'); | |
const syncRequest = require('sync-request'); | |
// Copyright Jan Paul Posma, 2017. Licensed under MIT license. | |
function log(message) { | |
console.log(`[monorepo-tester] ${message}`); | |
} | |
function exec(command, cwd) { | |
log(`[exec] running "${command}" in "/${cwd || ''}"...`); | |
const stdout = execSync(command, { cwd, stdio: 'inherit' }); | |
log(`[exec] finished "${command}"...`); | |
return stdout; | |
} | |
function copy(object) { | |
return JSON.parse(JSON.stringify(object)); | |
} | |
const options = commandLineArgs([ | |
{ name: 'files', type: String, multiple: true, defaultOption: true, defaultValue: [] }, | |
{ name: 'junitOutput', type: String }, | |
{ name: 'onlyProject', type: String, multiple: true }, | |
{ name: 'runParallelRunners', type: Boolean }, | |
{ name: 'setup', type: Boolean }, | |
]); | |
const config = require(path.join(process.cwd(), '.monorepo-tester.js')); | |
let projects = {}; | |
{ | |
if (options.onlyProject) { | |
// Iterate over config.projects to guarantee order. | |
Object.keys(config.projects).forEach((projectName) => { | |
if (options.onlyProject.contains(projectName)) { | |
projects[projectName] = config.projects[projectName]; | |
} | |
}); | |
log(`--onlyProject used, running for projects: ${Object.keys(projects).join(', ')}...`); | |
} else if (process.env.CI_PULL_REQUEST) { | |
const splitPRUrl = process.env.CI_PULL_REQUEST.split('/'); | |
const prNumber = parseInt(splitPRUrl[splitPRUrl.length - 1]); | |
log(`Pull request detected: #${prNumber}...`); | |
const prURL = `https://api.github.com/repos/${process.env.CIRCLE_PROJECT_USERNAME}/${process.env.CIRCLE_PROJECT_REPONAME}/pulls/1?access_token=${process.env.GITHUB_BOT_TOKEN}`; | |
const request = syncRequest('GET', prURL, { headers: { | |
Accept: 'application/vnd.github.v3+json', | |
'Content-type': 'application/json', | |
'User-Agent': 'monorepo-tester', | |
} }); | |
const { commits } = JSON.parse(request.body); | |
const filesChanged = exec(`git diff --name-only HEAD~${commits}`).split('\n'); | |
for (const fileName of filesChanged) { | |
const projectName = fileName.split(path.sep)[0]; | |
if (!config.projects[projectName]) { | |
projects = copy(config.projects); | |
log(`Detected non-project file change in "${fileName}", using all projects...`); | |
break; | |
} | |
if (!projects[projectName]) { | |
projects[projectName] = config.projects[projectName]; | |
} | |
} | |
log(`Using projects: ${Object.keys(projects).join(', ')}...`); | |
} else { | |
projects = copy(config.projects); | |
log(`Using all projects: ${Object.keys(projects).join(', ')}...`); | |
} | |
} | |
{ | |
if (options.setup) { | |
log('Running setup...'); | |
Object.keys(projects).forEach((projectName) => { | |
const project = projects[projectName]; | |
project.setupCommands.forEach((setupCommand, index) => { | |
log(`[${projectName}] Running setupCommand ${index}...`); | |
exec(setupCommand, projectName); | |
}); | |
}); | |
process.exit(); | |
} else { | |
log('Skipping setup...'); | |
} | |
} | |
{ | |
if (process.env.CIRCLE_NODE_TOTAL > 1 && process.env.CIRCLE_NODE_INDEX !== undefined) { | |
log(`Parallelism detected; pruning projects that are not on node ${process.env.CIRCLE_NODE_INDEX}...`); | |
let nodeIndex = 0; | |
const projectNames = Object.keys(projects); | |
projectNames.forEach((projectName) => { | |
if (projects[projectName].fileRunner) { | |
log(`[${projectName}] contains fileRunner, keeping...`); | |
} else { | |
if (nodeIndex === parseInt(process.env.CIRCLE_NODE_INDEX)) { | |
log(`[${projectName}] running on node ${nodeIndex}, keeping...`); | |
} else { | |
log(`[${projectName}] running on node ${nodeIndex}, removing...`); | |
delete projects[projectName]; | |
} | |
nodeIndex = (nodeIndex + 1) % process.env.CIRCLE_NODE_TOTAL; | |
} | |
}); | |
log(`Running these projects on this node: ${Object.keys(projects).join(', ')}...`); | |
} | |
} | |
const internalPathsByProject = {}; | |
{ | |
options.files.forEach((fileName) => { | |
const splitFileName = fileName.split(path.sep); | |
const projectName = splitFileName[0]; | |
const internalPath = splitFileName.slice(1).join(path.sep); | |
if (!internalPath) throw new Error(`Top-level paths not allowed: "${fileName}"`); | |
if (!config.projects[projectName]) throw new Error(`Unknown project: "${projectName}"`); | |
internalPathsByProject[projectName] = internalPathsByProject[projectName] || [] | |
internalPathsByProject[projectName].push(internalPath); | |
}); | |
} | |
{ | |
log('Running tests...'); | |
Object.keys(projects).forEach((projectName) => { | |
const project = projects[projectName]; | |
const files = internalPathsByProject[projectName] || []; | |
if (project.parallelRunners && project.fileRunner) { | |
throw new Error(`[${projectName}] Cannot have both parallelRunners and fileRunner for same project`); | |
} | |
if (project.parallelRunners) { | |
if (options.runParallelRunners) { | |
project.parallelRunners.forEach((parallelRunner, index) => { | |
log(`[${projectName}] Running parallelRunner ${index}...`); | |
exec(parallelRunner, projectName); | |
}); | |
} else { | |
log(`[${projectName}] skipping parallelRunners...`); | |
} | |
} | |
if (project.fileRunner && files.length > 0) { | |
log(`[${projectName}] Running fileRunner...`); | |
exec(`${project.fileRunner} ${files.join(' ')}`, projectName); | |
} | |
}); | |
} | |
{ | |
if (!options.junitOutput) { | |
log('Skipping JUnit XML merging (no --junitOutput given)...'); | |
} else { | |
log('Merging JUnit XML outputs...'); | |
const junitTestSuites = []; | |
Object.keys(projects).forEach((projectName) => { | |
const project = projects[projectName]; | |
if (project.junitOutput) { | |
const file = path.join(projectName, project.junitOutput); | |
if (!existsSync(file)) { | |
log(`[${projectName}] "${file}" not found, skipping...`); | |
return; | |
} | |
log(`[${projectName}] Parsing "${file}"...`); | |
jmerge.getTestsuites(file, (error, result) => { | |
if (error) { | |
log(`Error merging JUnit XML outputs: ${error}`); | |
process.exit(1); | |
} else { | |
result = result.replace(/ file="(\.\/)?/g, ` file="${projectName}/`); | |
result = result.replace(/ classname="/g, ` classname="${projectName}.`); | |
result = result.replace(/ name="/g, ` name="[${projectName}] `); | |
junitTestSuites.push(result); | |
} | |
}) | |
} | |
}); | |
const merged = `<?xml version="1.0"?>\n<testsuites>\n${junitTestSuites.join('\n')}</testsuites>\n`; | |
exec(`mkdir -p ${path.dirname(options.junitOutput)}`); | |
writeFileSync(options.junitOutput, merged); | |
log(`Written to ${options.junitOutput}...`); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment