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) | ||||
| 
 | ||||
|     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) | ||||
|       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 | ||||
|       # 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 | ||||
|       # don't have to parse each JUnit report of each of the 201 builds. | ||||
|       failed_builds.length <= MAX_TRACKABLE_FAILURES | ||||
|  | @ -51,25 +51,29 @@ module Ci | |||
|     end | ||||
| 
 | ||||
|     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| | ||||
|         Ci::TestCase.transaction do | ||||
|           ci_test_cases = Ci::TestCase.find_or_create_by_batch(project, key_hashes) | ||||
|           failures = test_case_failures(ci_test_cases, failed_test_cases) | ||||
|       failed_unit_tests.each_slice(100) do |batch| | ||||
|         Ci::UnitTest.transaction do | ||||
|           unit_test_attrs = ci_unit_test_attrs(batch) | ||||
|           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 | ||||
| 
 | ||||
|     def gather_failed_test_cases(failed_builds) | ||||
|       failed_builds.each_with_object({}) do |build, failed_test_cases| | ||||
|     def gather_failed_unit_tests_from_reports(failed_builds) | ||||
|       failed_builds.each_with_object({}) do |build, failed_unit_tests| | ||||
|         test_suite = generate_test_suite!(build) | ||||
|         test_suite.failed.keys.each do |key| | ||||
|           failed_test_cases[key] = build | ||||
|         test_suite.failed.each do |key, unit_test| | ||||
|           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 | ||||
|  | @ -79,12 +83,24 @@ module Ci | |||
|       build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new) | ||||
|     end | ||||
| 
 | ||||
|     def test_case_failures(ci_test_cases, failed_test_cases) | ||||
|       ci_test_cases.map do |test_case| | ||||
|         build = failed_test_cases[test_case.key_hash] | ||||
|     def ci_unit_test_attrs(batch) | ||||
|       batch.map do |item| | ||||
|         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, | ||||
|           failed_at: build.finished_at | ||||
|         } | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ | |||
|             %li | ||||
|               = link_to title, filter_tags_path(sort: value), class: ("is-active" if @sort == value) | ||||
|       - 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') | ||||
|       = 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') | ||||
|  |  | |||
|  | @ -15,5 +15,5 @@ | |||
|     = render 'shared/notes/hints' | ||||
|   .error-alert | ||||
|   .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" | ||||
|  |  | |||
|  | @ -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; | ||||
| 
 | ||||
| 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 ( | ||||
|     id integer 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_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 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 | ||||
|     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 | ||||
|     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_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 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_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_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 | ||||
|     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 | ||||
|     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 | ||||
|     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 | ||||
|     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 | ||||
|     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 | ||||
|     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 | ||||
| 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 | ||||
| redirect_to: '../../geo/replication/usage.md' | ||||
| --- | ||||
| 
 | ||||
| <!-- 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)** | ||||
| 
 | ||||
| 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 | ||||
| ``` | ||||
| <!-- 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 --> | ||||
|  |  | |||
|  | @ -6,32 +6,32 @@ module Gitlab | |||
|       class TestFailureHistory | ||||
|         include Gitlab::Utils::StrongMemoize | ||||
| 
 | ||||
|         def initialize(failed_test_cases, project) | ||||
|           @failed_test_cases = build_map(failed_test_cases) | ||||
|         def initialize(failed_junit_tests, project) | ||||
|           @failed_junit_tests = build_map(failed_junit_tests) | ||||
|           @project = project | ||||
|         end | ||||
| 
 | ||||
|         def load! | ||||
|           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 | ||||
| 
 | ||||
|         private | ||||
| 
 | ||||
|         attr_reader :report, :project, :failed_test_cases | ||||
|         attr_reader :report, :project, :failed_junit_tests | ||||
| 
 | ||||
|         def recent_failures_count | ||||
|           ::Ci::TestCaseFailure.recent_failures_count( | ||||
|           ::Ci::UnitTestFailure.recent_failures_count( | ||||
|             project: project, | ||||
|             test_case_keys: failed_test_cases.keys | ||||
|             unit_test_keys: failed_junit_tests.keys | ||||
|           ) | ||||
|         end | ||||
| 
 | ||||
|         def build_map(test_cases) | ||||
|         def build_map(junit_tests) | ||||
|           {}.tap do |hash| | ||||
|             test_cases.each do |test_case| | ||||
|               hash[test_case.key] = test_case | ||||
|             junit_tests.each do |test| | ||||
|               hash[test.key] = test | ||||
|             end | ||||
|           end | ||||
|         end | ||||
|  |  | |||
|  | @ -1,8 +1,10 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| FactoryBot.define do | ||||
|   factory :ci_test_case, class: 'Ci::TestCase' do | ||||
|   factory :ci_unit_test, class: 'Ci::UnitTest' do | ||||
|     project | ||||
|     suite_name { 'rspec' } | ||||
|     name { 'Math#add returns sum' } | ||||
|     key_hash { Digest::SHA256.hexdigest(SecureRandom.hex) } | ||||
|   end | ||||
| end | ||||
|  | @ -1,9 +1,9 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| 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 | ||||
|     test_case factory: :ci_test_case | ||||
|     unit_test factory: :ci_unit_test | ||||
|     failed_at { Time.current } | ||||
|   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! } | ||||
| 
 | ||||
|     before do | ||||
|       allow(Ci::TestCaseFailure) | ||||
|       allow(Ci::UnitTestFailure) | ||||
|         .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( | ||||
|           failed_rspec.key => 2, | ||||
|           failed_java.key => 1 | ||||
|  |  | |||
|  | @ -55,9 +55,7 @@ RSpec.describe Emails::MergeRequests do | |||
|     it_behaves_like 'appearance header and footer not enabled' | ||||
| 
 | ||||
|     it 'is sent as the merge request author' do | ||||
|       sender = subject.header[:from].addrs[0] | ||||
|       expect(sender.display_name).to eq(merge_request.author.name) | ||||
|       expect(sender.address).to eq(gitlab_sender) | ||||
|       expect_sender(merge_request.author) | ||||
|     end | ||||
| 
 | ||||
|     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 'is sent as the author' do | ||||
|       sender = subject.header[:from].addrs[0] | ||||
|       expect(sender.display_name).to eq(current_user.name) | ||||
|       expect(sender.address).to eq(gitlab_sender) | ||||
|       expect_sender(current_user) | ||||
|     end | ||||
| 
 | ||||
|     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 'is sent as the merge author' do | ||||
|       sender = subject.header[:from].addrs[0] | ||||
|       expect(sender.display_name).to eq(merge_author.name) | ||||
|       expect(sender.address).to eq(gitlab_sender) | ||||
|       expect_sender(merge_author) | ||||
|     end | ||||
| 
 | ||||
|     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 'is sent as the author' do | ||||
|       sender = subject.header[:from].addrs[0] | ||||
|       expect(sender.display_name).to eq(current_user.name) | ||||
|       expect(sender.address).to eq(gitlab_sender) | ||||
|       expect_sender(current_user) | ||||
|     end | ||||
| 
 | ||||
|     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.') } | ||||
|     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 | ||||
|  |  | |||
|  | @ -69,11 +69,8 @@ RSpec.describe Notify do | |||
|       it_behaves_like 'an email sent to a user' | ||||
| 
 | ||||
|       it 'is sent to the assignee as the author' do | ||||
|         sender = subject.header[:from].addrs.first | ||||
| 
 | ||||
|         aggregate_failures do | ||||
|           expect(sender.display_name).to eq(current_user.name) | ||||
|           expect(sender.address).to eq(gitlab_sender) | ||||
|           expect_sender(current_user) | ||||
|           expect(subject).to deliver_to(recipient.notification_email) | ||||
|         end | ||||
|       end | ||||
|  | @ -146,9 +143,7 @@ RSpec.describe Notify do | |||
|         it_behaves_like 'appearance header and footer not enabled' | ||||
| 
 | ||||
|         it 'is sent as the author' do | ||||
|           sender = subject.header[:from].addrs[0] | ||||
|           expect(sender.display_name).to eq(current_user.name) | ||||
|           expect(sender.address).to eq(gitlab_sender) | ||||
|           expect_sender(current_user) | ||||
|         end | ||||
| 
 | ||||
|         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 'is sent as the author' do | ||||
|           sender = subject.header[:from].addrs[0] | ||||
|           expect(sender.display_name).to eq(current_user.name) | ||||
|           expect(sender.address).to eq(gitlab_sender) | ||||
|           expect_sender(current_user) | ||||
|         end | ||||
| 
 | ||||
|         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 'is sent as the author' do | ||||
|           sender = subject.header[:from].addrs[0] | ||||
|           expect(sender.display_name).to eq(current_user.name) | ||||
|           expect(sender.address).to eq(gitlab_sender) | ||||
|           expect_sender(current_user) | ||||
|         end | ||||
| 
 | ||||
|         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 'is sent as the author' do | ||||
|           sender = subject.header[:from].addrs[0] | ||||
|           expect(sender.display_name).to eq(current_user.name) | ||||
|           expect(sender.address).to eq(gitlab_sender) | ||||
|           expect_sender(current_user) | ||||
|         end | ||||
| 
 | ||||
|         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 'is sent as the author' do | ||||
|           sender = subject.header[:from].addrs[0] | ||||
|           expect(sender.display_name).to eq(current_user.name) | ||||
|           expect(sender.address).to eq(gitlab_sender) | ||||
|           expect_sender(current_user) | ||||
|         end | ||||
| 
 | ||||
|         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 'is sent as the push user' do | ||||
|           sender = subject.header[:from].addrs[0] | ||||
| 
 | ||||
|           expect(sender.display_name).to eq(push_user.name) | ||||
|           expect(sender.address).to eq(gitlab_sender) | ||||
|           expect_sender(push_user) | ||||
|         end | ||||
| 
 | ||||
|         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 'is sent to the given recipient as the author' do | ||||
|           sender = subject.header[:from].addrs[0] | ||||
| 
 | ||||
|           aggregate_failures do | ||||
|             expect(sender.display_name).to eq(note_author.name) | ||||
|             expect(sender.address).to eq(gitlab_sender) | ||||
|             expect_sender(note_author) | ||||
|             expect(subject).to deliver_to(recipient.notification_email) | ||||
|           end | ||||
|         end | ||||
|  | @ -1162,11 +1143,8 @@ RSpec.describe Notify do | |||
|         it_behaves_like 'it should have Gmail Actions links' | ||||
| 
 | ||||
|         it 'is sent to the given recipient as the author' do | ||||
|           sender = subject.header[:from].addrs[0] | ||||
| 
 | ||||
|           aggregate_failures do | ||||
|             expect(sender.display_name).to eq(note_author.name) | ||||
|             expect(sender.address).to eq(gitlab_sender) | ||||
|             expect_sender(note_author) | ||||
|             expect(subject).to deliver_to(recipient.notification_email) | ||||
|           end | ||||
|         end | ||||
|  | @ -1221,12 +1199,6 @@ RSpec.describe Notify do | |||
|         issue.issue_email_participants.create!(email: 'service.desk@example.com') | ||||
|       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 | ||||
|         subject { described_class.service_desk_thank_you_email(issue.id) } | ||||
| 
 | ||||
|  | @ -1244,14 +1216,16 @@ RSpec.describe Notify do | |||
|         end | ||||
| 
 | ||||
|         it 'uses service bot name by default' do | ||||
|           expect_sender(User.support_bot.name) | ||||
|           expect_sender(User.support_bot) | ||||
|         end | ||||
| 
 | ||||
|         context 'when custom outgoing name is set' do | ||||
|           let_it_be(:settings) { create(:service_desk_setting, project: project, outgoing_name: 'some custom name') } | ||||
| 
 | ||||
|           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 | ||||
| 
 | ||||
|  | @ -1259,7 +1233,7 @@ RSpec.describe Notify do | |||
|           let_it_be(:settings) { create(:service_desk_setting, project: project, outgoing_name: '') } | ||||
| 
 | ||||
|           it 'uses service bot name' do | ||||
|             expect_sender(User.support_bot.name) | ||||
|             expect_sender(User.support_bot) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|  | @ -1276,7 +1250,7 @@ RSpec.describe Notify do | |||
|         end | ||||
| 
 | ||||
|         it 'uses author\'s name in "from" header' do | ||||
|           expect_sender(first_note.author.name) | ||||
|           expect_sender(first_note.author) | ||||
|         end | ||||
| 
 | ||||
|         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 'is sent as the author' do | ||||
|       sender = subject.header[:from].addrs[0] | ||||
|       expect(sender.display_name).to eq(user.name) | ||||
|       expect(sender.address).to eq(gitlab_sender) | ||||
|       expect_sender(user) | ||||
|     end | ||||
| 
 | ||||
|     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 'is sent as the author' do | ||||
|       sender = subject.header[:from].addrs[0] | ||||
|       expect(sender.display_name).to eq(user.name) | ||||
|       expect(sender.address).to eq(gitlab_sender) | ||||
|       expect_sender(user) | ||||
|     end | ||||
| 
 | ||||
|     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 'is sent as the author' do | ||||
|       sender = subject.header[:from].addrs[0] | ||||
|       expect(sender.display_name).to eq(user.name) | ||||
|       expect(sender.address).to eq(gitlab_sender) | ||||
|       expect_sender(user) | ||||
|     end | ||||
| 
 | ||||
|     it 'has the correct subject' do | ||||
|  | @ -1748,9 +1716,7 @@ RSpec.describe Notify do | |||
|     it_behaves_like 'appearance header and footer not enabled' | ||||
| 
 | ||||
|     it 'is sent as the author' do | ||||
|       sender = subject.header[:from].addrs[0] | ||||
|       expect(sender.display_name).to eq(user.name) | ||||
|       expect(sender.address).to eq(gitlab_sender) | ||||
|       expect_sender(user) | ||||
|     end | ||||
| 
 | ||||
|     it 'has the correct subject' do | ||||
|  | @ -1777,9 +1743,7 @@ RSpec.describe Notify do | |||
|     it_behaves_like 'appearance header and footer not enabled' | ||||
| 
 | ||||
|     it 'is sent as the author' do | ||||
|       sender = subject.header[:from].addrs[0] | ||||
|       expect(sender.display_name).to eq(user.name) | ||||
|       expect(sender.address).to eq(gitlab_sender) | ||||
|       expect_sender(user) | ||||
|     end | ||||
| 
 | ||||
|     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 'is sent as the author' do | ||||
|       sender = subject.header[:from].addrs[0] | ||||
|       expect(sender.display_name).to eq(user.name) | ||||
|       expect(sender.address).to eq(gitlab_sender) | ||||
|       expect_sender(user) | ||||
|     end | ||||
| 
 | ||||
|     it 'has the correct subject and body' do | ||||
|  | @ -1962,12 +1924,8 @@ RSpec.describe Notify do | |||
|     it_behaves_like 'an unsubscribeable thread' | ||||
| 
 | ||||
|     it 'is sent to the given recipient as the author' do | ||||
|       sender = subject.header[:from].addrs[0] | ||||
| 
 | ||||
|       aggregate_failures do | ||||
|         expect(sender.display_name).to eq(review.author_name) | ||||
|         expect(sender.address).to eq(gitlab_sender) | ||||
|         expect(subject).to deliver_to(recipient.notification_email) | ||||
|         expect_sender(review.author) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|  | @ -2002,4 +1960,10 @@ RSpec.describe Notify do | |||
|       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 | ||||
|  |  | |||
|  | @ -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 | ||||
|       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) | ||||
|       end | ||||
| 
 | ||||
|       it 'creates test case failures records' do | ||||
|       it 'creates unit test failures records' do | ||||
|         execute_service | ||||
| 
 | ||||
|         expect(Ci::TestCase.count).to eq(2) | ||||
|         expect(Ci::TestCaseFailure.count).to eq(2) | ||||
|         expect(Ci::UnitTest.count).to eq(2) | ||||
|         expect(Ci::UnitTestFailure.count).to eq(2) | ||||
|       end | ||||
| 
 | ||||
|       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 | ||||
|           execute_service | ||||
| 
 | ||||
|           expect(Ci::TestCase.count).to eq(0) | ||||
|           expect(Ci::TestCaseFailure.count).to eq(0) | ||||
|           expect(Ci::UnitTest.count).to eq(0) | ||||
|           expect(Ci::UnitTestFailure.count).to eq(0) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|  | @ -43,12 +43,12 @@ RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do | |||
|         it 'does not fail but does not persist new data' do | ||||
|           expect { described_class.new(pipeline).execute }.not_to raise_error | ||||
| 
 | ||||
|           expect(Ci::TestCase.count).to eq(2) | ||||
|           expect(Ci::TestCaseFailure.count).to eq(2) | ||||
|           expect(Ci::UnitTest.count).to eq(2) | ||||
|           expect(Ci::UnitTestFailure.count).to eq(2) | ||||
|         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 | ||||
|           stub_const("#{described_class.name}::MAX_TRACKABLE_FAILURES", 1) | ||||
|         end | ||||
|  | @ -56,16 +56,16 @@ RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do | |||
|         it 'does not persist data' do | ||||
|           execute_service | ||||
| 
 | ||||
|           expect(Ci::TestCase.count).to eq(0) | ||||
|           expect(Ci::TestCaseFailure.count).to eq(0) | ||||
|           expect(Ci::UnitTest.count).to eq(0) | ||||
|           expect(Ci::UnitTestFailure.count).to eq(0) | ||||
|         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 | ||||
|           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 | ||||
|           create(:ci_build, :failed, :test_reports_with_duplicate_failed_test_names, pipeline: pipeline, project: project) | ||||
|         end | ||||
|  | @ -73,23 +73,23 @@ RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do | |||
|         it 'does not persist data' do | ||||
|           execute_service | ||||
| 
 | ||||
|           expect(Ci::TestCase.count).to eq(0) | ||||
|           expect(Ci::TestCaseFailure.count).to eq(0) | ||||
|           expect(Ci::UnitTest.count).to eq(0) | ||||
|           expect(Ci::UnitTestFailure.count).to eq(0) | ||||
|         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 | ||||
|         # 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) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not fail but does not persist duplicate data' do | ||||
|         expect { execute_service }.not_to raise_error | ||||
| 
 | ||||
|         expect(Ci::TestCase.count).to eq(1) | ||||
|         expect(Ci::TestCaseFailure.count).to eq(1) | ||||
|         expect(Ci::UnitTest.count).to eq(1) | ||||
|         expect(Ci::UnitTestFailure.count).to eq(1) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|  | @ -102,8 +102,8 @@ RSpec.describe Ci::TestFailureHistoryService, :aggregate_failures do | |||
|       it 'does not persist data' do | ||||
|         execute_service | ||||
| 
 | ||||
|         expect(Ci::TestCase.count).to eq(0) | ||||
|         expect(Ci::TestCaseFailure.count).to eq(0) | ||||
|         expect(Ci::UnitTest.count).to eq(0) | ||||
|         expect(Ci::UnitTestFailure.count).to eq(0) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -1662,7 +1662,7 @@ RSpec.describe NotificationService, :mailer do | |||
|         notification.issue_due(issue) | ||||
|         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 | ||||
| 
 | ||||
|       it_behaves_like 'participating notifications' do | ||||
|  |  | |||
|  | @ -225,7 +225,7 @@ RSpec.shared_examples 'a note email' do | |||
|     sender = subject.header[:from].addrs[0] | ||||
| 
 | ||||
|     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(subject).to deliver_to(recipient.notification_email) | ||||
|     end | ||||
|  |  | |||
|  | @ -40,8 +40,8 @@ RSpec.describe ::Ci::TestFailureHistoryWorker do | |||
| 
 | ||||
|       subject | ||||
| 
 | ||||
|       expect(Ci::TestCase.count).to eq(2) | ||||
|       expect(Ci::TestCaseFailure.count).to eq(2) | ||||
|       expect(Ci::UnitTest.count).to eq(2) | ||||
|       expect(Ci::UnitTestFailure.count).to eq(2) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue