canvas-lms/Jenkinsfile.selenium.flakey...

221 lines
6.9 KiB
Groovy

#!/usr/bin/env groovy
/*
* Copyright (C) 2019 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@groovy.transform.Field
def result_test_count = -1
@groovy.transform.Field
def result_node_count = -1
@groovy.transform.Field
def changed_tests = ''
@groovy.transform.Field
def fsc_timeout = false
def getImageTagVersion() {
def flags = load 'build/new-jenkins/groovy/commit-flags.groovy'
return flags.getImageTagVersion()
}
def computeTestCount() {
// oops, probably should have added an easier way to _count_ tests...
sh 'rm -vrf tmp'
sh 'mkdir -v tmp'
sh 'chmod -vv 777 tmp'
sh """
docker run --volume \$WORKSPACE/.git:/usr/src/app/.git \
--volume \$WORKSPACE/tmp:/usr/src/app/tmp \
--env FSC_IGNORE_FILES \
\$PATCHSET_TAG \
bundle exec flakey_spec_catcher --use-parent --dry-run-quiet > tmp/test_list
"""
changed_tests = readFile('tmp/test_list').trim()
echo "raw result from catcher: \n====\n$changed_tests\n===="
def test_count = 0
if (changed_tests) {
test_count = changed_tests.split('\n').length
}
echo "expected tests to run: $test_count"
result_test_count = test_count
}
def computeDistributedCount() {
if (result_test_count < 0)
throw IllegalStateException("call computeTestCount() first")
// this type of distributed thing always needs a hard cutoff
if (env.DISTRIBUTED_CUT_OFF.toInteger() < result_test_count)
throw IllegalStateException("unable to process more than ${env.DISTRIBUTED_CUT_OFF} tests")
if (result_test_count == 0) {
result_node_count = 0
}
else {
// force a round down
// this will have the following node counts.
// test | nodes
// 1-14 | 1
// 15-24 | 2
// 25-34 | 3
// ...
def distributed_offset = env.DISTRIBUTED_OFFSET.toInteger()
def distributed_factor = env.DISTRIBUTED_FACTOR.toInteger()
result_node_count = (int) ((result_test_count + distributed_offset) / distributed_factor)
}
}
def executeFlakeySpecCatcher(prefix = 'main') {
sh 'rm -vrf tmp'
try {
timeout(30) {
sh 'build/new-jenkins/docker-compose-pull.sh'
sh 'build/new-jenkins/docker-compose-pull-selenium.sh'
sh 'build/new-jenkins/docker-compose-build-up.sh'
sh 'build/new-jenkins/docker-compose-create-migrate-database.sh'
sh 'build/new-jenkins/rspec-flakey-spec-catcher.sh'
}
}
// Don't fail the build for timeouts
catch (org.jenkinsci.plugins.workflow.steps.FlowInterruptedException e) {
if (!e.causes[0] instanceof org.jenkinsci.plugins.workflow.steps.TimeoutStepExecution.ExceededTimeout) {
throw e
} else {
echo "Not failing the build due to timeouts"
fsc_timeout = true
}
}
finally {
sh "mkdir -vp tmp/$prefix"
sh(
script: "docker cp \$(docker-compose ps -q web):/usr/src/app/tmp/fsc.out ./tmp/$prefix/fsc.out",
returnStatus: true
)
archiveArtifacts(artifacts: "tmp/$prefix/fsc.out", allowEmptyArchive: true)
}
}
def sendSlack(success) {
def color = success ? "good" : "danger"
def jobInfo = "<https://gerrit.instructure.com/$env.GERRIT_CHANGE_NUMBER|Gerrit> | <$env.BUILD_URL|Jenkins>"
def message = "$jobInfo\n$changed_tests"
if (fsc_timeout) {
message = "Timeout Occurred!\n$message"
}
slackSend channel: '#flakey_spec_catcher_noisy', color: color, message: message
}
pipeline {
agent { label 'canvas-docker' }
options { ansiColor('xterm') }
environment {
COMPOSE_FILE = 'docker-compose.new-jenkins.yml:docker-compose.new-jenkins-selenium.yml:docker-compose.new-jenkins-flakey-spec-catcher.yml'
// there will be a node for every 10 tests
DISTRIBUTED_FACTOR = '10'
// this causes distribution to trigger at an offset
DISTRIBUTED_OFFSET = '5'
// if someone is trying to update more than these amount of tests, then just fail.
// that will be at DISTRIBUTED_CUT_OFF / DISTRIBUTED_FACTOR nodes.
DISTRIBUTED_CUT_OFF = '300'
// fsc errors when running specs from gems.
// until we figure out how to run them, we should ignore them
FSC_IGNORE_FILES = "gems/.*/spec/"
}
stages {
stage('Checkout and clean') {
steps {
timeout(time: 5) {
sh 'build/new-jenkins/docker-cleanup.sh'
sh 'rm -vrf ./tmp/'
}
}
}
stage('Print Env Variables') {
steps {
sh 'build/new-jenkins/print-env-excluding-secrets.sh'
}
}
stage("Compute Build Distribution") {
steps {
script {
computeTestCount()
computeDistributedCount()
echo "expected nodes to run on for $result_test_count tests: $result_node_count"
}
}
}
stage("Run Flakey Spec Catcher") {
when { expression { result_test_count > 0 } }
steps {
script {
if (result_node_count <= 1) {
echo "running on this node"
executeFlakeySpecCatcher()
}
else {
echo "running on multiple nodes: $result_node_count"
def nodes = [:];
for(int i = 0; i < result_node_count; i++) {
// make sure to create a new index variable so this value gets
// captured by the lambda for FSC_NODE_INDEX
def index = i
def node_number = (index).toString().padLeft(2, '0')
nodes["flakey set $node_number"] = {
withEnv(["FSC_NODE_TOTAL=$result_node_count", "FSC_NODE_INDEX=$index"]) {
node('canvas-docker') {
stage("Running Flakey Set $node_number") {
try {
sh 'rm -vrf ./tmp'
checkout scm
sh 'build/new-jenkins/docker-cleanup.sh'
executeFlakeySpecCatcher("node$node_number")
}
finally {
sh 'rm -vrf ./tmp'
sh 'build/new-jenkins/docker-cleanup.sh --allow-failure'
}
}
}
}
}
}
parallel(nodes)
}
}
}
post {
success {
sendSlack(true)
}
failure {
sendSlack(false)
}
}
}
}
post {
cleanup {
sh 'rm -vrf ./tmp/'
sh 'build/new-jenkins/docker-cleanup.sh --allow-failure'
}
}
}