diff options
57 files changed, 712 insertions, 161 deletions
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue b/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue new file mode 100644 index 00000000000..49eb04e331b --- /dev/null +++ b/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue @@ -0,0 +1,80 @@ +<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> diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue index 0e160e8d568..ea0baaf4569 100644 --- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue +++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue @@ -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.') }} diff --git a/app/assets/stylesheets/pages/error_list.scss b/app/assets/stylesheets/pages/error_list.scss index 88fdcc47492..a61a85649b8 100644 --- a/app/assets/stylesheets/pages/error_list.scss +++ b/app/assets/stylesheets/pages/error_list.scss @@ -1,5 +1,3 @@ -$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; } diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 74a1985ca50..517f2312a76 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -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 diff --git a/app/models/ci/daily_report_result.rb b/app/models/ci/daily_report_result.rb new file mode 100644 index 00000000000..3c1c5f11ed4 --- /dev/null +++ b/app/models/ci/daily_report_result.rb @@ -0,0 +1,22 @@ +# 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 diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 61b28a3e712..ef22b429df9 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index 8578cd0e44a..4892c5310ec 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 0c7dfac5776..6972a465c30 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 # diff --git a/app/models/user_bot_type_enums.rb b/app/models/user_bot_type_enums.rb deleted file mode 100644 index 1a9c02a3998..00000000000 --- a/app/models/user_bot_type_enums.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module UserBotTypeEnums - def self.bots - { - alert_bot: 2 - } - end -end - -UserBotTypeEnums.prepend_if_ee('EE::UserBotTypeEnums') diff --git a/app/models/user_type_enums.rb b/app/models/user_type_enums.rb index 4e9dd70aee8..2d0d2f3a4ce 100644 --- a/app/models/user_type_enums.rb +++ b/app/models/user_type_enums.rb @@ -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 diff --git a/app/services/ci/daily_report_result_service.rb b/app/services/ci/daily_report_result_service.rb new file mode 100644 index 00000000000..79b5015c076 --- /dev/null +++ b/app/services/ci/daily_report_result_service.rb @@ -0,0 +1,30 @@ +# 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 diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 28fab10d931..3df86e3314d 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -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: diff --git a/app/workers/ci/daily_report_results_worker.rb b/app/workers/ci/daily_report_results_worker.rb new file mode 100644 index 00000000000..314fd44f86c --- /dev/null +++ b/app/workers/ci/daily_report_results_worker.rb @@ -0,0 +1,16 @@ +# 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 diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb deleted file mode 100644 index d84612c52d1..00000000000 --- a/app/workers/pipeline_success_worker.rb +++ /dev/null @@ -1,13 +0,0 @@ -# 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 diff --git a/changelogs/unreleased/196838-remove-routing-ff.yml b/changelogs/unreleased/196838-remove-routing-ff.yml new file mode 100644 index 00000000000..51529a12b6a --- /dev/null +++ b/changelogs/unreleased/196838-remove-routing-ff.yml @@ -0,0 +1,5 @@ +--- +title: Improve Advanced global search performance by using routing +merge_request: 27398 +author: +type: performance diff --git a/changelogs/unreleased/208755.yml b/changelogs/unreleased/208755.yml new file mode 100644 index 00000000000..d8b02650e22 --- /dev/null +++ b/changelogs/unreleased/208755.yml @@ -0,0 +1,5 @@ +--- +title: Update icons in Sentry Error Tracking list for ignored/resolved errors +merge_request: 27125 +author: +type: other diff --git a/changelogs/unreleased/208897-migrate-bot-type-to-user-type.yml b/changelogs/unreleased/208897-migrate-bot-type-to-user-type.yml new file mode 100644 index 00000000000..b629827ffeb --- /dev/null +++ b/changelogs/unreleased/208897-migrate-bot-type-to-user-type.yml @@ -0,0 +1,5 @@ +--- +title: Move bots functionality to user_type column +merge_request: 26981 +author: +type: performance diff --git a/changelogs/unreleased/eb-code-coverage-graph-storage.yml b/changelogs/unreleased/eb-code-coverage-graph-storage.yml new file mode 100644 index 00000000000..6ed6e2cae6b --- /dev/null +++ b/changelogs/unreleased/eb-code-coverage-graph-storage.yml @@ -0,0 +1,5 @@ +--- +title: Store daily code coverages into ci_daily_report_results table +merge_request: 24695 +author: +type: added diff --git a/config/pseudonymizer.yml b/config/pseudonymizer.yml index 1f4ed9a8421..7b5f8aad255 100644 --- a/config/pseudonymizer.yml +++ b/config/pseudonymizer.yml @@ -469,6 +469,7 @@ tables: - ghost - last_activity_on - notified_of_own_activity + - user_type - bot_type - preferred_language - theme_id diff --git a/db/migrate/20200204131831_create_daily_report_results.rb b/db/migrate/20200204131831_create_daily_report_results.rb new file mode 100644 index 00000000000..e18d4efb7fa --- /dev/null +++ b/db/migrate/20200204131831_create_daily_report_results.rb @@ -0,0 +1,22 @@ +# 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 diff --git a/db/migrate/20200311074438_migrate_bot_type_to_user_type.rb b/db/migrate/20200311074438_migrate_bot_type_to_user_type.rb new file mode 100644 index 00000000000..b13842794d3 --- /dev/null +++ b/db/migrate/20200311074438_migrate_bot_type_to_user_type.rb @@ -0,0 +1,13 @@ +# 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 diff --git a/db/migrate/20200311082301_add_user_state_index.rb b/db/migrate/20200311082301_add_user_state_index.rb new file mode 100644 index 00000000000..953941c2a53 --- /dev/null +++ b/db/migrate/20200311082301_add_user_state_index.rb @@ -0,0 +1,21 @@ +# 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 diff --git a/db/schema.rb b/db/schema.rb index 55c99cb1027..1e32f4e6bd4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/lib/gitlab/background_migration/user_mentions/models/note.rb b/lib/gitlab/background_migration/user_mentions/models/note.rb index 7a1a0223bc7..c32292ad704 100644 --- a/lib/gitlab/background_migration/user_mentions/models/note.rb +++ b/lib/gitlab/background_migration/user_mentions/models/note.rb @@ -24,7 +24,7 @@ module Gitlab end def for_project_noteable? - !for_personal_snippet? + !for_personal_snippet? && !for_epic? end def skip_project_check? diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 11f93ac0bce..16a07a52d62 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -21333,6 +21333,9 @@ msgstr "" msgid "Undo" msgstr "" +msgid "Undo Ignore" +msgstr "" + msgid "Undo ignore" msgstr "" diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index c6c842ce1a1..08e4920b020 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -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], diff --git a/spec/factories/ci/daily_report_results.rb b/spec/factories/ci/daily_report_results.rb new file mode 100644 index 00000000000..e2255e8a134 --- /dev/null +++ b/spec/factories/ci/daily_report_results.rb @@ -0,0 +1,13 @@ +# 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 diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 34f6da682b6..0ce567e11fe 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -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 diff --git a/spec/frontend/error_tracking/components/error_tracking_actions_spec.js b/spec/frontend/error_tracking/components/error_tracking_actions_spec.js new file mode 100644 index 00000000000..b22805f5227 --- /dev/null +++ b/spec/frontend/error_tracking/components/error_tracking_actions_spec.js @@ -0,0 +1,93 @@ +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'); + }); + }); + }); +}); diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js index cd6dd5c7519..3bea1d343be 100644 --- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js +++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js @@ -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', diff --git a/spec/frontend/error_tracking/components/list_mock.json b/spec/frontend/error_tracking/components/list_mock.json index a6e94c1a026..54ae0a4c7cf 100644 --- a/spec/frontend/error_tracking/components/list_mock.json +++ b/spec/frontend/error_tracking/components/list_mock.json @@ -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 diff --git a/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb b/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb index a273dcf9e5c..ff8b9dd1005 100644 --- a/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb +++ b/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb @@ -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 diff --git a/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb index b521ad0c6ea..326a41a3af7 100644 --- a/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::CodeEventFetcher do let(:stage_name) { :code } diff --git a/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb index aa12bc21d22..9a4193b09f5 100644 --- a/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_stage_spec' describe Gitlab::CycleAnalytics::CodeStage do let(:stage_name) { :code } diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb index afb7b6a13b0..a72e2952782 100644 --- a/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::IssueEventFetcher do let(:stage_name) { :issue } diff --git a/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb index 497db88d850..021d31bf160 100644 --- a/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_stage_spec' describe Gitlab::CycleAnalytics::IssueStage do let(:stage_name) { :issue } diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb index 17786cd02c6..587f185b970 100644 --- a/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::PlanEventFetcher do let(:stage_name) { :plan } diff --git a/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb index 01a46f5ba65..e391fa6b999 100644 --- a/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_stage_spec' describe Gitlab::CycleAnalytics::PlanStage do let(:stage_name) { :plan } diff --git a/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb index 3ecfad49acd..aeca72e8c91 100644 --- a/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::ProductionEventFetcher do let(:stage_name) { :production } diff --git a/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb index eceea474988..aeeae291e2e 100644 --- a/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_stage_spec' describe Gitlab::CycleAnalytics::ProductionStage do let(:stage_name) { 'Total' } diff --git a/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb index 2c2169be58c..3eb62b45e6f 100644 --- a/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::ReviewEventFetcher do let(:stage_name) { :review } diff --git a/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb index 0f36a8c5c36..14100ee6f73 100644 --- a/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_stage_spec' describe Gitlab::CycleAnalytics::ReviewStage do let(:stage_name) { :review } diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb index 016d2e8da5b..525f1608a70 100644 --- a/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::StagingEventFetcher do let(:stage_name) { :staging } diff --git a/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb index 306b08a60e1..930892edd31 100644 --- a/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_stage_spec' describe Gitlab::CycleAnalytics::StagingStage do let(:stage_name) { :staging } diff --git a/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb index be7c0e9dd59..d550f083600 100644 --- a/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::TestEventFetcher do let(:stage_name) { :test } diff --git a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb index e347f36dfce..56e90520e72 100644 --- a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_stage_spec' describe Gitlab::CycleAnalytics::TestStage do let(:stage_name) { :test } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 37b3e4a4a22..eb28e730499 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -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 diff --git a/spec/migrations/migrate_bot_type_to_user_type_spec.rb b/spec/migrations/migrate_bot_type_to_user_type_spec.rb new file mode 100644 index 00000000000..9686aae0cd3 --- /dev/null +++ b/spec/migrations/migrate_bot_type_to_user_type_spec.rb @@ -0,0 +1,20 @@ +# 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 diff --git a/spec/models/ci/daily_report_result_spec.rb b/spec/models/ci/daily_report_result_spec.rb new file mode 100644 index 00000000000..61aa58c6692 --- /dev/null +++ b/spec/models/ci/daily_report_result_spec.rb @@ -0,0 +1,62 @@ +# 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 diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index f18c77988c8..c3f2e3aebdd 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -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 diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 849494e7cd4..c87272b2309 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -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 diff --git a/spec/services/ci/daily_report_result_service_spec.rb b/spec/services/ci/daily_report_result_service_spec.rb new file mode 100644 index 00000000000..793fc956acb --- /dev/null +++ b/spec/services/ci/daily_report_result_service_spec.rb @@ -0,0 +1,140 @@ +# 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 diff --git a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb index a3f0c84bd1f..a40c38106e2 100644 --- a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb @@ -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 diff --git a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb index 9c16fb6f6dc..851ed9c65a3 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb @@ -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 diff --git a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb index c053af010b3..c053af010b3 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics_event_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb index a00359ce979..a00359ce979 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics_event_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb diff --git a/spec/workers/ci/daily_report_results_worker_spec.rb b/spec/workers/ci/daily_report_results_worker_spec.rb new file mode 100644 index 00000000000..b6543b32b09 --- /dev/null +++ b/spec/workers/ci/daily_report_results_worker_spec.rb @@ -0,0 +1,34 @@ +# 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 |