diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-12 16:26:10 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-12 16:26:10 +0000 |
commit | 6653ccc011dec86e5140a5d09ea3b2357eab6714 (patch) | |
tree | 897193f37bcd98152a0ac214f80a3c4cfe1047c5 /tooling | |
parent | bff35a05aed6a31380a73c39113808fd262c2c37 (diff) | |
download | gitlab-ce-13.10.0-rc41.tar.gz |
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc41
Diffstat (limited to 'tooling')
-rw-r--r-- | tooling/danger/changelog.rb | 41 | ||||
-rw-r--r-- | tooling/danger/helper.rb | 99 | ||||
-rw-r--r-- | tooling/overcommit/Gemfile | 1 | ||||
-rw-r--r-- | tooling/overcommit/Gemfile.lock | 14 | ||||
-rw-r--r-- | tooling/rspec_flaky/config.rb | 27 | ||||
-rw-r--r-- | tooling/rspec_flaky/example.rb | 53 | ||||
-rw-r--r-- | tooling/rspec_flaky/flaky_example.rb | 40 | ||||
-rw-r--r-- | tooling/rspec_flaky/flaky_examples_collection.rb | 38 | ||||
-rw-r--r-- | tooling/rspec_flaky/listener.rb | 67 | ||||
-rw-r--r-- | tooling/rspec_flaky/report.rb | 56 |
10 files changed, 400 insertions, 36 deletions
diff --git a/tooling/danger/changelog.rb b/tooling/danger/changelog.rb index f7f505f51a6..86184b38459 100644 --- a/tooling/danger/changelog.rb +++ b/tooling/danger/changelog.rb @@ -30,25 +30,35 @@ module Tooling If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry), feel free to ignore this message. MSG + REQUIRED_CHANGELOG_REASONS = { + db_changes: 'introduces a database migration', + feature_flag_removed: 'removes a feature flag' + }.freeze REQUIRED_CHANGELOG_MESSAGE = <<~MSG To create a changelog entry, run the following: #{CREATE_CHANGELOG_COMMAND} - This merge request requires a changelog entry because it [introduces a database migration](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry). + This merge request requires a changelog entry because it [%<reason>s](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry). MSG + def required_reasons + [].tap do |reasons| + reasons << :db_changes if helper.changes.added.has_category?(:migration) + reasons << :feature_flag_removed if helper.changes.deleted.has_category?(:feature_flag) + end + end + def required? - git.added_files.any? { |path| path =~ %r{\Adb/(migrate|post_migrate)/} } + required_reasons.any? end - alias_method :db_changes?, :required? def optional? categories_need_changelog? && without_no_changelog_label? end def found - @found ||= git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} } + @found ||= helper.changes.added.by_category(:changelog).files.first end def ee_changelog? @@ -57,35 +67,34 @@ module Tooling def modified_text CHANGELOG_MODIFIED_URL_TEXT + - format(OPTIONAL_CHANGELOG_MESSAGE, mr_iid: mr_iid, mr_title: sanitized_mr_title) + format(OPTIONAL_CHANGELOG_MESSAGE, mr_iid: helper.mr_iid, mr_title: sanitized_mr_title) end - def required_text - CHANGELOG_MISSING_URL_TEXT + - format(REQUIRED_CHANGELOG_MESSAGE, mr_iid: mr_iid, mr_title: sanitized_mr_title) + def required_texts + required_reasons.each_with_object({}) do |required_reason, memo| + memo[required_reason] = + CHANGELOG_MISSING_URL_TEXT + + format(REQUIRED_CHANGELOG_MESSAGE, reason: REQUIRED_CHANGELOG_REASONS.fetch(required_reason), mr_iid: helper.mr_iid, mr_title: sanitized_mr_title) + end end def optional_text CHANGELOG_MISSING_URL_TEXT + - format(OPTIONAL_CHANGELOG_MESSAGE, mr_iid: mr_iid, mr_title: sanitized_mr_title) + format(OPTIONAL_CHANGELOG_MESSAGE, mr_iid: helper.mr_iid, mr_title: sanitized_mr_title) end private - def mr_iid - gitlab.mr_json["iid"] - end - def sanitized_mr_title - TitleLinting.sanitize_mr_title(gitlab.mr_json["title"]) + TitleLinting.sanitize_mr_title(helper.mr_title) end def categories_need_changelog? - (helper.changes_by_category.keys - NO_CHANGELOG_CATEGORIES).any? + (helper.changes.categories - NO_CHANGELOG_CATEGORIES).any? end def without_no_changelog_label? - (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty? + (helper.mr_labels & NO_CHANGELOG_LABELS).empty? end end end diff --git a/tooling/danger/helper.rb b/tooling/danger/helper.rb index 60026ee9c70..ef5b2e16bb0 100644 --- a/tooling/danger/helper.rb +++ b/tooling/danger/helper.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'delegate' + require_relative 'teammate' require_relative 'title_linting' @@ -86,13 +88,84 @@ module Tooling end end - # @return [Hash<String,Array<String>>] + Change = Struct.new(:file, :change_type, :category) + + class Changes < ::SimpleDelegator + def added + select_by_change_type(:added) + end + + def modified + select_by_change_type(:modified) + end + + def deleted + select_by_change_type(:deleted) + end + + def renamed_before + select_by_change_type(:renamed_before) + end + + def renamed_after + select_by_change_type(:renamed_after) + end + + def has_category?(category) + any? { |change| change.category == category } + end + + def by_category(category) + Changes.new(select { |change| change.category == category }) + end + + def categories + map(&:category).uniq + end + + def files + map(&:file) + end + + private + + def select_by_change_type(change_type) + Changes.new(select { |change| change.change_type == change_type }) + end + end + + # @return [Hash<Symbol,Array<String>>] def changes_by_category all_changed_files.each_with_object(Hash.new { |h, k| h[k] = [] }) do |file, hash| categories_for_file(file).each { |category| hash[category] << file } end end + # @return [Changes] + def changes + Changes.new([]).tap do |changes| + git.added_files.each do |file| + categories_for_file(file).each { |category| changes << Change.new(file, :added, category) } + end + + git.modified_files.each do |file| + categories_for_file(file).each { |category| changes << Change.new(file, :modified, category) } + end + + git.deleted_files.each do |file| + categories_for_file(file).each { |category| changes << Change.new(file, :deleted, category) } + end + + git.renamed_files.map { |x| x[:before] }.each do |file| + categories_for_file(file).each { |category| changes << Change.new(file, :renamed_before, category) } + end + + git.renamed_files.map { |x| x[:after] }.each do |file| + categories_for_file(file).each { |category| changes << Change.new(file, :renamed_after, category) } + end + end + end + # Determines the categories a file is in, e.g., `[:frontend]`, `[:backend]`, or `%i[frontend engineering_productivity]` # using filename regex and specific change regex if given. # @@ -130,8 +203,13 @@ module Tooling CATEGORIES = { [%r{usage_data\.rb}, %r{^(\+|-).*\s+(count|distinct_count|estimate_batch_distinct_count)\(.*\)(.*)$}] => [:database, :backend], + %r{\A(ee/)?config/feature_flags/} => :feature_flag, + + %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} => :changelog, + %r{\Adoc/.*(\.(md|png|gif|jpg))\z} => :docs, %r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :docs, + %r{\Adata/whats_new/} => :docs, %r{\A(ee/)?app/(assets|views)/} => :frontend, %r{\A(ee/)?public/} => :frontend, @@ -145,7 +223,6 @@ module Tooling \.nvmrc | \.prettierignore | \.prettierrc | - \.scss-lint.yml | \.stylelintrc | \.haml-lint.yml | \.haml-lint_todo.yml | @@ -160,6 +237,7 @@ module Tooling \.gitlab/ci/frontend\.gitlab-ci\.yml )\z}x => %i[frontend engineering_productivity], + %r{\A(ee/)?db/(geo/)?(migrate|post_migrate)/} => [:database, :migration], %r{\A(ee/)?db/(?!fixtures)[^/]+} => :database, %r{\A(ee/)?lib/gitlab/(database|background_migration|sql|github_import)(/|\.rb)} => :database, %r{\A(app/models/project_authorization|app/services/users/refresh_authorized_projects_service)(/|\.rb)} => :database, @@ -215,6 +293,12 @@ module Tooling usernames.map { |u| Tooling::Danger::Teammate.new('username' => u) } end + def mr_iid + return '' unless gitlab_helper + + gitlab_helper.mr_json['iid'] + end + def mr_title return '' unless gitlab_helper @@ -227,6 +311,12 @@ module Tooling gitlab_helper.mr_json['web_url'] end + def mr_labels + return [] unless gitlab_helper + + gitlab_helper.mr_labels + end + def mr_target_branch return '' unless gitlab_helper @@ -258,10 +348,9 @@ module Tooling end def mr_has_labels?(*labels) - return false unless gitlab_helper - labels = labels.flatten.uniq - (labels & gitlab_helper.mr_labels) == labels + + (labels & mr_labels) == labels end def labels_list(labels, sep: ', ') diff --git a/tooling/overcommit/Gemfile b/tooling/overcommit/Gemfile index 6d2c5bce29a..26dad738bab 100644 --- a/tooling/overcommit/Gemfile +++ b/tooling/overcommit/Gemfile @@ -5,5 +5,4 @@ source 'https://rubygems.org' gem 'overcommit' gem 'gitlab-styles', '~> 5.4.0', require: false -gem 'scss_lint', '~> 0.56.0', require: false gem 'haml_lint', '~> 0.34.0', require: false diff --git a/tooling/overcommit/Gemfile.lock b/tooling/overcommit/Gemfile.lock index f1d701b3e4b..13c611439b6 100644 --- a/tooling/overcommit/Gemfile.lock +++ b/tooling/overcommit/Gemfile.lock @@ -10,7 +10,6 @@ GEM ast (2.4.1) childprocess (3.0.0) concurrent-ruby (1.1.7) - ffi (1.12.2) gitlab-styles (5.4.0) rubocop (~> 0.89.1) rubocop-gitlab-security (~> 0.1.0) @@ -37,10 +36,6 @@ GEM ast (~> 2.4.1) rack (2.2.3) rainbow (3.0.0) - rake (12.3.3) - rb-fsevent (0.10.2) - rb-inotify (0.9.10) - ffi (>= 0.5.0, < 2) regexp_parser (1.8.2) rexml (3.2.4) rubocop (0.89.1) @@ -67,14 +62,6 @@ GEM rubocop (~> 0.87) rubocop-ast (>= 0.7.1) ruby-progressbar (1.10.1) - sass (3.5.5) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - scss_lint (0.56.0) - rake (>= 0.9, < 13) - sass (~> 3.5.3) sysexits (1.2.0) temple (0.8.2) thread_safe (0.3.6) @@ -91,7 +78,6 @@ DEPENDENCIES gitlab-styles (~> 5.4.0) haml_lint (~> 0.34.0) overcommit - scss_lint (~> 0.56.0) BUNDLED WITH 2.1.4 diff --git a/tooling/rspec_flaky/config.rb b/tooling/rspec_flaky/config.rb new file mode 100644 index 00000000000..ea18a601c11 --- /dev/null +++ b/tooling/rspec_flaky/config.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module RspecFlaky + class Config + def self.generate_report? + !!(ENV['FLAKY_RSPEC_GENERATE_REPORT'] =~ /1|true/) + end + + def self.suite_flaky_examples_report_path + ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'] || rails_path("rspec_flaky/suite-report.json") + end + + def self.flaky_examples_report_path + ENV['FLAKY_RSPEC_REPORT_PATH'] || rails_path("rspec_flaky/report.json") + end + + def self.new_flaky_examples_report_path + ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] || rails_path("rspec_flaky/new-report.json") + end + + def self.rails_path(path) + return path unless defined?(Rails) + + Rails.root.join(path) + end + end +end diff --git a/tooling/rspec_flaky/example.rb b/tooling/rspec_flaky/example.rb new file mode 100644 index 00000000000..18f8c5acc1c --- /dev/null +++ b/tooling/rspec_flaky/example.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'forwardable' +require 'digest' + +module RspecFlaky + # This is a wrapper class for RSpec::Core::Example + class Example + extend Forwardable + + def_delegators :execution_result, :status, :exception + + def initialize(rspec_example) + @rspec_example = rspec_example.respond_to?(:example) ? rspec_example.example : rspec_example + end + + def uid + @uid ||= Digest::MD5.hexdigest("#{description}-#{file}") + end + + def example_id + rspec_example.id + end + + def file + metadata[:file_path] + end + + def line + metadata[:line_number] + end + + def description + metadata[:full_description] + end + + def attempts + rspec_example.respond_to?(:attempts) ? rspec_example.attempts : 1 + end + + private + + attr_reader :rspec_example + + def metadata + rspec_example.metadata + end + + def execution_result + rspec_example.execution_result + end + end +end diff --git a/tooling/rspec_flaky/flaky_example.rb b/tooling/rspec_flaky/flaky_example.rb new file mode 100644 index 00000000000..4f3688dbeed --- /dev/null +++ b/tooling/rspec_flaky/flaky_example.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'ostruct' + +module RspecFlaky + # This represents a flaky RSpec example and is mainly meant to be saved in a JSON file + class FlakyExample < OpenStruct + def initialize(example) + if example.respond_to?(:example_id) + super( + example_id: example.example_id, + file: example.file, + line: example.line, + description: example.description, + last_attempts_count: example.attempts, + flaky_reports: 0) + else + super + end + end + + def update_flakiness!(last_attempts_count: nil) + self.first_flaky_at ||= Time.now + self.last_flaky_at = Time.now + self.flaky_reports += 1 + self.last_attempts_count = last_attempts_count if last_attempts_count + + if ENV['CI_PROJECT_URL'] && ENV['CI_JOB_ID'] + self.last_flaky_job = "#{ENV['CI_PROJECT_URL']}/-/jobs/#{ENV['CI_JOB_ID']}" + end + end + + def to_h + super.merge( + first_flaky_at: first_flaky_at, + last_flaky_at: last_flaky_at, + last_flaky_job: last_flaky_job) + end + end +end diff --git a/tooling/rspec_flaky/flaky_examples_collection.rb b/tooling/rspec_flaky/flaky_examples_collection.rb new file mode 100644 index 00000000000..acbfb411873 --- /dev/null +++ b/tooling/rspec_flaky/flaky_examples_collection.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'active_support/hash_with_indifferent_access' +require 'delegate' + +require_relative 'flaky_example' + +module RspecFlaky + class FlakyExamplesCollection < SimpleDelegator + def initialize(collection = {}) + unless collection.is_a?(Hash) + raise ArgumentError, "`collection` must be a Hash, #{collection.class} given!" + end + + collection_of_flaky_examples = + collection.map do |uid, example| + [ + uid, + example.is_a?(RspecFlaky::FlakyExample) ? example : RspecFlaky::FlakyExample.new(example) + ] + end + + super(Hash[collection_of_flaky_examples]) + end + + def to_h + transform_values { |example| example.to_h }.deep_symbolize_keys + end + + def -(other) + unless other.respond_to?(:key) + raise ArgumentError, "`other` must respond to `#key?`, #{other.class} does not!" + end + + self.class.new(reject { |uid, _| other.key?(uid) }) + end + end +end diff --git a/tooling/rspec_flaky/listener.rb b/tooling/rspec_flaky/listener.rb new file mode 100644 index 00000000000..a5c68d830db --- /dev/null +++ b/tooling/rspec_flaky/listener.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'json' + +require_relative 'config' +require_relative 'example' +require_relative 'flaky_example' +require_relative 'flaky_examples_collection' +require_relative 'report' + +module RspecFlaky + class Listener + # - suite_flaky_examples: contains all the currently tracked flacky example + # for the whole RSpec suite + # - flaky_examples: contains the examples detected as flaky during the + # current RSpec run + attr_reader :suite_flaky_examples, :flaky_examples + + def initialize(suite_flaky_examples_json = nil) + @flaky_examples = RspecFlaky::FlakyExamplesCollection.new + @suite_flaky_examples = init_suite_flaky_examples(suite_flaky_examples_json) + end + + def example_passed(notification) + current_example = RspecFlaky::Example.new(notification.example) + + return unless current_example.attempts > 1 + + flaky_example = suite_flaky_examples.fetch(current_example.uid) { RspecFlaky::FlakyExample.new(current_example) } + flaky_example.update_flakiness!(last_attempts_count: current_example.attempts) + + flaky_examples[current_example.uid] = flaky_example + end + + def dump_summary(_) + RspecFlaky::Report.new(flaky_examples).write(RspecFlaky::Config.flaky_examples_report_path) + # write_report_file(flaky_examples, RspecFlaky::Config.flaky_examples_report_path) + + new_flaky_examples = flaky_examples - suite_flaky_examples + if new_flaky_examples.any? + rails_logger_warn("\nNew flaky examples detected:\n") + rails_logger_warn(JSON.pretty_generate(new_flaky_examples.to_h)) # rubocop:disable Gitlab/Json + + RspecFlaky::Report.new(new_flaky_examples).write(RspecFlaky::Config.new_flaky_examples_report_path) + # write_report_file(new_flaky_examples, RspecFlaky::Config.new_flaky_examples_report_path) + end + end + + private + + def init_suite_flaky_examples(suite_flaky_examples_json = nil) + if suite_flaky_examples_json + RspecFlaky::Report.load_json(suite_flaky_examples_json).flaky_examples + else + return {} unless File.exist?(RspecFlaky::Config.suite_flaky_examples_report_path) + + RspecFlaky::Report.load(RspecFlaky::Config.suite_flaky_examples_report_path).flaky_examples + end + end + + def rails_logger_warn(text) + target = defined?(Rails) ? Rails.logger : Kernel + + target.warn(text) + end + end +end diff --git a/tooling/rspec_flaky/report.rb b/tooling/rspec_flaky/report.rb new file mode 100644 index 00000000000..3acfe7d2125 --- /dev/null +++ b/tooling/rspec_flaky/report.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'json' +require 'time' + +require_relative 'config' +require_relative 'flaky_examples_collection' + +module RspecFlaky + # This class is responsible for loading/saving JSON reports, and pruning + # outdated examples. + class Report < SimpleDelegator + OUTDATED_DAYS_THRESHOLD = 7 + + attr_reader :flaky_examples + + def self.load(file_path) + load_json(File.read(file_path)) + end + + def self.load_json(json) + new(RspecFlaky::FlakyExamplesCollection.new(JSON.parse(json))) + end + + def initialize(flaky_examples) + unless flaky_examples.is_a?(RspecFlaky::FlakyExamplesCollection) + raise ArgumentError, "`flaky_examples` must be a RspecFlaky::FlakyExamplesCollection, #{flaky_examples.class} given!" + end + + @flaky_examples = flaky_examples + super(flaky_examples) + end + + def write(file_path) + unless RspecFlaky::Config.generate_report? + Kernel.warn "! Generating reports is disabled. To enable it, please set the `FLAKY_RSPEC_GENERATE_REPORT=1` !" + return + end + + report_path_dir = File.dirname(file_path) + FileUtils.mkdir_p(report_path_dir) unless Dir.exist?(report_path_dir) + + File.write(file_path, JSON.pretty_generate(flaky_examples.to_h)) + end + + def prune_outdated(days: OUTDATED_DAYS_THRESHOLD) + outdated_date_threshold = Time.now - (3600 * 24 * days) + updated_hash = flaky_examples.dup + .delete_if do |uid, hash| + hash[:last_flaky_at] && Time.parse(hash[:last_flaky_at]).to_i < outdated_date_threshold.to_i + end + + self.class.new(RspecFlaky::FlakyExamplesCollection.new(updated_hash)) + end + end +end |