diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 15:40:28 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 15:40:28 +0000 |
commit | b595cb0c1dec83de5bdee18284abe86614bed33b (patch) | |
tree | 8c3d4540f193c5ff98019352f554e921b3a41a72 /scripts | |
parent | 2f9104a328fc8a4bddeaa4627b595166d24671d0 (diff) | |
download | gitlab-ce-b595cb0c1dec83de5bdee18284abe86614bed33b.tar.gz |
Add latest changes from gitlab-org/gitlab@15-2-stable-eev15.2.0-rc42
Diffstat (limited to 'scripts')
-rwxr-xr-x | scripts/checkout-mr-source-sha | 7 | ||||
-rwxr-xr-x | scripts/decomposition/generate-loose-foreign-key | 138 | ||||
-rwxr-xr-x | scripts/determine-qa-tests | 106 | ||||
-rw-r--r-- | scripts/frontend/stylelint/stylelint_duplicate_selectors.js | 25 | ||||
-rw-r--r-- | scripts/frontend/stylelint/stylelint_utility_classes.js | 25 | ||||
-rw-r--r-- | scripts/frontend/stylelint/stylelint_utility_map.js | 64 | ||||
-rw-r--r-- | scripts/frontend/stylelint/stylelint_utils.js | 78 | ||||
-rw-r--r-- | scripts/frontend/stylelint/utility_classes_map.js | 259 | ||||
-rwxr-xr-x | scripts/glfm/run-snapshot-tests.sh | 7 | ||||
-rw-r--r-- | scripts/lib/glfm/constants.rb | 2 | ||||
-rw-r--r-- | scripts/lib/glfm/parse_examples.rb | 59 | ||||
-rw-r--r-- | scripts/lib/glfm/render_wysiwyg_html_and_json.js | 112 | ||||
-rw-r--r-- | scripts/lib/glfm/update_example_snapshots.rb | 21 | ||||
-rwxr-xr-x | scripts/pipeline_test_report_builder.rb | 7 | ||||
-rwxr-xr-x | scripts/review_apps/gcp_cleanup.sh | 2 | ||||
-rwxr-xr-x | scripts/review_apps/review-apps.sh | 1 | ||||
-rw-r--r-- | scripts/rspec_helpers.sh | 5 | ||||
-rwxr-xr-x | scripts/undercoverage | 26 | ||||
-rw-r--r-- | scripts/utils.sh | 5 |
19 files changed, 231 insertions, 718 deletions
diff --git a/scripts/checkout-mr-source-sha b/scripts/checkout-mr-source-sha new file mode 100755 index 00000000000..962e3f1348d --- /dev/null +++ b/scripts/checkout-mr-source-sha @@ -0,0 +1,7 @@ +#!/bin/sh + +if [ -n "$CI_MERGE_REQUEST_SOURCE_BRANCH_SHA" ]; then + echo "Checking out \$CI_MERGE_REQUEST_SOURCE_BRANCH_SHA ($CI_MERGE_REQUEST_SOURCE_BRANCH_SHA) instead of \$CI_COMMIT_SHA (merge result commit $CI_COMMIT_SHA) so that code is in sync with gitlab images built upstream." + echo "See https://docs.gitlab.com/ee/development/testing_guide/end_to_end/index.html#with-pipeline-for-merged-results for more details." + git checkout -f ${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA} +fi diff --git a/scripts/decomposition/generate-loose-foreign-key b/scripts/decomposition/generate-loose-foreign-key index 35f84c64ce1..3f4c510020a 100755 --- a/scripts/decomposition/generate-loose-foreign-key +++ b/scripts/decomposition/generate-loose-foreign-key @@ -6,10 +6,8 @@ require 'optparse' $options = { - milestone: "#{Gitlab.version_info.major}.#{Gitlab.version_info.minor}", cross_schema: false, dry_run: false, - branch: true, rspec: true } @@ -24,18 +22,10 @@ OptionParser.new do |opts| $options[:dry_run] = v end - opts.on("-b", "--[no-]branch", "Create or not a new branch") do |v| - $options[:branch] = v - end - opts.on("-r", "--[no-]rspec", "Create or not a rspecs automatically") do |v| $options[:rspec] = v end - opts.on("-m", "--milestone MILESTONE", "Specify custom milestone (current: #{$options[:milestone]})") do |v| - $options[:milestone] = v - end - opts.on("-h", "--help", "Prints this help") do puts opts exit @@ -50,9 +40,7 @@ unless system("git diff --quiet") raise "There are uncommitted changes. Commit to continue." end -if Gitlab::Database.database_base_models.many? - raise 'Cannot run in multiple-databases mode. Use only `main:` in `config/database.yml`.' -end +$files_affected = [] puts "Re-creating current test database" ActiveRecord::Tasks::DatabaseTasks.drop_current @@ -86,6 +74,18 @@ def exec_cmd(*args, fail: nil) false end +def write_file(file_path, content) + $files_affected << file_path + File.write(file_path, content) +end + +def print_files_affected + puts "The following files have been generated/modified:" + $files_affected.each do |filepath| + puts filepath + end +end + def has_lfk?(definition) Gitlab::Database::LooseForeignKeys.definitions.any? do |lfk_definition| lfk_definition.from_table == definition.from_table && @@ -147,12 +147,10 @@ def add_definition_to_yaml(definition) end # emulate existing formatting - File.write( + write_file( Rails.root.join('config/gitlab_loose_foreign_keys.yml'), content.to_yaml.gsub(/^([- ] )/, ' \1') ) - - exec_cmd("git", "add", "config/gitlab_loose_foreign_keys.yml") end def generate_migration(definition) @@ -185,12 +183,9 @@ def generate_migration(definition) end EOF - File.write(migration_name, content) + write_file(migration_name, content) - exec_cmd("git", "add", migration_name, fail: "Failed to add migration file.") exec_cmd("bin/rails", "db:migrate", fail: "Failed to run db:migrate.") - exec_cmd("git", "add", "db/schema_migrations/#{timestamp}", "db/structure.sql", fail: "There are uncommitted changes. We should not have any.") - exec_cmd("git diff --exit-code --name-only", fail: "There are uncommitted changes. We should not have any.") end def class_by_table_name @@ -232,33 +227,7 @@ def add_test_to_specs(definition) lines = File.readlines(spec_path) insert_line = lines.count - 1 lines.insert(insert_line, "\n", *spec_test.lines) - File.write(spec_path, lines.join("")) - - # find a matching line - test_lines = (1..lines.count).select do |line| - lines[line-1].include?("it_behaves_like 'cleanup by a loose foreign key' do") - end.join(":") - - loop do - if system("bin/rspec", "#{spec_path}:#{test_lines}") - puts "Test seems fine?" - break - end - - puts "--------------------------------------------------" - puts "Test failed:" - puts "Edit: vim #{spec_path} (lines #{test_lines})" - puts "Re-run: bin/rspec #{spec_path}:#{test_lines}" - puts "--------------------------------------------------" - puts "Running bash. To exit do 'Ctrl-D' to re-run, or do 'Ctrl-C' to break (and ignore failure)." - puts - - unless exec_cmd("bash") - break - end - end - - exec_cmd("git", "add", spec_path, fail: "There are uncommitted changes. We should not have any.") + write_file(spec_path, lines.join("")) end def update_no_cross_db_foreign_keys_spec(definition) @@ -274,76 +243,7 @@ def update_no_cross_db_foreign_keys_spec(definition) return end - File.write(spec_path, updated.join("")) - exec_cmd("git", "add", spec_path, fail: "Failed to add changes from #{spec_path}") -end - -def commit_changes(definition) - branch_name = "remove-#{definition.to_table}_#{definition.from_table}_#{definition.column}-fk" - commit_title = "Swap FK #{definition.from_table} to #{definition.to_table} for LFK" - mr_title = "Swap FK #{definition.from_table}.#{definition.column} to #{definition.to_table} for LFK" - description = <<-EOF.strip_heredoc - Swaps FK for #{definition.from_table}.#{definition.column} to #{definition.to_table} - - Changelog: changed - EOF - - commit_message = "#{commit_title}\n\n#{description}" - - existing_branch = %x[git rev-parse --abbrev-ref HEAD].strip - - if $options[:branch] - unless exec_cmd("git", "checkout", "-b", branch_name) - raise "Failed to create branch: #{branch_name}" - end - end - - unless exec_cmd("git", "commit", "-m", commit_message) - raise "Failed to commit changes." - end - - if $options[:branch] - exec_cmd("git", "push", "origin", "-u", "HEAD", - "-o", "merge_request.create", - "-o", "merge_request.target=#{existing_branch}", - "-o", "merge_request.milestone=#{$options[:milestone]}", - "-o", "merge_request.title=#{mr_title}" - ) - - puts - puts "--------------------------------------------------" - puts "Put this as MR description:" - puts "--------------------------------------------------" - puts <<-EOF.strip_heredoc - ## What does this MR do and why? - - Per https://gitlab.com/groups/gitlab-org/-/epics/7249 - - As part of our CI "decomposition" efforts we need to remove all foreign keys that are cross-database (ie. between the planned \`main\` and \`ci\` databases). We are going to replace them all with ["loose foreign keys"](https://docs.gitlab.com/ee/development/database/loose_foreign_keys.html). - - Related: <DETAIL> - - ## Validations - - - **Best team to review (check off when reviewed):** TBD - - [ ] No way for user to access once parent is deleted. Please explain: <DETAIL> - - [ ] Possible to access once parent deleted but low user impact. Please explain: <DETAIL> - - [ ] Possible Sidekiq workers that may load directly and possibly lead to exceptions. Please explain: <DETAIL> - - [ ] Possible user impact to be evaluated or mitigated. Please explain: <DETAIL> - - [ ] Is this FK safe to be removed to avoid LOCKing problems? (Explanation: https://gitlab.com/groups/gitlab-org/-/epics/7249#note_819662046). Please explain: <DETAIL> - - ## MR acceptance checklist - - This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability. - - * [ ] I have evaluated the [MR acceptance checklist](https://docs.gitlab.com/ee/development/code_review.html#acceptance-checklist) for this MR. - - /label ~"ci-decomposition::phase4" ~"database::review pending" ~"devops::enablement" ~"group::sharding" ~"section::enablement" ~"sharding::active" ~"type::feature" ~"workflow::in dev" ~backend ~"ci-decomposition" ~database ~"Category:Sharding" - /milestone %"#{$options[:milestone]}" - /assign_reviewer @ahegyi - EOF - puts "--------------------------------------------------" - end + write_file(spec_path, updated.join("")) end all_foreign_keys = ActiveRecord::Base.connection.tables.flat_map do |table| @@ -400,6 +300,8 @@ all_foreign_keys.each_with_index do |definition, idx| generate_migration(definition) add_test_to_specs(definition) update_no_cross_db_foreign_keys_spec(definition) - commit_changes(definition) end + +print_files_affected + puts diff --git a/scripts/determine-qa-tests b/scripts/determine-qa-tests new file mode 100755 index 00000000000..b1e9d8e9312 --- /dev/null +++ b/scripts/determine-qa-tests @@ -0,0 +1,106 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'optparse' + +# This script returns end-to-end tests or test directories. The returned value is stored in QA_TESTS +# variable for executing only those tests. + +class DetermineQATests # rubocop:disable Gitlab/NamespacedClass + def initialize(options) + @changed_files = options.delete(:changed_files) + @mr_labels = options.delete(:mr_labels) || [] + end + + def execute + # If only e2e test files have changed, run only those tests + qa_tests = if has_qa_spec_only_changes? + changed_files + + # If only non qa files have changed, use the devops MR label to run the files in the related directory + # However, if a feature flag file has changed, do not return any specific test/test directory + elsif has_non_qa_only_changes? && mr_labels.any? && !has_dev_ops_feature_flag_changes? + devops_stage = devops_stage_from_mr_labels + + qa_spec_directories_for_devops_stage(devops_stage) if devops_stage + end + + trim_path(qa_tests).join(' ') if qa_tests + end + + private + + attr_reader :changed_files, :mr_labels + + # Are the changed files only qa specs? + # + # @return [Boolean] whether the changes files are only qa specs + def has_qa_spec_only_changes? + changed_files.all? { |file_path| file_path =~ %r{^qa/qa/specs/features/} } + end + + # Are the changed files only outside the qa directory? + # + # @return [Boolean] whether the changes files are outside of qa directory + def has_non_qa_only_changes? + changed_files.none? { |file_path| file_path =~ %r{^qa/} } + end + + # Are the changed files for development and ops feature flags? + # + # @return [Boolean] whether the changes files are for development and ops feature flags + def has_dev_ops_feature_flag_changes? + changed_files.any? { |file_path| file_path =~ %r{/feature_flags/(development|ops)/.*\.yml} } + end + + # Remove the leading `qa/` from the file or directory paths + # + # @param [Array] paths Array of file or directory paths + # @return [Array] Array of files or directories with the first occurance of `qa/` removed + def trim_path(paths) + paths.map { |path| path.delete_prefix("qa/") } + end + + # Extract devops stage from MR labels + # + # @return [String] a devops stage + def devops_stage_from_mr_labels + mr_labels.find { |label| label =~ /^devops::/ }&.delete_prefix('devops::') + end + + # Get qa spec directories for devops stage + # + # @param [String] devops_stage a devops stage + # @return [Array] qa spec directories + def qa_spec_directories_for_devops_stage(devops_stage) + Dir.glob("qa/qa/specs/**/*/").select { |dir| dir =~ %r{\d+_#{devops_stage}/$} } + end +end + +if $0 == __FILE__ + options = {} + + OptionParser.new do |opts| + opts.on("-f", "--files CHANGED_FILES_PATH", String, + "A path to a file containing a list of changed files") do |value| + changed_files_path = value + abort("ERROR: The specified changed files path does not exist") unless File.exist?(changed_files_path) + + changed_files = File.read(changed_files_path).split(' ') + abort("ERROR: There are no changed files") if changed_files.empty? + + options[:changed_files] = changed_files + end + + opts.on("-l", "--labels MR_LABELS", String, "A comma separated list of MR labels") do |value| + options[:mr_labels] = Array(value&.split(',')).compact + end + + opts.on("-h", "--help", "Prints this help") do + puts opts + exit + end + end.parse! + + puts DetermineQATests.new(options).execute +end diff --git a/scripts/frontend/stylelint/stylelint_duplicate_selectors.js b/scripts/frontend/stylelint/stylelint_duplicate_selectors.js deleted file mode 100644 index 77c1f1292b7..00000000000 --- a/scripts/frontend/stylelint/stylelint_duplicate_selectors.js +++ /dev/null @@ -1,25 +0,0 @@ -const stylelint = require('stylelint'); -const utils = require('./stylelint_utils'); - -const ruleName = 'stylelint-gitlab/duplicate-selectors'; - -const messages = stylelint.utils.ruleMessages(ruleName, { - expected: (selector1, selector2) => { - return `"${selector1}" and "${selector2}" have the same properties.`; - }, -}); - -module.exports = stylelint.createPlugin(ruleName, (enabled) => { - if (!enabled) { - return; - } - - // eslint-disable-next-line consistent-return - return (root, result) => { - const selectorGroups = {}; - utils.createPropertiesHashmap(root, result, ruleName, messages, selectorGroups, true); - }; -}); - -module.exports.ruleName = ruleName; -module.exports.messages = messages; diff --git a/scripts/frontend/stylelint/stylelint_utility_classes.js b/scripts/frontend/stylelint/stylelint_utility_classes.js deleted file mode 100644 index ad2b2ddbb20..00000000000 --- a/scripts/frontend/stylelint/stylelint_utility_classes.js +++ /dev/null @@ -1,25 +0,0 @@ -const stylelint = require('stylelint'); -const utils = require('./stylelint_utils'); -const utilityClasses = require('./utility_classes_map'); - -const ruleName = 'stylelint-gitlab/utility-classes'; - -const messages = stylelint.utils.ruleMessages(ruleName, { - expected: (selector1, selector2) => { - return `"${selector1}" has the same properties as our BS4 utility class "${selector2}" so please use that instead.`; - }, -}); - -module.exports = stylelint.createPlugin(ruleName, (enabled) => { - if (!enabled) { - return; - } - - // eslint-disable-next-line consistent-return - return (root, result) => { - utils.createPropertiesHashmap(root, result, ruleName, messages, utilityClasses, false); - }; -}); - -module.exports.ruleName = ruleName; -module.exports.messages = messages; diff --git a/scripts/frontend/stylelint/stylelint_utility_map.js b/scripts/frontend/stylelint/stylelint_utility_map.js deleted file mode 100644 index 187b2065823..00000000000 --- a/scripts/frontend/stylelint/stylelint_utility_map.js +++ /dev/null @@ -1,64 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const postcss = require('postcss'); -const prettier = require('prettier'); -const sass = require('sass'); - -const utils = require('./stylelint_utils'); - -const ROOT_PATH = path.resolve(__dirname, '../../..'); -const hashMapPath = path.resolve(__dirname, './utility_classes_map.js'); - -// -// This creates a JS based hash map (saved in utility_classes_map.js) of the different values in the utility classes -// -sass.render( - { - data: ` - @import './functions'; - @import './variables'; - @import './mixins'; - @import './utilities'; - `, - includePaths: [path.resolve(ROOT_PATH, 'node_modules/bootstrap/scss')], - }, - (err, result) => { - if (err) { - return console.error('Error ', err); - } - - const cssResult = result.css.toString(); - - // We just use postcss to create a CSS tree - return postcss([]) - .process(cssResult, { - // This suppresses a postcss warning - from: undefined, - }) - .then((processedResult) => { - const selectorGroups = {}; - utils.createPropertiesHashmap( - processedResult.root, - processedResult, - null, - null, - selectorGroups, - true, - ); - - const prettierOptions = prettier.resolveConfig.sync(hashMapPath); - const prettyHashmap = prettier.format( - `module.exports = ${JSON.stringify(selectorGroups)};`, - prettierOptions, - ); - - fs.writeFile(hashMapPath, prettyHashmap, (e) => { - if (e) { - return console.log(e); - } - - return console.log('The file was saved!'); - }); - }); - }, -); diff --git a/scripts/frontend/stylelint/stylelint_utils.js b/scripts/frontend/stylelint/stylelint_utils.js deleted file mode 100644 index c9d9c7d9aad..00000000000 --- a/scripts/frontend/stylelint/stylelint_utils.js +++ /dev/null @@ -1,78 +0,0 @@ -const md5 = require('md5'); -const stylelint = require('stylelint'); - -module.exports.createPropertiesHashmap = ( - ruleRoot, - result, - ruleName, - messages, - selectorGroups, - addSelectors, -) => { - ruleRoot.walkRules((rule) => { - const selector = rule.selector.replace(/(?:\r\n|\r|\n)/g, ' '); - - if ( - rule && - rule.parent && - rule.parent.type !== 'atrule' && - !( - selector.includes('-webkit-') || - selector.includes('-moz-') || - selector.includes('-o-') || - selector.includes('-ms-') || - selector.includes(':') - ) - ) { - let cssArray = []; - rule.nodes.forEach((property) => { - const { prop, value } = property; - if (property && value) { - const propval = `${prop}${value}${property.important ? '!important' : ''}`; - cssArray.push(propval); - } - }); - - cssArray = cssArray.sort(); - const cssContent = cssArray.toString(); - - if (cssContent) { - const hashValue = md5(cssContent); - const selObj = selectorGroups[hashValue]; - - const selectorLine = `${selector} (${ - rule.source.input.file ? `${rule.source.input.file} -` : '' - }${rule.source.start.line}:${rule.source.start.column})`; - - if (selObj) { - if (selectorGroups[hashValue].selectors.indexOf(selector) === -1) { - let lastSelector = - selectorGroups[hashValue].selectors[selectorGroups[hashValue].selectors.length - 1]; - - // So we have nicer formatting if it is the same file, we remove the filename - lastSelector = lastSelector.replace(`${rule.source.input.file} - `, ''); - - if (messages) { - stylelint.utils.report({ - result, - ruleName, - message: messages.expected(selector, lastSelector), - node: rule, - word: rule.node, - }); - } - - if (addSelectors) { - selectorGroups[hashValue].selectors.push(selectorLine); - } - } - } else if (addSelectors) { - // eslint-disable-next-line no-param-reassign - selectorGroups[hashValue] = { - selectors: [selectorLine], - }; - } - } - } - }); -}; diff --git a/scripts/frontend/stylelint/utility_classes_map.js b/scripts/frontend/stylelint/utility_classes_map.js deleted file mode 100644 index a174812ff93..00000000000 --- a/scripts/frontend/stylelint/utility_classes_map.js +++ /dev/null @@ -1,259 +0,0 @@ -module.exports = { - '99097f29a9473b56eacdb9ff0681c366': { selectors: ['.align-baseline (1:1)'] }, - d969b318bb994e104e8c965006d71cb7: { selectors: ['.align-top (5:1)'] }, - '8cd54ab97b9cc43cb9d13d2ea7c601c7': { selectors: ['.align-middle (9:1)'] }, - dd06eb6c49e979b7a9fdaa7119aa0a0b: { selectors: ['.align-bottom (13:1)'] }, - '0af1e90cbc468615e299ec9f49e97c4a': { selectors: ['.align-text-bottom (17:1)'] }, - '50af706df238cf59bdc634fc684ba0c9': { selectors: ['.align-text-top (21:1)'] }, - c968922e6e47445362129a684b5913c0: { selectors: ['.bg-primary (25:1)'] }, - '3c397f9786c24cff4779a11cf5b3d7e7': { selectors: ['.bg-secondary (35:1)'] }, - '659677469a4477267fabc1788f7cad4e': { selectors: ['.bg-success (45:1)'] }, - '56d246d5b6a708a4c6f78dbd2444106c': { selectors: ['.bg-info (55:1)'] }, - '6bec0a33df3a6380c30103db5c273455': { selectors: ['.bg-warning (65:1)'] }, - '0ce5d074c8667ce6c32360658f428d5d': { selectors: ['.bg-danger (75:1)'] }, - '0d0269c62a01e97caa9039d227a25d12': { selectors: ['.bg-light (85:1)'] }, - '3a56309ad8c5b46ebcc3b13fe1987ac1': { selectors: ['.bg-dark (95:1)'] }, - '0e252f8dd392a33343d3d5efc1e3194a': { selectors: ['.bg-white (105:1)'] }, - '3af6f52f0ed4f98e797d5f10a35ca6bc': { selectors: ['.bg-transparent (109:1)'] }, - '16da7fdce74577ceab356509db565612': { selectors: ['.border (113:1)'] }, - '929622517ca05efde3b51e5f1a57064e': { selectors: ['.border-top (117:1)'] }, - '7283090353df54f1d515a6ceddfb9693': { selectors: ['.border-right (121:1)'] }, - bd5670d71332c652b46db82949042e31: { selectors: ['.border-bottom (125:1)'] }, - fa71e003d04734a898a85cc5285e3cbb: { selectors: ['.border-left (129:1)'] }, - ed482cea071e316f29d78fd93c3f3644: { selectors: ['.border-0 (133:1)'] }, - '90cb661baf21e10be6e479cb0544b1a7': { selectors: ['.border-top-0 (137:1)'] }, - '8a32707eaa09fc998bf8cc915710b60c': { selectors: ['.border-right-0 (141:1)'] }, - a6f01957e142a000e7742b31ac6c2331: { selectors: ['.border-bottom-0 (145:1)'] }, - c740fe952cc1985ee14f7d1c7a359a29: { selectors: ['.border-left-0 (149:1)'] }, - af9dd93e9780306ffa4bb25a6384902f: { selectors: ['.border-primary (153:1)'] }, - afa290dfe58cca06be5924ceae1b019b: { selectors: ['.border-secondary (157:1)'] }, - '9b1ac460bdddf1e0164d7bf988cc2da8': { selectors: ['.border-success (161:1)'] }, - '091cbf41d6be12061382fa571ee1ce82': { selectors: ['.border-info (165:1)'] }, - '3ada321d4a387901dad6d80e1b6be3fd': { selectors: ['.border-warning (169:1)'] }, - '13b4713dd52c1e359d1b43dd658cb249': { selectors: ['.border-danger (173:1)'] }, - '0048e110875ea22b04104d55e764a367': { selectors: ['.border-light (177:1)'] }, - a900b6b567c9a911326cdd0e19f40f8e: { selectors: ['.border-dark (181:1)'] }, - '78bcd867ac9677c743c2bc33b872f27b': { selectors: ['.border-white (185:1)'] }, - '35ef133b860874a5879e9451c491bc1c': { selectors: ['.rounded-sm (189:1)'] }, - e0fc10c49c7b7f4d1924336d21a4f64e: { selectors: ['.rounded (193:1)'] }, - '1b74b9d0a7d6a59281b5b5cae43c859a': { selectors: ['.rounded-top (197:1)'] }, - '20b75f55f39e662e038d51a6442c03df': { selectors: ['.rounded-right (202:1)'] }, - '83ea6db794873239c21f44af25618677': { selectors: ['.rounded-bottom (207:1)'] }, - '8464e9e8001e65dfc06397436a5eebd7': { selectors: ['.rounded-left (212:1)'] }, - a1148f40e8c509b2bcc829e2181c7582: { selectors: ['.rounded-lg (217:1)'] }, - '59c2f788287fa43caf5891adfc5c796e': { selectors: ['.rounded-circle (221:1)'] }, - '7f35b0a4b74ee7174b13cb841df0bdb3': { selectors: ['.rounded-pill (225:1)'] }, - '31a632ba94f8c41558bd6044458f1459': { selectors: ['.rounded-0 (229:1)'] }, - '16aaf53ab29d6b248b0257f2fa413914': { selectors: ['.d-none (239:1)'] }, - '4f42736ac9217039ed791b4306e60aeb': { selectors: ['.d-inline (243:1)'] }, - '067efa04b76649e8afcdceb9f5f7e870': { selectors: ['.d-inline-block (247:1)'] }, - de54f49149fb9b512aa79ad9ada838f2: { selectors: ['.d-block (251:1)'] }, - '80fc32acbc0c28ee890a160c23529d26': { selectors: ['.d-table (255:1)'] }, - '6a87b1db48298ca94cbe5dee79a6eed1': { selectors: ['.d-table-row (259:1)'] }, - b9896f0d94760bf5920f47904e9f7512: { selectors: ['.d-table-cell (263:1)'] }, - d25c51f38c4d057209b96c664de68c44: { selectors: ['.d-flex (267:1)'] }, - e72d46b636d5b8e17e771daa95793f33: { selectors: ['.d-inline-flex (271:1)'] }, - '2c433b7c14a5ae32cfa8ec7867ee8526': { selectors: ['.embed-responsive (460:1)'] }, - '56b318b8d8eb845b769d60cefcd131bb': { - selectors: [ - '.embed-responsive .embed-responsive-item, .embed-responsive iframe, .embed-responsive embed, .embed-responsive object, .embed-responsive video (471:1)', - ], - }, - c5009af89633c4d2f71a0a9fa333630d: { selectors: ['.flex-row (501:1)'] }, - '7b06a3d956579cd64b6f5b1a57255369': { selectors: ['.flex-column (505:1)'] }, - '21744a8c4dc6ed1519903b4236b00af4': { selectors: ['.flex-row-reverse (509:1)'] }, - '18d903735f9c71070b6d8166aa1112f1': { selectors: ['.flex-column-reverse (513:1)'] }, - e2a57aa8196347d4da84f33a4f551325: { selectors: ['.flex-wrap (517:1)'] }, - b6b29f75b174b5609f9e7d5eef457b70: { selectors: ['.flex-nowrap (521:1)'] }, - '839230fc7c0abacb6418b49d8f10b27f': { selectors: ['.flex-wrap-reverse (525:1)'] }, - '924d9b261944a8e8ff684d5b519062bb': { selectors: ['.flex-fill (529:1)'] }, - '5ed396aeb08464b7df8fc37d29455319': { selectors: ['.flex-grow-0 (533:1)'] }, - '94251dc4d012339a3e37df6196fc79bb': { selectors: ['.flex-grow-1 (537:1)'] }, - '0eeed7dabca0452a46574776a4485e6e': { selectors: ['.flex-shrink-0 (541:1)'] }, - c69e60d5e51a1b74d22b172ab98ef9d5: { selectors: ['.flex-shrink-1 (545:1)'] }, - '03401c1a81eb6d4639f020f76dd60176': { selectors: ['.justify-content-start (549:1)'] }, - '39e3d2c344e78869c98ef099249e717d': { selectors: ['.justify-content-end (553:1)'] }, - '3b2d00c0bea857ab78a71b0872842980': { selectors: ['.justify-content-center (557:1)'] }, - b1c3c9edd20ed7c08b43863d38ebee40: { selectors: ['.justify-content-between (561:1)'] }, - a11b4b1d983c5fa75777f273e998f73e: { selectors: ['.justify-content-around (565:1)'] }, - '50e33f29f65bfffa6a3591fcb6045ca9': { selectors: ['.align-items-start (569:1)'] }, - e44b276e47415ec19b74cc16740ada1d: { selectors: ['.align-items-end (573:1)'] }, - '4a9d2716bca651758564059dceed1271': { selectors: ['.align-items-center (577:1)'] }, - fd970b017f7f558f30cb273bf71ede7d: { selectors: ['.align-items-baseline (581:1)'] }, - '7204b6b00b69f8af1e4a24c9b6e7f7f9': { selectors: ['.align-items-stretch (585:1)'] }, - '350fbb74eb70bd05f9438067c3990e9f': { selectors: ['.align-content-start (589:1)'] }, - '74d61397b4fcbf608f3dba39ab3b2a1b': { selectors: ['.align-content-end (593:1)'] }, - eed1ab4ee9e5b327a434512176741548: { selectors: ['.align-content-center (597:1)'] }, - '19878ab832978ef7e1746ac2fe4084b2': { selectors: ['.align-content-between (601:1)'] }, - dded333d0522692809517039f5a727c1: { selectors: ['.align-content-around (605:1)'] }, - '5cbb83ea2af7e9db8ef13f4c7d6db875': { selectors: ['.align-content-stretch (609:1)'] }, - dc3df46d3c023184d375a63a71916646: { selectors: ['.align-self-auto (613:1)'] }, - '0c6d2d8c9732c571f9cf61a4b1d2877f': { selectors: ['.align-self-start (617:1)'] }, - fe7c6071e3e17214df1bdd38850d9ff0: { selectors: ['.align-self-end (621:1)'] }, - '9611bbad74d72d50cf238088576a5089': { selectors: ['.align-self-center (625:1)'] }, - '4bc5edddf5981866946175bfedb7247f': { selectors: ['.align-self-baseline (629:1)'] }, - '4fffdd27ec60120ec9ed16fd7feef801': { selectors: ['.align-self-stretch (633:1)'] }, - '39e658501f5502b35919f02fa9591360': { selectors: ['.float-left (1185:1)'] }, - b51436c537ffc4269b1533e44d7c3467: { selectors: ['.float-right (1189:1)'] }, - c4597a87d2c793a6d696cfe06f6c95ce: { selectors: ['.float-none (1193:1)'] }, - '16b2e7e5b666b69b9581be6a44947d6f': { selectors: ['.user-select-all (1249:1)'] }, - f5c4d8f25b8338031e719edc77659558: { selectors: ['.user-select-auto (1253:1)'] }, - '71ba65d43dcc2b1c3f47ac2578983e83': { selectors: ['.user-select-none (1257:1)'] }, - '5902adc842d83ad70b89ece99b82a7a9': { selectors: ['.overflow-auto (1261:1)'] }, - bfb19f18b91fa46f0502e32153287cc4: { selectors: ['.overflow-hidden (1265:1)'] }, - aaf8dc6e0768e59f3098a98a5c144d66: { selectors: ['.position-static (1269:1)'] }, - '79592de2383045d15ab57d35aa1dab95': { selectors: ['.position-relative (1273:1)'] }, - a7c272f53d0368730bde4c2740ffb5c3: { selectors: ['.position-absolute (1277:1)'] }, - dad0bb92d53f17cf8affc10f77824b7f: { selectors: ['.position-fixed (1281:1)'] }, - '6da0f6a7354a75fe6c95b08a4cabc06f': { selectors: ['.position-sticky (1285:1)'] }, - '66602e21eea7b673883155c8f42b9590': { selectors: ['.fixed-top (1289:1)'] }, - '33ea70eb6db7f6ab3469680f182abb19': { selectors: ['.fixed-bottom (1297:1)'] }, - '947009e63b4547795ef7cc873f4e4ae9': { selectors: ['.sr-only (1313:1)'] }, - '9220ad156a70c2586b15fe2b9b6108b2': { selectors: ['.shadow-sm (1334:1)'] }, - b1b8c0ff70767ca2160811a026766982: { selectors: ['.shadow (1338:1)'] }, - da0792abe99964acb6692a03c61d6dd8: { selectors: ['.shadow-lg (1342:1)'] }, - bb2173057af1cf20e687469b2d9cbb3c: { selectors: ['.shadow-none (1346:1)'] }, - '6d8abb6519a186483b25429ab8b9765e': { selectors: ['.w-25 (1350:1)'] }, - a087c1ffdf8ead76cdd37445b414d63e: { selectors: ['.w-50 (1354:1)'] }, - '28180742013a90275be5633e6ec0dd51': { selectors: ['.w-75 (1358:1)'] }, - '195a03bc95a0af0ba6c8824db97a0b2f': { selectors: ['.w-100 (1362:1)'] }, - e67c74b650d6236b03be9dfc10c78e32: { selectors: ['.w-auto (1366:1)'] }, - c1b6262b3ee069addc1fbe46f64aac4e: { selectors: ['.h-25 (1370:1)'] }, - a520396ae349bef86145e0761aa0699e: { selectors: ['.h-50 (1374:1)'] }, - '7c53b57d54beb087fd7ab8b669c5fe60': { selectors: ['.h-75 (1378:1)'] }, - ad74f1972cb745b7a78b03e16a387f21: { selectors: ['.h-100 (1382:1)'] }, - '2cd49c3d63d260ba4f0b23c559ad05e0': { selectors: ['.h-auto (1386:1)'] }, - '0b43071a67efc45ee1735fdc2491313c': { selectors: ['.mw-100 (1390:1)'] }, - eac31a6f08e5c935e24b97df0fdad579: { selectors: ['.mh-100 (1394:1)'] }, - bd5f5d687d100d127abeb044fc78a3fd: { selectors: ['.min-vw-100 (1398:1)'] }, - '8dec7662784b943d3ca0615df59d7970': { selectors: ['.min-vh-100 (1402:1)'] }, - eacc17f16197f6c0291ced3e7cfc067e: { selectors: ['.vw-100 (1406:1)'] }, - '1a2ec1c9ad7db8d4156ca76a81416c49': { selectors: ['.vh-100 (1410:1)'] }, - cfdb4f497b16074959bfd3deb7ea9c42: { selectors: ['.m-0 (1414:1)'] }, - '4d666c270ba50524312d97c4b937d153': { selectors: ['.mt-0, .my-0 (1418:1)'] }, - eccf47ccd76ceffb4b139cb6c080b5ac: { selectors: ['.mr-0, .mx-0 (1423:1)'] }, - '9bc513e73c0bdc6efdf170cb31de16d1': { selectors: ['.mb-0, .my-0 (1428:1)'] }, - e99cfc55b03f0e67f11628b19889ad7b: { selectors: ['.ml-0, .mx-0 (1433:1)'] }, - e1c39484d90d2acaa00973531f47f738: { selectors: ['.m-1 (1438:1)'] }, - '63791bc02eccfdfa2c01621a801e565f': { selectors: ['.mt-1, .my-1 (1442:1)'] }, - bcb27ab9d7dcfdd0d7cacad02709966c: { selectors: ['.mr-1, .mx-1 (1447:1)'] }, - cb5d1c4328e25b5bc93be9a252973690: { selectors: ['.mb-1, .my-1 (1452:1)'] }, - b80b1010c7dcfbb30bed9015c4f2e969: { selectors: ['.ml-1, .mx-1 (1457:1)'] }, - ecab1c9cdf8a562e3c0f70307aeafa89: { selectors: ['.m-2 (1462:1)'] }, - '6505fe17fbbd88b1884113a754aa82ab': { selectors: ['.mt-2, .my-2 (1466:1)'] }, - '6f0c7d09d1e729f332c4671ccc2b48c0': { selectors: ['.mr-2, .mx-2 (1471:1)'] }, - '70ef7b668b382b3c747b2d73e08cdbed': { selectors: ['.mb-2, .my-2 (1476:1)'] }, - '2d7f277cc78ed324a8fc1f71ab281e1f': { selectors: ['.ml-2, .mx-2 (1481:1)'] }, - '8ebcfe52fd4024861082ffb1735747a7': { selectors: ['.m-3 (1486:1)'] }, - '9965fb516bdb72b87023a533123a8035': { selectors: ['.mt-3, .my-3 (1490:1)'] }, - b1fcbbb1dc6226f6da6000830088e051: { selectors: ['.mr-3, .mx-3 (1495:1)'] }, - '02204826cfbe3da98535c0d802870940': { selectors: ['.mb-3, .my-3 (1500:1)'] }, - '0259859060250ae6b730218733e7a437': { selectors: ['.ml-3, .mx-3 (1505:1)'] }, - '8cf300dab2a4994a105eeddda826f2e6': { selectors: ['.m-4 (1510:1)'] }, - '1ba62fdddd3349f52a452050688905c7': { selectors: ['.mt-4, .my-4 (1514:1)'] }, - '66a104129fa13db5a0829567fba6ee41': { selectors: ['.mr-4, .mx-4 (1519:1)'] }, - eefcc4c10b79e70e8e8a5a66fb2b7aa1: { selectors: ['.mb-4, .my-4 (1524:1)'] }, - eb1503656dc920d15a31116956fdffa4: { selectors: ['.ml-4, .mx-4 (1529:1)'] }, - '79cbb6e5c9b73fd0be29d4fc5733a099': { selectors: ['.m-5 (1534:1)'] }, - '67d8671699df706a428e7da42a7141cb': { selectors: ['.mt-5, .my-5 (1538:1)'] }, - e9cb4a0a8a60ff018c87a0b7efa9de29: { selectors: ['.mr-5, .mx-5 (1543:1)'] }, - '93f579214354dbd8cb60209c068f0086': { selectors: ['.mb-5, .my-5 (1548:1)'] }, - '2a789d4af97d2b87fd0bf2b4626120cd': { selectors: ['.ml-5, .mx-5 (1553:1)'] }, - '64a89d28e8287c1a0ac153001082644c': { selectors: ['.p-0 (1558:1)'] }, - b03aa6db5ddf110bbdbefbbec43fda30: { selectors: ['.pt-0, .py-0 (1562:1)'] }, - e38192ca32a98888d4c4876880f4fece: { selectors: ['.pr-0, .px-0 (1567:1)'] }, - '70fe8ef50e999ddd29506f672c107069': { selectors: ['.pb-0, .py-0 (1572:1)'] }, - '9355e8cd9109049726475ba356661bcf': { selectors: ['.pl-0, .px-0 (1577:1)'] }, - '0d4c53468c2658c5324b9ec7a8ca6de2': { selectors: ['.p-1 (1582:1)'] }, - d74e430b2a56b3a4e20065c972b7fa3f: { selectors: ['.pt-1, .py-1 (1586:1)'] }, - '21e4644967aedd19888b6f4a700b629b': { selectors: ['.pr-1, .px-1 (1591:1)'] }, - e315a7b9b7a1d0df3ea7d95af5203a0b: { selectors: ['.pb-1, .py-1 (1596:1)'] }, - '14630ca122e1d9830a9ef5591c4097d0': { selectors: ['.pl-1, .px-1 (1601:1)'] }, - '5b1c65e5139e86e5f4755824f8b77d13': { selectors: ['.p-2 (1606:1)'] }, - '244af70950a1e200d3849f75ce51d707': { selectors: ['.pt-2, .py-2 (1610:1)'] }, - b583832738cad724c7c23e5c14ac9bfb: { selectors: ['.pr-2, .px-2 (1615:1)'] }, - e1e633c4f1375e8276154192d8899e39: { selectors: ['.pb-2, .py-2 (1620:1)'] }, - '676b01e25f0dbb3f7d2f2529231cda08': { selectors: ['.pl-2, .px-2 (1625:1)'] }, - '9b5165e3333b22801f2287f7983d7516': { selectors: ['.p-3 (1630:1)'] }, - '5bcaa9df87a507f6cd14659ea176bdc5': { selectors: ['.pt-3, .py-3 (1634:1)'] }, - f706637180776c5589385599705a2409: { selectors: ['.pr-3, .px-3 (1639:1)'] }, - '41157cfbcf47990b383b5b0379386ab2': { selectors: ['.pb-3, .py-3 (1644:1)'] }, - cac1e7a204bb6a1f42707b684ad46238: { selectors: ['.pl-3, .px-3 (1649:1)'] }, - '43e0671cd41a4b7590284888b607a134': { selectors: ['.p-4 (1654:1)'] }, - '116b0f95ebde1ff8907e488413a88854': { selectors: ['.pt-4, .py-4 (1658:1)'] }, - ecb06765fe691d892df000eebbb23dcc: { selectors: ['.pr-4, .px-4 (1663:1)'] }, - '1331503a48d36025c861e660bc615048': { selectors: ['.pb-4, .py-4 (1668:1)'] }, - f8665f7e547e499abd7ac63813b274f5: { selectors: ['.pl-4, .px-4 (1673:1)'] }, - '4160a315459f1b5a98255863f42136fe': { selectors: ['.p-5 (1678:1)'] }, - f55a6b2de6a434ec7b4375f06f4fad75: { selectors: ['.pt-5, .py-5 (1682:1)'] }, - '19391dc45c8d7730a86d521c28f52c3f': { selectors: ['.pr-5, .px-5 (1687:1)'] }, - '15898bcb7ff74a60006f9931422b4ad3': { selectors: ['.pb-5, .py-5 (1692:1)'] }, - '6290bdc6355aed1e9b27379003aa4828': { selectors: ['.pl-5, .px-5 (1697:1)'] }, - '04a144abd49b1402e3588fb4ec237ec0': { selectors: ['.m-n1 (1702:1)'] }, - b81793fb475f7cc3a0698aa92ae41777: { selectors: ['.mt-n1, .my-n1 (1706:1)'] }, - '3c39187fca578a65e7aa2d704208cde8': { selectors: ['.mr-n1, .mx-n1 (1711:1)'] }, - c6c24440dd9edef71c7bc7da951dccd5: { selectors: ['.mb-n1, .my-n1 (1716:1)'] }, - '8b528f327ea3e562fc61d13c9f3901d8': { selectors: ['.ml-n1, .mx-n1 (1721:1)'] }, - '4a1926d211f81e5c96d4c9c4f2097774': { selectors: ['.m-n2 (1726:1)'] }, - f78f1986440a943aeb5553cb11b2f23c: { selectors: ['.mt-n2, .my-n2 (1730:1)'] }, - '941a9ea73cc97224a3f55ef65ab727c9': { selectors: ['.mr-n2, .mx-n2 (1735:1)'] }, - '074441007089a0c89cda9bfcb31c763a': { selectors: ['.mb-n2, .my-n2 (1740:1)'] }, - '7f9e45f6286b38eec32d471b7c1b28b6': { selectors: ['.ml-n2, .mx-n2 (1745:1)'] }, - c322ab87c4e181d835e6c17a1ca179ad: { selectors: ['.m-n3 (1750:1)'] }, - dee7caca54972db68f956a7cfd5d372b: { selectors: ['.mt-n3, .my-n3 (1754:1)'] }, - '60c8b6aa0702bbc3eed911f7483876ce': { selectors: ['.mr-n3, .mx-n3 (1759:1)'] }, - '595290cc6428614c03bac4631fe3fc12': { selectors: ['.mb-n3, .my-n3 (1764:1)'] }, - '7b968bc3150d9768117e099261cf23df': { selectors: ['.ml-n3, .mx-n3 (1769:1)'] }, - '90ca4942944fbc4dd543a821244daa31': { selectors: ['.m-n4 (1774:1)'] }, - '1716842c880f907fc62889f928273e0c': { selectors: ['.mt-n4, .my-n4 (1778:1)'] }, - '087ab1596162227935a974af5e856174': { selectors: ['.mr-n4, .mx-n4 (1783:1)'] }, - f53133714696eaf034f3efe057ac82fe: { selectors: ['.mb-n4, .my-n4 (1788:1)'] }, - ce56811b70ca493d65e3f63acfa2d9df: { selectors: ['.ml-n4, .mx-n4 (1793:1)'] }, - '7f52135983abe2eccfbb72e5d16a6a20': { selectors: ['.m-n5 (1798:1)'] }, - de8fc351c8c23bd867cf10a14642899a: { selectors: ['.mt-n5, .my-n5 (1802:1)'] }, - b134cecaa2883916d516ae6a2b1a3891: { selectors: ['.mr-n5, .mx-n5 (1807:1)'] }, - '124ccc6a210fadb889b2742eb0f9b540': { selectors: ['.mb-n5, .my-n5 (1812:1)'] }, - c3948b63b7b0c864f4f78faf640fd9a0: { selectors: ['.ml-n5, .mx-n5 (1817:1)'] }, - e57ec4fe9e8ed36e38f1c50041fc9f47: { selectors: ['.m-auto (1822:1)'] }, - f10380665932186d1effe0674a74ba12: { selectors: ['.mt-auto, .my-auto (1826:1)'] }, - '2ce71a27023eb50a47c24a99399faa28': { selectors: ['.mr-auto, .mx-auto (1831:1)'] }, - '196c77d357d314678cd3a99cfacbea96': { selectors: ['.mb-auto, .my-auto (1836:1)'] }, - ca007ce268b463a6bf42145cf5ce3685: { selectors: ['.ml-auto, .mx-auto (1841:1)'] }, - '695821b1faf108a559486730b246876a': { selectors: ['.text-monospace (3590:1)'] }, - a8fc5ca823f51d72673577064387a029: { selectors: ['.text-justify (3594:1)'] }, - '04e83ef005e9c653fb368008f8ae8a33': { selectors: ['.text-wrap (3598:1)'] }, - '0bb94dfab7ca2c9892ebbd993b2baf0f': { selectors: ['.text-nowrap (3602:1)'] }, - aea4958ce85ddc0cbffca1015c3a7eba: { selectors: ['.text-truncate (3606:1)'] }, - '52b9443947b6b94a5c7e1b837da115e2': { selectors: ['.text-left (3612:1)'] }, - baaf5136fc6e1c54ba29b6040f166d5f: { selectors: ['.text-right (3616:1)'] }, - '282aa4319bee75af06cc2632b7124e26': { selectors: ['.text-center (3620:1)'] }, - '1cb1c8ad9b560eca25ebcefe95c1b7fa': { selectors: ['.text-lowercase (3676:1)'] }, - '45234533eac658ba2857e9c4d3bc78a5': { selectors: ['.text-uppercase (3680:1)'] }, - f9e3f64237f2e81b6aed84223a0ceb1d: { selectors: ['.text-capitalize (3684:1)'] }, - '09caca3d36aa9f3ef815e0da7e1a16b4': { selectors: ['.font-weight-light (3688:1)'] }, - '23544682bb214a76e383e032d4056be4': { selectors: ['.font-weight-lighter (3692:1)'] }, - '25189f4fad18eaeef19e349c6680834c': { selectors: ['.font-weight-normal (3696:1)'] }, - b2a9507678ec557603eb8ec077f0eb1f: { selectors: ['.font-weight-bold (3700:1)'] }, - '9d05a94a577558235edce4c8fd9ab639': { selectors: ['.font-weight-bolder (3704:1)'] }, - '7d2da06b621a98a8599e5ec82e39eac8': { selectors: ['.font-italic (3708:1)'] }, - '0020d10e4fce033b418aace7c3143b82': { selectors: ['.text-white (3712:1)'] }, - '34ad81e372a038e6f78ae4f22bd4813d': { selectors: ['.text-primary (3716:1)'] }, - '9fde9a179d24755438ace2a874dda817': { - selectors: ['.text-secondary (3724:1)', '.text-muted (3784:1)'], - }, - '9ffcb1532b3fb397c0e818850683da29': { selectors: ['.text-success (3732:1)'] }, - f28fd089809bcd15d5684b158a0af98d: { selectors: ['.text-info (3740:1)'] }, - '6cac1cb5ee5149e91e45d15d0bdae310': { selectors: ['.text-warning (3748:1)'] }, - '2faab1e0abf22b20fdf05b9b01fff29b': { selectors: ['.text-danger (3756:1)'] }, - '46b52fea531aaaf29b63c40be2356849': { selectors: ['.text-light (3764:1)'] }, - '78f31d1ab6529decf28e0366a8ee81aa': { selectors: ['.text-dark (3772:1)'] }, - '45330b41b77e8880ad7680c51e0f61c4': { selectors: ['.text-body (3780:1)'] }, - '60d93588f62b5e85eb4f11dfd3461897': { selectors: ['.text-black-50 (3788:1)'] }, - '7dea35658553032ff7b7cc0287613b7c': { selectors: ['.text-white-50 (3792:1)'] }, - '61bf92980cac3d51d0cf1ba24c948fa1': { selectors: ['.text-hide (3796:1)'] }, - '187bfde85cb4d8bdaa41735f54f5c4f8': { selectors: ['.text-decoration-none (3804:1)'] }, - fd57878967b8dce947091cdc114efd83: { selectors: ['.text-break (3808:1)'] }, - '43b8d67240c3b09505d94522a62a977c': { selectors: ['.text-reset (3813:1)'] }, - '7dcad258820769677bc60871fafe9b93': { selectors: ['.visible (3817:1)'] }, - '0f8833af4e2f4a6fc785bd7edc1e75b3': { selectors: ['.invisible (3821:1)'] }, -}; diff --git a/scripts/glfm/run-snapshot-tests.sh b/scripts/glfm/run-snapshot-tests.sh index 70fdae60edc..59a7c8f06b0 100755 --- a/scripts/glfm/run-snapshot-tests.sh +++ b/scripts/glfm/run-snapshot-tests.sh @@ -24,10 +24,9 @@ printf "\nStarting GLFM snapshot example tests. See https://docs.gitlab.com/ee/d printf "Set 'FOCUSED_MARKDOWN_EXAMPLES=example_name_1[,...]' for focused examples, with example name(s) from https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#glfm_specificationexample_snapshotsexamples_indexyml.\n" printf "${Color_Off}" -# This section can be uncommented as soon as this is merged: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89953 -#printf "\n${BBlue}Running frontend 'yarn jest spec/frontend/content_editor/markdown_snapshot_spec.js'...${Color_Off}\n\n" -#yarn jest spec/frontend/content_editor/markdown_snapshot_spec.js -#printf "\n${BBlue}'yarn jest spec/frontend/content_editor/markdown_snapshot_spec.js' passed!${Color_Off}\n\n" +printf "\n${BBlue}Running frontend 'yarn jest spec/frontend/content_editor/markdown_snapshot_spec.js'...${Color_Off}\n\n" +yarn jest spec/frontend/content_editor/markdown_snapshot_spec.js +printf "\n${BBlue}'yarn jest spec/frontend/content_editor/markdown_snapshot_spec.js' passed!${Color_Off}\n\n" printf "\n${BBlue}Running backend 'bundle exec rspec spec/requests/api/markdown_snapshot_spec.rb'...${Color_Off}\n\n" bundle exec rspec spec/requests/api/markdown_snapshot_spec.rb diff --git a/scripts/lib/glfm/constants.rb b/scripts/lib/glfm/constants.rb index e5917fc5cdb..42977248c0d 100644 --- a/scripts/lib/glfm/constants.rb +++ b/scripts/lib/glfm/constants.rb @@ -21,7 +21,7 @@ module Glfm GLFM_SPEC_TXT_PATH = specification_path.join('output/spec.txt') # Example Snapshot (ES) files - es_fixtures_path = File.expand_path("../../../spec/fixtures/glfm/example_snapshots", __dir__) + es_fixtures_path = File.expand_path("../../../glfm_specification/example_snapshots", __dir__) ES_EXAMPLES_INDEX_YML_PATH = File.join(es_fixtures_path, 'examples_index.yml') ES_MARKDOWN_YML_PATH = File.join(es_fixtures_path, 'markdown.yml') ES_HTML_YML_PATH = File.join(es_fixtures_path, 'html.yml') diff --git a/scripts/lib/glfm/parse_examples.rb b/scripts/lib/glfm/parse_examples.rb index 1c6afb800c3..14634bcfb3e 100644 --- a/scripts/lib/glfm/parse_examples.rb +++ b/scripts/lib/glfm/parse_examples.rb @@ -13,7 +13,11 @@ # 1. Capture all nested headers, not just the most recent. # 2. Raise an exception if an unexpected state is encountered. # -# Comments indicate where changes or additions were made. +# Comments indicate where changes, deletions, or additions were made. +# +# See more detailed documentation of rules regarding the handling of headers +# in the comments at the top of `Glfm::UpdateExampleSnapshots#add_example_names`, +# in `scripts/lib/glfm/update_example_snapshots.rb` module Glfm module ParseExamples REGULAR_TEXT = 0 @@ -51,22 +55,29 @@ module Glfm state = REGULAR_TEXT example_number += 1 end_line = line_number - unless extensions.include?('disabled') - tests << - { - markdown: markdown_lines.join.tr('→', "\t"), - html: html_lines.join.tr('→', "\t"), - example: example_number, - start_line: start_line, - end_line: end_line, - section: headertext, - extensions: extensions, - headers: headers.dup # new logic compared to original Python code - } - start_line = 0 - markdown_lines = [] - html_lines = [] - end + + # NOTE: The original implementation completely excludes disabled examples, but we need + # to include them in order to correctly count the header numbering, so we set a flag + # instead. This will need to be accounted for when we run conformance testing. + + # unless extensions.include?('disabled') # commented logic compared to original Python code + tests << + { + markdown: markdown_lines.join.tr('→', "\t"), + html: html_lines.join.tr('→', "\t"), + example: example_number, + start_line: start_line, + end_line: end_line, + section: headertext, + extensions: extensions, + headers: headers.dup, # new logic compared to original Python code + disabled: extensions.include?('disabled') # new logic compared to original Python code + } + # end # commented logic compared to original Python code + + start_line = 0 + markdown_lines = [] + html_lines = [] elsif stripped_line == "." # Else if the example divider line... state = HTML_OUTPUT @@ -80,19 +91,23 @@ module Glfm html_lines.append(line) elsif state == REGULAR_TEXT && line =~ header_regex # Else if we are in regular text and it is a header line - # NOTE: This assumes examples are only nested up to 2 levels deep (H2) + # NOTE: This assumes examples are within the section under + # Heading level 2 with Heading levels above 2 ignored # Extract the header text from the line headertext = line.gsub(header_regex, '').strip + # The 'headers' array is new logic compared to the original Python code + # reset the headers array if we found a new H1 - headers = [] if line =~ h1_regex # new logic compared to original Python code + headers = [] if line =~ h1_regex - # pop the last entry from the headers array if we found a new H2 - headers.pop if headers.length == 2 && line =~ h2_regex # new logic compared to original Python code + # headers should be size 2 or less [<H1_headertext>, <H2_headertext>] + # pop the last entry from the headers array if we are in an H2 and found a new H2 + headers.pop if headers.length == 2 && line =~ h2_regex # push the new header text to the headers array - headers << headertext # New logic compared to original Python code + headers << headertext if line =~ h1_regex || line =~ h2_regex else # Else if we are in regular text... diff --git a/scripts/lib/glfm/render_wysiwyg_html_and_json.js b/scripts/lib/glfm/render_wysiwyg_html_and_json.js index 58b440d7ab2..ed8bfdb4638 100644 --- a/scripts/lib/glfm/render_wysiwyg_html_and_json.js +++ b/scripts/lib/glfm/render_wysiwyg_html_and_json.js @@ -1,117 +1,7 @@ import fs from 'fs'; -import { DOMSerializer } from 'prosemirror-model'; import jsYaml from 'js-yaml'; -// TODO: DRY up duplication with spec/frontend/content_editor/services/markdown_serializer_spec.js -// See https://gitlab.com/groups/gitlab-org/-/epics/7719#plan -import Blockquote from '~/content_editor/extensions/blockquote'; -import Bold from '~/content_editor/extensions/bold'; -import BulletList from '~/content_editor/extensions/bullet_list'; -import Code from '~/content_editor/extensions/code'; -import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight'; -import DescriptionItem from '~/content_editor/extensions/description_item'; -import DescriptionList from '~/content_editor/extensions/description_list'; -import Details from '~/content_editor/extensions/details'; -import DetailsContent from '~/content_editor/extensions/details_content'; -import Division from '~/content_editor/extensions/division'; -import Emoji from '~/content_editor/extensions/emoji'; -import Figure from '~/content_editor/extensions/figure'; -import FigureCaption from '~/content_editor/extensions/figure_caption'; -import FootnoteDefinition from '~/content_editor/extensions/footnote_definition'; -import FootnoteReference from '~/content_editor/extensions/footnote_reference'; -import FootnotesSection from '~/content_editor/extensions/footnotes_section'; -import HardBreak from '~/content_editor/extensions/hard_break'; -import Heading from '~/content_editor/extensions/heading'; -import HorizontalRule from '~/content_editor/extensions/horizontal_rule'; -import Image from '~/content_editor/extensions/image'; -import InlineDiff from '~/content_editor/extensions/inline_diff'; -import Italic from '~/content_editor/extensions/italic'; -import Link from '~/content_editor/extensions/link'; -import ListItem from '~/content_editor/extensions/list_item'; -import OrderedList from '~/content_editor/extensions/ordered_list'; -import Strike from '~/content_editor/extensions/strike'; -import Table from '~/content_editor/extensions/table'; -import TableCell from '~/content_editor/extensions/table_cell'; -import TableHeader from '~/content_editor/extensions/table_header'; -import TableRow from '~/content_editor/extensions/table_row'; -import TaskItem from '~/content_editor/extensions/task_item'; -import TaskList from '~/content_editor/extensions/task_list'; -import createMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer'; -import { createTestEditor } from 'jest/content_editor/test_utils'; import { setTestTimeout } from 'jest/__helpers__/timeout'; - -const tiptapEditor = createTestEditor({ - extensions: [ - Blockquote, - Bold, - BulletList, - Code, - CodeBlockHighlight, - DescriptionItem, - DescriptionList, - Details, - DetailsContent, - Division, - Emoji, - FootnoteDefinition, - FootnoteReference, - FootnotesSection, - Figure, - FigureCaption, - HardBreak, - Heading, - HorizontalRule, - Image, - InlineDiff, - Italic, - Link, - ListItem, - OrderedList, - Strike, - Table, - TableCell, - TableHeader, - TableRow, - TaskItem, - TaskList, - ], -}); - -async function renderMarkdownToHTMLAndJSON(markdown, schema, deserializer) { - let prosemirrorDocument; - try { - const { document } = await deserializer.deserialize({ schema, content: markdown }); - prosemirrorDocument = document; - } catch (e) { - const errorMsg = `Error - check implementation:\n${e.message}`; - return { - html: errorMsg, - json: errorMsg, - }; - } - - const documentFragment = DOMSerializer.fromSchema(schema).serializeFragment( - prosemirrorDocument.content, - ); - const htmlString = documentFragment.firstChild.outerHTML; - - const json = prosemirrorDocument.toJSON(); - const jsonString = JSON.stringify(json, null, 2); - return { html: htmlString, json: jsonString }; -} - -function renderHtmlAndJsonForAllExamples(markdownExamples) { - const { schema } = tiptapEditor; - const deserializer = createMarkdownDeserializer(); - const exampleNames = Object.keys(markdownExamples); - - return exampleNames.reduce(async (promisedExamples, exampleName) => { - const markdown = markdownExamples[exampleName]; - const htmlAndJson = await renderMarkdownToHTMLAndJSON(markdown, schema, deserializer); - const examples = await promisedExamples; - examples[exampleName] = htmlAndJson; - return examples; - }, Promise.resolve({})); -} +import { renderHtmlAndJsonForAllExamples } from 'jest/content_editor/render_html_and_json_for_all_examples'; /* eslint-disable no-undef */ jest.mock('~/emoji'); diff --git a/scripts/lib/glfm/update_example_snapshots.rb b/scripts/lib/glfm/update_example_snapshots.rb index 9ffa54cd5d4..893d8d9c014 100644 --- a/scripts/lib/glfm/update_example_snapshots.rb +++ b/scripts/lib/glfm/update_example_snapshots.rb @@ -40,18 +40,27 @@ module Glfm add_example_names(all_examples) + reject_disabled_examples(all_examples) + write_snapshot_example_files(all_examples, skip_static_and_wysiwyg: skip_static_and_wysiwyg) end private def add_example_names(all_examples) - # NOTE: This method assumes: + # NOTE: This method and the parse_examples method assume: # 1. Section 2 is the first section which contains examples - # 2. Examples are always nested exactly than 2 levels deep in an H2 - # 3. We assume that the Appendix doesn't ever contain any examples, so it doesn't show up + # 2. Examples are always nested exactly 2 levels deep in an H2 + # 3. There may exist H3 headings with no examples (e.g. "Motivation" in the GLFM spec.txt) + # 4. The Appendix doesn't ever contain any examples, so it doesn't show up # in the H1 header count. So, even though due to the concatenation it appears before the # GitLab examples sections, it doesn't result in their header counts being off by +1. + # 5. If an example contains the 'disabled' string extension, it is skipped (and will thus + # result in a skip in the `spec_txt_example_position`). This behavior is taken from the + # GFM `spec_test.py` script (but it's NOT in the original CommonMark `spec_test.py`). + # 6. If a section contains ONLY disabled examples, the section numbering will still be + # incremented to match the rendered HTML specification section numbering. + # 7. Every H2 must contain at least one example, but it is allowed that they are all disabled. h1_count = 1 # examples start in H1 section 2; section 1 is the overview with no examples. h2_count = 0 @@ -86,6 +95,10 @@ module Glfm end end + def reject_disabled_examples(all_examples) + all_examples.reject! { |example| example[:disabled] } + end + def write_snapshot_example_files(all_examples, skip_static_and_wysiwyg:) output("Reading #{GLFM_EXAMPLE_STATUS_YML_PATH}...") glfm_examples_statuses = YAML.safe_load(File.open(GLFM_EXAMPLE_STATUS_YML_PATH)) @@ -231,7 +244,7 @@ module Glfm name = example.fetch(:name) json = if glfm_examples_statuses.dig(name, 'skip_update_example_snapshot_prosemirror_json') - existing_hash.dig(name) + existing_hash[name] else wysiwyg_html_and_json_hash.dig(name, 'json') end diff --git a/scripts/pipeline_test_report_builder.rb b/scripts/pipeline_test_report_builder.rb index 2101decf59a..649b68427ea 100755 --- a/scripts/pipeline_test_report_builder.rb +++ b/scripts/pipeline_test_report_builder.rb @@ -72,6 +72,10 @@ class PipelineTestReportBuilder # Please see for more info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69053#note_709939709 def test_report_for_build(pipeline, build_id) fetch("#{pipeline['web_url']}/tests/suite.json?build_ids[]=#{build_id}") + rescue Net::HTTPServerException => e + raise e unless e.response.code.to_i == 404 + + puts "Artifacts not found. They may have expired. Skipping this build." end def build_test_report_json_for_pipeline(pipeline) @@ -92,7 +96,8 @@ class PipelineTestReportBuilder test_report['suites'] ||= [] failed_builds_for_test_stage.each do |failed_build| - test_report['suites'] << test_report_for_build(pipeline, failed_build['id']) + suite = test_report_for_build(pipeline, failed_build['id']) + test_report['suites'] << suite if suite end end diff --git a/scripts/review_apps/gcp_cleanup.sh b/scripts/review_apps/gcp_cleanup.sh index 3225631e8c7..114ac6f7ec0 100755 --- a/scripts/review_apps/gcp_cleanup.sh +++ b/scripts/review_apps/gcp_cleanup.sh @@ -3,7 +3,7 @@ source scripts/utils.sh function setup_gcp_dependencies() { - apk add jq + apt-get update && apt-get install -y jq gcloud auth activate-service-account --key-file="${REVIEW_APPS_GCP_CREDENTIALS}" gcloud config set project "${REVIEW_APPS_GCP_PROJECT}" diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index 8a868edce3a..b6ac7b4281b 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -248,6 +248,7 @@ function download_chart() { helm repo add gitlab https://charts.gitlab.io echoinfo "Building the gitlab chart's dependencies..." + helm dependency build "gitlab-${GITLAB_HELM_CHART_REF}" } function base_config_changed() { diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh index af09d6d0edd..b31e3663eaa 100644 --- a/scripts/rspec_helpers.sh +++ b/scripts/rspec_helpers.sh @@ -304,12 +304,15 @@ function retry_failed_rspec_examples() { # Disable Crystalball on retry to not overwrite the existing report export CRYSTALBALL="false" + # Disable simplecov so retried tests don't override test coverage report + export SIMPLECOV=0 + # Retry only the tests that failed on first try rspec_simple_job "--only-failures --pattern \"${KNAPSACK_TEST_FILE_PATTERN}\"" "${JUNIT_RETRY_FILE}" rspec_run_status=$? # Merge the JUnit report from retry into the first-try report - junit_merge "${JUNIT_RETRY_FILE}" "${JUNIT_RESULT_FILE}" + junit_merge "${JUNIT_RETRY_FILE}" "${JUNIT_RESULT_FILE}" --update-only exit $rspec_run_status } diff --git a/scripts/undercoverage b/scripts/undercoverage index d6e6197911e..b9266b015a7 100755 --- a/scripts/undercoverage +++ b/scripts/undercoverage @@ -1,3 +1,25 @@ -#!/usr/bin/env bash +#!/usr/bin/env ruby -bundle exec undercover -c "${1:-$(git merge-base origin/master HEAD)}" +# frozen_string_literal: true + +require 'undercover' + +module Undercover + class Changeset + # Rugged merge_base complains when graft/shallow + # (https://github.com/libgit2/rugged/issues/846) + # + # So we assume we provide the merge-base ourself. Modified from + # https://github.com/grodowski/undercover/blob/32e62f66682ee566032b5970437ed2934ef29701/lib/undercover/changeset.rb#L74-L78 + def compare_base_obj + return unless compare_base + + repo.lookup(compare_base.to_s) + end + end +end + +compare_base = ARGV[0] +compare_base ||= IO.popen(%w(git merge-base origin/master HEAD)) { |p| p.read.chomp } + +Undercover::CLI.run(%W(-c #{compare_base})) diff --git a/scripts/utils.sh b/scripts/utils.sh index 4f339bbc850..d9205921963 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -38,11 +38,12 @@ function bundle_install_script() { exit 1; fi; - gem install bundler --no-document --conservative --version 2.3.15 + gem --version bundle --version + gem install bundler --no-document --conservative --version 2.3.15 + test -d jh && bundle config set --local gemfile 'jh/Gemfile' bundle config set path "$(pwd)/vendor" bundle config set clean 'true' - test -d jh && bundle config set --local gemfile 'jh/Gemfile' echo "${BUNDLE_WITHOUT}" bundle config |