Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									472a7da0e5
								
							
						
					
					
						commit
						bf420c684d
					
				|  | @ -70,7 +70,7 @@ class Notify < ApplicationMailer | ||||||
|     return unless sender = User.find(sender_id) |     return unless sender = User.find(sender_id) | ||||||
| 
 | 
 | ||||||
|     address = default_sender_address |     address = default_sender_address | ||||||
|     address.display_name = sender_name.presence || sender.name |     address.display_name = sender_name.presence || "#{sender.name} (#{sender.to_reference})" | ||||||
| 
 | 
 | ||||||
|     if send_from_user_email && can_send_from_user_email?(sender) |     if send_from_user_email && can_send_from_user_email?(sender) | ||||||
|       address.address = sender.email |       address.address = sender.email | ||||||
|  |  | ||||||
|  | @ -1,35 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| module Ci |  | ||||||
|   class TestCase < ApplicationRecord |  | ||||||
|     extend Gitlab::Ci::Model |  | ||||||
| 
 |  | ||||||
|     validates :project, :key_hash, presence: true |  | ||||||
| 
 |  | ||||||
|     has_many :test_case_failures, class_name: 'Ci::TestCaseFailure' |  | ||||||
| 
 |  | ||||||
|     belongs_to :project |  | ||||||
| 
 |  | ||||||
|     scope :by_project_and_keys, -> (project, keys) { where(project_id: project.id, key_hash: keys) } |  | ||||||
| 
 |  | ||||||
|     class << self |  | ||||||
|       def find_or_create_by_batch(project, test_case_keys) |  | ||||||
|         # Insert records first. Existing ones will be skipped. |  | ||||||
|         insert_all(test_case_attrs(project, test_case_keys)) |  | ||||||
| 
 |  | ||||||
|         # Find all matching records now that we are sure they all are persisted. |  | ||||||
|         by_project_and_keys(project, test_case_keys) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       private |  | ||||||
| 
 |  | ||||||
|       def test_case_attrs(project, test_case_keys) |  | ||||||
|         # NOTE: Rails 6.1 will add support for insert_all on relation so that |  | ||||||
|         # we will be able to do project.test_cases.insert_all. |  | ||||||
|         test_case_keys.map do |hashed_key| |  | ||||||
|           { project_id: project.id, key_hash: hashed_key } |  | ||||||
|         end |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -1,29 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| module Ci |  | ||||||
|   class TestCaseFailure < ApplicationRecord |  | ||||||
|     extend Gitlab::Ci::Model |  | ||||||
| 
 |  | ||||||
|     REPORT_WINDOW = 14.days |  | ||||||
| 
 |  | ||||||
|     validates :test_case, :build, :failed_at, presence: true |  | ||||||
| 
 |  | ||||||
|     belongs_to :test_case, class_name: "Ci::TestCase", foreign_key: :test_case_id |  | ||||||
|     belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id |  | ||||||
| 
 |  | ||||||
|     def self.recent_failures_count(project:, test_case_keys:, date_range: REPORT_WINDOW.ago..Time.current) |  | ||||||
|       joins(:test_case) |  | ||||||
|         .where( |  | ||||||
|           ci_test_cases: { |  | ||||||
|             project_id: project.id, |  | ||||||
|             key_hash: test_case_keys |  | ||||||
|           }, |  | ||||||
|           ci_test_case_failures: { |  | ||||||
|             failed_at: date_range |  | ||||||
|           } |  | ||||||
|         ) |  | ||||||
|         .group(:key_hash) |  | ||||||
|         .count('ci_test_case_failures.id') |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -0,0 +1,46 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | module Ci | ||||||
|  |   class UnitTest < ApplicationRecord | ||||||
|  |     extend Gitlab::Ci::Model | ||||||
|  | 
 | ||||||
|  |     MAX_NAME_SIZE = 255 | ||||||
|  |     MAX_SUITE_NAME_SIZE = 255 | ||||||
|  | 
 | ||||||
|  |     validates :project, :key_hash, :name, :suite_name, presence: true | ||||||
|  | 
 | ||||||
|  |     has_many :unit_test_failures, class_name: 'Ci::UnitTestFailure' | ||||||
|  | 
 | ||||||
|  |     belongs_to :project | ||||||
|  | 
 | ||||||
|  |     scope :by_project_and_keys, -> (project, keys) { where(project_id: project.id, key_hash: keys) } | ||||||
|  | 
 | ||||||
|  |     class << self | ||||||
|  |       def find_or_create_by_batch(project, unit_test_attrs) | ||||||
|  |         # Insert records first. Existing ones will be skipped. | ||||||
|  |         insert_all(build_insert_attrs(project, unit_test_attrs)) | ||||||
|  | 
 | ||||||
|  |         # Find all matching records now that we are sure they all are persisted. | ||||||
|  |         by_project_and_keys(project, gather_keys(unit_test_attrs)) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       private | ||||||
|  | 
 | ||||||
|  |       def build_insert_attrs(project, unit_test_attrs) | ||||||
|  |         # NOTE: Rails 6.1 will add support for insert_all on relation so that | ||||||
|  |         # we will be able to do project.test_cases.insert_all. | ||||||
|  |         unit_test_attrs.map do |attrs| | ||||||
|  |           attrs.merge( | ||||||
|  |             project_id: project.id, | ||||||
|  |             name: attrs[:name].truncate(MAX_NAME_SIZE), | ||||||
|  |             suite_name: attrs[:suite_name].truncate(MAX_SUITE_NAME_SIZE) | ||||||
|  |           ) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       def gather_keys(unit_test_attrs) | ||||||
|  |         unit_test_attrs.map { |attrs| attrs[:key_hash] } | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,29 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | module Ci | ||||||
|  |   class UnitTestFailure < ApplicationRecord | ||||||
|  |     extend Gitlab::Ci::Model | ||||||
|  | 
 | ||||||
|  |     REPORT_WINDOW = 14.days | ||||||
|  | 
 | ||||||
|  |     validates :unit_test, :build, :failed_at, presence: true | ||||||
|  | 
 | ||||||
|  |     belongs_to :unit_test, class_name: "Ci::UnitTest", foreign_key: :unit_test_id | ||||||
|  |     belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id | ||||||
|  | 
 | ||||||
|  |     def self.recent_failures_count(project:, unit_test_keys:, date_range: REPORT_WINDOW.ago..Time.current) | ||||||
|  |       joins(:unit_test) | ||||||
|  |         .where( | ||||||
|  |           ci_unit_tests: { | ||||||
|  |             project_id: project.id, | ||||||
|  |             key_hash: unit_test_keys | ||||||
|  |           }, | ||||||
|  |           ci_unit_test_failures: { | ||||||
|  |             failed_at: date_range | ||||||
|  |           } | ||||||
|  |         ) | ||||||
|  |         .group(:key_hash) | ||||||
|  |         .count('ci_unit_test_failures.id') | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -34,7 +34,7 @@ module Ci | ||||||
| 
 | 
 | ||||||
|       # We fetch for up to MAX_TRACKABLE_FAILURES + 1 builds. So if ever we get |       # We fetch for up to MAX_TRACKABLE_FAILURES + 1 builds. So if ever we get | ||||||
|       # 201 total number of builds with the assumption that each job has at least |       # 201 total number of builds with the assumption that each job has at least | ||||||
|       # 1 failed test case, then we have at least 201 failed test cases which exceeds |       # 1 failed unit test, then we have at least 201 failed unit tests which exceeds | ||||||
|       # the MAX_TRACKABLE_FAILURES of 200. If this is the case, let's early exit so we |       # the MAX_TRACKABLE_FAILURES of 200. If this is the case, let's early exit so we | ||||||
|       # don't have to parse each JUnit report of each of the 201 builds. |       # don't have to parse each JUnit report of each of the 201 builds. | ||||||
|       failed_builds.length <= MAX_TRACKABLE_FAILURES |       failed_builds.length <= MAX_TRACKABLE_FAILURES | ||||||
|  | @ -51,25 +51,29 @@ module Ci | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def track_failures |     def track_failures | ||||||
|       failed_test_cases = gather_failed_test_cases(failed_builds) |       failed_unit_tests = gather_failed_unit_tests_from_reports(failed_builds) | ||||||
| 
 | 
 | ||||||
|       return if failed_test_cases.size > MAX_TRACKABLE_FAILURES |       return if failed_unit_tests.size > MAX_TRACKABLE_FAILURES | ||||||
| 
 | 
 | ||||||
|       failed_test_cases.keys.each_slice(100) do |key_hashes| |       failed_unit_tests.each_slice(100) do |batch| | ||||||
|         Ci::TestCase.transaction do |         Ci::UnitTest.transaction do | ||||||
|           ci_test_cases = Ci::TestCase.find_or_create_by_batch(project, key_hashes) |           unit_test_attrs = ci_unit_test_attrs(batch) | ||||||
|           failures = test_case_failures(ci_test_cases, failed_test_cases) |           ci_unit_tests = Ci::UnitTest.find_or_create_by_batch(project, unit_test_attrs) | ||||||
| 
 | 
 | ||||||
|           Ci::TestCaseFailure.insert_all(failures) |           failures = ci_unit_test_failure_attrs(ci_unit_tests, failed_unit_tests) | ||||||
|  |           Ci::UnitTestFailure.insert_all(failures) | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def gather_failed_test_cases(failed_builds) |     def gather_failed_unit_tests_from_reports(failed_builds) | ||||||
|       failed_builds.each_with_object({}) do |build, failed_test_cases| |       failed_builds.each_with_object({}) do |build, failed_unit_tests| | ||||||
|         test_suite = generate_test_suite!(build) |         test_suite = generate_test_suite!(build) | ||||||
|         test_suite.failed.keys.each do |key| |         test_suite.failed.each do |key, unit_test| | ||||||
|           failed_test_cases[key] = build |           failed_unit_tests[key] = { | ||||||
|  |             build: build, # This will be used in ci_unit_test_failure_attrs | ||||||
|  |             unit_test: unit_test # This will be used in ci_unit_test_attrs | ||||||
|  |           } | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  | @ -79,12 +83,24 @@ module Ci | ||||||
|       build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new) |       build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def test_case_failures(ci_test_cases, failed_test_cases) |     def ci_unit_test_attrs(batch) | ||||||
|       ci_test_cases.map do |test_case| |       batch.map do |item| | ||||||
|         build = failed_test_cases[test_case.key_hash] |         unit_test = item.last[:unit_test] | ||||||
| 
 | 
 | ||||||
|         { |         { | ||||||
|           test_case_id: test_case.id, |           key_hash: unit_test.key, | ||||||
|  |           name: unit_test.name, | ||||||
|  |           suite_name: unit_test.suite_name | ||||||
|  |         } | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def ci_unit_test_failure_attrs(ci_unit_tests, failed_unit_tests) | ||||||
|  |       ci_unit_tests.map do |ci_unit_test| | ||||||
|  |         build = failed_unit_tests[ci_unit_test.key_hash][:build] | ||||||
|  | 
 | ||||||
|  |         { | ||||||
|  |           unit_test_id: ci_unit_test.id, | ||||||
|           build_id: build.id, |           build_id: build.id, | ||||||
|           failed_at: build.finished_at |           failed_at: build.finished_at | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -24,7 +24,7 @@ | ||||||
|             %li |             %li | ||||||
|               = link_to title, filter_tags_path(sort: value), class: ("is-active" if @sort == value) |               = link_to title, filter_tags_path(sort: value), class: ("is-active" if @sort == value) | ||||||
|       - if can?(current_user, :admin_tag, @project) |       - if can?(current_user, :admin_tag, @project) | ||||||
|         = link_to new_project_tag_path(@project), class: 'btn gl-button btn-success', data: { qa_selector: "new_tag_button" } do |         = link_to new_project_tag_path(@project), class: 'btn gl-button btn-confirm', data: { qa_selector: "new_tag_button" } do | ||||||
|           = s_('TagsPage|New tag') |           = s_('TagsPage|New tag') | ||||||
|       = link_to project_tags_path(@project, rss_url_options), title: _("Tags feed"), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-block has-tooltip' do |       = link_to project_tags_path(@project, rss_url_options), title: _("Tags feed"), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-block has-tooltip' do | ||||||
|         = sprite_icon('rss', css_class: 'qa-rss-icon') |         = sprite_icon('rss', css_class: 'qa-rss-icon') | ||||||
|  |  | ||||||
|  | @ -15,5 +15,5 @@ | ||||||
|     = render 'shared/notes/hints' |     = render 'shared/notes/hints' | ||||||
|   .error-alert |   .error-alert | ||||||
|   .gl-mt-3 |   .gl-mt-3 | ||||||
|     = f.submit 'Save changes', class: 'btn gl-button btn-success' |     = f.submit 'Save changes', class: 'btn gl-button btn-confirm' | ||||||
|     = link_to "Cancel", project_tag_path(@project, @tag.name), class: "btn gl-button btn-default btn-cancel" |     = link_to "Cancel", project_tag_path(@project, @tag.name), class: "btn gl-button btn-default btn-cancel" | ||||||
|  |  | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | --- | ||||||
|  | title: Add Username to Email From Header in Notifications | ||||||
|  | merge_request: 56588 | ||||||
|  | author: | ||||||
|  | type: changed | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | --- | ||||||
|  | title: Move from btn-success to btn-confirm in projects/tags directory | ||||||
|  | merge_request: 56940 | ||||||
|  | author: Yogi (@yo) | ||||||
|  | type: changed | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | --- | ||||||
|  | title: Create new unit test tables | ||||||
|  | merge_request: 56137 | ||||||
|  | author: | ||||||
|  | type: other | ||||||
|  | @ -0,0 +1,31 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class CreateCiUnitTests < ActiveRecord::Migration[6.0] | ||||||
|  |   include Gitlab::Database::MigrationHelpers | ||||||
|  | 
 | ||||||
|  |   DOWNTIME = false | ||||||
|  | 
 | ||||||
|  |   disable_ddl_transaction! | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     unless table_exists?(:ci_unit_tests) | ||||||
|  |       create_table :ci_unit_tests do |t| | ||||||
|  |         t.bigint :project_id, null: false | ||||||
|  |         t.text :key_hash, null: false | ||||||
|  |         t.text :name, null: false | ||||||
|  |         t.text :suite_name, null: false | ||||||
|  | 
 | ||||||
|  |         t.index [:project_id, :key_hash], unique: true | ||||||
|  |         # NOTE: FK for projects will be added on a separate migration as per guidelines | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     add_text_limit :ci_unit_tests, :key_hash, 64 | ||||||
|  |     add_text_limit :ci_unit_tests, :name, 255 | ||||||
|  |     add_text_limit :ci_unit_tests, :suite_name, 255 | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def down | ||||||
|  |     drop_table :ci_unit_tests | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,19 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class AddProjectsFkToCiUnitTests < ActiveRecord::Migration[6.0] | ||||||
|  |   include Gitlab::Database::MigrationHelpers | ||||||
|  | 
 | ||||||
|  |   DOWNTIME = false | ||||||
|  | 
 | ||||||
|  |   disable_ddl_transaction! | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     add_concurrent_foreign_key :ci_unit_tests, :projects, column: :project_id, on_delete: :cascade | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def down | ||||||
|  |     with_lock_retries do | ||||||
|  |       remove_foreign_key :ci_unit_tests, column: :project_id | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,24 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class CreateCiUnitTestFailures < ActiveRecord::Migration[6.0] | ||||||
|  |   DOWNTIME = false | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     create_table :ci_unit_test_failures do |t| | ||||||
|  |       t.datetime_with_timezone :failed_at, null: false | ||||||
|  |       t.bigint :unit_test_id, null: false | ||||||
|  |       t.bigint :build_id, null: false | ||||||
|  | 
 | ||||||
|  |       t.index [:unit_test_id, :failed_at, :build_id], name: 'index_unit_test_failures_unique_columns', unique: true, order: { failed_at: :desc } | ||||||
|  |       t.index :build_id | ||||||
|  |       # NOTE: Adding the index for failed_at now for later use when we do scheduled clean up | ||||||
|  |       t.index :failed_at, order: { failed_at: :desc }, name: 'index_unit_test_failures_failed_at' | ||||||
|  |       t.foreign_key :ci_unit_tests, column: :unit_test_id, on_delete: :cascade | ||||||
|  |       # NOTE: FK for ci_builds will be added on a separate migration as per guidelines | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def down | ||||||
|  |     drop_table :ci_unit_test_failures | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,19 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class AddCiBuildsFkToCiUnitTestFailures < ActiveRecord::Migration[6.0] | ||||||
|  |   include Gitlab::Database::MigrationHelpers | ||||||
|  | 
 | ||||||
|  |   DOWNTIME = false | ||||||
|  | 
 | ||||||
|  |   disable_ddl_transaction! | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     add_concurrent_foreign_key :ci_unit_test_failures, :ci_builds, column: :build_id, on_delete: :cascade | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def down | ||||||
|  |     with_lock_retries do | ||||||
|  |       remove_foreign_key :ci_unit_test_failures, column: :build_id | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | cf63d7ffd6bfb93c25c894b26424e9890b43652b4f0bfc259917a4857ff414e2 | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | 4c1ae24594ccb85706a4c9836ed1fc8ce47d68863262e90b9109ddc1d83d121b | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | 8f9957b7f7744e3d72bba1b2bf9bd2c9a06203091bf8f9dcafc69755db25fef0 | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | 43af4a4200ba87ebb50627d341bb324896cbe0c36896d50dd81a8a9cfb2eb426 | ||||||
|  | @ -11130,6 +11130,42 @@ CREATE SEQUENCE ci_triggers_id_seq | ||||||
| 
 | 
 | ||||||
| ALTER SEQUENCE ci_triggers_id_seq OWNED BY ci_triggers.id; | ALTER SEQUENCE ci_triggers_id_seq OWNED BY ci_triggers.id; | ||||||
| 
 | 
 | ||||||
|  | CREATE TABLE ci_unit_test_failures ( | ||||||
|  |     id bigint NOT NULL, | ||||||
|  |     failed_at timestamp with time zone NOT NULL, | ||||||
|  |     unit_test_id bigint NOT NULL, | ||||||
|  |     build_id bigint NOT NULL | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | CREATE SEQUENCE ci_unit_test_failures_id_seq | ||||||
|  |     START WITH 1 | ||||||
|  |     INCREMENT BY 1 | ||||||
|  |     NO MINVALUE | ||||||
|  |     NO MAXVALUE | ||||||
|  |     CACHE 1; | ||||||
|  | 
 | ||||||
|  | ALTER SEQUENCE ci_unit_test_failures_id_seq OWNED BY ci_unit_test_failures.id; | ||||||
|  | 
 | ||||||
|  | CREATE TABLE ci_unit_tests ( | ||||||
|  |     id bigint NOT NULL, | ||||||
|  |     project_id bigint NOT NULL, | ||||||
|  |     key_hash text NOT NULL, | ||||||
|  |     name text NOT NULL, | ||||||
|  |     suite_name text NOT NULL, | ||||||
|  |     CONSTRAINT check_248fae1a3b CHECK ((char_length(name) <= 255)), | ||||||
|  |     CONSTRAINT check_b288215ffe CHECK ((char_length(key_hash) <= 64)), | ||||||
|  |     CONSTRAINT check_c2d57b3c49 CHECK ((char_length(suite_name) <= 255)) | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | CREATE SEQUENCE ci_unit_tests_id_seq | ||||||
|  |     START WITH 1 | ||||||
|  |     INCREMENT BY 1 | ||||||
|  |     NO MINVALUE | ||||||
|  |     NO MAXVALUE | ||||||
|  |     CACHE 1; | ||||||
|  | 
 | ||||||
|  | ALTER SEQUENCE ci_unit_tests_id_seq OWNED BY ci_unit_tests.id; | ||||||
|  | 
 | ||||||
| CREATE TABLE ci_variables ( | CREATE TABLE ci_variables ( | ||||||
|     id integer NOT NULL, |     id integer NOT NULL, | ||||||
|     key character varying NOT NULL, |     key character varying NOT NULL, | ||||||
|  | @ -19121,6 +19157,10 @@ ALTER TABLE ONLY ci_trigger_requests ALTER COLUMN id SET DEFAULT nextval('ci_tri | ||||||
| 
 | 
 | ||||||
| ALTER TABLE ONLY ci_triggers ALTER COLUMN id SET DEFAULT nextval('ci_triggers_id_seq'::regclass); | ALTER TABLE ONLY ci_triggers ALTER COLUMN id SET DEFAULT nextval('ci_triggers_id_seq'::regclass); | ||||||
| 
 | 
 | ||||||
|  | ALTER TABLE ONLY ci_unit_test_failures ALTER COLUMN id SET DEFAULT nextval('ci_unit_test_failures_id_seq'::regclass); | ||||||
|  | 
 | ||||||
|  | ALTER TABLE ONLY ci_unit_tests ALTER COLUMN id SET DEFAULT nextval('ci_unit_tests_id_seq'::regclass); | ||||||
|  | 
 | ||||||
| ALTER TABLE ONLY ci_variables ALTER COLUMN id SET DEFAULT nextval('ci_variables_id_seq'::regclass); | ALTER TABLE ONLY ci_variables ALTER COLUMN id SET DEFAULT nextval('ci_variables_id_seq'::regclass); | ||||||
| 
 | 
 | ||||||
| ALTER TABLE ONLY cluster_agent_tokens ALTER COLUMN id SET DEFAULT nextval('cluster_agent_tokens_id_seq'::regclass); | ALTER TABLE ONLY cluster_agent_tokens ALTER COLUMN id SET DEFAULT nextval('cluster_agent_tokens_id_seq'::regclass); | ||||||
|  | @ -20291,6 +20331,12 @@ ALTER TABLE ONLY ci_trigger_requests | ||||||
| ALTER TABLE ONLY ci_triggers | ALTER TABLE ONLY ci_triggers | ||||||
|     ADD CONSTRAINT ci_triggers_pkey PRIMARY KEY (id); |     ADD CONSTRAINT ci_triggers_pkey PRIMARY KEY (id); | ||||||
| 
 | 
 | ||||||
|  | ALTER TABLE ONLY ci_unit_test_failures | ||||||
|  |     ADD CONSTRAINT ci_unit_test_failures_pkey PRIMARY KEY (id); | ||||||
|  | 
 | ||||||
|  | ALTER TABLE ONLY ci_unit_tests | ||||||
|  |     ADD CONSTRAINT ci_unit_tests_pkey PRIMARY KEY (id); | ||||||
|  | 
 | ||||||
| ALTER TABLE ONLY ci_variables | ALTER TABLE ONLY ci_variables | ||||||
|     ADD CONSTRAINT ci_variables_pkey PRIMARY KEY (id); |     ADD CONSTRAINT ci_variables_pkey PRIMARY KEY (id); | ||||||
| 
 | 
 | ||||||
|  | @ -22172,6 +22218,10 @@ CREATE INDEX index_ci_triggers_on_owner_id ON ci_triggers USING btree (owner_id) | ||||||
| 
 | 
 | ||||||
| CREATE INDEX index_ci_triggers_on_project_id ON ci_triggers USING btree (project_id); | CREATE INDEX index_ci_triggers_on_project_id ON ci_triggers USING btree (project_id); | ||||||
| 
 | 
 | ||||||
|  | CREATE INDEX index_ci_unit_test_failures_on_build_id ON ci_unit_test_failures USING btree (build_id); | ||||||
|  | 
 | ||||||
|  | CREATE UNIQUE INDEX index_ci_unit_tests_on_project_id_and_key_hash ON ci_unit_tests USING btree (project_id, key_hash); | ||||||
|  | 
 | ||||||
| CREATE INDEX index_ci_variables_on_key ON ci_variables USING btree (key); | CREATE INDEX index_ci_variables_on_key ON ci_variables USING btree (key); | ||||||
| 
 | 
 | ||||||
| CREATE UNIQUE INDEX index_ci_variables_on_project_id_and_key_and_environment_scope ON ci_variables USING btree (project_id, key, environment_scope); | CREATE UNIQUE INDEX index_ci_variables_on_project_id_and_key_and_environment_scope ON ci_variables USING btree (project_id, key, environment_scope); | ||||||
|  | @ -23884,6 +23934,10 @@ CREATE INDEX index_u2f_registrations_on_key_handle ON u2f_registrations USING bt | ||||||
| 
 | 
 | ||||||
| CREATE INDEX index_u2f_registrations_on_user_id ON u2f_registrations USING btree (user_id); | CREATE INDEX index_u2f_registrations_on_user_id ON u2f_registrations USING btree (user_id); | ||||||
| 
 | 
 | ||||||
|  | CREATE INDEX index_unit_test_failures_failed_at ON ci_unit_test_failures USING btree (failed_at DESC); | ||||||
|  | 
 | ||||||
|  | CREATE UNIQUE INDEX index_unit_test_failures_unique_columns ON ci_unit_test_failures USING btree (unit_test_id, failed_at DESC, build_id); | ||||||
|  | 
 | ||||||
| CREATE INDEX index_uploads_on_checksum ON uploads USING btree (checksum); | CREATE INDEX index_uploads_on_checksum ON uploads USING btree (checksum); | ||||||
| 
 | 
 | ||||||
| CREATE INDEX index_uploads_on_model_id_and_model_type ON uploads USING btree (model_id, model_type); | CREATE INDEX index_uploads_on_model_id_and_model_type ON uploads USING btree (model_id, model_type); | ||||||
|  | @ -24517,6 +24571,9 @@ ALTER TABLE ONLY notification_settings | ||||||
| ALTER TABLE ONLY lists | ALTER TABLE ONLY lists | ||||||
|     ADD CONSTRAINT fk_0d3f677137 FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE; |     ADD CONSTRAINT fk_0d3f677137 FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE; | ||||||
| 
 | 
 | ||||||
|  | ALTER TABLE ONLY ci_unit_test_failures | ||||||
|  |     ADD CONSTRAINT fk_0f09856e1f FOREIGN KEY (build_id) REFERENCES ci_builds(id) ON DELETE CASCADE; | ||||||
|  | 
 | ||||||
| ALTER TABLE ONLY project_pages_metadata | ALTER TABLE ONLY project_pages_metadata | ||||||
|     ADD CONSTRAINT fk_0fd5b22688 FOREIGN KEY (pages_deployment_id) REFERENCES pages_deployments(id) ON DELETE SET NULL; |     ADD CONSTRAINT fk_0fd5b22688 FOREIGN KEY (pages_deployment_id) REFERENCES pages_deployments(id) ON DELETE SET NULL; | ||||||
| 
 | 
 | ||||||
|  | @ -24763,6 +24820,9 @@ ALTER TABLE ONLY geo_event_log | ||||||
| ALTER TABLE ONLY lists | ALTER TABLE ONLY lists | ||||||
|     ADD CONSTRAINT fk_7a5553d60f FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE; |     ADD CONSTRAINT fk_7a5553d60f FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE; | ||||||
| 
 | 
 | ||||||
|  | ALTER TABLE ONLY ci_unit_tests | ||||||
|  |     ADD CONSTRAINT fk_7a8fabf0a8 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; | ||||||
|  | 
 | ||||||
| ALTER TABLE ONLY protected_branches | ALTER TABLE ONLY protected_branches | ||||||
|     ADD CONSTRAINT fk_7a9c6d93e7 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; |     ADD CONSTRAINT fk_7a9c6d93e7 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; | ||||||
| 
 | 
 | ||||||
|  | @ -25423,6 +25483,9 @@ ALTER TABLE ONLY group_custom_attributes | ||||||
| ALTER TABLE ONLY incident_management_oncall_rotations | ALTER TABLE ONLY incident_management_oncall_rotations | ||||||
|     ADD CONSTRAINT fk_rails_256e0bc604 FOREIGN KEY (oncall_schedule_id) REFERENCES incident_management_oncall_schedules(id) ON DELETE CASCADE; |     ADD CONSTRAINT fk_rails_256e0bc604 FOREIGN KEY (oncall_schedule_id) REFERENCES incident_management_oncall_schedules(id) ON DELETE CASCADE; | ||||||
| 
 | 
 | ||||||
|  | ALTER TABLE ONLY ci_unit_test_failures | ||||||
|  |     ADD CONSTRAINT fk_rails_259da3e79c FOREIGN KEY (unit_test_id) REFERENCES ci_unit_tests(id) ON DELETE CASCADE; | ||||||
|  | 
 | ||||||
| ALTER TABLE ONLY analytics_devops_adoption_snapshots | ALTER TABLE ONLY analytics_devops_adoption_snapshots | ||||||
|     ADD CONSTRAINT fk_rails_25da9a92c0 FOREIGN KEY (segment_id) REFERENCES analytics_devops_adoption_segments(id) ON DELETE CASCADE; |     ADD CONSTRAINT fk_rails_25da9a92c0 FOREIGN KEY (segment_id) REFERENCES analytics_devops_adoption_segments(id) ON DELETE CASCADE; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,27 @@ | ||||||
|  | --- | ||||||
|  | stage: Enablement | ||||||
|  | group: Geo | ||||||
|  | info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments | ||||||
|  | type: howto | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | <!-- Please update EE::GitLab::GeoGitAccess::GEO_SERVER_DOCS_URL if this file is moved) --> | ||||||
|  | 
 | ||||||
|  | # Using a Geo Site **(PREMIUM SELF)** | ||||||
|  | 
 | ||||||
|  | After you set up the [database replication and configure the Geo nodes](../index.md#setup-instructions), use your closest GitLab site as you would do with the primary one. | ||||||
|  | 
 | ||||||
|  | You can push directly to a **secondary** site (for both HTTP, SSH including Git LFS), and the request will be proxied to the primary site instead ([introduced](https://about.gitlab.com/releases/2018/09/22/gitlab-11-3-released/) in [GitLab Premium](https://about.gitlab.com/pricing/#self-managed) 11.3). | ||||||
|  | 
 | ||||||
|  | Example of the output you will see when pushing to a **secondary** site: | ||||||
|  | 
 | ||||||
|  | ```shell | ||||||
|  | $ git push | ||||||
|  | remote: | ||||||
|  | remote: This request to a Geo secondary node will be forwarded to the | ||||||
|  | remote: Geo primary node: | ||||||
|  | remote: | ||||||
|  | remote:   ssh://git@primary.geo/user/repo.git | ||||||
|  | remote: | ||||||
|  | Everything up-to-date | ||||||
|  | ``` | ||||||
|  | @ -1,27 +1,8 @@ | ||||||
| --- | --- | ||||||
| stage: Enablement | redirect_to: '../../geo/replication/usage.md' | ||||||
| group: Geo |  | ||||||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments |  | ||||||
| type: howto |  | ||||||
| --- | --- | ||||||
| 
 | 
 | ||||||
| <!-- Please update EE::GitLab::GeoGitAccess::GEO_SERVER_DOCS_URL if this file is moved) --> | This document was moved to [another location](../../geo/replication/usage.md). | ||||||
| 
 | 
 | ||||||
| # Using a Geo Server **(PREMIUM SELF)** | <!-- This redirect file can be deleted after 2022-04-01 --> | ||||||
| 
 | <!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> | ||||||
| After you set up the [database replication and configure the Geo nodes](../index.md#setup-instructions), use your closest GitLab node as you would a normal standalone GitLab instance. |  | ||||||
| 
 |  | ||||||
| Pushing directly to a **secondary** node (for both HTTP, SSH including Git LFS) was [introduced](https://about.gitlab.com/releases/2018/09/22/gitlab-11-3-released/) in [GitLab Premium](https://about.gitlab.com/pricing/#self-managed) 11.3. |  | ||||||
| 
 |  | ||||||
| Example of the output you will see when pushing to a **secondary** node: |  | ||||||
| 
 |  | ||||||
| ```shell |  | ||||||
| $ git push |  | ||||||
| remote: |  | ||||||
| remote: You're pushing to a Geo secondary. We'll help you by proxying this |  | ||||||
| remote: request to the primary: |  | ||||||
| remote: |  | ||||||
| remote:   ssh://git@primary.geo/user/repo.git |  | ||||||
| remote: |  | ||||||
| Everything up-to-date |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
|  | @ -6,32 +6,32 @@ module Gitlab | ||||||
|       class TestFailureHistory |       class TestFailureHistory | ||||||
|         include Gitlab::Utils::StrongMemoize |         include Gitlab::Utils::StrongMemoize | ||||||
| 
 | 
 | ||||||
|         def initialize(failed_test_cases, project) |         def initialize(failed_junit_tests, project) | ||||||
|           @failed_test_cases = build_map(failed_test_cases) |           @failed_junit_tests = build_map(failed_junit_tests) | ||||||
|           @project = project |           @project = project | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         def load! |         def load! | ||||||
|           recent_failures_count.each do |key_hash, count| |           recent_failures_count.each do |key_hash, count| | ||||||
|             failed_test_cases[key_hash].set_recent_failures(count, project.default_branch_or_master) |             failed_junit_tests[key_hash].set_recent_failures(count, project.default_branch_or_master) | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         private |         private | ||||||
| 
 | 
 | ||||||
|         attr_reader :report, :project, :failed_test_cases |         attr_reader :report, :project, :failed_junit_tests | ||||||
| 
 | 
 | ||||||
|         def recent_failures_count |         def recent_failures_count | ||||||
|           ::Ci::TestCaseFailure.recent_failures_count( |           ::Ci::UnitTestFailure.recent_failures_count( | ||||||
|             project: project, |             project: project, | ||||||
|             test_case_keys: failed_test_cases.keys |             unit_test_keys: failed_junit_tests.keys | ||||||
|           ) |           ) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         def build_map(test_cases) |         def build_map(junit_tests) | ||||||
|           {}.tap do |hash| |           {}.tap do |hash| | ||||||
|             test_cases.each do |test_case| |             junit_tests.each do |test| | ||||||
|               hash[test_case.key] = test_case |               hash[test.key] = test | ||||||
|             end |             end | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
|  |  | ||||||
|  | @ -1,8 +1,10 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| FactoryBot.define do | FactoryBot.define do | ||||||
|   factory :ci_test_case, class: 'Ci::TestCase' do |   factory :ci_unit_test, class: 'Ci::UnitTest' do | ||||||
|     project |     project | ||||||
|  |     suite_name { 'rspec' } | ||||||
|  |     name { 'Math#add returns sum' } | ||||||
|     key_hash { Digest::SHA256.hexdigest(SecureRandom.hex) } |     key_hash { Digest::SHA256.hexdigest(SecureRandom.hex) } | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| FactoryBot.define do | FactoryBot.define do | ||||||
|   factory :ci_test_case_failure, class: 'Ci::TestCaseFailure' do |   factory :ci_unit_test_failure, class: 'Ci::UnitTestFailure' do | ||||||
|     build factory: :ci_build |     build factory: :ci_build | ||||||
|     test_case factory: :ci_test_case |     unit_test factory: :ci_unit_test | ||||||
|     failed_at { Time.current } |     failed_at { Time.current } | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  | @ -13,9 +13,9 @@ RSpec.describe Gitlab::Ci::Reports::TestFailureHistory, :aggregate_failures do | ||||||
|     subject(:load_history) { described_class.new([failed_rspec, failed_java], project).load! } |     subject(:load_history) { described_class.new([failed_rspec, failed_java], project).load! } | ||||||
| 
 | 
 | ||||||
|     before do |     before do | ||||||
|       allow(Ci::TestCaseFailure) |       allow(Ci::UnitTestFailure) | ||||||
|         .to receive(:recent_failures_count) |         .to receive(:recent_failures_count) | ||||||
|         .with(project: project, test_case_keys: [failed_rspec.key, failed_java.key]) |         .with(project: project, unit_test_keys: [failed_rspec.key, failed_java.key]) | ||||||
|         .and_return( |         .and_return( | ||||||
|           failed_rspec.key => 2, |           failed_rspec.key => 2, | ||||||
|           failed_java.key => 1 |           failed_java.key => 1 | ||||||
|  |  | ||||||
|  | @ -55,9 +55,7 @@ RSpec.describe Emails::MergeRequests do | ||||||
|     it_behaves_like 'appearance header and footer not enabled' |     it_behaves_like 'appearance header and footer not enabled' | ||||||
| 
 | 
 | ||||||
|     it 'is sent as the merge request author' do |     it 'is sent as the merge request author' do | ||||||
|       sender = subject.header[:from].addrs[0] |       expect_sender(merge_request.author) | ||||||
|       expect(sender.display_name).to eq(merge_request.author.name) |  | ||||||
|       expect(sender.address).to eq(gitlab_sender) |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'has the correct subject and body' do |     it 'has the correct subject and body' do | ||||||
|  | @ -85,9 +83,7 @@ RSpec.describe Emails::MergeRequests do | ||||||
|     it_behaves_like 'appearance header and footer not enabled' |     it_behaves_like 'appearance header and footer not enabled' | ||||||
| 
 | 
 | ||||||
|     it 'is sent as the author' do |     it 'is sent as the author' do | ||||||
|       sender = subject.header[:from].addrs[0] |       expect_sender(current_user) | ||||||
|       expect(sender.display_name).to eq(current_user.name) |  | ||||||
|       expect(sender.address).to eq(gitlab_sender) |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'has the correct subject and body' do |     it 'has the correct subject and body' do | ||||||
|  | @ -120,9 +116,7 @@ RSpec.describe Emails::MergeRequests do | ||||||
|     it_behaves_like 'appearance header and footer not enabled' |     it_behaves_like 'appearance header and footer not enabled' | ||||||
| 
 | 
 | ||||||
|     it 'is sent as the merge author' do |     it 'is sent as the merge author' do | ||||||
|       sender = subject.header[:from].addrs[0] |       expect_sender(merge_author) | ||||||
|       expect(sender.display_name).to eq(merge_author.name) |  | ||||||
|       expect(sender.address).to eq(gitlab_sender) |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'has the correct subject and body' do |     it 'has the correct subject and body' do | ||||||
|  | @ -153,9 +147,7 @@ RSpec.describe Emails::MergeRequests do | ||||||
|     it_behaves_like 'appearance header and footer not enabled' |     it_behaves_like 'appearance header and footer not enabled' | ||||||
| 
 | 
 | ||||||
|     it 'is sent as the author' do |     it 'is sent as the author' do | ||||||
|       sender = subject.header[:from].addrs[0] |       expect_sender(current_user) | ||||||
|       expect(sender.display_name).to eq(current_user.name) |  | ||||||
|       expect(sender.address).to eq(gitlab_sender) |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'has the correct subject and body' do |     it 'has the correct subject and body' do | ||||||
|  | @ -229,4 +221,10 @@ RSpec.describe Emails::MergeRequests do | ||||||
|       it { expect(subject).to have_content('attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15 MB.') } |       it { expect(subject).to have_content('attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15 MB.') } | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   def expect_sender(user) | ||||||
|  |     sender = subject.header[:from].addrs[0] | ||||||
|  |     expect(sender.display_name).to eq("#{user.name} (@#{user.username})") | ||||||
|  |     expect(sender.address).to eq(gitlab_sender) | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -69,11 +69,8 @@ RSpec.describe Notify do | ||||||
|       it_behaves_like 'an email sent to a user' |       it_behaves_like 'an email sent to a user' | ||||||
| 
 | 
 | ||||||
|       it 'is sent to the assignee as the author' do |       it 'is sent to the assignee as the author' do | ||||||
|         sender = subject.header[:from].addrs.first |  | ||||||
| 
 |  | ||||||
|         aggregate_failures do |         aggregate_failures do | ||||||
|           expect(sender.display_name).to eq(current_user.name) |           expect_sender(current_user) | ||||||
|           expect(sender.address).to eq(gitlab_sender) |  | ||||||
|           expect(subject).to deliver_to(recipient.notification_email) |           expect(subject).to deliver_to(recipient.notification_email) | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|  | @ -146,9 +143,7 @@ RSpec.describe Notify do | ||||||
|         it_behaves_like 'appearance header and footer not enabled' |         it_behaves_like 'appearance header and footer not enabled' | ||||||
| 
 | 
 | ||||||
|         it 'is sent as the author' do |         it 'is sent as the author' do | ||||||
|           sender = subject.header[:from].addrs[0] |           expect_sender(current_user) | ||||||
|           expect(sender.display_name).to eq(current_user.name) |  | ||||||
|           expect(sender.address).to eq(gitlab_sender) |  | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'has the correct subject and body' do |         it 'has the correct subject and body' do | ||||||
|  | @ -187,9 +182,7 @@ RSpec.describe Notify do | ||||||
|         it_behaves_like 'appearance header and footer not enabled' |         it_behaves_like 'appearance header and footer not enabled' | ||||||
| 
 | 
 | ||||||
|         it 'is sent as the author' do |         it 'is sent as the author' do | ||||||
|           sender = subject.header[:from].addrs[0] |           expect_sender(current_user) | ||||||
|           expect(sender.display_name).to eq(current_user.name) |  | ||||||
|           expect(sender.address).to eq(gitlab_sender) |  | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'has the correct subject and body' do |         it 'has the correct subject and body' do | ||||||
|  | @ -251,9 +244,7 @@ RSpec.describe Notify do | ||||||
|         it_behaves_like 'appearance header and footer not enabled' |         it_behaves_like 'appearance header and footer not enabled' | ||||||
| 
 | 
 | ||||||
|         it 'is sent as the author' do |         it 'is sent as the author' do | ||||||
|           sender = subject.header[:from].addrs[0] |           expect_sender(current_user) | ||||||
|           expect(sender.display_name).to eq(current_user.name) |  | ||||||
|           expect(sender.address).to eq(gitlab_sender) |  | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'has the correct subject and body' do |         it 'has the correct subject and body' do | ||||||
|  | @ -389,9 +380,7 @@ RSpec.describe Notify do | ||||||
|         it_behaves_like 'appearance header and footer not enabled' |         it_behaves_like 'appearance header and footer not enabled' | ||||||
| 
 | 
 | ||||||
|         it 'is sent as the author' do |         it 'is sent as the author' do | ||||||
|           sender = subject.header[:from].addrs[0] |           expect_sender(current_user) | ||||||
|           expect(sender.display_name).to eq(current_user.name) |  | ||||||
|           expect(sender.address).to eq(gitlab_sender) |  | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'has the correct subject and body' do |         it 'has the correct subject and body' do | ||||||
|  | @ -456,9 +445,7 @@ RSpec.describe Notify do | ||||||
|         it_behaves_like 'appearance header and footer not enabled' |         it_behaves_like 'appearance header and footer not enabled' | ||||||
| 
 | 
 | ||||||
|         it 'is sent as the author' do |         it 'is sent as the author' do | ||||||
|           sender = subject.header[:from].addrs[0] |           expect_sender(current_user) | ||||||
|           expect(sender.display_name).to eq(current_user.name) |  | ||||||
|           expect(sender.address).to eq(gitlab_sender) |  | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'has the correct subject and body' do |         it 'has the correct subject and body' do | ||||||
|  | @ -486,10 +473,7 @@ RSpec.describe Notify do | ||||||
|         it_behaves_like 'appearance header and footer not enabled' |         it_behaves_like 'appearance header and footer not enabled' | ||||||
| 
 | 
 | ||||||
|         it 'is sent as the push user' do |         it 'is sent as the push user' do | ||||||
|           sender = subject.header[:from].addrs[0] |           expect_sender(push_user) | ||||||
| 
 |  | ||||||
|           expect(sender.display_name).to eq(push_user.name) |  | ||||||
|           expect(sender.address).to eq(gitlab_sender) |  | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'has the correct subject and body' do |         it 'has the correct subject and body' do | ||||||
|  | @ -1002,11 +986,8 @@ RSpec.describe Notify do | ||||||
|         it_behaves_like 'it should have Gmail Actions links' |         it_behaves_like 'it should have Gmail Actions links' | ||||||
| 
 | 
 | ||||||
|         it 'is sent to the given recipient as the author' do |         it 'is sent to the given recipient as the author' do | ||||||
|           sender = subject.header[:from].addrs[0] |  | ||||||
| 
 |  | ||||||
|           aggregate_failures do |           aggregate_failures do | ||||||
|             expect(sender.display_name).to eq(note_author.name) |             expect_sender(note_author) | ||||||
|             expect(sender.address).to eq(gitlab_sender) |  | ||||||
|             expect(subject).to deliver_to(recipient.notification_email) |             expect(subject).to deliver_to(recipient.notification_email) | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
|  | @ -1162,11 +1143,8 @@ RSpec.describe Notify do | ||||||
|         it_behaves_like 'it should have Gmail Actions links' |         it_behaves_like 'it should have Gmail Actions links' | ||||||
| 
 | 
 | ||||||
|         it 'is sent to the given recipient as the author' do |         it 'is sent to the given recipient as the author' do | ||||||
|           sender = subject.header[:from].addrs[0] |  | ||||||
| 
 |  | ||||||
|           aggregate_failures do |           aggregate_failures do | ||||||
|             expect(sender.display_name).to eq(note_author.name) |             expect_sender(note_author) | ||||||
|             expect(sender.address).to eq(gitlab_sender) |  | ||||||
|             expect(subject).to deliver_to(recipient.notification_email) |             expect(subject).to deliver_to(recipient.notification_email) | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
|  | @ -1221,12 +1199,6 @@ RSpec.describe Notify do | ||||||
|         issue.issue_email_participants.create!(email: 'service.desk@example.com') |         issue.issue_email_participants.create!(email: 'service.desk@example.com') | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       def expect_sender(username) |  | ||||||
|         sender = subject.header[:from].addrs[0] |  | ||||||
|         expect(sender.display_name).to eq(username) |  | ||||||
|         expect(sender.address).to eq(gitlab_sender) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       describe 'thank you email' do |       describe 'thank you email' do | ||||||
|         subject { described_class.service_desk_thank_you_email(issue.id) } |         subject { described_class.service_desk_thank_you_email(issue.id) } | ||||||
| 
 | 
 | ||||||
|  | @ -1244,14 +1216,16 @@ RSpec.describe Notify do | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'uses service bot name by default' do |         it 'uses service bot name by default' do | ||||||
|           expect_sender(User.support_bot.name) |           expect_sender(User.support_bot) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         context 'when custom outgoing name is set' do |         context 'when custom outgoing name is set' do | ||||||
|           let_it_be(:settings) { create(:service_desk_setting, project: project, outgoing_name: 'some custom name') } |           let_it_be(:settings) { create(:service_desk_setting, project: project, outgoing_name: 'some custom name') } | ||||||
| 
 | 
 | ||||||
|           it 'uses custom name in "from" header' do |           it 'uses custom name in "from" header' do | ||||||
|             expect_sender('some custom name') |             sender = subject.header[:from].addrs[0] | ||||||
|  |             expect(sender.display_name).to eq('some custom name') | ||||||
|  |             expect(sender.address).to eq(gitlab_sender) | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|  | @ -1259,7 +1233,7 @@ RSpec.describe Notify do | ||||||
|           let_it_be(:settings) { create(:service_desk_setting, project: project, outgoing_name: '') } |           let_it_be(:settings) { create(:service_desk_setting, project: project, outgoing_name: '') } | ||||||
| 
 | 
 | ||||||
|           it 'uses service bot name' do |           it 'uses service bot name' do | ||||||
|             expect_sender(User.support_bot.name) |             expect_sender(User.support_bot) | ||||||
|           end |           end | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|  | @ -1276,7 +1250,7 @@ RSpec.describe Notify do | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'uses author\'s name in "from" header' do |         it 'uses author\'s name in "from" header' do | ||||||
|           expect_sender(first_note.author.name) |           expect_sender(first_note.author) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'has the correct subject and body' do |         it 'has the correct subject and body' do | ||||||
|  | @ -1672,9 +1646,7 @@ RSpec.describe Notify do | ||||||
|     it_behaves_like 'appearance header and footer not enabled' |     it_behaves_like 'appearance header and footer not enabled' | ||||||
| 
 | 
 | ||||||
|     it 'is sent as the author' do |     it 'is sent as the author' do | ||||||
|       sender = subject.header[:from].addrs[0] |       expect_sender(user) | ||||||
|       expect(sender.display_name).to eq(user.name) |  | ||||||
|       expect(sender.address).to eq(gitlab_sender) |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'has the correct subject and body' do |     it 'has the correct subject and body' do | ||||||
|  | @ -1699,9 +1671,7 @@ RSpec.describe Notify do | ||||||
|     it_behaves_like 'appearance header and footer not enabled' |     it_behaves_like 'appearance header and footer not enabled' | ||||||
| 
 | 
 | ||||||
|     it 'is sent as the author' do |     it 'is sent as the author' do | ||||||
|       sender = subject.header[:from].addrs[0] |       expect_sender(user) | ||||||
|       expect(sender.display_name).to eq(user.name) |  | ||||||
|       expect(sender.address).to eq(gitlab_sender) |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'has the correct subject and body' do |     it 'has the correct subject and body' do | ||||||
|  | @ -1725,9 +1695,7 @@ RSpec.describe Notify do | ||||||
|     it_behaves_like 'appearance header and footer not enabled' |     it_behaves_like 'appearance header and footer not enabled' | ||||||
| 
 | 
 | ||||||
|     it 'is sent as the author' do |     it 'is sent as the author' do | ||||||
|       sender = subject.header[:from].addrs[0] |       expect_sender(user) | ||||||
|       expect(sender.display_name).to eq(user.name) |  | ||||||
|       expect(sender.address).to eq(gitlab_sender) |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'has the correct subject' do |     it 'has the correct subject' do | ||||||
|  | @ -1748,9 +1716,7 @@ RSpec.describe Notify do | ||||||
|     it_behaves_like 'appearance header and footer not enabled' |     it_behaves_like 'appearance header and footer not enabled' | ||||||
| 
 | 
 | ||||||
|     it 'is sent as the author' do |     it 'is sent as the author' do | ||||||
|       sender = subject.header[:from].addrs[0] |       expect_sender(user) | ||||||
|       expect(sender.display_name).to eq(user.name) |  | ||||||
|       expect(sender.address).to eq(gitlab_sender) |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'has the correct subject' do |     it 'has the correct subject' do | ||||||
|  | @ -1777,9 +1743,7 @@ RSpec.describe Notify do | ||||||
|     it_behaves_like 'appearance header and footer not enabled' |     it_behaves_like 'appearance header and footer not enabled' | ||||||
| 
 | 
 | ||||||
|     it 'is sent as the author' do |     it 'is sent as the author' do | ||||||
|       sender = subject.header[:from].addrs[0] |       expect_sender(user) | ||||||
|       expect(sender.display_name).to eq(user.name) |  | ||||||
|       expect(sender.address).to eq(gitlab_sender) |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'has the correct subject and body' do |     it 'has the correct subject and body' do | ||||||
|  | @ -1870,9 +1834,7 @@ RSpec.describe Notify do | ||||||
|     it_behaves_like 'appearance header and footer not enabled' |     it_behaves_like 'appearance header and footer not enabled' | ||||||
| 
 | 
 | ||||||
|     it 'is sent as the author' do |     it 'is sent as the author' do | ||||||
|       sender = subject.header[:from].addrs[0] |       expect_sender(user) | ||||||
|       expect(sender.display_name).to eq(user.name) |  | ||||||
|       expect(sender.address).to eq(gitlab_sender) |  | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'has the correct subject and body' do |     it 'has the correct subject and body' do | ||||||
|  | @ -1962,12 +1924,8 @@ RSpec.describe Notify do | ||||||
|     it_behaves_like 'an unsubscribeable thread' |     it_behaves_like 'an unsubscribeable thread' | ||||||
| 
 | 
 | ||||||
|     it 'is sent to the given recipient as the author' do |     it 'is sent to the given recipient as the author' do | ||||||
|       sender = subject.header[:from].addrs[0] |  | ||||||
| 
 |  | ||||||
|       aggregate_failures do |       aggregate_failures do | ||||||
|         expect(sender.display_name).to eq(review.author_name) |         expect_sender(review.author) | ||||||
|         expect(sender.address).to eq(gitlab_sender) |  | ||||||
|         expect(subject).to deliver_to(recipient.notification_email) |  | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  | @ -2002,4 +1960,10 @@ RSpec.describe Notify do | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   def expect_sender(user) | ||||||
|  |     sender = subject.header[:from].addrs[0] | ||||||
|  |     expect(sender.display_name).to eq("#{user.name} (@#{user.username})") | ||||||
|  |     expect(sender.address).to eq(gitlab_sender) | ||||||
|  |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -1,73 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| require 'spec_helper' |  | ||||||
| 
 |  | ||||||
| RSpec.describe Ci::TestCaseFailure do |  | ||||||
|   describe 'relationships' do |  | ||||||
|     it { is_expected.to belong_to(:build) } |  | ||||||
|     it { is_expected.to belong_to(:test_case) } |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   describe 'validations' do |  | ||||||
|     subject { build(:ci_test_case_failure) } |  | ||||||
| 
 |  | ||||||
|     it { is_expected.to validate_presence_of(:test_case) } |  | ||||||
|     it { is_expected.to validate_presence_of(:build) } |  | ||||||
|     it { is_expected.to validate_presence_of(:failed_at) } |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   describe '.recent_failures_count' do |  | ||||||
|     let_it_be(:project) { create(:project) } |  | ||||||
| 
 |  | ||||||
|     subject(:recent_failures) do |  | ||||||
|       described_class.recent_failures_count( |  | ||||||
|         project: project, |  | ||||||
|         test_case_keys: test_case_keys |  | ||||||
|       ) |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     context 'when test case failures are within the date range and are for the test case keys' do |  | ||||||
|       let(:tc_1) { create(:ci_test_case, project: project) } |  | ||||||
|       let(:tc_2) { create(:ci_test_case, project: project) } |  | ||||||
|       let(:test_case_keys) { [tc_1.key_hash, tc_2.key_hash] } |  | ||||||
| 
 |  | ||||||
|       before do |  | ||||||
|         create_list(:ci_test_case_failure, 3, test_case: tc_1, failed_at: 1.day.ago) |  | ||||||
|         create_list(:ci_test_case_failure, 2, test_case: tc_2, failed_at: 3.days.ago) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'returns the number of failures for each test case key hash for the past 14 days by default' do |  | ||||||
|         expect(recent_failures).to eq( |  | ||||||
|           tc_1.key_hash => 3, |  | ||||||
|           tc_2.key_hash => 2 |  | ||||||
|         ) |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     context 'when test case failures are within the date range but are not for the test case keys' do |  | ||||||
|       let(:tc) { create(:ci_test_case, project: project) } |  | ||||||
|       let(:test_case_keys) { ['some-other-key-hash'] } |  | ||||||
| 
 |  | ||||||
|       before do |  | ||||||
|         create(:ci_test_case_failure, test_case: tc, failed_at: 1.day.ago) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'excludes them from the count' do |  | ||||||
|         expect(recent_failures[tc.key_hash]).to be_nil |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     context 'when test case failures are not within the date range but are for the test case keys' do |  | ||||||
|       let(:tc) { create(:ci_test_case, project: project) } |  | ||||||
|       let(:test_case_keys) { [tc.key_hash] } |  | ||||||
| 
 |  | ||||||
|       before do |  | ||||||
|         create(:ci_test_case_failure, test_case: tc, failed_at: 15.days.ago) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'excludes them from the count' do |  | ||||||
|         expect(recent_failures[tc.key_hash]).to be_nil |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -1,31 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| require 'spec_helper' |  | ||||||
| 
 |  | ||||||
| RSpec.describe Ci::TestCase do |  | ||||||
|   describe 'relationships' do |  | ||||||
|     it { is_expected.to belong_to(:project) } |  | ||||||
|     it { is_expected.to have_many(:test_case_failures) } |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   describe 'validations' do |  | ||||||
|     subject { build(:ci_test_case) } |  | ||||||
| 
 |  | ||||||
|     it { is_expected.to validate_presence_of(:project) } |  | ||||||
|     it { is_expected.to validate_presence_of(:key_hash) } |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   describe '.find_or_create_by_batch' do |  | ||||||
|     it 'finds or creates records for the given test case keys', :aggregate_failures do |  | ||||||
|       project = create(:project) |  | ||||||
|       existing_tc = create(:ci_test_case, project: project) |  | ||||||
|       new_key = Digest::SHA256.hexdigest(SecureRandom.hex) |  | ||||||
|       keys = [existing_tc.key_hash, new_key] |  | ||||||
| 
 |  | ||||||
|       result = described_class.find_or_create_by_batch(project, keys) |  | ||||||
| 
 |  | ||||||
|       expect(result.map(&:key_hash)).to match_array([existing_tc.key_hash, new_key]) |  | ||||||
|       expect(result).to all(be_persisted) |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end |  | ||||||
|  | @ -0,0 +1,73 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'spec_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe Ci::UnitTestFailure do | ||||||
|  |   describe 'relationships' do | ||||||
|  |     it { is_expected.to belong_to(:build) } | ||||||
|  |     it { is_expected.to belong_to(:unit_test) } | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe 'validations' do | ||||||
|  |     subject { build(:ci_unit_test_failure) } | ||||||
|  | 
 | ||||||
|  |     it { is_expected.to validate_presence_of(:unit_test) } | ||||||
|  |     it { is_expected.to validate_presence_of(:build) } | ||||||
|  |     it { is_expected.to validate_presence_of(:failed_at) } | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '.recent_failures_count' do | ||||||
|  |     let_it_be(:project) { create(:project) } | ||||||
|  | 
 | ||||||
|  |     subject(:recent_failures) do | ||||||
|  |       described_class.recent_failures_count( | ||||||
|  |         project: project, | ||||||
|  |         unit_test_keys: unit_test_keys | ||||||
|  |       ) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when unit test failures are within the date range and are for the unit test keys' do | ||||||
|  |       let(:test_1) { create(:ci_unit_test, project: project) } | ||||||
|  |       let(:test_2) { create(:ci_unit_test, project: project) } | ||||||
|  |       let(:unit_test_keys) { [test_1.key_hash, test_2.key_hash] } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         create_list(:ci_unit_test_failure, 3, unit_test: test_1, failed_at: 1.day.ago) | ||||||
|  |         create_list(:ci_unit_test_failure, 2, unit_test: test_2, failed_at: 3.days.ago) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'returns the number of failures for each unit test key hash for the past 14 days by default' do | ||||||
|  |         expect(recent_failures).to eq( | ||||||
|  |           test_1.key_hash => 3, | ||||||
|  |           test_2.key_hash => 2 | ||||||
|  |         ) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when unit test failures are within the date range but are not for the unit test keys' do | ||||||
|  |       let(:test) { create(:ci_unit_test, project: project) } | ||||||
|  |       let(:unit_test_keys) { ['some-other-key-hash'] } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         create(:ci_unit_test_failure, unit_test: test, failed_at: 1.day.ago) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'excludes them from the count' do | ||||||
|  |         expect(recent_failures[test.key_hash]).to be_nil | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when unit test failures are not within the date range but are for the unit test keys' do | ||||||
|  |       let(:test) { create(:ci_unit_test, project: project) } | ||||||
|  |       let(:unit_test_keys) { [test.key_hash] } | ||||||
|  | 
 | ||||||
|  |       before do | ||||||
|  |         create(:ci_unit_test_failure, unit_test: test, failed_at: 15.days.ago) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'excludes them from the count' do | ||||||
|  |         expect(recent_failures[test.key_hash]).to be_nil | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,87 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | require 'spec_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe Ci::UnitTest do | ||||||
|  |   describe 'relationships' do | ||||||
|  |     it { is_expected.to belong_to(:project) } | ||||||
|  |     it { is_expected.to have_many(:unit_test_failures) } | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe 'validations' do | ||||||
|  |     subject { build(:ci_unit_test) } | ||||||
|  | 
 | ||||||
|  |     it { is_expected.to validate_presence_of(:project) } | ||||||
|  |     it { is_expected.to validate_presence_of(:key_hash) } | ||||||
|  |     it { is_expected.to validate_presence_of(:name) } | ||||||
|  |     it { is_expected.to validate_presence_of(:suite_name) } | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '.find_or_create_by_batch' do | ||||||
|  |     let(:project) { create(:project) } | ||||||
|  | 
 | ||||||
|  |     it 'finds or creates records for the given unit test keys', :aggregate_failures do | ||||||
|  |       existing_test = create(:ci_unit_test, project: project, suite_name: 'rspec', name: 'Math#sum adds numbers') | ||||||
|  |       new_key = Digest::SHA256.hexdigest(SecureRandom.hex) | ||||||
|  |       attrs = [ | ||||||
|  |         { | ||||||
|  |           key_hash: existing_test.key_hash, | ||||||
|  |           name: 'This new name will not apply', | ||||||
|  |           suite_name: 'This new suite name will not apply' | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           key_hash: new_key, | ||||||
|  |           name: 'Component works', | ||||||
|  |           suite_name: 'jest' | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  | 
 | ||||||
|  |       result = described_class.find_or_create_by_batch(project, attrs) | ||||||
|  | 
 | ||||||
|  |       expect(result).to match_array([ | ||||||
|  |         have_attributes( | ||||||
|  |           key_hash: existing_test.key_hash, | ||||||
|  |           suite_name: 'rspec', | ||||||
|  |           name: 'Math#sum adds numbers' | ||||||
|  |         ), | ||||||
|  |         have_attributes( | ||||||
|  |           key_hash: new_key, | ||||||
|  |           suite_name: 'jest', | ||||||
|  |           name: 'Component works' | ||||||
|  |         ) | ||||||
|  |       ]) | ||||||
|  | 
 | ||||||
|  |       expect(result).to all(be_persisted) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when a given name or suite_name exceeds the string size limit' do | ||||||
|  |       before do | ||||||
|  |         stub_const("#{described_class}::MAX_NAME_SIZE", 6) | ||||||
|  |         stub_const("#{described_class}::MAX_SUITE_NAME_SIZE", 6) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'truncates the values before storing the information' do | ||||||
|  |         new_key = Digest::SHA256.hexdigest(SecureRandom.hex) | ||||||
|  |         attrs = [ | ||||||
|  |           { | ||||||
|  |             key_hash: new_key, | ||||||
|  |             name: 'abcdefg', | ||||||
|  |             suite_name: 'abcdefg' | ||||||
|  |           } | ||||||
|  |         ] | ||||||
|  | 
 | ||||||
|  |         result = described_class.find_or_create_by_batch(project, attrs) | ||||||
|  | 
 | ||||||
|  |         expect(result).to match_array([ | ||||||
|  |           have_attributes( | ||||||
|  |             key_hash: new_key, | ||||||
|  |             suite_name: 'abc...', | ||||||
|  |             name: 'abc...' | ||||||
|  |           ) | ||||||
|  |         ]) | ||||||
|  | 
 | ||||||
|  |         expect(result).to all(be_persisted) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -11,15 +11,15 @@ RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do | ||||||
| 
 | 
 | ||||||
|     context 'when pipeline has failed builds with test reports' do |     context 'when pipeline has failed builds with test reports' do | ||||||
|       before do |       before do | ||||||
|         # The test report has 2 test case failures |         # The test report has 2 unit test failures | ||||||
|         create(:ci_build, :failed, :test_reports, pipeline: pipeline, project: project) |         create(:ci_build, :failed, :test_reports, pipeline: pipeline, project: project) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'creates test case failures records' do |       it 'creates unit test failures records' do | ||||||
|         execute_service |         execute_service | ||||||
| 
 | 
 | ||||||
|         expect(Ci::TestCase.count).to eq(2) |         expect(Ci::UnitTest.count).to eq(2) | ||||||
|         expect(Ci::TestCaseFailure.count).to eq(2) |         expect(Ci::UnitTestFailure.count).to eq(2) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'when pipeline is not for the default branch' do |       context 'when pipeline is not for the default branch' do | ||||||
|  | @ -30,8 +30,8 @@ RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do | ||||||
|         it 'does not persist data' do |         it 'does not persist data' do | ||||||
|           execute_service |           execute_service | ||||||
| 
 | 
 | ||||||
|           expect(Ci::TestCase.count).to eq(0) |           expect(Ci::UnitTest.count).to eq(0) | ||||||
|           expect(Ci::TestCaseFailure.count).to eq(0) |           expect(Ci::UnitTestFailure.count).to eq(0) | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  | @ -43,12 +43,12 @@ RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do | ||||||
|         it 'does not fail but does not persist new data' do |         it 'does not fail but does not persist new data' do | ||||||
|           expect { described_class.new(pipeline).execute }.not_to raise_error |           expect { described_class.new(pipeline).execute }.not_to raise_error | ||||||
| 
 | 
 | ||||||
|           expect(Ci::TestCase.count).to eq(2) |           expect(Ci::UnitTest.count).to eq(2) | ||||||
|           expect(Ci::TestCaseFailure.count).to eq(2) |           expect(Ci::UnitTestFailure.count).to eq(2) | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'when number of failed test cases exceed the limit' do |       context 'when number of failed unit tests exceed the limit' do | ||||||
|         before do |         before do | ||||||
|           stub_const("#{described_class.name}::MAX_TRACKABLE_FAILURES", 1) |           stub_const("#{described_class.name}::MAX_TRACKABLE_FAILURES", 1) | ||||||
|         end |         end | ||||||
|  | @ -56,16 +56,16 @@ RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do | ||||||
|         it 'does not persist data' do |         it 'does not persist data' do | ||||||
|           execute_service |           execute_service | ||||||
| 
 | 
 | ||||||
|           expect(Ci::TestCase.count).to eq(0) |           expect(Ci::UnitTest.count).to eq(0) | ||||||
|           expect(Ci::TestCaseFailure.count).to eq(0) |           expect(Ci::UnitTestFailure.count).to eq(0) | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'when number of failed test cases across multiple builds exceed the limit' do |       context 'when number of failed unit tests across multiple builds exceed the limit' do | ||||||
|         before do |         before do | ||||||
|           stub_const("#{described_class.name}::MAX_TRACKABLE_FAILURES", 2) |           stub_const("#{described_class.name}::MAX_TRACKABLE_FAILURES", 2) | ||||||
| 
 | 
 | ||||||
|           # This other test report has 1 unique test case failure which brings us to 3 total failures across all builds |           # This other test report has 1 unique unit test failure which brings us to 3 total failures across all builds | ||||||
|           # thus exceeding the limit of 2 for MAX_TRACKABLE_FAILURES |           # thus exceeding the limit of 2 for MAX_TRACKABLE_FAILURES | ||||||
|           create(:ci_build, :failed, :test_reports_with_duplicate_failed_test_names, pipeline: pipeline, project: project) |           create(:ci_build, :failed, :test_reports_with_duplicate_failed_test_names, pipeline: pipeline, project: project) | ||||||
|         end |         end | ||||||
|  | @ -73,23 +73,23 @@ RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do | ||||||
|         it 'does not persist data' do |         it 'does not persist data' do | ||||||
|           execute_service |           execute_service | ||||||
| 
 | 
 | ||||||
|           expect(Ci::TestCase.count).to eq(0) |           expect(Ci::UnitTest.count).to eq(0) | ||||||
|           expect(Ci::TestCaseFailure.count).to eq(0) |           expect(Ci::UnitTestFailure.count).to eq(0) | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     context 'when test failure data have duplicates within the same payload (happens when the JUnit report has duplicate test case names but have different failures)' do |     context 'when test failure data have duplicates within the same payload (happens when the JUnit report has duplicate unit test names but have different failures)' do | ||||||
|       before do |       before do | ||||||
|         # The test report has 2 test case failures but with the same test case keys |         # The test report has 2 unit test failures but with the same unit test keys | ||||||
|         create(:ci_build, :failed, :test_reports_with_duplicate_failed_test_names, pipeline: pipeline, project: project) |         create(:ci_build, :failed, :test_reports_with_duplicate_failed_test_names, pipeline: pipeline, project: project) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'does not fail but does not persist duplicate data' do |       it 'does not fail but does not persist duplicate data' do | ||||||
|         expect { execute_service }.not_to raise_error |         expect { execute_service }.not_to raise_error | ||||||
| 
 | 
 | ||||||
|         expect(Ci::TestCase.count).to eq(1) |         expect(Ci::UnitTest.count).to eq(1) | ||||||
|         expect(Ci::TestCaseFailure.count).to eq(1) |         expect(Ci::UnitTestFailure.count).to eq(1) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  | @ -102,8 +102,8 @@ RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do | ||||||
|       it 'does not persist data' do |       it 'does not persist data' do | ||||||
|         execute_service |         execute_service | ||||||
| 
 | 
 | ||||||
|         expect(Ci::TestCase.count).to eq(0) |         expect(Ci::UnitTest.count).to eq(0) | ||||||
|         expect(Ci::TestCaseFailure.count).to eq(0) |         expect(Ci::UnitTestFailure.count).to eq(0) | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -1662,7 +1662,7 @@ RSpec.describe NotificationService, :mailer do | ||||||
|         notification.issue_due(issue) |         notification.issue_due(issue) | ||||||
|         email = find_email_for(@subscriber) |         email = find_email_for(@subscriber) | ||||||
| 
 | 
 | ||||||
|         expect(email.header[:from].display_names).to eq([issue.author.name]) |         expect(email.header[:from].display_names).to eq(["#{issue.author.name} (@#{issue.author.username})"]) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it_behaves_like 'participating notifications' do |       it_behaves_like 'participating notifications' do | ||||||
|  |  | ||||||
|  | @ -225,7 +225,7 @@ RSpec.shared_examples 'a note email' do | ||||||
|     sender = subject.header[:from].addrs[0] |     sender = subject.header[:from].addrs[0] | ||||||
| 
 | 
 | ||||||
|     aggregate_failures do |     aggregate_failures do | ||||||
|       expect(sender.display_name).to eq(note_author.name) |       expect(sender.display_name).to eq("#{note_author.name} (@#{note_author.username})") | ||||||
|       expect(sender.address).to eq(gitlab_sender) |       expect(sender.address).to eq(gitlab_sender) | ||||||
|       expect(subject).to deliver_to(recipient.notification_email) |       expect(subject).to deliver_to(recipient.notification_email) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -40,8 +40,8 @@ RSpec.describe ::Ci::TestFailureHistoryWorker do | ||||||
| 
 | 
 | ||||||
|       subject |       subject | ||||||
| 
 | 
 | ||||||
|       expect(Ci::TestCase.count).to eq(2) |       expect(Ci::UnitTest.count).to eq(2) | ||||||
|       expect(Ci::TestCaseFailure.count).to eq(2) |       expect(Ci::UnitTestFailure.count).to eq(2) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue