Skip to content

Instantly share code, notes, and snippets.

@janpaul123
Last active March 9, 2017 00:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save janpaul123/6bcb1f0f7c017f656b4d195d85f1c35d to your computer and use it in GitHub Desktop.
Save janpaul123/6bcb1f0f7c017f656b4d195d85f1c35d to your computer and use it in GitHub Desktop.
#!/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