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