Skip to content

Commit

Permalink
Revert "Revert "Crystalball post-merge map generation""
Browse files Browse the repository at this point in the history
Also moves config/initializer/crystalball.rb to spec/support
to prevent it from running in prod

This reverts commit 9a9c68b.

Test-plan:
- passes cd to edge without failure
- generates map successfully in PoC build

Change-Id: I67c02ebaea06e11b3a3721aa49217da16e75bd32
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/279848
Reviewed-by: Michael Hargiss <[email protected]>
Reviewed-by: James Butters <[email protected]>
Tested-by: Service Cloud Jenkins <[email protected]>
QA-Review: Brian Watson <[email protected]>
Product-Review: Brian Watson <[email protected]>
  • Loading branch information
brianlwatson committed Dec 1, 2021
1 parent aea9cbb commit 96fbbf8
Show file tree
Hide file tree
Showing 8 changed files with 390 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
/config/*.yml
/config/credentials.yml.enc
!/config/credentials.test.yml
!/config/crystalball.yml
/config/environments/*-local.rb
/config/locales/generated/
/config/RAILS6_1
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.d/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,6 @@

# performance tools for instrumenting rspec tests
gem "stackprof"

gem "crystalball", "0.7.0", require: false
end
140 changes: 140 additions & 0 deletions Jenkinsfile.crystalball
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/usr/bin/env groovy

/*
* Copyright (C) 2021 - 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/>.
*/

library 'canvas-builds-library'
loadLocalLibrary('local-lib', 'build/new-jenkins/library')

// if the build never starts or gets into a node block, then we
// can never load a file. and a very noisy/confusing error is thrown.
def ignoreBuildNeverStartedError(block) {
try {
block()
}
catch (org.jenkinsci.plugins.workflow.steps.MissingContextVariableException ex) {
if (!ex.message.startsWith('Required context class hudson.FilePath is missing')) {
throw ex
}
else {
echo "ignored MissingContextVariableException: \n${ex.message}"
}
// we can ignore this very noisy error
}
}

def getMigrationsTag(name) {
(env.GERRIT_REFSPEC.contains('master')) || !migrations.cacheLoadFailed() ? migrations.imageMergeTag(name) : migrations.imagePatchsetTag(name)
}

def getPatchsetTag() {
(env.GERRIT_REFSPEC.contains('master')) ? "${configuration.buildRegistryPath()}:${env.GERRIT_BRANCH}" : imageTag.patchset()
}

def getResultsHTMLUrl() {
return "${env.BUILD_URL}/artifact/crystalball_map.yml"
}

pipeline {
agent { label 'canvas-docker' }
options {
ansiColor('xterm')
timestamps()
}

environment {
BUILD_REGISTRY_FQDN = configuration.buildRegistryFQDN()
POSTGRES = configuration.postgres()
RUBY = configuration.ruby() // RUBY_VERSION is a reserved keyword for ruby installs
// e.g. canvas-lms:01.123456.78-postgres-12-ruby-2.6
PATCHSET_TAG = getPatchsetTag()

CASSANDRA_PREFIX = configuration.buildRegistryPath('cassandra-migrations')
DYNAMODB_PREFIX = configuration.buildRegistryPath('dynamodb-migrations')
POSTGRES_PREFIX = configuration.buildRegistryPath('postgres-migrations')

IMAGE_CACHE_MERGE_SCOPE = configuration.gerritBranchSanitized()
RSPEC_PROCESSES = 6

CASSANDRA_IMAGE_TAG = "$CASSANDRA_PREFIX:$IMAGE_CACHE_MERGE_SCOPE-$RSPEC_PROCESSES"
DYNAMODB_IMAGE_TAG = "$DYNAMODB_PREFIX:$IMAGE_CACHE_MERGE_SCOPE-$RSPEC_PROCESSES"
POSTGRES_IMAGE_TAG = "$POSTGRES_PREFIX:$IMAGE_CACHE_MERGE_SCOPE-$RSPEC_PROCESSES"
}

stages {
stage('Setup') {
steps {
cleanAndSetup()
}
}

stage('Parallel Run Tests') {
steps {
script {
def stages = [:]

distribution.stashBuildScripts()
rspecStage.createDistribution(stages)

parallel(stages)
}
}
}
}

post {
always {
script {
ignoreBuildNeverStartedError {
node('master') {
buildSummaryReport.publishReport('Build Summary Report', currentBuild.getResult() == 'SUCCESS' ? 'SUCCESS' : 'FAILURE')
}
}

copyArtifacts(
filter: 'tmp/crystalball/**',
optional: false,
projectName: env.JOB_NAME,
selector: specific(env.BUILD_NUMBER),
)

sh """
docker-compose run -v \$(pwd)/\$LOCAL_WORKDIR/tmp/crystalball/:/tmp/crystalball \
-v \$(pwd)/\$LOCAL_WORKDIR/build:/usr/src/app/build \
--name crystalball-parser \
web bash -c 'ruby build/new-jenkins/crystalball_merge_coverage.rb /tmp/crystalball/'
"""

sh 'docker cp crystalball-parser:/usr/src/app/crystalball_map.yml .'
archiveArtifacts allowEmptyArchive: true, artifacts: 'crystalball_map.yml'

// Only alert on periodic jobs, not ones resulting from manual tests
if (env.GERRIT_EVENT_TYPE != 'comment-added') {
slackSend channel: '#crystalball-noisy', message: "<$env.BUILD_URL/testReport|Latest Crystalball Map Generated> - <${getResultsHTMLUrl()}|Map>"
}
}
}
cleanup {
script {
ignoreBuildNeverStartedError {
libraryScript.execute 'bash/docker-cleanup.sh --allow-failure'
}
}
}
}
}
52 changes: 52 additions & 0 deletions build/new-jenkins/crystalball_merge_coverage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

#
# Copyright (C) 2021 - 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/>.
#

path = "/tmp/crystalball"
map_header = nil
map_body = {}
Dir.glob("#{path}/**/*_map.yml") do |filename|
puts "Looking through #{filename}"
doc = File.read(filename)
(header, body) = doc.split("---").reject(&:empty?)
map_header ||= header
body.split("\n").slice_when { |_before, after| after.include?(":") }.each do |group|
spec = group.shift
changed_files = group

next if spec.empty? || changed_files.count.zero?

raise "#{spec} already has entries: #{map_body[spec]}" unless map_body[spec].nil?

map_body[spec] = changed_files
end
end

File.open("crystalball_map.yml", "w") do |file|
file << "---"
file << map_header
file << "---"
file << "\n"
map_body.each do |spec, app_files|
file.puts spec
file.puts app_files.join("\n")
end
end

puts "Crystalball Map Created for #{map_body.keys.count} tests!"
12 changes: 11 additions & 1 deletion build/new-jenkins/library/vars/rspecStage.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def createDistribution(nestedStages) {

def baseEnvVars = [
"ENABLE_AXE_SELENIUM=${env.ENABLE_AXE_SELENIUM}",
"ENABLE_CRYSTALBALL=${env.ENABLE_CRYSTALBALL}",
'POSTGRES_PASSWORD=sekret',
'SELENIUM_VERSION=3.141.59-20201119',
"RSPECQ_ENABLED=${env.RSPECQ_ENABLED}"
Expand Down Expand Up @@ -103,6 +104,14 @@ def tearDownNode(prefix) {
sh 'build/new-jenkins/docker-copy-files.sh /usr/src/app/log/results tmp/rspec_results canvas_ --allow-error --clean-dir'
sh "build/new-jenkins/docker-copy-files.sh /usr/src/app/log/spec_failures/ tmp/spec_failures/$prefix canvas_ --allow-error --clean-dir"

// Don't generate map pre-merge
if (env.ENABLE_CRYSTALBALL == '1' && env.RSPECQ_ENABLED != '1') {
sh 'build/new-jenkins/docker-copy-files.sh /usr/src/app/log/results/crystalball_results tmp/crystalball canvas_ --allow-error --clean-dir'
sh 'ls tmp/crystalball'
sh 'ls -R'
archiveArtifacts allowEmptyArchive: true, artifacts: 'tmp/crystalball/**/*'
}

if (configuration.getBoolean('upload-docker-logs', 'false')) {
sh "docker ps -aq | xargs -I{} -n1 -P1 docker logs --timestamps --details {} 2>&1 > tmp/docker-${prefix}-${CI_NODE_INDEX}.log"
archiveArtifacts(artifacts: "tmp/docker-${prefix}-${CI_NODE_INDEX}.log")
Expand Down Expand Up @@ -149,6 +158,7 @@ def runRspecqSuite() {
return
}
sh(script: 'docker-compose exec -T -e ENABLE_AXE_SELENIUM \
-e ENABLE_CRYSTALBALL \
-e RSPECQ_ENABLED \
-e SENTRY_DSN \
-e RSPECQ_UPDATE_TIMINGS \
Expand Down Expand Up @@ -181,7 +191,7 @@ def runRspecqSuite() {

def runLegacySuite() {
try {
sh(script: 'docker-compose exec -T -e RSPEC_PROCESSES -e ENABLE_AXE_SELENIUM canvas bash -c \'build/new-jenkins/rspec-with-retries.sh\'', label: 'Run Tests')
sh(script: 'docker-compose exec -T -e RSPEC_PROCESSES -e ENABLE_AXE_SELENIUM -e ENABLE_CRYSTALBALL canvas bash -c \'build/new-jenkins/rspec-with-retries.sh\'', label: 'Run Tests')
} catch (org.jenkinsci.plugins.workflow.steps.FlowInterruptedException e) {
if (e.causes[0] instanceof org.jenkinsci.plugins.workflow.steps.TimeoutStepExecution.ExceededTimeout) {
/* groovylint-disable-next-line GStringExpressionWithinString */
Expand Down
28 changes: 28 additions & 0 deletions config/crystalball.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Custom path to your execution map. Can be file or folder. Default: `tmp/crystalball_data.yml`
execution_map_path: './crystalball_map.yml'
# Maximum amount of examples which will be run automatically. Default: no limit.
# If prediction size is over the limit Crystalball will prune prediction to fit the limit.
# examples_limit: 1
#
# Custom prediction builder class to use. Default: 'Crystalball::RSpec::StandardPredictionBuilder'
prediction_builder_class_name: 'Crystalball::RSpec::StandardPredictionBuilder'
# #
# Set of requires. Usually used to require custom predictor class file. Default: []
requires:
- './spec/support/crystalball.rb'
#
# Custom map expiration period in seconds. 0 = Disable expiration check. Default: 86400 (1 day)
map_expiration_period : 0
#
# Custom RSpec runner class to use. Default: 'Crystalball::RSpec::Runner'
runner_class_name: 'Crystalball::RSpec::DryRunner'
#
# File path to save dry-run prediction results
dry_run_output_file_path: './crystalball_spec_list.txt'
#
# "From" value for `git diff` command. default: 'HEAD'
diff_from: 'HEAD'
#
# "To" value for `git diff` command. default: nil
diff_to:

23 changes: 23 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

require "securerandom"
require "tmpdir"
require "crystalball"

ENV["RAILS_ENV"] = "test"
require_relative "../config/environment"
Expand Down Expand Up @@ -156,6 +157,28 @@ def view_assigns
end
end

# Don't do map generation in pre-merge, which runs rspecq
if ENV["ENABLE_CRYSTALBALL"] == "1" && ENV["RSPECQ_ENABLED"] != "1"
Crystalball::MapGenerator.start! do |config|
config.register Crystalball::MapGenerator::CoverageStrategy.new
config.map_storage_path = "log/results/crystalball_results/#{ENV.fetch("PARALLEL_INDEX", "0")}_map.yml"
end
end

module Crystalball
class MapGenerator
class CoverageStrategy
def call(example_map, example)
puts "Calling Coverage Strategy for #{example.inspect}"
before = Coverage.peek_result
yield example_map, example
after = Coverage.peek_result
example_map.push(*execution_detector.detect(before, after))
end
end
end
end

module RSpec::Rails
module ViewExampleGroup
module ExampleMethods
Expand Down
Loading

0 comments on commit 96fbbf8

Please sign in to comment.