Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
8cc88269ff
commit
2d2181e35c
|
@ -342,8 +342,9 @@ rspec fast_spec_helper minimal:
|
||||||
db:rollback:
|
db:rollback:
|
||||||
extends: .db-job-base
|
extends: .db-job-base
|
||||||
script:
|
script:
|
||||||
- bundle exec rake db:migrate:main VERSION=20181228175414
|
- if [[ -d "ee/" ]]; then task="db:migrate:main"; else task="db:migrate"; fi
|
||||||
- bundle exec rake db:migrate:main SKIP_SCHEMA_VERSION_CHECK=true
|
- bundle exec rake "${task}" VERSION=20181228175414
|
||||||
|
- bundle exec rake "${task}" SKIP_SCHEMA_VERSION_CHECK=true
|
||||||
|
|
||||||
db:migrate:reset:
|
db:migrate:reset:
|
||||||
extends: .db-job-base
|
extends: .db-job-base
|
||||||
|
@ -368,7 +369,8 @@ db:migrate-from-previous-major-version:
|
||||||
- git checkout -f $CI_COMMIT_SHA
|
- git checkout -f $CI_COMMIT_SHA
|
||||||
- SETUP_DB=false USE_BUNDLE_INSTALL=true bash scripts/prepare_build.sh
|
- SETUP_DB=false USE_BUNDLE_INSTALL=true bash scripts/prepare_build.sh
|
||||||
script:
|
script:
|
||||||
- run_timed_command "bundle exec rake db:migrate:main"
|
- if [[ -d "ee/" ]]; then task="db:migrate:main"; else task="db:migrate"; fi
|
||||||
|
- run_timed_command "bundle exec rake ${task}"
|
||||||
|
|
||||||
db:check-schema:
|
db:check-schema:
|
||||||
extends:
|
extends:
|
||||||
|
@ -377,7 +379,8 @@ db:check-schema:
|
||||||
variables:
|
variables:
|
||||||
TAG_TO_CHECKOUT: "v14.4.0"
|
TAG_TO_CHECKOUT: "v14.4.0"
|
||||||
script:
|
script:
|
||||||
- run_timed_command "bundle exec rake db:migrate:main"
|
- if [[ -d "ee/" ]]; then task="db:migrate:main"; else task="db:migrate"; fi
|
||||||
|
- run_timed_command "bundle exec rake ${task}"
|
||||||
- scripts/schema_changed.sh
|
- scripts/schema_changed.sh
|
||||||
- scripts/validate_migration_timestamps
|
- scripts/validate_migration_timestamps
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,9 @@ import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_opt
|
||||||
import { sprintf, __ } from '~/locale';
|
import { sprintf, __ } from '~/locale';
|
||||||
import defaultSortableConfig from '~/sortable/sortable_config';
|
import defaultSortableConfig from '~/sortable/sortable_config';
|
||||||
import Tracking from '~/tracking';
|
import Tracking from '~/tracking';
|
||||||
|
import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
|
||||||
import { toggleFormEventPrefix, DraggableItemTypes } from '../constants';
|
import { toggleFormEventPrefix, DraggableItemTypes } from '../constants';
|
||||||
import eventHub from '../eventhub';
|
import eventHub from '../eventhub';
|
||||||
import listQuery from '../graphql/board_lists_deferred.query.graphql';
|
|
||||||
import BoardCard from './board_card.vue';
|
import BoardCard from './board_card.vue';
|
||||||
import BoardNewIssue from './board_new_issue.vue';
|
import BoardNewIssue from './board_new_issue.vue';
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,10 @@ import sidebarEventHub from '~/sidebar/event_hub';
|
||||||
import Tracking from '~/tracking';
|
import Tracking from '~/tracking';
|
||||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
|
import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
|
||||||
import AccessorUtilities from '../../lib/utils/accessor';
|
import AccessorUtilities from '../../lib/utils/accessor';
|
||||||
import { inactiveId, LIST, ListType, toggleFormEventPrefix } from '../constants';
|
import { inactiveId, LIST, ListType, toggleFormEventPrefix } from '../constants';
|
||||||
import eventHub from '../eventhub';
|
import eventHub from '../eventhub';
|
||||||
import listQuery from '../graphql/board_lists_deferred.query.graphql';
|
|
||||||
import ItemCount from './item_count.vue';
|
import ItemCount from './item_count.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -87,6 +87,9 @@ export default {
|
||||||
deleteModalTitle() {
|
deleteModalTitle() {
|
||||||
return sprintf(__('Delete %{name}'), { name: this.name });
|
return sprintf(__('Delete %{name}'), { name: this.name });
|
||||||
},
|
},
|
||||||
|
lockBtnQASelector() {
|
||||||
|
return this.canLock ? 'lock_button' : 'disabled_lock_button';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -102,7 +105,7 @@ export default {
|
||||||
:is-locked="isLocked"
|
:is-locked="isLocked"
|
||||||
:can-lock="canLock"
|
:can-lock="canLock"
|
||||||
data-testid="lock"
|
data-testid="lock"
|
||||||
data-qa-selector="lock_button"
|
:data-qa-selector="lockBtnQASelector"
|
||||||
/>
|
/>
|
||||||
<gl-button v-gl-modal="replaceModalId" data-testid="replace">
|
<gl-button v-gl-modal="replaceModalId" data-testid="replace">
|
||||||
{{ $options.i18n.replace }}
|
{{ $options.i18n.replace }}
|
||||||
|
|
|
@ -272,6 +272,7 @@ export default {
|
||||||
:show-checkbox="showBulkEditSidebar"
|
:show-checkbox="showBulkEditSidebar"
|
||||||
:checkbox-checked="allIssuablesChecked"
|
:checkbox-checked="allIssuablesChecked"
|
||||||
class="gl-flex-grow-1 gl-border-t-none row-content-block"
|
class="gl-flex-grow-1 gl-border-t-none row-content-block"
|
||||||
|
data-qa-selector="issuable_search_container"
|
||||||
@checked-input="handleAllIssuablesCheckedInput"
|
@checked-input="handleAllIssuablesCheckedInput"
|
||||||
@onFilter="$emit('filter', $event)"
|
@onFilter="$emit('filter', $event)"
|
||||||
@onSort="$emit('sort', $event)"
|
@onSort="$emit('sort', $event)"
|
||||||
|
@ -302,6 +303,8 @@ export default {
|
||||||
v-for="issuable in issuables"
|
v-for="issuable in issuables"
|
||||||
:key="issuableId(issuable)"
|
:key="issuableId(issuable)"
|
||||||
:class="{ 'gl-cursor-grab': isManualOrdering }"
|
:class="{ 'gl-cursor-grab': isManualOrdering }"
|
||||||
|
data-qa-selector="issuable_container"
|
||||||
|
:data-qa-issuable-title="issuable.title"
|
||||||
:issuable-symbol="issuableSymbol"
|
:issuable-symbol="issuableSymbol"
|
||||||
:issuable="issuable"
|
:issuable="issuable"
|
||||||
:enable-label-permalinks="enableLabelPermalinks"
|
:enable-label-permalinks="enableLabelPermalinks"
|
||||||
|
|
|
@ -46,7 +46,9 @@ export default {
|
||||||
@click="$emit('click', tab.name)"
|
@click="$emit('click', tab.name)"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<span :title="tab.titleTooltip">{{ tab.title }}</span>
|
<span :title="tab.titleTooltip" :data-qa-selector="`${tab.name}_issuables_tab`">
|
||||||
|
{{ tab.title }}
|
||||||
|
</span>
|
||||||
<gl-badge
|
<gl-badge
|
||||||
v-if="tabCounts && isTabCountNumeric(tab)"
|
v-if="tabCounts && isTabCountNumeric(tab)"
|
||||||
variant="muted"
|
variant="muted"
|
||||||
|
|
|
@ -51,7 +51,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||||
|
|
||||||
before_action only: :show do
|
before_action only: :show do
|
||||||
push_frontend_feature_flag(:real_time_issue_sidebar, @project, default_enabled: :yaml)
|
push_frontend_feature_flag(:real_time_issue_sidebar, @project, default_enabled: :yaml)
|
||||||
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
|
push_frontend_feature_flag(:confidential_notes, project&.group, default_enabled: :yaml)
|
||||||
push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml)
|
push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml)
|
||||||
push_frontend_feature_flag(:paginated_issue_discussions, @project, default_enabled: :yaml)
|
push_frontend_feature_flag(:paginated_issue_discussions, @project, default_enabled: :yaml)
|
||||||
end
|
end
|
||||||
|
|
|
@ -144,10 +144,6 @@ class ApplicationSetting < ApplicationRecord
|
||||||
length: { maximum: 2000, message: _('is too long (maximum is %{count} characters)') },
|
length: { maximum: 2000, message: _('is too long (maximum is %{count} characters)') },
|
||||||
allow_blank: true
|
allow_blank: true
|
||||||
|
|
||||||
validates :spam_check_api_key,
|
|
||||||
presence: true,
|
|
||||||
if: :spam_check_endpoint_enabled
|
|
||||||
|
|
||||||
validates :unique_ips_limit_per_user,
|
validates :unique_ips_limit_per_user,
|
||||||
numericality: { greater_than_or_equal_to: 1 },
|
numericality: { greater_than_or_equal_to: 1 },
|
||||||
presence: true,
|
presence: true,
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ProtectedRefNameSanitizer
|
||||||
|
def sanitize_name(name)
|
||||||
|
name = CGI.unescapeHTML(name)
|
||||||
|
name = Sanitize.fragment(name)
|
||||||
|
|
||||||
|
# Sanitize.fragment escapes HTML chars, so unescape again to allow names
|
||||||
|
# like `feature->master`
|
||||||
|
CGI.unescapeHTML(name)
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
module ProtectedBranches
|
module ProtectedBranches
|
||||||
class BaseService < ::BaseService
|
class BaseService < ::BaseService
|
||||||
|
include ProtectedRefNameSanitizer
|
||||||
|
|
||||||
# current_user - The user that performs the action
|
# current_user - The user that performs the action
|
||||||
# params - A hash of parameters
|
# params - A hash of parameters
|
||||||
def initialize(project, current_user = nil, params = {})
|
def initialize(project, current_user = nil, params = {})
|
||||||
|
@ -14,22 +16,13 @@ module ProtectedBranches
|
||||||
# overridden in EE::ProtectedBranches module
|
# overridden in EE::ProtectedBranches module
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
def filtered_params
|
def filtered_params
|
||||||
return unless params
|
return unless params
|
||||||
|
|
||||||
params[:name] = sanitize_branch_name(params[:name]) if params[:name].present?
|
params[:name] = sanitize_name(params[:name]) if params[:name].present?
|
||||||
params
|
params
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def sanitize_branch_name(name)
|
|
||||||
name = CGI.unescapeHTML(name)
|
|
||||||
name = Sanitize.fragment(name)
|
|
||||||
|
|
||||||
# Sanitize.fragment escapes HTML chars, so unescape again to allow names
|
|
||||||
# like `feature->master`
|
|
||||||
CGI.unescapeHTML(name)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ProtectedTags
|
||||||
|
class BaseService < ::BaseService
|
||||||
|
include ProtectedRefNameSanitizer
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def filtered_params
|
||||||
|
return unless params
|
||||||
|
|
||||||
|
params[:name] = sanitize_name(params[:name]) if params[:name].present?
|
||||||
|
params
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,13 +1,13 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ProtectedTags
|
module ProtectedTags
|
||||||
class CreateService < BaseService
|
class CreateService < ProtectedTags::BaseService
|
||||||
attr_reader :protected_tag
|
attr_reader :protected_tag
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
|
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
|
||||||
|
|
||||||
project.protected_tags.create(params)
|
project.protected_tags.create(filtered_params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ProtectedTags
|
module ProtectedTags
|
||||||
class UpdateService < BaseService
|
class UpdateService < ProtectedTags::BaseService
|
||||||
def execute(protected_tag)
|
def execute(protected_tag)
|
||||||
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
|
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
|
||||||
|
|
||||||
protected_tag.update(params)
|
protected_tag.update(filtered_params)
|
||||||
protected_tag
|
protected_tag
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
title: Provide group membership level in OIDC claim
|
|
||||||
merge_request: 27264
|
|
||||||
author: Bastian Blank
|
|
||||||
type: added
|
|
|
@ -154,7 +154,6 @@ module API
|
||||||
optional :spam_check_endpoint_enabled, type: Boolean, desc: 'Enable Spam Check via external API endpoint'
|
optional :spam_check_endpoint_enabled, type: Boolean, desc: 'Enable Spam Check via external API endpoint'
|
||||||
given spam_check_endpoint_enabled: ->(val) { val } do
|
given spam_check_endpoint_enabled: ->(val) { val } do
|
||||||
requires :spam_check_endpoint_url, type: String, desc: 'The URL of the external Spam Check service endpoint'
|
requires :spam_check_endpoint_url, type: String, desc: 'The URL of the external Spam Check service endpoint'
|
||||||
requires :spam_check_api_key, type: String, desc: 'The API key used by GitLab for accessing the Spam Check service endpoint'
|
|
||||||
end
|
end
|
||||||
optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
|
optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
|
||||||
optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.'
|
optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.'
|
||||||
|
|
|
@ -5,9 +5,9 @@ module QA
|
||||||
module Project
|
module Project
|
||||||
module Issue
|
module Issue
|
||||||
class Index < Page::Base
|
class Index < Page::Base
|
||||||
view 'app/assets/javascripts/issues_list/components/issuable.vue' do
|
view 'app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue' do
|
||||||
element :issue_container
|
element :issuable_container
|
||||||
element :issue_link
|
element :issuable_search_container
|
||||||
end
|
end
|
||||||
|
|
||||||
view 'app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue' do
|
view 'app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue' do
|
||||||
|
@ -25,8 +25,8 @@ module QA
|
||||||
element :import_issues_dropdown
|
element :import_issues_dropdown
|
||||||
end
|
end
|
||||||
|
|
||||||
view 'app/views/shared/issuable/_nav.html.haml' do
|
view 'app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue' do
|
||||||
element :closed_issues_link
|
element :closed_issuables_tab, ':data-qa-selector="`${tab.name}_issuables_tab`"' # rubocop:disable QA/ElementWithPattern
|
||||||
end
|
end
|
||||||
|
|
||||||
def avatar_counter
|
def avatar_counter
|
||||||
|
@ -37,8 +37,8 @@ module QA
|
||||||
click_link(title)
|
click_link(title)
|
||||||
end
|
end
|
||||||
|
|
||||||
def click_closed_issues_link
|
def click_closed_issues_tab
|
||||||
click_element :closed_issues_link
|
click_element(:closed_issuables_tab)
|
||||||
end
|
end
|
||||||
|
|
||||||
def click_export_as_csv_button
|
def click_export_as_csv_button
|
||||||
|
@ -73,11 +73,17 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_issue?(issue)
|
def has_issue?(issue)
|
||||||
has_element? :issue_container, issue_title: issue.title
|
has_element? :issuable_container, issuable_title: issue.title
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_no_issue?(issue)
|
def has_no_issue?(issue)
|
||||||
has_no_element? :issue_container, issue_title: issue.title
|
has_no_element? :issuable_container, issuable_title: issue.title
|
||||||
|
end
|
||||||
|
|
||||||
|
def wait_for_vue_issues_list_ff
|
||||||
|
Support::Retrier.retry_until(max_duration: 60, reload_page: page, retry_on_exception: true, sleep_interval: 5) do
|
||||||
|
find_element(:closed_issuables_tab)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,19 +1,26 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module QA
|
module QA
|
||||||
RSpec.describe 'Plan', :smoke do
|
# TODO: Remove :requires_admin when the `Runtime::Feature.enable` method call is removed
|
||||||
|
RSpec.describe 'Plan', :smoke, :requires_admin do
|
||||||
describe 'Issue creation' do
|
describe 'Issue creation' do
|
||||||
let(:closed_issue) { Resource::Issue.fabricate_via_api! }
|
let(:project) { Resource::Project.fabricate_via_api! }
|
||||||
|
let(:closed_issue) { Resource::Issue.fabricate_via_api! { |issue| issue.project = project } }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
Runtime::Feature.enable(:vue_issues_list, group: project.group)
|
||||||
|
|
||||||
Flow::Login.sign_in
|
Flow::Login.sign_in
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates an issue', :mobile, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1185' do
|
it 'creates an issue', :mobile, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1185' do
|
||||||
issue = Resource::Issue.fabricate_via_browser_ui!
|
issue = Resource::Issue.fabricate_via_browser_ui! { |issue| issue.project = project }
|
||||||
|
|
||||||
Page::Project::Menu.perform(&:click_issues)
|
Page::Project::Menu.perform(&:click_issues)
|
||||||
|
|
||||||
|
# TODO: Remove this method when the `Runtime::Feature.enable` method call is removed
|
||||||
|
Page::Project::Issue::Index.perform(&:wait_for_vue_issues_list_ff)
|
||||||
|
|
||||||
Page::Project::Issue::Index.perform do |index|
|
Page::Project::Issue::Index.perform do |index|
|
||||||
expect(index).to have_issue(issue)
|
expect(index).to have_issue(issue)
|
||||||
end
|
end
|
||||||
|
@ -29,10 +36,14 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
Page::Project::Menu.perform(&:click_issues)
|
Page::Project::Menu.perform(&:click_issues)
|
||||||
|
|
||||||
|
# TODO: Remove this method when the `Runtime::Feature.enable` method call is removed
|
||||||
|
Page::Project::Issue::Index.perform(&:wait_for_vue_issues_list_ff)
|
||||||
|
|
||||||
Page::Project::Issue::Index.perform do |index|
|
Page::Project::Issue::Index.perform do |index|
|
||||||
expect(index).not_to have_issue(closed_issue)
|
expect(index).not_to have_issue(closed_issue)
|
||||||
|
|
||||||
index.click_closed_issues_link
|
index.click_closed_issues_tab
|
||||||
|
|
||||||
expect(index).to have_issue(closed_issue)
|
expect(index).to have_issue(closed_issue)
|
||||||
end
|
end
|
||||||
|
@ -45,7 +56,7 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
Resource::Issue.fabricate_via_api!.visit!
|
Resource::Issue.fabricate_via_api! { |issue| issue.project = project }.visit!
|
||||||
end
|
end
|
||||||
|
|
||||||
# The following example is excluded from running in `review-qa-smoke` job
|
# The following example is excluded from running in `review-qa-smoke` job
|
||||||
|
|
|
@ -65,6 +65,7 @@ module QA
|
||||||
# a) The terminal JS package has loaded, and
|
# a) The terminal JS package has loaded, and
|
||||||
# b) It's not stuck in a "Loading/Starting" state, and
|
# b) It's not stuck in a "Loading/Starting" state, and
|
||||||
# c) There's no alert stating there was a problem
|
# c) There's no alert stating there was a problem
|
||||||
|
# d) There are no JS console errors
|
||||||
#
|
#
|
||||||
# The terminal itself is a third-party package so we assume it is
|
# The terminal itself is a third-party package so we assume it is
|
||||||
# adequately tested elsewhere.
|
# adequately tested elsewhere.
|
||||||
|
@ -78,6 +79,17 @@ module QA
|
||||||
expect(edit).to have_finished_loading
|
expect(edit).to have_finished_loading
|
||||||
expect(edit).to have_terminal_screen
|
expect(edit).to have_terminal_screen
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# It takes a few seconds for console errors to appear
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
errors = page.driver.browser.logs.get(:browser)
|
||||||
|
.select { |e| e.level == "SEVERE" }
|
||||||
|
.to_a
|
||||||
|
|
||||||
|
if errors.present?
|
||||||
|
raise("Console error(s):\n#{errors.join("\n\n")}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,99 +1,210 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative "../../qa"
|
||||||
|
|
||||||
require "influxdb-client"
|
require "influxdb-client"
|
||||||
require "terminal-table"
|
require "terminal-table"
|
||||||
require "slack-notifier"
|
require "slack-notifier"
|
||||||
|
require "colorize"
|
||||||
|
|
||||||
module QA
|
module QA
|
||||||
module Tools
|
module Tools
|
||||||
class ReliableReport
|
class ReliableReport
|
||||||
def initialize(run_type, range = 30)
|
include Support::API
|
||||||
@results = 2
|
|
||||||
@slack_channel = "#quality-reports"
|
# Project for report creation: https://gitlab.com/gitlab-org/gitlab
|
||||||
|
PROJECT_ID = 278964
|
||||||
|
|
||||||
|
def initialize(range)
|
||||||
@range = range
|
@range = range
|
||||||
@run_type = run_type
|
@influxdb_bucket = "e2e-test-stats"
|
||||||
@stable_title = "Top #{results} stable specs for past #{@range} days in '#{run_type}' runs"
|
@slack_channel = "#quality-reports"
|
||||||
@unstable_title = "Top #{results} unstable reliable specs for past #{@range} days in '#{run_type}' runs"
|
@influxdb_url = ENV["QA_INFLUXDB_URL"] || raise("Missing QA_INFLUXDB_URL env variable")
|
||||||
|
@influxdb_token = ENV["QA_INFLUXDB_TOKEN"] || raise("Missing QA_INFLUXDB_TOKEN env variable")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run reliable reporter
|
||||||
|
#
|
||||||
|
# @param [Integer] range amount of days for results range
|
||||||
|
# @param [String] report_in_issue_and_slack
|
||||||
|
# @return [void]
|
||||||
|
def self.run(range: 14, report_in_issue_and_slack: "false")
|
||||||
|
reporter = new(range)
|
||||||
|
|
||||||
|
reporter.print_report
|
||||||
|
reporter.report_in_issue_and_slack if report_in_issue_and_slack == "true"
|
||||||
|
rescue StandardError => e
|
||||||
|
puts "Report creation failed! Error: '#{e}'".colorize(:red)
|
||||||
|
reporter.notify_failure(e)
|
||||||
|
exit(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Print top stable specs
|
# Print top stable specs
|
||||||
#
|
#
|
||||||
# @return [void]
|
# @return [void]
|
||||||
def show_top_stable
|
def print_report
|
||||||
results_table(:stable).each { |table| puts "#{table}\n\n" }
|
puts "#{stable_summary_table}\n\n"
|
||||||
|
stable_results_tables.each { |stage, table| puts "#{table}\n\n" }
|
||||||
|
return puts("No unstable reliable tests present!".colorize(:yellow)) if unstable_reliable_test_runs.empty?
|
||||||
|
|
||||||
|
puts "#{unstable_summary_table}\n\n"
|
||||||
|
unstable_reliable_results_tables.each { |stage, table| puts "#{table}\n\n" }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Post top stable spec report to slack
|
# Create report issue
|
||||||
#
|
#
|
||||||
# @return [void]
|
# @return [void]
|
||||||
def notify_top_stable
|
def report_in_issue_and_slack
|
||||||
puts "\nSending top stable spec report to #{slack_channel} slack channel"
|
puts "Creating report".colorize(:green)
|
||||||
slack_args = { icon_emoji: ":mtg_green:", username: "Stable Spec Report" }
|
response = post(
|
||||||
notifier.post(text: "*#{stable_title}*", **slack_args)
|
"#{gitlab_api_url}/projects/#{PROJECT_ID}/issues",
|
||||||
results_table(:stable).each { |table| notifier.post(text: "```#{table}```", **slack_args) }
|
{ title: "Reliable spec report", description: report_issue_body, labels: "Quality,test" },
|
||||||
|
headers: { "PRIVATE-TOKEN" => gitlab_access_token }
|
||||||
|
)
|
||||||
|
web_url = parse_body(response)[:web_url]
|
||||||
|
puts "Created report issue: #{web_url}"
|
||||||
|
|
||||||
|
puts "Sending slack notification".colorize(:green)
|
||||||
|
notifier.post(
|
||||||
|
icon_emoji: ":tanuki-protect:",
|
||||||
|
text: <<~TEXT
|
||||||
|
```#{stable_summary_table}```
|
||||||
|
```#{unstable_summary_table}```
|
||||||
|
|
||||||
|
#{web_url}
|
||||||
|
TEXT
|
||||||
|
)
|
||||||
|
puts "Done!"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Print top unstable specs
|
# Notify failure
|
||||||
#
|
#
|
||||||
|
# @param [StandardError] error
|
||||||
# @return [void]
|
# @return [void]
|
||||||
def show_top_unstable
|
def notify_failure(error)
|
||||||
return puts("No unstable tests present!") if top_unstable_reliable.empty?
|
notifier.post(
|
||||||
|
text: "Reliable reporter failed to create report. Error: ```#{error}```",
|
||||||
results_table(:unstable).each { |table| puts "#{table}\n\n" }
|
icon_emoji: ":sadpanda:"
|
||||||
end
|
)
|
||||||
|
|
||||||
# Post top unstable reliable spec report to slack
|
|
||||||
#
|
|
||||||
# @return [void]
|
|
||||||
def notify_top_unstable
|
|
||||||
return puts("No unstable tests present!") if top_unstable_reliable.empty?
|
|
||||||
|
|
||||||
puts "\nSending top unstable reliable spec report to #{slack_channel} slack channel"
|
|
||||||
slack_args = { icon_emoji: ":sadpanda:", username: "Unstable Spec Report" }
|
|
||||||
notifier.post(text: "*#{unstable_title}*", **slack_args)
|
|
||||||
results_table(:unstable).each { |table| notifier.post(text: "```#{table}```", **slack_args) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
attr_reader :results,
|
attr_reader :range, :influxdb_bucket, :slack_channel, :influxdb_url, :influxdb_token
|
||||||
:slack_channel,
|
|
||||||
:range,
|
# Markdown formatted report issue body
|
||||||
:run_type,
|
#
|
||||||
:stable_title,
|
# @return [String]
|
||||||
:unstable_title
|
def report_issue_body
|
||||||
|
issue = []
|
||||||
|
issue << "[[_TOC_]]"
|
||||||
|
issue << "# Candidates for promotion to reliable\n\n```\n#{stable_summary_table}\n```"
|
||||||
|
issue << results_markdown(stable_results_tables)
|
||||||
|
return issue.join("\n\n") if unstable_reliable_test_runs.empty?
|
||||||
|
|
||||||
|
issue << "# Reliable specs with failures\n\n```\n#{unstable_summary_table}\n```"
|
||||||
|
issue << results_markdown(unstable_reliable_results_tables)
|
||||||
|
issue.join("\n\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Stable spec summary table
|
||||||
|
#
|
||||||
|
# @return [Terminal::Table]
|
||||||
|
def stable_summary_table
|
||||||
|
@stable_summary_table ||= terminal_table(
|
||||||
|
rows: stable_test_runs.map { |stage, specs| [stage, specs.length] },
|
||||||
|
title: "Stable spec summary for past #{range} days".ljust(50),
|
||||||
|
headings: %w[STAGE COUNT]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unstable reliable summary table
|
||||||
|
#
|
||||||
|
# @return [Terminal::Table]
|
||||||
|
def unstable_summary_table
|
||||||
|
@unstable_summary_table ||= terminal_table(
|
||||||
|
rows: unstable_reliable_test_runs.map { |stage, specs| [stage, specs.length] },
|
||||||
|
title: "Unstable spec summary for past #{range} days".ljust(50),
|
||||||
|
headings: %w[STAGE COUNT]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Result tables for stable specs
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
|
def stable_results_tables
|
||||||
|
@stable_results ||= results_tables(:stable)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Result table for unstable specs
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
|
def unstable_reliable_results_tables
|
||||||
|
@unstable_results ||= results_tables(:unstable)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Markdown formatted tables
|
||||||
|
#
|
||||||
|
# @param [Hash] results
|
||||||
|
# @return [String]
|
||||||
|
def results_markdown(results)
|
||||||
|
results.map do |stage, table|
|
||||||
|
<<~STAGE.strip
|
||||||
|
## #{stage}
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Executions table</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
#{table}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
STAGE
|
||||||
|
end.join("\n\n")
|
||||||
|
end
|
||||||
|
|
||||||
# Results table
|
# Results table
|
||||||
#
|
#
|
||||||
# @param [Symbol] type result type - :stable, :unstable
|
# @param [Symbol] type result type - :stable, :unstable
|
||||||
# @return [Hash]
|
# @return [Hash<Symbol, Terminal::Table>]
|
||||||
def results_table(type)
|
def results_tables(type)
|
||||||
(type == :stable ? top_stable : top_unstable_reliable).map do |stage, specs|
|
(type == :stable ? stable_test_runs : unstable_reliable_test_runs).to_h do |stage, specs|
|
||||||
terminal_table(
|
headings = ["name", "runs", "failures", "failure rate"]
|
||||||
|
|
||||||
|
[stage, terminal_table(
|
||||||
rows: specs.map { |k, v| [name_column(k, v[:file]), *table_params(v.values)] },
|
rows: specs.map { |k, v| [name_column(k, v[:file]), *table_params(v.values)] },
|
||||||
title: "Top #{type} specs in '#{stage}' stage"
|
title: "Top #{type} specs in '#{stage}' stage for past #{range} days",
|
||||||
)
|
headings: headings.map(&:upcase)
|
||||||
|
)]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Top stable specs
|
# Stable specs
|
||||||
#
|
#
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
def top_stable
|
def stable_test_runs
|
||||||
@top_stable ||= runs(reliable: false).transform_values do |specs|
|
@top_stable ||= begin
|
||||||
specs.sort_by { |k, v| [v[:failure_rate], -v[:runs]] }[0..results - 1].to_h
|
stable_specs = test_runs(reliable: false).transform_values do |specs|
|
||||||
|
specs
|
||||||
|
.reject { |k, v| v[:failure_rate] != 0 }
|
||||||
|
.sort_by { |k, v| -v[:runs] }
|
||||||
|
.to_h
|
||||||
|
end
|
||||||
|
|
||||||
|
stable_specs.reject { |k, v| v.empty? }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Top unstable reliable specs
|
# Unstable reliable specs
|
||||||
#
|
#
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
def top_unstable_reliable
|
def unstable_reliable_test_runs
|
||||||
@top_unstable_reliable ||= begin
|
@top_unstable_reliable ||= begin
|
||||||
unstable = runs(reliable: true).transform_values do |specs|
|
unstable = test_runs(reliable: true).transform_values do |specs|
|
||||||
specs
|
specs
|
||||||
.reject { |k, v| v[:failure_rate] == 0 }
|
.reject { |k, v| v[:failure_rate] == 0 }
|
||||||
.sort_by { |k, v| -v[:failure_rate] }[0..results - 1]
|
.sort_by { |k, v| -v[:failure_rate] }
|
||||||
.to_h
|
.to_h
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -104,9 +215,9 @@ module QA
|
||||||
# Terminal table for result formatting
|
# Terminal table for result formatting
|
||||||
#
|
#
|
||||||
# @return [Terminal::Table]
|
# @return [Terminal::Table]
|
||||||
def terminal_table(rows:, title: nil)
|
def terminal_table(rows:, headings:, title: nil)
|
||||||
Terminal::Table.new(
|
Terminal::Table.new(
|
||||||
headings: ["name", "runs", "failed", "failure rate"],
|
headings: headings,
|
||||||
style: { all_separators: true },
|
style: { all_separators: true },
|
||||||
title: title,
|
title: title,
|
||||||
rows: rows
|
rows: rows
|
||||||
|
@ -127,20 +238,19 @@ module QA
|
||||||
# @param [String] file
|
# @param [String] file
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def name_column(name, file)
|
def name_column(name, file)
|
||||||
spec_name = name.length > 100 ? "#{name} ".scan(/.{1,100} /).map(&:strip).join("\n") : name
|
spec_name = name.length > 150 ? "#{name} ".scan(/.{1,150} /).map(&:strip).join("\n") : name
|
||||||
name_line = "name: '#{spec_name}'"
|
name_line = "name: '#{spec_name}'"
|
||||||
file_line = "file: '#{file}'"
|
file_line = "file: '#{file}'"
|
||||||
|
|
||||||
"#{name_line}\n#{file_line.ljust(110)}"
|
"#{name_line}\n#{file_line.ljust(160)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Test executions grouped by name
|
# Test executions grouped by name
|
||||||
#
|
#
|
||||||
# @param [Boolean] reliable
|
# @param [Boolean] reliable
|
||||||
# @return [Hash<String, Hash>]
|
# @return [Hash<String, Hash>]
|
||||||
def runs(reliable:)
|
def test_runs(reliable:)
|
||||||
puts("Fetching data on #{reliable ? 'reliable ' : ''}test execution for past 30 days in '#{run_type}' runs")
|
puts("Fetching data on #{reliable ? 'reliable ' : ''}test execution for past #{range} days\n".colorize(:green))
|
||||||
puts
|
|
||||||
|
|
||||||
all_runs = query_api.query(query: query(reliable)).values
|
all_runs = query_api.query(query: query(reliable)).values
|
||||||
all_runs.each_with_object(Hash.new { |hsh, key| hsh[key] = {} }) do |table, result|
|
all_runs.each_with_object(Hash.new { |hsh, key| hsh[key] = {} }) do |table, result|
|
||||||
|
@ -168,11 +278,18 @@ module QA
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def query(reliable)
|
def query(reliable)
|
||||||
<<~QUERY
|
<<~QUERY
|
||||||
from(bucket: "e2e-test-stats")
|
from(bucket: "#{influxdb_bucket}")
|
||||||
|> range(start: -#{range}d)
|
|> range(start: -#{range}d)
|
||||||
|> filter(fn: (r) => r._measurement == "test-stats" and
|
|> filter(fn: (r) => r._measurement == "test-stats")
|
||||||
r.run_type == "#{run_type}" and
|
|> filter(fn: (r) => r.run_type == "staging-full" or
|
||||||
r.status != "pending" and
|
r.run_type == "staging-sanity" or
|
||||||
|
r.run_type == "staging-sanity-no-admin" or
|
||||||
|
r.run_type == "production-full" or
|
||||||
|
r.run_type == "production-sanity" or
|
||||||
|
r.run_type == "package-and-qa" or
|
||||||
|
r.run_type == "nightly"
|
||||||
|
)
|
||||||
|
|> filter(fn: (r) => r.status != "pending" and
|
||||||
r.merge_request == "false" and
|
r.merge_request == "false" and
|
||||||
r.quarantined == "false" and
|
r.quarantined == "false" and
|
||||||
r.reliable == "#{reliable}" and
|
r.reliable == "#{reliable}" and
|
||||||
|
@ -196,7 +313,7 @@ module QA
|
||||||
@influx_client ||= InfluxDB2::Client.new(
|
@influx_client ||= InfluxDB2::Client.new(
|
||||||
influxdb_url,
|
influxdb_url,
|
||||||
influxdb_token,
|
influxdb_token,
|
||||||
bucket: "e2e-test-stats",
|
bucket: influxdb_bucket,
|
||||||
org: "gitlab-qa",
|
org: "gitlab-qa",
|
||||||
precision: InfluxDB2::WritePrecision::NANOSECOND
|
precision: InfluxDB2::WritePrecision::NANOSECOND
|
||||||
)
|
)
|
||||||
|
@ -209,29 +326,29 @@ module QA
|
||||||
@notifier ||= Slack::Notifier.new(
|
@notifier ||= Slack::Notifier.new(
|
||||||
slack_webhook_url,
|
slack_webhook_url,
|
||||||
channel: slack_channel,
|
channel: slack_channel,
|
||||||
username: "Reliable spec reporter"
|
username: "Reliable Spec Report"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# InfluxDb instance url
|
# Gitlab access token
|
||||||
#
|
#
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def influxdb_url
|
def gitlab_access_token
|
||||||
@influxdb_url ||= ENV["QA_INFLUXDB_URL"] || raise("Missing QA_INFLUXDB_URL environment variable")
|
@gitlab_access_token ||= ENV["GITLAB_ACCESS_TOKEN"] || raise("Missing GITLAB_ACCESS_TOKEN env variable")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Influxdb token
|
# Gitlab api url
|
||||||
#
|
#
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def influxdb_token
|
def gitlab_api_url
|
||||||
@influxdb_token ||= ENV["QA_INFLUXDB_TOKEN"] || raise("Missing QA_INFLUXDB_TOKEN environment variable")
|
@gitlab_api_url ||= ENV["CI_API_V4_URL"] || raise("Missing CI_API_V4_URL env variable")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Slack webhook url
|
# Slack webhook url
|
||||||
#
|
#
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def slack_webhook_url
|
def slack_webhook_url
|
||||||
@slack_webhook_url ||= ENV["CI_SLACK_WEBHOOK_URL"] || raise("Missing CI_SLACK_WEBHOOK_URL environment variable")
|
@slack_webhook_url ||= ENV["SLACK_WEBHOOK"] || raise("Missing SLACK_WEBHOOK env variable")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,23 +3,21 @@
|
||||||
describe QA::Tools::ReliableReport do
|
describe QA::Tools::ReliableReport do
|
||||||
include QA::Support::Helpers::StubEnv
|
include QA::Support::Helpers::StubEnv
|
||||||
|
|
||||||
subject(:reporter) { described_class.new(run_type, range) }
|
subject(:run) { described_class.run(range: range, report_in_issue_and_slack: create_issue) }
|
||||||
|
|
||||||
|
let(:gitlab_response) { instance_double("RestClient::Response", code: 200, body: { web_url: issue_url }.to_json) }
|
||||||
let(:slack_notifier) { instance_double("Slack::Notifier", post: nil) }
|
let(:slack_notifier) { instance_double("Slack::Notifier", post: nil) }
|
||||||
let(:influx_client) { instance_double("InfluxDB2::Client", create_query_api: query_api) }
|
let(:influx_client) { instance_double("InfluxDB2::Client", create_query_api: query_api) }
|
||||||
let(:query_api) { instance_double("InfluxDB2::QueryApi") }
|
let(:query_api) { instance_double("InfluxDB2::QueryApi") }
|
||||||
|
|
||||||
let(:slack_channel) { "#quality-reports" }
|
let(:slack_channel) { "#quality-reports" }
|
||||||
let(:run_type) { "package-and-qa" }
|
let(:range) { 14 }
|
||||||
let(:range) { 30 }
|
let(:issue_url) { "https://gitlab.com/issue/1" }
|
||||||
let(:results) { 2 }
|
|
||||||
|
|
||||||
let(:runs) { { 0 => stable_spec, 1 => unstable_spec } }
|
let(:runs) do
|
||||||
|
values = { "name" => "stable spec", "status" => "passed", "file_path" => "some/spec.rb", "stage" => "manage" }
|
||||||
let(:spec_values) { { "file_path" => "some/spec.rb", "stage" => "manage" } }
|
{
|
||||||
let(:stable_spec) do
|
0 => instance_double(
|
||||||
values = { "name" => "stable spec", "status" => "passed", **spec_values }
|
|
||||||
instance_double(
|
|
||||||
"InfluxDB2::FluxTable",
|
"InfluxDB2::FluxTable",
|
||||||
records: [
|
records: [
|
||||||
instance_double("InfluxDB2::FluxRecord", values: values),
|
instance_double("InfluxDB2::FluxRecord", values: values),
|
||||||
|
@ -27,11 +25,13 @@ describe QA::Tools::ReliableReport do
|
||||||
instance_double("InfluxDB2::FluxRecord", values: values)
|
instance_double("InfluxDB2::FluxRecord", values: values)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:unstable_spec) do
|
let(:reliable_runs) do
|
||||||
values = { "name" => "unstable spec", "status" => "failed", **spec_values }
|
values = { "name" => "unstable spec", "status" => "failed", "file_path" => "some/spec.rb", "stage" => "create" }
|
||||||
instance_double(
|
{
|
||||||
|
0 => instance_double(
|
||||||
"InfluxDB2::FluxTable",
|
"InfluxDB2::FluxTable",
|
||||||
records: [
|
records: [
|
||||||
instance_double("InfluxDB2::FluxRecord", values: { **values, "status" => "passed" }),
|
instance_double("InfluxDB2::FluxRecord", values: { **values, "status" => "passed" }),
|
||||||
|
@ -39,15 +39,23 @@ describe QA::Tools::ReliableReport do
|
||||||
instance_double("InfluxDB2::FluxRecord", values: values)
|
instance_double("InfluxDB2::FluxRecord", values: values)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def flux_query(reliable)
|
def flux_query(reliable:)
|
||||||
<<~QUERY
|
<<~QUERY
|
||||||
from(bucket: "e2e-test-stats")
|
from(bucket: "e2e-test-stats")
|
||||||
|> range(start: -#{range}d)
|
|> range(start: -#{range}d)
|
||||||
|> filter(fn: (r) => r._measurement == "test-stats" and
|
|> filter(fn: (r) => r._measurement == "test-stats")
|
||||||
r.run_type == "#{run_type}" and
|
|> filter(fn: (r) => r.run_type == "staging-full" or
|
||||||
r.status != "pending" and
|
r.run_type == "staging-sanity" or
|
||||||
|
r.run_type == "staging-sanity-no-admin" or
|
||||||
|
r.run_type == "production-full" or
|
||||||
|
r.run_type == "production-sanity" or
|
||||||
|
r.run_type == "package-and-qa" or
|
||||||
|
r.run_type == "nightly"
|
||||||
|
)
|
||||||
|
|> filter(fn: (r) => r.status != "pending" and
|
||||||
r.merge_request == "false" and
|
r.merge_request == "false" and
|
||||||
r.quarantined == "false" and
|
r.quarantined == "false" and
|
||||||
r.reliable == "#{reliable}" and
|
r.reliable == "#{reliable}" and
|
||||||
|
@ -57,9 +65,32 @@ describe QA::Tools::ReliableReport do
|
||||||
QUERY
|
QUERY
|
||||||
end
|
end
|
||||||
|
|
||||||
def table(rows, title = nil)
|
def markdown_section(summary, result, stage, type)
|
||||||
|
<<~SECTION.strip
|
||||||
|
```
|
||||||
|
#{summary_table(summary, type)}
|
||||||
|
```
|
||||||
|
|
||||||
|
## #{stage}
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Executions table</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
#{table(result, ['NAME', 'RUNS', 'FAILURES', 'FAILURE RATE'], "Top #{type} specs in '#{stage}' stage for past #{range} days")}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
SECTION
|
||||||
|
end
|
||||||
|
|
||||||
|
def summary_table(summary, type)
|
||||||
|
table(summary, %w[STAGE COUNT], "#{type.capitalize} spec summary for past #{range} days".ljust(50))
|
||||||
|
end
|
||||||
|
|
||||||
|
def table(rows, headings, title)
|
||||||
Terminal::Table.new(
|
Terminal::Table.new(
|
||||||
headings: ["name", "runs", "failed", "failure rate"],
|
headings: headings,
|
||||||
style: { all_separators: true },
|
style: { all_separators: true },
|
||||||
title: title,
|
title: title,
|
||||||
rows: rows
|
rows: rows
|
||||||
|
@ -68,7 +99,7 @@ describe QA::Tools::ReliableReport do
|
||||||
|
|
||||||
def name_column(spec_name)
|
def name_column(spec_name)
|
||||||
name = "name: '#{spec_name}'"
|
name = "name: '#{spec_name}'"
|
||||||
file = "file: 'spec.rb'".ljust(110)
|
file = "file: 'spec.rb'".ljust(160)
|
||||||
|
|
||||||
"#{name}\n#{file}"
|
"#{name}\n#{file}"
|
||||||
end
|
end
|
||||||
|
@ -76,73 +107,85 @@ describe QA::Tools::ReliableReport do
|
||||||
before do
|
before do
|
||||||
stub_env("QA_INFLUXDB_URL", "url")
|
stub_env("QA_INFLUXDB_URL", "url")
|
||||||
stub_env("QA_INFLUXDB_TOKEN", "token")
|
stub_env("QA_INFLUXDB_TOKEN", "token")
|
||||||
stub_env("CI_SLACK_WEBHOOK_URL", "slack_url")
|
stub_env("SLACK_WEBHOOK", "slack_url")
|
||||||
|
stub_env("CI_API_V4_URL", "gitlab_api_url")
|
||||||
|
stub_env("GITLAB_ACCESS_TOKEN", "gitlab_token")
|
||||||
|
|
||||||
|
allow(RestClient::Request).to receive(:execute).and_return(gitlab_response)
|
||||||
allow(Slack::Notifier).to receive(:new).and_return(slack_notifier)
|
allow(Slack::Notifier).to receive(:new).and_return(slack_notifier)
|
||||||
allow(InfluxDB2::Client).to receive(:new).and_return(influx_client)
|
allow(InfluxDB2::Client).to receive(:new).and_return(influx_client)
|
||||||
allow(query_api).to receive(:query).with(query: query).and_return(runs)
|
|
||||||
|
allow(query_api).to receive(:query).with(query: flux_query(reliable: false)).and_return(runs)
|
||||||
|
allow(query_api).to receive(:query).with(query: flux_query(reliable: true)).and_return(reliable_runs)
|
||||||
end
|
end
|
||||||
|
|
||||||
context "with stable spec report" do
|
context "without report creation" do
|
||||||
let(:query) { flux_query(false) }
|
let(:create_issue) { "false" }
|
||||||
let(:fetch_message) { "Fetching data on test execution for past #{range} days in '#{run_type}' runs" }
|
|
||||||
let(:slack_send_message) { "Sending top stable spec report to #{slack_channel} slack channel" }
|
|
||||||
let(:message_title) { "Top #{results} stable specs for past #{range} days in '#{run_type}' runs" }
|
|
||||||
let(:table_title) { "Top stable specs in 'manage' stage" }
|
|
||||||
let(:rows) do
|
|
||||||
[
|
|
||||||
[name_column("stable spec"), 3, 0, "0%"],
|
|
||||||
[name_column("unstable spec"), 3, 2, "66.67%"]
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
it "prints top stable spec report to console" do
|
it "does not create report issue", :aggregate_failures do
|
||||||
expect { reporter.show_top_stable }.to output("#{fetch_message}\n\n#{table(rows, table_title)}\n\n").to_stdout
|
expect { run }.to output.to_stdout
|
||||||
end
|
|
||||||
|
|
||||||
it "sends top stable spec report to slack" do
|
expect(RestClient::Request).not_to have_received(:execute)
|
||||||
slack_args = { icon_emoji: ":mtg_green:", username: "Stable Spec Report" }
|
|
||||||
|
|
||||||
expect { reporter.notify_top_stable }.to output("\n#{slack_send_message}\n#{fetch_message}\n\n").to_stdout
|
|
||||||
expect(slack_notifier).to have_received(:post).with(text: "*#{message_title}*", **slack_args)
|
|
||||||
expect(slack_notifier).to have_received(:post).with(text: "```#{table(rows, table_title)}```", **slack_args)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "with unstable spec report" do
|
|
||||||
let(:query) { flux_query(true) }
|
|
||||||
let(:fetch_message) { "Fetching data on reliable test execution for past #{range} days in '#{run_type}' runs" }
|
|
||||||
let(:slack_send_message) { "Sending top unstable reliable spec report to #{slack_channel} slack channel" }
|
|
||||||
let(:message_title) { "Top #{results} unstable reliable specs for past #{range} days in '#{run_type}' runs" }
|
|
||||||
let(:table_title) { "Top unstable specs in 'manage' stage" }
|
|
||||||
let(:rows) { [[name_column("unstable spec"), 3, 2, "66.67%"]] }
|
|
||||||
|
|
||||||
it "prints top unstable spec report to console" do
|
|
||||||
expect { reporter.show_top_unstable }.to output("#{fetch_message}\n\n#{table(rows, table_title)}\n\n").to_stdout
|
|
||||||
end
|
|
||||||
|
|
||||||
it "sends top unstable reliable spec report to slack" do
|
|
||||||
slack_args = { icon_emoji: ":sadpanda:", username: "Unstable Spec Report" }
|
|
||||||
|
|
||||||
expect { reporter.notify_top_unstable }.to output("#{fetch_message}\n\n\n#{slack_send_message}\n").to_stdout
|
|
||||||
expect(slack_notifier).to have_received(:post).with(text: "*#{message_title}*", **slack_args)
|
|
||||||
expect(slack_notifier).to have_received(:post).with(text: "```#{table(rows, table_title)}```", **slack_args)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "without unstable reliable specs" do
|
|
||||||
let(:query) { flux_query(true) }
|
|
||||||
let(:runs) { { 0 => stable_spec } }
|
|
||||||
let(:fetch_message) { "Fetching data on reliable test execution for past #{range} days in '#{run_type}' runs" }
|
|
||||||
let(:no_result_message) { "No unstable tests present!" }
|
|
||||||
|
|
||||||
it "prints no result message to console" do
|
|
||||||
expect { reporter.show_top_unstable }.to output("#{fetch_message}\n\n#{no_result_message}\n").to_stdout
|
|
||||||
end
|
|
||||||
|
|
||||||
it "skips slack notification" do
|
|
||||||
expect { reporter.notify_top_unstable }.to output("#{fetch_message}\n\n#{no_result_message}\n").to_stdout
|
|
||||||
expect(slack_notifier).not_to have_received(:post)
|
expect(slack_notifier).not_to have_received(:post)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "with report creation" do
|
||||||
|
let(:create_issue) { "true" }
|
||||||
|
let(:issue_body) do
|
||||||
|
<<~TXT.strip
|
||||||
|
[[_TOC_]]
|
||||||
|
|
||||||
|
# Candidates for promotion to reliable
|
||||||
|
|
||||||
|
#{markdown_section([['manage', 1]], [[name_column('stable spec'), 3, 0, '0%']], 'manage', 'stable')}
|
||||||
|
|
||||||
|
# Reliable specs with failures
|
||||||
|
|
||||||
|
#{markdown_section([['create', 1]], [[name_column('unstable spec'), 3, 2, '66.67%']], 'create', 'unstable')}
|
||||||
|
TXT
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates report issue", :aggregate_failures do
|
||||||
|
expect { run }.to output.to_stdout
|
||||||
|
|
||||||
|
expect(RestClient::Request).to have_received(:execute).with(
|
||||||
|
method: :post,
|
||||||
|
url: "gitlab_api_url/projects/278964/issues",
|
||||||
|
verify_ssl: false,
|
||||||
|
headers: { "PRIVATE-TOKEN" => "gitlab_token" },
|
||||||
|
payload: {
|
||||||
|
title: "Reliable spec report",
|
||||||
|
description: issue_body,
|
||||||
|
labels: "Quality,test"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(slack_notifier).to have_received(:post).with(
|
||||||
|
icon_emoji: ":tanuki-protect:",
|
||||||
|
text: <<~TEXT
|
||||||
|
```#{summary_table([['manage', 1]], 'stable')}```
|
||||||
|
```#{summary_table([['create', 1]], 'unstable')}```
|
||||||
|
|
||||||
|
#{issue_url}
|
||||||
|
TEXT
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with failure" do
|
||||||
|
let(:create_issue) { "true" }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(query_api).to receive(:query).and_raise("Connection error!")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "notifies failure", :aggregate_failures do
|
||||||
|
expect { expect { run }.to raise_error(SystemExit) }.to output.to_stdout
|
||||||
|
|
||||||
|
expect(slack_notifier).to have_received(:post).with(
|
||||||
|
icon_emoji: ":sadpanda:",
|
||||||
|
text: "Reliable reporter failed to create report. Error: ```Connection error!```"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,19 +3,8 @@
|
||||||
|
|
||||||
require_relative "../qa/tools/reliable_report"
|
require_relative "../qa/tools/reliable_report"
|
||||||
|
|
||||||
desc "Fetch top most reliable specs"
|
desc "Fetch reliable and unreliable spec data and create report"
|
||||||
task :reliable_spec_report, [:run_type, :range, :create_slack_report] do |_task, args|
|
task :reliable_spec_report, [:range, :report_in_issue_and_slack] do |_task, args|
|
||||||
report = QA::Tools::ReliableReport.new(args[:run_type] || "package-and-qa", args[:range])
|
QA::Tools::ReliableReport.run(**args)
|
||||||
|
|
||||||
report.show_top_stable
|
|
||||||
report.notify_top_stable if args[:create_slack_report] == 'true'
|
|
||||||
end
|
|
||||||
|
|
||||||
desc "Fetch top most unstable reliable specs"
|
|
||||||
task :unreliable_spec_report, [:run_type, :range, :create_slack_report] do |_task, args|
|
|
||||||
report = QA::Tools::ReliableReport.new(args[:run_type] || "package-and-qa", args[:range])
|
|
||||||
|
|
||||||
report.show_top_unstable
|
|
||||||
report.notify_top_unstable if args[:create_slack_report] == 'true'
|
|
||||||
end
|
end
|
||||||
# rubocop:enable Rails/RakeEnvironment
|
# rubocop:enable Rails/RakeEnvironment
|
||||||
|
|
|
@ -8,7 +8,7 @@ import BoardNewIssue from '~/boards/components/board_new_issue.vue';
|
||||||
import BoardNewItem from '~/boards/components/board_new_item.vue';
|
import BoardNewItem from '~/boards/components/board_new_item.vue';
|
||||||
import defaultState from '~/boards/stores/state';
|
import defaultState from '~/boards/stores/state';
|
||||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||||
import listQuery from '~/boards/graphql/board_lists_deferred.query.graphql';
|
import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
|
||||||
import {
|
import {
|
||||||
mockList,
|
mockList,
|
||||||
mockIssuesByListId,
|
mockIssuesByListId,
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||||
import { boardListQueryResponse, mockLabelList } from 'jest/boards/mock_data';
|
import { boardListQueryResponse, mockLabelList } from 'jest/boards/mock_data';
|
||||||
import BoardListHeader from '~/boards/components/board_list_header.vue';
|
import BoardListHeader from '~/boards/components/board_list_header.vue';
|
||||||
import { ListType } from '~/boards/constants';
|
import { ListType } from '~/boards/constants';
|
||||||
import listQuery from '~/boards/graphql/board_lists_deferred.query.graphql';
|
import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
|
||||||
|
|
||||||
Vue.use(VueApollo);
|
Vue.use(VueApollo);
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
|
@ -523,15 +523,6 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "missing spam_check_api_key value when spam_check_endpoint_enabled is true" do
|
|
||||||
it "returns a blank parameter error message" do
|
|
||||||
put api("/application/settings", admin), params: { spam_check_endpoint_enabled: true, spam_check_endpoint_url: "https://example.com/spam_check" }
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
|
||||||
expect(json_response['error']).to eq('spam_check_api_key is missing')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "overly long spam_check_api_key" do
|
context "overly long spam_check_api_key" do
|
||||||
it "fails to update the settings with too long spam_check_api_key" do
|
it "fails to update the settings with too long spam_check_api_key" do
|
||||||
put api("/application/settings", admin), params: { spam_check_api_key: "0123456789" * 500 }
|
put api("/application/settings", admin), params: { spam_check_api_key: "0123456789" * 500 }
|
||||||
|
|
|
@ -7,17 +7,54 @@ RSpec.describe ProtectedTags::CreateService do
|
||||||
let(:user) { project.owner }
|
let(:user) { project.owner }
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{
|
{
|
||||||
name: 'master',
|
name: name,
|
||||||
create_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }]
|
create_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
|
let(:name) { 'tag' }
|
||||||
|
|
||||||
subject(:service) { described_class.new(project, user, params) }
|
subject(:service) { described_class.new(project, user, params) }
|
||||||
|
|
||||||
it 'creates a new protected tag' do
|
it 'creates a new protected tag' do
|
||||||
expect { service.execute }.to change(ProtectedTag, :count).by(1)
|
expect { service.execute }.to change(ProtectedTag, :count).by(1)
|
||||||
expect(project.protected_tags.last.create_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
|
expect(project.protected_tags.last.create_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when name has escaped HTML' do
|
||||||
|
let(:name) { 'tag->test' }
|
||||||
|
|
||||||
|
it 'creates the new protected tag matching the unescaped version' do
|
||||||
|
expect { service.execute }.to change(ProtectedTag, :count).by(1)
|
||||||
|
expect(project.protected_tags.last.name).to eq('tag->test')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and name contains HTML tags' do
|
||||||
|
let(:name) { '<b>tag</b>' }
|
||||||
|
|
||||||
|
it 'creates the new protected tag with sanitized name' do
|
||||||
|
expect { service.execute }.to change(ProtectedTag, :count).by(1)
|
||||||
|
expect(project.protected_tags.last.name).to eq('tag')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and contains unsafe HTML' do
|
||||||
|
let(:name) { '<script>alert('foo');</script>' }
|
||||||
|
|
||||||
|
it 'does not create the new protected tag' do
|
||||||
|
expect { service.execute }.not_to change(ProtectedTag, :count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when name contains unescaped HTML tags' do
|
||||||
|
let(:name) { '<b>tag</b>' }
|
||||||
|
|
||||||
|
it 'creates the new protected tag with sanitized name' do
|
||||||
|
expect { service.execute }.to change(ProtectedTag, :count).by(1)
|
||||||
|
expect(project.protected_tags.last.name).to eq('tag')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,17 +6,50 @@ RSpec.describe ProtectedTags::UpdateService do
|
||||||
let(:protected_tag) { create(:protected_tag) }
|
let(:protected_tag) { create(:protected_tag) }
|
||||||
let(:project) { protected_tag.project }
|
let(:project) { protected_tag.project }
|
||||||
let(:user) { project.owner }
|
let(:user) { project.owner }
|
||||||
let(:params) { { name: 'new protected tag name' } }
|
let(:params) { { name: new_name } }
|
||||||
|
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
|
let(:new_name) { 'new protected tag name' }
|
||||||
|
let(:result) { service.execute(protected_tag) }
|
||||||
|
|
||||||
subject(:service) { described_class.new(project, user, params) }
|
subject(:service) { described_class.new(project, user, params) }
|
||||||
|
|
||||||
it 'updates a protected tag' do
|
it 'updates a protected tag' do
|
||||||
result = service.execute(protected_tag)
|
|
||||||
|
|
||||||
expect(result.reload.name).to eq(params[:name])
|
expect(result.reload.name).to eq(params[:name])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when name has escaped HTML' do
|
||||||
|
let(:new_name) { 'tag->test' }
|
||||||
|
|
||||||
|
it 'updates protected tag name with unescaped HTML' do
|
||||||
|
expect(result.reload.name).to eq('tag->test')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and name contains HTML tags' do
|
||||||
|
let(:new_name) { '<b>tag</b>' }
|
||||||
|
|
||||||
|
it 'updates protected tag name with sanitized name' do
|
||||||
|
expect(result.reload.name).to eq('tag')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and contains unsafe HTML' do
|
||||||
|
let(:new_name) { '<script>alert('foo');</script>' }
|
||||||
|
|
||||||
|
it 'does not update the protected tag' do
|
||||||
|
expect(result.reload.name).to eq(protected_tag.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when name contains unescaped HTML tags' do
|
||||||
|
let(:new_name) { '<b>tag</b>' }
|
||||||
|
|
||||||
|
it 'updates protected tag name with sanitized name' do
|
||||||
|
expect(result.reload.name).to eq('tag')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'without admin_project permissions' do
|
context 'without admin_project permissions' do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue