automatically split jsg nodes

refs DE-123

Test Plan:
1. Ensure that the number of tests run per JS job is the same
2. Ensure that test reporting works correctly

flag = none

Change-Id: If25f6f4fe6af6d8982b5aed62c61c65b1d4daca9
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/242341
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Ryan Norton <rnorton@instructure.com>
QA-Review: Aaron Ogata <aogata@instructure.com>
Product-Review: Aaron Ogata <aogata@instructure.com>
This commit is contained in:
Aaron Ogata 2020-07-09 15:43:39 -07:00
parent 321238cfb3
commit ff78f2bf0b
3 changed files with 86 additions and 14 deletions

View File

@ -20,11 +20,33 @@
library "canvas-builds-library"
def DEFAULT_NODE_COUNT = 1
def JSG_NODE_COUNT = 3
def copyFiles(dockerName, dockerPath, hostPath) {
sh "mkdir -vp ./$hostPath"
sh "docker cp \$(docker ps -qa -f name=$dockerName):/usr/src/app/$dockerPath ./$hostPath"
}
def makeKarmaStage(group, ciNode, ciTotal) {
return {
withEnv([
"CI_NODE_INDEX=${ciNode}",
"CI_NODE_TOTAL=${ciTotal}",
"CONTAINER_NAME=tests-karma-${group}-${ciNode}",
"JSPEC_GROUP=${group}"
]) {
try {
credentials.withSentryCredentials {
sh 'build/new-jenkins/js/tests-karma.sh'
}
} finally {
copyFiles(env.CONTAINER_NAME, 'coverage-js', "./tmp/${env.CONTAINER_NAME}")
}
}
}
}
pipeline {
agent { label 'canvas-docker' }
options { ansiColor('xterm') }
@ -86,18 +108,12 @@ pipeline {
sh 'build/new-jenkins/js/tests-quizzes.sh'
}
['coffee', 'jsa', 'jsg', 'jsh'].each { group ->
tests["Karma - Spec Group - ${group}"] = {
withEnv(["CONTAINER_NAME=tests-karma-${group}", "JSPEC_GROUP=${group}"]) {
try {
credentials.withSentryCredentials {
sh 'build/new-jenkins/js/tests-karma.sh'
}
} finally {
copyFiles(env.CONTAINER_NAME, 'coverage-js', "./tmp/${env.CONTAINER_NAME}")
}
}
for(int i = 0; i < JSG_NODE_COUNT; i++) {
tests["Karma - Spec Group - jsg${i}"] = makeKarmaStage('jsg', i, JSG_NODE_COUNT)
}
['coffee', 'jsa', 'jsh'].each { group ->
tests["Karma - Spec Group - ${group}"] = makeKarmaStage(group, 0, DEFAULT_NODE_COUNT)
}
}

View File

@ -8,4 +8,4 @@ if [[ "${COVERAGE:-}" == "1" ]]; then
sentry="-e SENTRY_URL -e SENTRY_DSN -e SENTRY_ORG -e SENTRY_PROJECT -e SENTRY_AUTH_TOKEN -e DEPRECATION_SENTRY_DSN"
fi
docker-compose run --name $CONTAINER_NAME -e COVERAGE -e FORCE_FAILURE $sentry karma yarn test:karma:headless
docker-compose run --name $CONTAINER_NAME -e CI_NODE_INDEX -e CI_NODE_TOTAL -e COVERAGE -e FORCE_FAILURE $sentry karma yarn test:karma:headless

View File

@ -18,6 +18,7 @@
process.env.NODE_ENV = 'test'
const fs = require('fs')
const path = require('path')
const webpack = require('webpack')
const testWebpackConfig = require('./frontend_build/baseWebpackConfig')
@ -55,7 +56,45 @@ testWebpackConfig.plugins.push(new webpack.EnvironmentPlugin({
GIT_COMMIT: null
}))
const getAllFiles = (dirPath, arrayOfFiles, callback) => {
const files = fs.readdirSync(dirPath)
files.forEach(file => {
if (fs.statSync(dirPath + '/' + file).isDirectory()) {
arrayOfFiles = getAllFiles(dirPath + '/' + file, arrayOfFiles, callback)
} else {
const filePath = callback(dirPath + '/' + file)
if (filePath) {
arrayOfFiles.push(filePath)
}
}
})
return arrayOfFiles
}
const isPartitionMatch = (resource, partitions, partitionIndex) => {
return isNaN(partitionIndex) || partitions[partitionIndex].indexOf(resource) >= 0
}
const makeSortedPartitions = (arr, partitionCount) => {
const sortedArr = arr.sort()
const sortedArrLength = sortedArr.length
const chunkSize = Math.floor(sortedArrLength / partitionCount)
const R = []
for (let i = 0; i < sortedArrLength; i += chunkSize) {
R.push(sortedArr.slice(i, i + chunkSize))
}
return R
}
if (process.env.JSPEC_GROUP) {
const nodeIndex = +process.env.CI_NODE_INDEX
const nodeTotal = +process.env.CI_NODE_TOTAL
let ignoreResource = () => {
throw new Error(`Unknown JSPEC_GROUP ${process.env.JSPEC_GROUP}`)
}
@ -75,13 +114,30 @@ if (process.env.JSPEC_GROUP) {
)
}
} else if (process.env.JSPEC_GROUP === 'jsg') {
let partitions = null
if (!isNaN(nodeIndex) && !isNaN(nodeTotal)) {
const allFiles = getAllFiles(CONTEXT_JSX_SPEC, [], filePath => {
const relativePath = filePath.replace(CONTEXT_JSX_SPEC, '.').replace(/\.js$/, '')
return RESOURCE_JSG_SPLIT_SPEC.test(relativePath) ? relativePath : null
})
partitions = makeSortedPartitions(allFiles, nodeTotal)
}
ignoreResource = (resource, context) => {
return (
context.endsWith(CONTEXT_COFFEESCRIPT_SPEC) ||
context.endsWith(CONTEXT_EMBER_GRADEBOOK_SPEC) ||
(context.endsWith(CONTEXT_JSX_SPEC) &&
RESOURCE_JSX_SPEC.test(resource) &&
!RESOURCE_JSG_SPLIT_SPEC.test(resource))
!RESOURCE_JSG_SPLIT_SPEC.test(resource)) ||
(partitions &&
context.endsWith(CONTEXT_JSX_SPEC) &&
RESOURCE_JSX_SPEC.test(resource) &&
RESOURCE_JSG_SPLIT_SPEC.test(resource) &&
!isPartitionMatch(resource, partitions, nodeIndex))
)
}
} else if (process.env.JSPEC_GROUP === 'jsh') {