Add istanbul-instrumenter-loader for crystalball map

Note: Ensure that istanbul is only enabled for crystalball before
this is merged

closes OUT-4918
flag=none

Test-plan:
- crystalball map should include JS files
- ensure that CRYSTALBALL_MAP isn't set to 1 in standard pre-merge

Change-Id: I5ae2f32177640e3caeb77871918644890eb4ae30
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/280813
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: James Butters <jbutters@instructure.com>
QA-Review: Brian Watson <bwatson@instructure.com>
Product-Review: Brian Watson <bwatson@instructure.com>
This commit is contained in:
Brian Watson 2021-12-09 21:31:30 -07:00
parent 00583725d3
commit 4a49800443
10 changed files with 154 additions and 38 deletions

View File

@ -3,12 +3,14 @@ COPY --chown=docker:docker --from=local/cache-helper-collect-webpack /tmp/dst ${
ARG JS_BUILD_NO_UGLIFY=0
ARG RAILS_LOAD_ALL_LOCALES=0
ARG CRYSTALBALL_MAP=0
RUN COMPILE_ASSETS_API_DOCS=0 \
COMPILE_ASSETS_BRAND_CONFIGS=0 \
COMPILE_ASSETS_NPM_INSTALL=0 \
COMPILE_ASSETS_STYLEGUIDE=0 \
JS_BUILD_NO_UGLIFY="$JS_BUILD_NO_UGLIFY" \
RAILS_LOAD_ALL_LOCALES="$RAILS_LOAD_ALL_LOCALES" \
CRYSTALBALL_MAP="$CRYSTALBALL_MAP" \
bundle exec rails canvas:compile_assets
FROM local/ruby-runner AS webpack-cache

View File

@ -20,6 +20,7 @@
library 'canvas-builds-library'
loadLocalLibrary('local-lib', 'build/new-jenkins/library')
final static RUN_MIGRATIONS_STAGE = 'Run Migrations'
// 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.
@ -64,9 +65,17 @@ pipeline {
// e.g. canvas-lms:01.123456.78-postgres-12-ruby-2.6
PATCHSET_TAG = getPatchsetTag()
BASE_RUNNER_PREFIX = configuration.buildRegistryPath('base-runner')
CASSANDRA_PREFIX = configuration.buildRegistryPath('cassandra-migrations')
DYNAMODB_PREFIX = configuration.buildRegistryPath('dynamodb-migrations')
KARMA_BUILDER_PREFIX = configuration.buildRegistryPath('karma-builder')
KARMA_RUNNER_PREFIX = configuration.buildRegistryPath('karma-runner')
LINTERS_RUNNER_PREFIX = configuration.buildRegistryPath('linters-runner')
POSTGRES_PREFIX = configuration.buildRegistryPath('postgres-migrations')
RUBY_RUNNER_PREFIX = configuration.buildRegistryPath('ruby-runner')
YARN_RUNNER_PREFIX = configuration.buildRegistryPath('yarn-runner')
WEBPACK_BUILDER_PREFIX = configuration.buildRegistryPath('webpack-builder')
WEBPACK_CACHE_PREFIX = configuration.buildRegistryPath('webpack-cache')
IMAGE_CACHE_MERGE_SCOPE = configuration.gerritBranchSanitized()
RSPEC_PROCESSES = 6
@ -74,24 +83,65 @@ pipeline {
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"
POSTGRES_CLIENT = configuration.postgresClient()
}
stages {
stage('Setup') {
stage('Pre-Setup') {
steps {
cleanAndSetup()
}
}
stage('Parallel Run Tests') {
stage('Crystalball Map') {
steps {
script {
def stages = [:]
def postBuildHandler = [
onStageEnded: { stageName, stageConfig, result ->
buildSummaryReport.addFailureRun('Main Build', currentBuild)
}
]
distribution.stashBuildScripts()
rspecStage.createDistribution(stages)
extendedStage('Root').hooks(postBuildHandler).obeysAllowStages(false).reportTimings(false).execute {
def rootStages = [:]
parallel(stages)
extendedStage('Builder').nodeRequirements(label: 'canvas-docker', podTemplate: null).obeysAllowStages(false).reportTimings(false).queue(rootStages) {
extendedStage('Setup')
.hooks(buildSummaryReportHooks.call())
.obeysAllowStages(false)
.timeout(2)
.execute { setupStage() }
extendedStage('Build Docker Image (Pre-Merge)')
.hooks(buildSummaryReportHooks.call())
.obeysAllowStages(false)
.required(configuration.isChangeMerged())
.timeout(20)
.execute(buildDockerImageStage.&premergeCacheImage)
extendedStage('Build Docker Image')
.hooks(buildSummaryReportHooks.call())
.obeysAllowStages(false)
.timeout(20)
.execute(buildDockerImageStage.&patchsetImage)
extendedStage(RUN_MIGRATIONS_STAGE)
.hooks(buildSummaryReportHooks.call())
.obeysAllowStages(false)
.timeout(10)
.execute { runMigrationsStage() }
}
extendedStage("${RUN_MIGRATIONS_STAGE} (Waiting for Dependencies)").obeysAllowStages(false).waitsFor(RUN_MIGRATIONS_STAGE, 'Builder').queue(rootStages) { stageConfig, buildConfig ->
def nestedStages = [:]
rspecStage.createLegacyDistribution(nestedStages)
parallel(nestedStages)
}
parallel(rootStages)
}
}
}
}

View File

@ -17,7 +17,6 @@
# 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 = {}
@ -34,7 +33,18 @@ Dir.glob("#{path}/**/*_map.yml") do |filename|
raise "#{spec} already has entries: #{map_body[spec]}" unless map_body[spec].nil?
map_body[spec] = changed_files
# JS files will be added to the map based on the parent directory of the file only
# TODO: we should have a flag to filter JS at this level
changed_files.map! do |file|
if /(\.js|\.ts|\.tsx)/.match?(file)
# Wrap in File.dirname if we want to filter by directories
file.gsub(%r{("|/usr/src/app/)}, "")
else
file
end
end
map_body[spec] = changed_files.uniq
end
end

View File

@ -71,6 +71,7 @@ BASE_RUNNER_BUILD_ARGS=(
WEBPACK_CACHE_BUILD_ARGS=(
--build-arg JS_BUILD_NO_UGLIFY="$JS_BUILD_NO_UGLIFY"
--build-arg RAILS_LOAD_ALL_LOCALES="$RAILS_LOAD_ALL_LOCALES"
--build-arg CRYSTALBALL_MAP="$CRYSTALBALL_MAP"
)
BASE_RUNNER_PARTS=(
$BASE_IMAGE_ID

View File

@ -3,6 +3,9 @@
set -o errexit -o errtrace -o nounset -o pipefail -o xtrace
WORKSPACE=${WORKSPACE:-$(pwd)}
CRYSTALBALL_MAP=${CRYSTALBALL_MAP:-0}
echo "CRYSTALBALL_MAP VALUE ${CRYSTALBALL_MAP}"
export CACHE_VERSION="2020-02-02.1"

View File

@ -95,6 +95,7 @@ def jsImage() {
"PATCHSET_TAG=${env.PATCHSET_TAG}",
"RAILS_LOAD_ALL_LOCALES=${getRailsLoadAllLocales()}",
"WEBPACK_BUILDER_IMAGE=${env.WEBPACK_BUILDER_IMAGE}",
"CRYSTALBALL_MAP=${env.CRYSTALBALL_MAP}"
]) {
sh "./build/new-jenkins/js/docker-build.sh $KARMA_RUNNER_IMAGE"
}
@ -124,6 +125,7 @@ def premergeCacheImage() {
"CACHE_LOAD_FALLBACK_SCOPE=${env.IMAGE_CACHE_BUILD_SCOPE}",
"CACHE_SAVE_SCOPE=${env.IMAGE_CACHE_MERGE_SCOPE}",
'COMPILE_ADDITIONAL_ASSETS=0',
"CRYSTALBALL_MAP=${env.CRYSTALBALL_MAP}",
'JS_BUILD_NO_UGLIFY=1',
'RAILS_LOAD_ALL_LOCALES=0',
"RUBY_RUNNER_PREFIX=${env.RUBY_RUNNER_PREFIX}",
@ -164,6 +166,7 @@ def patchsetImage() {
"CACHE_SAVE_SCOPE=${cacheScope}",
"CACHE_UNIQUE_SCOPE=${env.IMAGE_CACHE_UNIQUE_SCOPE}",
"COMPILE_ADDITIONAL_ASSETS=${configuration.isChangeMerged() ? 1 : 0}",
"CRYSTALBALL_MAP=${env.CRYSTALBALL_MAP}",
"JS_BUILD_NO_UGLIFY=${configuration.isChangeMerged() ? 0 : 1}",
"RAILS_LOAD_ALL_LOCALES=${getRailsLoadAllLocales()}",
"RUBY_RUNNER_PREFIX=${env.RUBY_RUNNER_PREFIX}",

View File

@ -21,6 +21,8 @@ import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException
@Field static final SUCCESS_NOT_BUILT = [buildResult: 'SUCCESS', stageResult: 'NOT_BUILT']
@Field static final SUCCESS_UNSTABLE = [buildResult: 'SUCCESS', stageResult: 'UNSTABLE']
@Field static final RSPEC_NODE_REQUIREMENTS = [label: 'canvas-docker']
def createDistribution(nestedStages) {
def rspecqNodeTotal = configuration.getInteger('rspecq-ci-node-total')
@ -45,6 +47,32 @@ def createDistribution(nestedStages) {
"RSPECQ_UPDATE_TIMINGS=${env.GERRIT_EVENT_TYPE == 'change-merged' ? '1' : '0'}",
]
extendedStage('RSpecQ Reporter for Rspec')
.envVars(rspecqEnvVars)
.hooks(buildSummaryReportHooks.call() + [onNodeAcquired: setupNodeHook])
.nodeRequirements(RSPEC_NODE_REQUIREMENTS)
.timeout(15)
.queue(nestedStages, this.&runReporter)
rspecqNodeTotal.times { index ->
extendedStage("RSpecQ Test Set ${(index + 1).toString().padLeft(2, '0')}")
.envVars(rspecqEnvVars + ["CI_NODE_INDEX=$index"])
.hooks(buildSummaryReportHooks.call() + [onNodeAcquired: setupNodeHook, onNodeReleasing: { tearDownNode('spec') }])
.nodeRequirements(RSPEC_NODE_REQUIREMENTS)
.timeout(15)
.queue(nestedStages, this.&runRspecqSuite)
}
}
def createLegacyDistribution(nestedStages) {
def setupNodeHook = this.&setupNode
def baseEnvVars = [
"ENABLE_AXE_SELENIUM=${env.ENABLE_AXE_SELENIUM}",
"ENABLE_CRYSTALBALL=${env.ENABLE_CRYSTALBALL}",
'POSTGRES_PASSWORD=sekret',
'SELENIUM_VERSION=3.141.59-20210929'
]
// Used only for crystalball map generation
def seleniumNodeTotal = configuration.getInteger('selenium-ci-node-total')
def seleniumEnvVars = baseEnvVars + [
@ -55,35 +83,16 @@ def createDistribution(nestedStages) {
"RERUNS_RETRY=${configuration.getInteger('selenium-rerun-retry')}",
"RSPEC_PROCESSES=${configuration.getInteger('selenium-processes')}",
'TEST_PATTERN=^./(spec|gems/plugins/.*/spec_canvas)/selenium',
'CRYSTALBALL_MAP=1'
]
def rspecNodeRequirements = [label: 'canvas-docker']
if (env.ENABLE_CRYSTALBALL != '1') {
extendedStage('RSpecQ Reporter for Rspec')
.envVars(rspecqEnvVars)
.hooks(buildSummaryReportHooks.call() + [onNodeAcquired: setupNodeHook])
.nodeRequirements(rspecNodeRequirements)
seleniumNodeTotal.times { index ->
extendedStage("Selenium Test Set ${(index + 1).toString().padLeft(2, '0')}")
.envVars(seleniumEnvVars + ["CI_NODE_INDEX=$index"])
.hooks([onNodeAcquired: setupNodeHook, onNodeReleasing: { tearDownNode('selenium') }])
.nodeRequirements(RSPEC_NODE_REQUIREMENTS)
.timeout(15)
.queue(nestedStages, this.&runReporter)
rspecqNodeTotal.times { index ->
extendedStage("RSpecQ Test Set ${(index + 1).toString().padLeft(2, '0')}")
.envVars(rspecqEnvVars + ["CI_NODE_INDEX=$index"])
.hooks(buildSummaryReportHooks.call() + [onNodeAcquired: setupNodeHook, onNodeReleasing: { tearDownNode('spec') }])
.nodeRequirements(rspecNodeRequirements)
.timeout(15)
.queue(nestedStages, this.&runRspecqSuite)
}
} else {
seleniumNodeTotal.times { index ->
extendedStage("Selenium Test Set ${(index + 1).toString().padLeft(2, '0')}")
.envVars(seleniumEnvVars + ["CI_NODE_INDEX=$index"])
.hooks([onNodeAcquired: setupNodeHook, onNodeReleasing: { tearDownNode('selenium') }])
.nodeRequirements(rspecNodeRequirements)
.timeout(15)
.queue(nestedStages, this.&runLegacySuite)
}
.queue(nestedStages, this.&runLegacySuite)
}
}
@ -128,7 +137,7 @@ def tearDownNode(prefix) {
archiveArtifacts allowEmptyArchive: true, artifacts: 'tmp/coverage/**/*'
}
if (env.ENABLE_CRYSTALBALL == '1') {
if (env.CRYSTALBALL_MAP == '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'
@ -215,7 +224,7 @@ def runRspecqSuite() {
def runLegacySuite() {
try {
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')
sh(script: 'docker-compose exec -T -e RSPEC_PROCESSES -e ENABLE_AXE_SELENIUM -e CRYSTALBALL_MAP 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 */

View File

@ -255,7 +255,8 @@ shared_context "in-process server selenium tests" do
browser_errors_we_dont_care_about.none? { |s| e.message.include?(s) }
end
if javascript_errors.present?
# Crystalball is going to get a few JS errors when using istanbul-instrumenter
if javascript_errors.present? && ENV["CRYSTALBALL_MAP"] != "1"
raise javascript_errors.map(&:message).join("\n\n")
end
end

View File

@ -162,7 +162,7 @@ if ENV["ENABLE_AXE_SELENIUM"] == "1"
end
end
if ENV["ENABLE_CRYSTALBALL"] == "1"
if ENV["CRYSTALBALL_MAP"] == "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"
@ -177,6 +177,11 @@ if ENV["ENABLE_CRYSTALBALL"] == "1"
yield example_map, example
after = Coverage.peek_result
example_map.push(*execution_detector.detect(before, after))
# rubocop:disable Specs/NoExecuteScript
js_coverage = SeleniumDriverSetup.driver.execute_script("return window.__coverage__")&.keys&.uniq
# rubocop:enable Specs/NoExecuteScript
example_map.used_files.concat(js_coverage) if js_coverage
end
end
end

View File

@ -16,4 +16,36 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
module.exports = require('./ui-build/webpack')
const webpack = require('./ui-build/webpack')
// since istanbul-instrumenter-loader adds so much overhead, only use it when generating crystalball map
if (process.env.CRYSTALBALL_MAP === '1') {
const path = require('path')
const {canvasDir} = require('./ui-build/params')
webpack.module.rules.unshift({
test: /\.(js|ts|tsx)$/,
include: [
path.resolve(canvasDir, 'ui'),
path.resolve(canvasDir, 'packages/jquery-kyle-menu'),
path.resolve(canvasDir, 'packages/jquery-sticky'),
path.resolve(canvasDir, 'packages/jquery-popover'),
path.resolve(canvasDir, 'packages/jquery-selectmenu'),
path.resolve(canvasDir, 'packages/mathml'),
path.resolve(canvasDir, 'packages/persistent-array'),
path.resolve(canvasDir, 'packages/slickgrid'),
path.resolve(canvasDir, 'packages/with-breakpoints'),
path.resolve(canvasDir, 'spec/javascripts/jsx'),
path.resolve(canvasDir, 'spec/coffeescripts'),
/gems\/plugins\/.*\/app\/(jsx|coffeescripts)\//
],
exclude: [/test\//, /spec/],
use: {
loader: 'istanbul-instrumenter-loader',
options: {esModules: true, produceSourceMap: true}
},
enforce: 'post'
})
}
module.exports = webpack