summaryrefslogtreecommitdiff
path: root/tooling
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-03-12 16:26:10 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-03-12 16:26:10 +0000
commit6653ccc011dec86e5140a5d09ea3b2357eab6714 (patch)
tree897193f37bcd98152a0ac214f80a3c4cfe1047c5 /tooling
parentbff35a05aed6a31380a73c39113808fd262c2c37 (diff)
downloadgitlab-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.rb41
-rw-r--r--tooling/danger/helper.rb99
-rw-r--r--tooling/overcommit/Gemfile1
-rw-r--r--tooling/overcommit/Gemfile.lock14
-rw-r--r--tooling/rspec_flaky/config.rb27
-rw-r--r--tooling/rspec_flaky/example.rb53
-rw-r--r--tooling/rspec_flaky/flaky_example.rb40
-rw-r--r--tooling/rspec_flaky/flaky_examples_collection.rb38
-rw-r--r--tooling/rspec_flaky/listener.rb67
-rw-r--r--tooling/rspec_flaky/report.rb56
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