Commit 6763d278 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent ed9165c2
<script>
import { GlButton, GlIcon, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
const IGNORED = 'ignored';
const RESOLVED = 'resolved';
const UNRESOLVED = 'unresolved';
const statusValidation = [IGNORED, RESOLVED, UNRESOLVED];
export default {
components: {
GlButton,
GlIcon,
GlButtonGroup,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
error: {
type: Object,
required: true,
validator: ({ status }) => statusValidation.includes(status),
},
},
computed: {
ignoreBtn() {
return this.error.status !== IGNORED
? { status: IGNORED, icon: 'eye-slash', title: __('Ignore') }
: { status: UNRESOLVED, icon: 'eye', title: __('Undo Ignore') };
},
resolveBtn() {
return this.error.status !== RESOLVED
? { status: RESOLVED, icon: 'check-circle', title: __('Resolve') }
: { status: UNRESOLVED, icon: 'canceled-circle', title: __('Unresolve') };
},
detailsLink() {
return `error_tracking/${this.error.id}/details`;
},
},
};
</script>
<template>
<div>
<gl-button-group class="flex-column flex-md-row ml-0 ml-md-n4">
<gl-button
:key="ignoreBtn.status"
:ref="`${ignoreBtn.title.toLowerCase()}Error`"
v-gl-tooltip.hover
class="d-block mb-2 mb-md-0 w-100"
:title="ignoreBtn.title"
@click="$emit('update-issue-status', { errorId: error.id, status: ignoreBtn.status })"
>
<gl-icon class="d-none d-md-inline m-0" :name="ignoreBtn.icon" :size="12" />
<span class="d-md-none">{{ ignoreBtn.title }}</span>
</gl-button>
<gl-button
:key="resolveBtn.status"
:ref="`${resolveBtn.title.toLowerCase()}Error`"
v-gl-tooltip.hover
class="d-block mb-2 mb-md-0 w-100"
:title="resolveBtn.title"
@click="$emit('update-issue-status', { errorId: error.id, status: resolveBtn.status })"
>
<gl-icon class="d-none d-md-inline m-0" :name="resolveBtn.icon" :size="12" />
<span class="d-md-none">{{ resolveBtn.title }}</span>
</gl-button>
</gl-button-group>
<gl-button
:href="detailsLink"
category="secondary"
variant="info"
class="d-block d-md-none mb-2 mb-md-0"
>
{{ __('More details') }}
</gl-button>
</div>
</template>
......@@ -13,12 +13,12 @@ import {
GlDropdownDivider,
GlTooltipDirective,
GlPagination,
GlButtonGroup,
} from '@gitlab/ui';
import AccessorUtils from '~/lib/utils/accessor';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale';
import { isEmpty } from 'lodash';
import ErrorTrackingActions from './error_tracking_actions.vue';
export const tableDataClass = 'table-col d-flex d-md-table-cell align-items-center';
......@@ -26,10 +26,6 @@ export default {
FIRST_PAGE: 1,
PREV_PAGE: 1,
NEXT_PAGE: 2,
statusButtons: [
{ status: 'ignored', icon: 'eye-slash', title: __('Ignore') },
{ status: 'resolved', icon: 'check-circle', title: __('Resolve') },
],
fields: [
{
key: 'error',
......@@ -58,12 +54,7 @@ export default {
{
key: 'status',
label: '',
tdClass: `table-col d-none d-md-table-cell align-items-center pl-md-0`,
},
{
key: 'details',
tdClass: 'table-col d-md-none d-flex align-items-center rounded-bottom bg-secondary',
thClass: 'invisible w-0',
tdClass: `${tableDataClass}`,
},
],
statusFilters: {
......@@ -89,7 +80,7 @@ export default {
GlFormInput,
GlPagination,
TimeAgo,
GlButtonGroup,
ErrorTrackingActions,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -206,7 +197,7 @@ export default {
this.filterValue = label;
return this.filterByStatus(status);
},
updateIssueStatus(errorId, status) {
updateIssueStatus({ errorId, status }) {
this.updateStatus({
endpoint: this.getIssueUpdatePath(errorId),
status,
......@@ -220,8 +211,10 @@ export default {
<template>
<div class="error-list">
<div v-if="errorTrackingEnabled">
<div class="row flex-column flex-sm-row align-items-sm-center row-top m-0 mt-sm-2 p-0 p-sm-3">
<div class="search-box flex-fill mr-sm-2 my-3 m-sm-0 p-3 p-sm-0 bg-secondary">
<div
class="row flex-column flex-md-row align-items-md-center m-0 mt-sm-2 p-3 p-sm-3 bg-secondary border"
>
<div class="search-box flex-fill mb-1 mb-md-0">
<div class="filtered-search-box mb-0">
<gl-dropdown
:text="__('Recent searches')"
......@@ -273,7 +266,7 @@ export default {
<gl-dropdown
:text="$options.statusFilters[statusFilter]"
class="status-dropdown mr-2"
class="status-dropdown mx-md-1 mb-1 mb-md-0"
menu-class="dropdown"
:disabled="loading"
>
......@@ -366,46 +359,7 @@ export default {
</div>
</template>
<template #cell(status)="errors">
<gl-button-group>
<gl-button
v-for="button in $options.statusButtons"
:key="button.status"
:ref="button.title.toLowerCase() + 'Error'"
v-gl-tooltip.hover
:title="button.title"
@click="updateIssueStatus(errors.item.id, button.status)"
>
<gl-icon :name="button.icon" :size="12" />
</gl-button>
</gl-button-group>
</template>
<template #cell(details)="errors">
<gl-button
category="primary"
variant="info"
block
class="mb-1 mt-2"
@click="updateIssueStatus(errors.item.id, 'resolved')"
>
{{ __('Resolve') }}
</gl-button>
<gl-button
category="secondary"
variant="default"
block
class="mb-2"
@click="updateIssueStatus(errors.item.id, 'ignored')"
>
{{ __('Ignore') }}
</gl-button>
<gl-button
:href="getDetailsLink(errors.item.id)"
category="secondary"
variant="info"
class="d-block mb-2"
>
{{ __('More details') }}
</gl-button>
<error-tracking-actions :error="errors.item" @update-issue-status="updateIssueStatus" />
</template>
<template #empty>
{{ __('No errors to display.') }}
......
$gray-border: 1px solid $border-color;
.error-list {
.sort-control {
.btn {
......@@ -13,19 +11,14 @@ $gray-border: 1px solid $border-color;
}
}
@include media-breakpoint-up(sm) {
.row-top {
border: $gray-border;
background-color: $gray-50;
}
}
@include media-breakpoint-down(md) {
@include media-breakpoint-down(sm) {
.error-list-table {
.table-col {
min-height: 68px;
&:last-child {
background-color: $gray-normal;
&::before {
content: none !important;
}
......
......@@ -174,6 +174,8 @@ module Ci
pipeline: Ci::Pipeline::PROJECT_ROUTE_AND_NAMESPACE_ROUTE)
end
scope :with_coverage, -> { where.not(coverage: nil) }
acts_as_taggable
add_authentication_token_field :token, encrypted: :optional
......
# frozen_string_literal: true
module Ci
class DailyReportResult < ApplicationRecord
extend Gitlab::Ci::Model
belongs_to :last_pipeline, class_name: 'Ci::Pipeline', foreign_key: :last_pipeline_id
belongs_to :project
# TODO: Refactor this out when BuildReportResult is implemented.
# They both need to share the same enum values for param.
REPORT_PARAMS = {
coverage: 0
}.freeze
enum param_type: REPORT_PARAMS
def self.upsert_reports(data)
upsert_all(data, unique_by: :index_daily_report_results_unique_columns) if data.any?
end
end
end
......@@ -82,6 +82,8 @@ module Ci
has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline
has_many :daily_report_results, class_name: 'Ci::DailyReportResult', foreign_key: :last_pipeline_id
accepts_nested_attributes_for :variables, reject_if: :persisted?
delegate :id, to: :project, prefix: true
......@@ -189,7 +191,10 @@ module Ci
end
after_transition [:created, :waiting_for_resource, :preparing, :pending, :running] => :success do |pipeline|
pipeline.run_after_commit { PipelineSuccessWorker.perform_async(pipeline.id) }
# We wait a little bit to ensure that all BuildFinishedWorkers finish first
# because this is where some metrics like code coverage is parsed and stored
# in CI build records which the daily build metrics worker relies on.
pipeline.run_after_commit { Ci::DailyReportResultsWorker.perform_in(10.minutes, pipeline.id) }
end
after_transition do |pipeline, transition|
......@@ -941,6 +946,14 @@ module Ci
Ci::PipelineEnums.ci_config_sources.key?(config_source.to_sym)
end
def source_ref_path
if branch? || merge_request?
Gitlab::Git::BRANCH_REF_PREFIX + source_ref.to_s
elsif tag?
Gitlab::Git::TAG_REF_PREFIX + source_ref.to_s
end
end
private
def pipeline_data
......
......@@ -314,6 +314,8 @@ class Project < ApplicationRecord
has_many :import_failures, inverse_of: :project
has_many :daily_report_results, class_name: 'Ci::DailyReportResult'
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data
......
......@@ -21,6 +21,7 @@ class User < ApplicationRecord
include OptionallySearch
include FromUnion
include BatchDestroyDependentAssociations
include IgnorableColumns
DEFAULT_NOTIFICATION_LEVEL = :participating
......@@ -59,9 +60,10 @@ class User < ApplicationRecord
MINIMUM_INACTIVE_DAYS = 180
enum bot_type: ::UserBotTypeEnums.bots
enum user_type: ::UserTypeEnums.types
ignore_column :bot_type, remove_with: '12.11', remove_after: '2020-04-22'
# Override Devise::Models::Trackable#update_tracked_fields!
# to limit database writes to at most once every hour
# rubocop: disable CodeReuse/ServiceClass
......@@ -337,8 +339,9 @@ class User < ApplicationRecord
scope :with_emails, -> { preload(:emails) }
scope :with_dashboard, -> (dashboard) { where(dashboard: dashboard) }
scope :with_public_profile, -> { where(private_profile: false) }
scope :bots, -> { where.not(bot_type: nil) }
scope :humans, -> { where(user_type: nil, bot_type: nil) }
scope :bots, -> { where(user_type: UserTypeEnums.bots.values) }
scope :not_bots, -> { humans.or(where.not(user_type: UserTypeEnums.bots.values)) }
scope :humans, -> { where(user_type: nil) }
scope :with_expiring_and_not_notified_personal_access_tokens, ->(at) do
where('EXISTS (?)',
......@@ -618,7 +621,7 @@ class User < ApplicationRecord
def alert_bot
email_pattern = "alert%s@#{Settings.gitlab.host}"
unique_internal(where(bot_type: :alert_bot), 'alert-bot', email_pattern) do |u|
unique_internal(where(user_type: :alert_bot), 'alert-bot', email_pattern) do |u|
u.bio = 'The GitLab alert bot'
u.name = 'GitLab Alert Bot'
end
......@@ -640,7 +643,7 @@ class User < ApplicationRecord
end
def bot?
bot_type.present?
UserTypeEnums.bots.has_key?(user_type)
end
def internal?
......@@ -652,7 +655,7 @@ class User < ApplicationRecord
end
def self.non_internal
without_ghosts.humans
without_ghosts.not_bots
end
#
......
# frozen_string_literal: true
module UserBotTypeEnums
def self.bots
{
alert_bot: 2
}
end
end
UserBotTypeEnums.prepend_if_ee('EE::UserBotTypeEnums')
......@@ -2,13 +2,13 @@
module UserTypeEnums
def self.types
bots
bots.merge(human: nil)
end
def self.bots
{
AlertBot: 2
}
alert_bot: 2
}.with_indifferent_access
end
end
......
# frozen_string_literal: true
module Ci
class DailyReportResultService
def execute(pipeline)
return unless Feature.enabled?(:ci_daily_code_coverage, pipeline.project, default_enabled: true)
DailyReportResult.upsert_reports(coverage_reports(pipeline))
end
private
def coverage_reports(pipeline)
base_attrs = {
project_id: pipeline.project_id,
ref_path: pipeline.source_ref_path,
param_type: DailyReportResult.param_types[:coverage],
date: pipeline.created_at.to_date,
last_pipeline_id: pipeline.id
}
pipeline.builds.with_coverage.map do |build|
base_attrs.merge(
title: build.group_name,
value: build.coverage
)
end
end
end
end
......@@ -605,6 +605,13 @@
:resource_boundary: :unknown
:weight: 1
:idempotent:
- :name: pipeline_background:ci_daily_report_results
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
- :name: pipeline_cache:expire_job_cache
:feature_category: :continuous_integration
:has_external_dependencies:
......@@ -745,13 +752,6 @@
:resource_boundary: :unknown
:weight: 5
:idempotent:
- :name: pipeline_processing:pipeline_success
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 5
:idempotent:
- :name: pipeline_processing:pipeline_update
:feature_category: :continuous_integration
:has_external_dependencies:
......
# frozen_string_literal: true
module Ci
class DailyReportResultsWorker
include ApplicationWorker
include PipelineBackgroundQueue
idempotent!
def perform(pipeline_id)
Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
Ci::DailyReportResultService.new.execute(pipeline)
end
end
end
end
# frozen_string_literal: true
class PipelineSuccessWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
include PipelineQueue
queue_namespace :pipeline_processing
urgency :high
def perform(pipeline_id)
# no-op
end
end
---
title: Improve Advanced global search performance by using routing
merge_request: 27398
author:
type: performance
---
title: Update icons in Sentry Error Tracking list for ignored/resolved errors
merge_request: 27125
author:
type: other
---
title: Move bots functionality to user_type column
merge_request: 26981
author:
type: performance
---
title: Store daily code coverages into ci_daily_report_results table
merge_request: 24695
author:
type: added
......@@ -469,6 +469,7 @@ tables:
- ghost
- last_activity_on
- notified_of_own_activity
- user_type
- bot_type
- preferred_language
- theme_id
......
# frozen_string_literal: true
class CreateDailyReportResults < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
create_table :ci_daily_report_results do |t|
t.date :date, null: false
t.bigint :project_id, null: false
t.bigint :last_pipeline_id, null: false
t.float :value, null: false
t.integer :param_type, limit: 8, null: false
t.string :ref_path, null: false # rubocop:disable Migration/AddLimitToStringColumns
t.string :title, null: false # rubocop:disable Migration/AddLimitToStringColumns
t.index :last_pipeline_id
t.index [:project_id, :ref_path, :param_type, :date, :title], name: 'index_daily_report_results_unique_columns', unique: true
t.foreign_key :projects, on_delete: :cascade
t.foreign_key :ci_pipelines, column: :last_pipeline_id, on_delete: :cascade
end
end
end
# frozen_string_literal: true
class MigrateBotTypeToUserType < ActiveRecord::Migration[6.0]
DOWNTIME = false
def up
execute('UPDATE users SET user_type = bot_type WHERE bot_type IS NOT NULL AND user_type IS NULL')
end
def down
execute('UPDATE users SET user_type = NULL WHERE bot_type IS NOT NULL AND bot_type = user_type')
end
end
# frozen_string_literal: true
class AddUserStateIndex < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index(:users, [:state, :user_type], where: 'ghost IS NOT TRUE', name: 'index_users_on_state_and_user_type_internal')
remove_concurrent_index_by_name(:users, 'index_users_on_state_and_internal_ee')
remove_concurrent_index_by_name(:users, 'index_users_on_state_and_internal')
end
def down
add_concurrent_index(:users, :state, where: 'ghost IS NOT TRUE AND bot_type IS NULL', name: 'index_users_on_state_and_internal_ee')
add_concurrent_index(:users, :state, where: 'ghost IS NOT TRUE', name: 'index_users_on_state_and_internal')
remove_concurrent_index_by_name(:users, 'index_users_on_state_and_internal_ee')
end
end
......@@ -738,6 +738,18 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do
t.index ["build_id"], name: "index_ci_builds_runner_session_on_build_id", unique: true
end
create_table "ci_daily_report_results", force: :cascade do |t|
t.date "date", null: false
t.bigint "project_id", null: false
t.bigint "last_pipeline_id", null: false
t.float "value", null: false
t.bigint "param_type", null: false
t.string "ref_path", null: false
t.string "title", null: false
t.index ["last_pipeline_id"], name: "index_ci_daily_report_results_on_last_pipeline_id"
t.index ["project_id", "ref_path", "param_type", "date", "title"], name: "index_daily_report_results_unique_columns", unique: true
end
create_table "ci_group_variables", id: :serial, force: :cascade do |t|
t.string "key", null: false
t.text "value"
......@@ -4419,9 +4431,8 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do
t.index ["name"], name: "index_users_on_name_trigram", opclass: :gin_trgm_ops, using: :gin
t.index ["public_email"], name: "index_users_on_public_email", where: "((public_email)::text <> ''::text)"
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
t.index ["state", "user_type"], name: "index_users_on_state_and_user_type_internal", where: "(ghost IS NOT TRUE)"
t.index ["state"], name: "index_users_on_state"
t.index ["state"], name: "index_users_on_state_and_internal", where: "(ghost IS NOT TRUE)"
t.index ["state"], name: "index_users_on_state_and_internal_ee", where: "((ghost IS NOT TRUE) AND (bot_type IS NULL))"
t.index ["static_object_token"], name: "index_users_on_static_object_token", unique: true
t.index ["unconfirmed_email"], name: "index_users_on_unconfirmed_email", where: "(unconfirmed_email IS NOT NULL)"
t.index ["user_type"], name: "index_users_on_user_type"
......@@ -4765,6 +4776,8 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do
add_foreign_key "ci_builds_metadata", "ci_builds", column: "build_id", on_delete: :cascade
add_foreign_key "ci_builds_metadata", "projects", on_delete: :cascade
add_foreign_key "ci_builds_runner_session", "ci_builds", column: "build_id", on_delete: :cascade
add_foreign_key "ci_daily_report_results", "ci_pipelines", column: "last_pipeline_id", on_delete: :cascade
add_foreign_key "ci_daily_report_results", "projects", on_delete: :cascade
add_foreign_key "ci_group_variables", "namespaces", column: "group_id", name: "fk_33ae4d58d8", on_delete: :cascade
add_foreign_key "ci_job_artifacts", "ci_builds", column: "job_id", on_delete: :cascade
add_foreign_key "ci_job_artifacts", "projects", on_delete: :cascade
......
......@@ -24,7 +24,7 @@ module Gitlab
end
def for_project_noteable?
!for_personal_snippet?
!for_personal_snippet? && !for_epic?
end
def skip_project_check?
......
......@@ -21333,6 +21333,9 @@ msgstr ""
msgid "Undo"
msgstr ""
msgid "Undo Ignore"
msgstr ""
msgid "Undo ignore"
msgstr ""
......
......@@ -132,6 +132,7 @@ describe 'Database schema' do
'Ci::Build' => %w[failure_reason],
'Ci::BuildMetadata' => %w[timeout_source],
'Ci::BuildTraceChunk' => %w[data_store],
'Ci::DailyReportResult' => %w[param_type],
'Ci::JobArtifact' => %w[file_type],
'Ci::Pipeline' => %w[source config_source failure_reason],
'Ci::Processable' => %w[failure_reason],
......
# frozen_string_literal: true
FactoryBot.define do
factory :ci_daily_report_result, class: 'Ci::DailyReportResult' do
ref_path { Gitlab::Git::BRANCH_REF_PREFIX + 'master' }
date { Time.zone.now.to_date }
project
last_pipeline factory: :ci_pipeline
param_type { Ci::DailyReportResult.param_types[:coverage] }
title { 'rspec' }
value { 77.0 }
end
end
......@@ -24,7 +24,7 @@ FactoryBot.define do
end
trait :bot do
bot_type { User.bot_types[:alert_bot] }
user_type { :alert_bot }
end
trait :external do
......
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import ErrorTrackingActions from '~/error_tracking/components/error_tracking_actions.vue';
describe('Error Tracking Actions', () => {
let wrapper;
function mountComponent(props) {
wrapper = shallowMount(ErrorTrackingActions, {
propsData: {
error: {
id: '1',
title: 'PG::ConnectionBad: FATAL',
type: 'error',
userCount: 0,
count: '52',
firstSeen: '2019-05-30T07:21:46Z',
lastSeen: '2019-11-06T03:21:39Z',
status: 'unresolved',
},
...props,
},
stubs: { GlButton },
});
}
beforeEach(() => {
mountComponent();
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
const findButtons = () => wrapper.findAll(GlButton);
describe('when error status is unresolved', () => {
it('renders the correct actions buttons to allow ignore and resolve', () => {
expect(findButtons().exists()).toBe(true);
return wrapper.vm.$nextTick().then(() => {
expect(
findButtons()
.at(0)
.attributes('title'),
).toBe('Ignore');
expect(
findButtons()
.at(1)
.attributes('title'),
).toBe('Resolve');
});
});
});
describe('when error status is ignored', () => {
beforeEach(() => {
mountComponent({ error: { status: 'ignored' } });
});
it('renders the correct action button to undo ignore', () => {
expect(findButtons().exists()).toBe(true);
return wrapper.vm.$nextTick().then(() => {
expect(
findButtons()
.at(0)
.attributes('title'),
).toBe('Undo Ignore');
});
});
});
describe('when error status is resolved', () => {
beforeEach(() => {
mountComponent({ error: { status: 'resolved' } });
});
it('renders the correct action button to undo unresolve', () => {
expect(findButtons().exists()).toBe(true);
return wrapper.vm.$nextTick().then(() => {
expect(
findButtons()
.at(1)
.attributes('title'),
).toBe('Unresolve');
});
});
});
});
......@@ -3,6 +3,7 @@ import Vuex from 'vuex';
import { GlEmptyState, GlLoadingIcon, GlFormInput, GlPagination, GlDropdown } from '@gitlab/ui';
import stubChildren from 'helpers/stub_children';
import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue';
import ErrorTrackingActions from '~/error_tracking/components/error_tracking_actions.vue';
import errorsList from './list_mock.json';
const localVue = createLocalVue();
......@@ -30,6 +31,7 @@ describe('ErrorTrackingList', () => {
.find(GlDropdown);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findPagination = () => wrapper.find(GlPagination);
const findErrorActions = () => wrapper.find(ErrorTrackingActions);
function mountComponent({
errorTrackingEnabled = true,
......@@ -151,15 +153,9 @@ describe('ErrorTrackingList', () => {
});
});
it('each error in the list should have an ignore button', () => {
it('each error in the list should have an action button set', () => {
findErrorListRows().wrappers.forEach(row => {
expect(row.contains('glicon-stub[name="eye-slash"]')).toBe(true);
});
});
it('each error in the list should have a resolve button', () => {
findErrorListRows().wrappers.forEach(row => {
expect(row.contains('glicon-stub[name="check-circle"]')).toBe(true);
expect(row.contains(ErrorTrackingActions)).toBe(true);
});
});
......@@ -237,8 +233,6 @@ describe('ErrorTrackingList', () => {
});
describe('When the ignore button on an error is clicked', () => {
const ignoreErrorButton = () => wrapper.find({ ref: 'ignoreError' });
beforeEach(() => {
store.state.list.loading = false;
store.state.list.errors = errorsList;
......@@ -253,7 +247,10 @@ describe('ErrorTrackingList', () => {
});
it('sends the "ignored" status and error ID', () => {
ignoreErrorButton().trigger('click');
findErrorActions().vm.$emit('update-issue-status', {
errorId: errorsList[0].id,
status: 'ignored',
});
expect(actions.updateStatus).toHaveBeenCalledWith(
expect.anything(),
{
......@@ -265,7 +262,7 @@ describe('ErrorTrackingList', () => {
});
it('calls an action to remove the item from the list', () => {
ignoreErrorButton().trigger('click');
findErrorActions().vm.$emit('update-issue-status', { errorId: '1', status: undefined });
expect(actions.removeIgnoredResolvedErrors).toHaveBeenCalledWith(
expect.anything(),
'1',
......@@ -275,8 +272,6 @@ describe('ErrorTrackingList', () => {
});
describe('When the resolve button on an error is clicked', () => {
const resolveErrorButton = () => wrapper.find({ ref: 'resolveError' });
beforeEach(() => {
store.state.list.loading = false;
store.state.list.errors = errorsList;
......@@ -291,7 +286,10 @@ describe('ErrorTrackingList', () => {
});
it('sends "resolved" status and error ID', () => {
resolveErrorButton().trigger('click');
findErrorActions().vm.$emit('update-issue-status', {
errorId: errorsList[0].id,
status: 'resolved',
});
expect(actions.updateStatus).toHaveBeenCalledWith(
expect.anything(),
{
......@@ -303,7 +301,7 @@ describe('ErrorTrackingList', () => {
});
it('calls an action to remove the item from the list', () => {
resolveErrorButton().trigger('click');
findErrorActions().vm.$emit('update-issue-status', { errorId: '1', status: undefined });
expect(actions.removeIgnoredResolvedErrors).toHaveBeenCalledWith(
expect.anything(),
'1',
......
......@@ -6,7 +6,8 @@
"userCount": 0,
"count": "52",
"firstSeen": "2019-05-30T07:21:46Z",
"lastSeen": "2019-11-06T03:21:39Z"
"lastSeen": "2019-11-06T03:21:39Z",
"status": "unresolved"
},
{
"id": "2",
......@@ -15,7 +16,8 @@
"userCount": 0,
"count": "12",
"firstSeen": "2019-10-19T03:53:56Z",
"lastSeen": "2019-11-05T03:51:54Z"
"lastSeen": "2019-11-05T03:51:54Z",
"status": "unresolved"
},
{
"id": "3",
......@@ -24,6 +26,7 @@
"userCount": 0,
"count": "275",
"firstSeen": "2019-02-12T07:22:36Z",
"lastSeen": "2019-10-22T03:20:48Z"
"lastSeen": "2019-10-22T03:20:48Z",
"status": "unresolved"
}
]
\ No newline at end of file
......@@ -12,6 +12,7 @@ describe Gitlab::BackgroundMigration::UserMentions::CreateResourceUserMention, s
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:notes) { table(:notes) }
let(:routes) { table(:routes) }
let(:author) { users.create!(email: 'author@example.com', notification_email: 'author@example.com', name: 'author', username: 'author', projects_limit: 10, state: 'active') }
let(:member) { users.create!(email: 'member@example.com', notification_email: 'member@example.com', name: 'member', username: 'member', projects_limit: 10, state: 'active') }
......@@ -32,13 +33,14 @@ describe Gitlab::BackgroundMigration::UserMentions::CreateResourceUserMention, s
before do
# build personal namespaces and routes for users
mentioned_users.each { |u| u.becomes(User).save! }
mentioned_users.each do |u|
namespace = namespaces.create!(path: u.username, name: u.name, runners_token: "my-token-u#{u.id}", owner_id: u.id, type: nil)
routes.create!(path: namespace.path, source_type: 'Namespace', source_id: namespace.id)
end
# build namespaces and routes for groups
mentioned_groups.each do |gr|
gr.name += '-org'
gr.path += '-org'
gr.becomes(Namespace).save!
routes.create!(path: gr.path, source_type: 'Namespace', source_id: gr.id)
end
end
......
# frozen_string_literal: true
require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_event_spec'
describe Gitlab::CycleAnalytics::CodeEventFetcher do
let(:stage_name) { :code }
......
# frozen_string_literal: true
require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_stage_spec'
describe Gitlab::CycleAnalytics::CodeStage do
let(:stage_name) { :code }
......
# frozen_string_literal: true
require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_event_spec'
describe Gitlab::CycleAnalytics::IssueEventFetcher do
let(:stage_name) { :issue }
......
# frozen_string_literal: true
require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_stage_spec'
describe Gitlab::CycleAnalytics::IssueStage do
let(:stage_name) { :issue }
......
# frozen_string_literal: true
require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_event_spec'
describe Gitlab::CycleAnalytics::PlanEventFetcher do
let(:stage_name) { :plan }
......
# frozen_string_literal: true
require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_stage_spec'
describe Gitlab::CycleAnalytics::PlanStage do
let(:stage_name) { :plan }
......
# frozen_string_literal: true
require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_event_spec'
describe Gitlab::CycleAnalytics::ProductionEventFetcher do
let(:stage_name) { :production }
......
# frozen_string_literal: true
require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_stage_spec'
describe Gitlab::CycleAnalytics::ProductionStage do
let(:stage_name) { 'Total' }
......
# frozen_string_literal: true
require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_event_spec'
describe Gitlab::CycleAnalytics::ReviewEventFetcher do
let(:stage_name) { :review }
......
# frozen_string_literal: true
require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_stage_spec'
describe Gitlab::CycleAnalytics::ReviewStage do
let(:stage_name) { :review }
......
# frozen_string_literal: true
require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_event_spec'
describe Gitlab::CycleAnalytics::StagingEventFetcher do
let(:stage_name) { :staging }
......
# frozen_string_literal: true
require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_stage_spec'
describe Gitlab::CycleAnalytics::StagingStage do
let(:stage_name) { :staging }
......
# frozen_string_literal: true
require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_event_spec'
describe Gitlab::CycleAnalytics::TestEventFetcher do
let(:stage_name) { :test }
......
# frozen_string_literal: true
require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_stage_spec'
describe Gitlab::CycleAnalytics::TestStage do
let(:stage_name) { :test }
......
......@@ -208,6 +208,7 @@ ci_pipelines:
- vulnerability_findings
- pipeline_config
- security_scans
- daily_report_results
pipeline_variables:
- pipeline
stages:
......@@ -470,6 +471,7 @@ project:
- status_page_setting
- requirements
- export_jobs
- daily_report_results
award_emoji:
- awardable
- user
......
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20200311074438_migrate_bot_type_to_user_type.rb')
describe MigrateBotTypeToUserType, :migration do
let(:users) { table(:users) }
it 'updates bots & ignores humans' do
users.create!(email: 'human', bot_type: nil, projects_limit: 0)
users.create!(email: 'support_bot', bot_type: 1, projects_limit: 0)
users.create!(email: 'alert_bot', bot_type: 2, projects_limit: 0)
users.create!(email: 'visual_review_bot', bot_type: 3, projects_limit: 0)
migrate!
expect(users.where('user_type IS NOT NULL').map(&:user_type)).to match_array([1, 2, 3])
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Ci::DailyReportResult do
describe '.upsert_reports' do
let!(:rspec_coverage) do
create(
:ci_daily_report_result,
title: 'rspec',
date: '2020-03-09',
value: 71.2
)
end
let!(:new_pipeline) { create(:ci_pipeline) }
it 'creates or updates matching report results' do
described_class.upsert_reports([
{
project_id: rspec_coverage.project_id,
ref_path: rspec_coverage.ref_path,
param_type: described_class.param_types[rspec_coverage.param_type],
last_pipeline_id: new_pipeline.id,
date: rspec_coverage.date,
title: 'rspec',
value: 81.0
},
{
project_id: rspec_coverage.project_id,
ref_path: rspec_coverage.ref_path,
param_type: described_class.param_types[rspec_coverage.param_type],
last_pipeline_id: new_pipeline.id,
date: rspec_coverage.date,
title: 'karma',
value: 87.0
}
])
rspec_coverage.reload
expect(rspec_coverage).to have_attributes(
last_pipeline_id: new_pipeline.id,
value: 81.0
)
expect(described_class.find_by_title('karma')).to have_attributes(
project_id: rspec_coverage.project_id,
ref_path: rspec_coverage.ref_path,
param_type: rspec_coverage.param_type,
last_pipeline_id: new_pipeline.id,
date: rspec_coverage.date,
value: 87.0
)
end
context 'when given data is empty' do
it 'does nothing' do
expect { described_class.upsert_reports([]) }.not_to raise_error
end
end
end
end
......@@ -1120,7 +1120,7 @@ describe Ci::Pipeline, :mailer do
let(:from_status) { status }
it 'schedules pipeline success worker' do
expect(PipelineSuccessWorker).to receive(:perform_async).with(pipeline.id)
expect(Ci::DailyReportResultsWorker).to receive(:perform_in).with(10.minutes, pipeline.id)
pipeline.succeed
end
......@@ -3114,4 +3114,25 @@ describe Ci::Pipeline, :mailer do
end
end
end
describe '#source_ref_path' do
subject { pipeline.source_ref_path }
context 'when pipeline is for a branch' do
it { is_expected.to eq(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.source_ref.to_s) }
end
context 'when pipeline is for a merge request' do
let(:merge_request) { create(:merge_request, source_project: project) }
let(:pipeline) { create(:ci_pipeline, project: project, head_pipeline_of: merge_request) }
it { is_expected.to eq(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.source_ref.to_s) }
end
context 'when pipeline is for a tag' do
let(:pipeline) { create(:ci_pipeline, project: project, tag: true) }
it { is_expected.to eq(Gitlab::Git::TAG_REF_PREFIX + pipeline.source_ref.to_s) }
end
end
end
......@@ -4244,12 +4244,12 @@ describe User, :do_not_mock_admin_mode do
let!(:non_internal) { [user] }
let!(:internal) { [ghost, alert_bot] }
it 'returns non internal users' do
it 'returns internal users' do
expect(described_class.internal).to eq(internal)
expect(internal.all?(&:internal?)).to eq(true)
end
it 'returns internal users' do
it 'returns non internal users' do
expect(described_class.non_internal).to eq(non_internal)
expect(non_internal.all?(&:internal?)).to eq(false)
end
......
# frozen_string_literal: true
require 'spec_helper'
describe Ci::DailyReportResultService, '#execute' do
let!(:pipeline) { create(:ci_pipeline, created_at: '2020-02-06 00:01:10') }
let!(:rspec_job) { create(:ci_build, pipeline: pipeline, name: '3/3 rspec', coverage: 80) }
let!(:karma_job) { create(:ci_build, pipeline: pipeline, name: '2/2 karma', coverage: 90) }
let!(:extra_job) { create(:ci_build, pipeline: pipeline, name: 'extra', coverage: nil) }
it 'creates daily code coverage record for each job in the pipeline that has coverage value' do
described_class.new.execute(pipeline)
Ci::DailyReportResult.find_by(title: 'rspec').tap do |coverage|
expect(coverage).to have_attributes(
project_id: pipeline.project.id,
last_pipeline_id: pipeline.id,
ref_path: pipeline.source_ref_path,
param_type: 'coverage',
title: rspec_job.group_name,
value: rspec_job.coverage,
date: pipeline.created_at.to_date
)
end
Ci::DailyReportResult.find_by(title: 'karma').tap do |coverage|
expect(coverage).to have_attributes(
project_id: pipeline.project.id,
last_pipeline_id: pipeline.id,
ref_path: pipeline.source_ref_path,
param_type: 'coverage',
title: karma_job.group_name,
value: karma_job.coverage,
date: pipeline.created_at.to_date
)
end
expect(Ci::DailyReportResult.find_by(title: 'extra')).to be_nil
end
context 'when there is an existing daily code coverage for the matching date, project, ref_path, and group name' do
let!(:new_pipeline) do
create(
:ci_pipeline,
project: pipeline.project,
ref: pipeline.ref,
created_at: '2020-02-06 00:02:20'
)
end
let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: '4/4 rspec', coverage: 84) }
let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: '3/3 karma', coverage: 92) }
before do
# Create the existing daily code coverage records
described_class.new.execute(pipeline)
end
it "updates the existing record's coverage value and last_pipeline_id" do
rspec_coverage = Ci::DailyReportResult.find_by(title: 'rspec')
karma_coverage = Ci::DailyReportResult.find_by(title: 'karma')
# Bump up the coverage values
described_class.new.execute(new_pipeline)
rspec_coverage.reload
karma_coverage.reload
expect(rspec_coverage).to have_attributes(
last_pipeline_id: new_pipeline.id,
value: new_rspec_job.coverage
)
expect(karma_coverage).to have_attributes(
last_pipeline_id: new_pipeline.id,
value: new_karma_job.coverage
)
end
end
context 'when the ID of the pipeline is older than the last_pipeline_id' do
let!(:new_pipeline) do
create(
:ci_pipeline,
project: pipeline.project,
ref: pipeline.ref,
created_at: '2020-02-06 00:02:20'
)
end
let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: '4/4 rspec', coverage: 84) }
let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: '3/3 karma', coverage: 92) }
before do
# Create the existing daily code coverage records
# but in this case, for the newer pipeline first.
described_class.new.execute(new_pipeline)
end
it 'updates the existing daily code coverage records' do
rspec_coverage = Ci::DailyReportResult.find_by(title: 'rspec')
karma_coverage = Ci::DailyReportResult.find_by(title: 'karma')
# Run another one but for the older pipeline.
# This simulates the scenario wherein the success worker
# of an older pipeline, for some network hiccup, was delayed
# and only got executed right after the newer pipeline's success worker.
# Ideally, we don't want to bump the coverage value with an older one
# but given this can be a rare edge case and can be remedied by re-running
# the pipeline we'll just let it be for now. In return, we are able to use
# Rails 6 shiny new method, upsert_all, and simplify the code a lot.
described_class.new.execute(pipeline)
rspec_coverage.reload
karma_coverage.reload
expect(rspec_coverage).to have_attributes(
last_pipeline_id: pipeline.id,
value: rspec_job.coverage
)
expect(karma_coverage).to have_attributes(
last_pipeline_id: pipeline.id,
value: karma_job.coverage
)
end
end
context 'when pipeline has no builds with coverage' do
let!(:new_pipeline) do
create(
:ci_pipeline,
created_at: '2020-02-06 00:02:20'
)
end
let!(:some_job) { create(:ci_build, pipeline: new_pipeline, name: 'foo') }
it 'does nothing' do
expect { described_class.new.execute(new_pipeline) }.not_to raise_error
end
end
end
......@@ -22,14 +22,6 @@ shared_examples 'resource mentions migration' do |migration_class, resource_clas
end
shared_examples 'resource notes mentions migration' do |migration_class, resource_class|
before do
note1.becomes(Note).save!
note2.becomes(Note).save!
note3.becomes(Note).save!
note4.becomes(Note).save!
note5.becomes(Note).save(validate: false)
end
it 'migrates mentions from note' do
join = migration_class::JOIN
conditions = migration_class::QUERY_CONDITIONS
......
......@@ -2,9 +2,9 @@
require 'spec_helper'
shared_examples 'base stage' do
ISSUES_MEDIAN = 30.minutes.to_i
ISSUES_MEDIAN = 30.minutes.to_i
shared_examples 'base stage' do
let(:stage) { described_class.new(options: { project: double }) }
before do
......
# frozen_string_literal: true
require 'spec_helper'
describe Ci::DailyReportResultsWorker do
describe '#perform' do
let!(:pipeline) { create(:ci_pipeline) }
subject { described_class.new.perform(pipeline_id) }
context 'when pipeline is found' do
let(:pipeline_id) { pipeline.id }
it 'executes service' do
expect_any_instance_of(Ci::DailyReportResultService)
.to receive(:execute).with(pipeline)
subject
end
end
context 'when pipeline is not found' do
let(:pipeline_id) { 123 }
it 'does not execute service' do
expect_any_instance_of(Ci::DailyReportResultService)
.not_to receive(:execute)
expect { subject }
.not_to raise_error
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment