Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-12-03 12:30:24 +00:00
parent efa1bb6a1f
commit 4ea1233d2e
44 changed files with 1531 additions and 1625 deletions

View File

@ -12,7 +12,6 @@ Layout/ArrayAlignment:
- 'ee/app/models/concerns/geo/verification_state.rb'
- 'ee/app/models/dast_site_profile.rb'
- 'ee/app/models/dora/base_metric.rb'
- 'ee/app/models/ee/application_setting.rb'
- 'ee/app/models/ee/epic.rb'
- 'ee/app/models/ee/user.rb'
- 'ee/app/models/geo/event_log.rb'

View File

@ -617,7 +617,6 @@ Layout/LineLength:
- 'ee/app/models/dast/site_profile_secret_variable.rb'
- 'ee/app/models/dora/lead_time_for_changes_metric.rb'
- 'ee/app/models/ee/alert_management/alert.rb'
- 'ee/app/models/ee/application_setting.rb'
- 'ee/app/models/ee/ci/build.rb'
- 'ee/app/models/ee/ci/daily_build_group_report_result.rb'
- 'ee/app/models/ee/ci/job_artifact.rb'

View File

@ -80,7 +80,6 @@ Lint/UnusedBlockArgument:
- 'ee/app/graphql/resolvers/incident_management/escalation_policies_resolver.rb'
- 'ee/app/graphql/resolvers/incident_management/oncall_rotations_resolver.rb'
- 'ee/app/graphql/types/health_status_enum.rb'
- 'ee/app/models/ee/application_setting.rb'
- 'ee/app/models/ee/ci/build.rb'
- 'ee/app/models/ee/epic.rb'
- 'ee/app/models/ee/namespace_setting.rb'

View File

@ -3,7 +3,6 @@
RSpec/BeEq:
Exclude:
- 'ee/spec/bin/custom_ability_spec.rb'
- 'ee/spec/controllers/admin/application_settings_controller_spec.rb'
- 'ee/spec/controllers/admin/audit_log_reports_controller_spec.rb'
- 'ee/spec/controllers/concerns/ee/routable_actions/sso_enforcement_redirect_spec.rb'
- 'ee/spec/controllers/concerns/routable_actions_spec.rb'

View File

@ -1,7 +1,6 @@
---
RSpec/ContextWording:
Exclude:
- 'ee/spec/controllers/admin/application_settings_controller_spec.rb'
- 'ee/spec/controllers/admin/audit_logs_controller_spec.rb'
- 'ee/spec/controllers/admin/dev_ops_report_controller_spec.rb'
- 'ee/spec/controllers/admin/emails_controller_spec.rb'

View File

@ -2,7 +2,6 @@
# Cop supports --autocorrect.
RSpec/Dialect:
Exclude:
- 'ee/spec/controllers/admin/application_settings_controller_spec.rb'
- 'ee/spec/helpers/ee/personal_access_tokens_helper_spec.rb'
- 'ee/spec/lib/ee/gitlab/gon_helper_spec.rb'
- 'ee/spec/lib/ee/gitlab/saas_spec.rb'

View File

@ -5,7 +5,6 @@ RSpec/ReceiveMessages:
- 'ee/spec/bin/custom_ability_spec.rb'
- 'ee/spec/components/namespaces/storage/limit_alert_component_spec.rb'
- 'ee/spec/components/namespaces/storage/repository_limit_alert_component_spec.rb'
- 'ee/spec/controllers/admin/application_settings_controller_spec.rb'
- 'ee/spec/controllers/concerns/analytics/cycle_analytics/value_stream_actions_spec.rb'
- 'ee/spec/controllers/concerns/audit_events/audit_events_params_spec.rb'
- 'ee/spec/controllers/ee/search_controller_spec.rb'

View File

@ -242,7 +242,6 @@ Style/GuardClause:
- 'ee/app/models/dast/profile.rb'
- 'ee/app/models/dast_site.rb'
- 'ee/app/models/dast_site_profile.rb'
- 'ee/app/models/ee/application_setting.rb'
- 'ee/app/models/ee/epic.rb'
- 'ee/app/models/ee/group_group_link.rb'
- 'ee/app/models/ee/group_member.rb'

View File

@ -254,7 +254,6 @@ Style/IfUnlessModifier:
- 'ee/app/models/concerns/geo/verifiable_replicator.rb'
- 'ee/app/models/concerns/security/scan_execution_policy.rb'
- 'ee/app/models/dast_site_profile.rb'
- 'ee/app/models/ee/application_setting.rb'
- 'ee/app/models/ee/ci/job_artifact.rb'
- 'ee/app/models/ee/group.rb'
- 'ee/app/models/ee/issue.rb'

View File

@ -12,32 +12,12 @@ module Emails
)
end
def project_import_complete(project_id, user_id, user_mapping_enabled)
user = User.find(user_id)
@project = Project.find(project_id)
@namespace = @project.root_ancestor
@hostname = @project.import_url
@user_mapping_available = user_mapping_enabled && !@namespace.user_namespace? && !@project.import_failed?
@is_group_owner = user.can?(:admin_namespace, @namespace)
@is_project_creator = user_id == @project.creator_id
title = safe_format(
s_('Import|Import from %{hostname} completed'),
hostname: @hostname
)
email_with_layout(
to: user.notification_email_or_default,
subject: subject(title)
)
end
def bulk_import_complete(user_id, bulk_import_id)
user = User.find(user_id)
@bulk_import = BulkImport.find(bulk_import_id)
@hostname = @bulk_import.configuration.url
title = safe_format(
s_('Import|Import from %{hostname} completed'),
s_('BulkImport|Import from %{hostname} completed'),
hostname: @hostname
)

View File

@ -389,13 +389,6 @@ class NotifyPreview < ActionMailer::Preview
).message
end
def project_import_complete
project_id = ProjectImportState.last.project_id
creator_id = Project.find(project_id).creator_id
Notify.project_import_complete(project_id, creator_id, true).message
end
def bulk_import_complete
bulk_import = BulkImport.last

View File

@ -8,7 +8,7 @@ module PartitionedTable
PARTITIONING_STRATEGIES = {
daily: Gitlab::Database::Partitioning::Time::DailyStrategy,
monthly: Gitlab::Database::Partitioning::MonthlyStrategy,
monthly: Gitlab::Database::Partitioning::Time::MonthlyStrategy,
sliding_list: Gitlab::Database::Partitioning::SlidingListStrategy,
ci_sliding_list: Gitlab::Database::Partitioning::CiSlidingListStrategy,
int_range: Gitlab::Database::Partitioning::IntRangeStrategy

View File

@ -32,10 +32,6 @@ module Namespaces
Array.wrap(owner)
end
def owner_ids
owners.map(&:id)
end
def member?(user, min_access_level = Gitlab::Access::GUEST)
return false unless user

View File

@ -6,8 +6,6 @@ class ProjectImportState < ApplicationRecord
self.table_name = "project_mirror_data"
attr_accessor :user_mapping_enabled
after_commit :expire_etag_cache
belongs_to :project, inverse_of: :import_state
@ -68,10 +66,7 @@ class ProjectImportState < ApplicationRecord
end
after_transition any => [:canceled, :failed] do |state, _|
project = state.project
state.user_mapping_enabled = project.import_data&.user_mapping_enabled?
project.remove_import_data
state.project.remove_import_data
end
before_transition started: [:finished, :canceled, :failed] do |state, _|
@ -87,7 +82,6 @@ class ProjectImportState < ApplicationRecord
after_transition started: :finished do |state, _|
project = state.project
state.user_mapping_enabled = project.import_data&.user_mapping_enabled?
project.reset_cache_and_import_attrs
if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
@ -95,12 +89,6 @@ class ProjectImportState < ApplicationRecord
Projects::AfterImportWorker.perform_async(project.id)
end
end
state.send_completion_notification
end
after_transition any => [:failed] do |state, _|
state.send_completion_notification(notify_group_owners: false)
end
end
@ -159,28 +147,6 @@ class ProjectImportState < ApplicationRecord
# import? does SQL work so only run it if it looks like there's an import running
status == 'started' && project.import?
end
def send_completion_notification(notify_group_owners: true)
run_after_commit do
root_ancestor = project.root_ancestor
notified_user_ids = [project.creator_id]
notified_user_ids |= root_ancestor.owner_ids if user_mapping_enabled? && notify_group_owners
notified_user_ids.each do |user_id|
Notify.project_import_complete(project_id, user_id, user_mapping_enabled?).deliver_later
end
end
end
private
# Return whether or not user mapping was enabled during the project's import to determine who to
# send completion emails to. user_mapping_enabled should be set if import_data is removed.
# This can be removed when all 3rd party project importer user mapping feature flags are removed.
def user_mapping_enabled?
user_mapping_enabled || project.import_data&.user_mapping_enabled?
end
end
ProjectImportState.prepend_mod_with('ProjectImportState')

View File

@ -49,6 +49,7 @@ module DraftNotes
keep_around_commits(created_notes)
draft_notes.delete_all
notification_service.async.new_review(review)
todo_service.new_review(review, current_user)
MergeRequests::ResolvedDiscussionNotificationService.new(project: project, current_user: current_user).execute(merge_request)
GraphqlTriggers.merge_request_merge_status_updated(merge_request)
after_publish

View File

@ -187,6 +187,14 @@ class TodoService
create_ssh_key_todos(ssh_keys, ::Todo::SSH_KEY_EXPIRED)
end
# When a merge request receives a review
#
# * Mark all outstanding todos on this MR for the current user as done
#
def new_review(review, current_user)
resolve_todos_for_target(review.merge_request, current_user)
end
# When user marks a target as todo
def mark_todo(target, current_user)
project = target.project

View File

@ -4,13 +4,24 @@ module WorkItems
module DataSync
module Widgets
class TimeTracking < Base
def before_create
return unless target_work_item.get_widget(:time_tracking)
target_work_item.time_estimate = work_item.time_estimate
end
def after_save_commit
# copy time tracking data, e.g.
# WorkItems::CopyTimelogsWorker.perform_async(work_item_id, target_work_item_id)
return unless params[:operation] == :move
return unless target_work_item.get_widget(:time_tracking)
return if work_item.timelogs.empty?
WorkItems::CopyTimelogsWorker.perform_async(work_item.id, target_work_item.id)
end
def post_move_cleanup
# do it
work_item.timelogs.each_batch(of: BATCH_SIZE) do |timelogs|
timelogs.delete_all
end
end
end
end

View File

@ -9,10 +9,10 @@
checkbox_options: { checked: group.namespace_settings&.remove_dormant_members }
.form-group
= f.label :remove_dormant_members_period, _('Days of inactivity before removal'), class: 'label-light'
= f.number_field :remove_dormant_members_period, class: 'form-control w-auto gl-form-input', min: 90,
= f.number_field :remove_dormant_members_period, class: 'form-control w-auto gl-form-input', min: 90, max: 1827,
required: true, value: group.namespace_settings&.remove_dormant_members_period,
disabled: !group.namespace_settings&.remove_dormant_members?
#group_remove_dormant_members_period_error.form-text.gl-text-red-500.gl-hidden
= _('Please enter a value of 90 days or more')
= _('Please enter a value between 90 and 1827 days (5 years).')
.form-text.gl-text-subtle
= _('Must be 90 days or more.')

View File

@ -1,39 +0,0 @@
- header_style = 'font-size:24px; text-align:center; line-height:30px;'
- text_style = 'font-size:16px; text-align:center; line-height:24px; margin-top: 24px;'
- button_style = 'border: 1px solid #694cc0; border-radius: 4px; font-size: 14px; padding: 8px 16px; background-color: #7b58cf; color: #fff; cursor: pointer;'
- strong_tag_pair = tag_pair(tag.strong, :strong_open, :strong_close)
- start_date = l(@project.created_at.to_date, format: :long)
%h1{ style: header_style }
= s_('Import|Import completed')
%p{ style: text_style }
- if @is_project_creator
= safe_format(s_('Import|The import you started on %{start_date} from %{strong_open}%{hostname}%{strong_close} has completed.'),
strong_tag_pair,
hostname: @hostname,
start_date: start_date)
- else
= safe_format(s_('Import|The import %{project_creator_name} started on %{start_date} from %{strong_open}%{hostname}%{strong_close} has completed.'),
strong_tag_pair,
project_creator_name: sanitize_name(@project.creator.name),
hostname: @hostname,
start_date: start_date)
- if !@user_mapping_available
= s_('Import|You can now review your import results.')
- elsif @is_group_owner
= s_('Import|You can reassign contributions on the "Members" page of the group.')
- else
= s_('Import|Users with the Owner role for the group can reassign contributions on the "Members" page.')
%p{ style: text_style }
- if @user_mapping_available && @is_group_owner
= link_to group_group_members_url(@namespace, tab: 'placeholders'), target: '_blank', rel: 'noopener noreferrer' do
%button{ type: 'button', style: button_style }
= s_('Import|Reassign contributions')
- else
= link_to import_history_index_url, target: '_blank', rel: 'noopener noreferrer' do
%button{ type: 'button', style: button_style }
= s_('Import|View import results')

View File

@ -1,20 +0,0 @@
<%= s_('Import|Import completed') %>
<% if @is_project_creator %>
<%= safe_format(s_('Import|The import you started on %{start_date} from %{strong_open}%{hostname}%{strong_close} has completed.'), strong_open: '', strong_close: '', hostname: @hostname, start_date: l(@project.created_at.to_date, format: :long)) -%>
<% else %>
<%= safe_format(s_('Import|The import %{project_creator_name} started on %{start_date} from %{strong_open}%{hostname}%{strong_close} has completed.'), strong_open: '', strong_close: '', hostname: @hostname, project_creator_name: sanitize_name(@project.creator.name), start_date: l(@project.created_at.to_date, format: :long)) -%>
<% end %>
<% if !@user_mapping_available %>
<%= s_('Import|You can now review your import results.') %>
<% elsif @is_group_owner %>
<%= s_('Import|You can reassign contributions on the "Members" page of the group.') %>
<% else %>
<%= s_('Import|Users with the Owner role for the group can reassign contributions on the "Members" page.') %>
<% end %>
<% if @user_mapping_available && @is_group_owner %>
<%= s_('Import|Reassign contributions') %>: <%= group_group_members_url(@namespace, tab: 'placeholders') %>
<% else %>
<%= s_('Import|View import results') %>: <%= import_history_index_url %>
<% end %>

View File

@ -0,0 +1,97 @@
---
stage: AI-powered
group: AI Framework
description: AI-powered features and functionality.
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# Get started with GitLab Duo
GitLab Duo is your AI-powered assistant. It can help you write, review, and edit code,
along with a variety of other tasks throughout your GitLab workflow.
It can help you troubleshoot your pipeline, write tests, address vulnerabilities, and more.
## Step 1: Ensure you have a subscription
Your organization has purchased a GitLab Duo add-on subscription: Either Duo Pro or Duo Enterprise.
Each subscription includes a set of AI-powered features to help improve your workflow.
After your organization purchases a subscription, an administrator must assign seats to users.
You likely received an email that notified you of your seat.
The AI-powered features you have access to use large language models (LLMs) to help streamline
your workflow. If you're on self-managed GitLab, your administrator can choose to use
GitLab models, or self-host their own models.
If you have issues accessing GitLab Duo features, ask your administrator.
They can check the health of the installation.
For more information, see:
- [Assign seats to users](../../subscriptions/subscription-add-ons.md#assign-gitlab-duo-seats).
- [Features included in Duo Pro and Duo Enterprise](https://about.gitlab.com/gitlab-duo/#pricing).
- [List of GitLab Duo features and their LLMs](../gitlab_duo/index.md).
- [Self-hosted models](../../administration/self_hosted_models/index.md).
- [Health check details](../gitlab_duo/turn_on_off.md#run-a-health-check-for-gitlab-duo).
## Step 2: Try Duo Chat in the UI
Confirm that Duo Chat is available in the GitLab UI.
Go to a project and in the upper-right corner, a button named **Ask Duo Chat** should be displayed.
If this button is available, it means everything is configured properly.
Try asking Duo Chat a question or type `/` to see a list of slash commands.
For more information, see:
- [GitLab Duo Chat](../gitlab_duo_chat/index.md).
## Step 3: Try other GitLab Duo features
GitLab Duo is available at different points in your workflow. From troubleshooting
CI/CD pipelines to writing test cases and reviewing code, GitLab Duo can help you
in a variety of ways.
If you want to test a feature, you can go to a failed CI/CD job and at the bottom
of the page, select **Troubleshoot**.
Or, in an issue that has a lot of comments, in the **Activity** section, select **View summary**.
GitLab Duo summarizes the contents of the issue.
For more information, see:
- [The complete list of GitLab Duo features](../gitlab_duo/index.md).
- [Turn on GitLab Duo features that are still in development](../gitlab_duo/turn_on_off.md#turn-on-beta-and-experimental-features).
## Step 4: Prepare to use GitLab Duo in your IDE
To use GitLab Duo, including Code Suggestions, in your IDE, you must:
- Install an extension in your IDE.
- Authenticate with GitLab from the IDE. You can use either OAuth or a personal access token.
Then you can confirm that GitLab Duo is available in your IDE and test some of the features.
For more information, see:
- [Set up the extension for VS Code](../../editor_extensions/visual_studio_code/index.md#set-up-the-gitlab-workflow-extension).
- [Set up the extension for JetBrains](../../editor_extensions/jetbrains_ide/index.md#download-the-extension).
- [Set up the extension for Visual Studio](../../editor_extensions/visual_studio/index.md).
- [Set up the extension for Neovim](../../editor_extensions/neovim/index.md#install-the-extension).
## Step 5: Turn on Code Suggestions in your IDE
Finally, go to the settings for the extension and confirm that Code Suggestions is enabled,
as well as the languages you want suggestions for.
You should also confirm that Duo Chat is enabled.
Then test Code Suggestions and Duo Chat in your IDE.
- Code Suggestions recommends code as you type.
- Duo Chat is available to ask questions about your code or anything else you need.
For more information, see:
- [Supported extensions and languages](../project/repository/code_suggestions/supported_extensions.md).
- [Code Suggestions](../project/repository/code_suggestions/index.md#use-code-suggestions).

View File

@ -17,12 +17,13 @@ These features aim to help increase velocity and solve key pain points across th
GitLab Duo features are available in [IDE extensions](../../editor_extensions/index.md) and the GitLab UI.
Some features are also available as part of [GitLab Duo Chat](../gitlab_duo_chat_examples.md).
- [Get started with GitLab Duo](../get_started/getting_started_gitlab_duo.md).
- [View a walkthrough of GitLab Duo Enterprise features](https://gitlab.navattic.com/duo-enterprise).
GitLab is [transparent](https://handbook.gitlab.com/handbook/values/#transparency).
As GitLab Duo features mature, the documentation will be updated to clearly state
how and where you can access these features.
**{play}** [View a walkthrough of GitLab Duo Enterprise features](https://gitlab.navattic.com/duo-enterprise).
## Generally available features
### GitLab Duo Chat

View File

@ -1,50 +1,13 @@
---
stage: AI-powered
group: AI Framework
description: AI-powered features and functionality.
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
ignore_in_report: true
redirect_to: '../get_started/getting_started_gitlab_duo.md'
remove_date: '2025-02-21'
---
# Get started with GitLab Duo Code Suggestions
<!-- markdownlint-disable -->
GitLab Duo is an AI-powered enhancement integrated into the GitLab DevSecOps platform, designed to boost productivity and efficiency. It leverages advanced large language models (LLMs) from Google Vertex AI and Anthropic Claude to provide various AI-driven features. These features operate independently, ensuring that the core functionality of GitLab remains unaffected even if an AI feature encounters issues.
This document was moved to [another location](../get_started/getting_started_gitlab_duo.md).
GitLab Duo Code Suggestions helps you write code more efficiently by using generative AI to suggest code while youre developing.
## Step 1: Understand how Code Suggestions works
Code Suggestions is made up of code completion and code generation. These are two distinct features that help developers write code more efficiently:
| | Code completion | Code generation |
| :---- | :---- | :---- |
| Purpose | Provides suggestions for completing the current line of code. | Generates new code based on a natural language comment. |
| Trigger | Triggers when typing, usually with a short delay. | Triggers when pressing Enter after writing a comment that includes specific keywords. |
| Scope | Limited to the current line or small block of code. | Can generate entire methods, functions, or even classes based on the context. |
| Accuracy | More accurate for simple tasks and short blocks of code. | Can be more accurate for complex tasks and large blocks of code due to its ability to analyze context and use a larger language model. |
| When to use | Use code completion for quick tasks, small code snippets, or when you need a slight boost in productivity. | Use code generation for more complex tasks, larger codebases, or when you want to write new code from scratch based on a natural language description. |
## Step 2: Supported languages, text editors and IDEs
To ensure your preferred tooling is supported, review the:
[Supported languages](../project/repository/code_suggestions/supported_extensions.md#supported-languages)
[Supported editors](../project/repository/code_suggestions/supported_extensions.md#supported-editor-extensions)
## Step 3: Enable Code Suggestions
First, [purchase seats for GitLab Duo](../../subscriptions/subscription-add-ons.md#purchase-gitlab-duo).
Then, assign seats to users to grant access to GitLab Duo for:
- [Self-managed](../../subscriptions/subscription-add-ons.md#for-self-managed)
- [Dedicated](../../subscriptions/subscription-add-ons.md#for-self-managed)
- [GitLab.com](../../subscriptions/subscription-add-ons.md#for-gitlabcom-1)
## Step 4: Using Code Suggestions
Follow the documentation to [use Code Suggestions](../project/repository/code_suggestions/index.md#use-code-suggestions).
## Step 5: Troubleshooting common challenges
[Troubleshoot](../project/repository/code_suggestions/troubleshooting.md) commonly faced challenges.
<!-- This redirect file can be deleted after <2025-02-21>. -->
<!-- Redirects that point to other docs in the same project expire in three months. -->
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->

View File

@ -1,119 +0,0 @@
# frozen_string_literal: true
module Gitlab
module Database
module Partitioning
class MonthlyStrategy
attr_reader :model, :partitioning_key, :retain_for, :retain_non_empty_partitions, :analyze_interval
# We create this many partitions in the future
HEADROOM = 6.months
delegate :table_name, to: :model
def initialize(
model, partitioning_key, retain_for: nil, retain_non_empty_partitions: false,
analyze_interval: nil)
@model = model
@partitioning_key = partitioning_key
@retain_for = retain_for
@retain_non_empty_partitions = retain_non_empty_partitions
@analyze_interval = analyze_interval
end
def current_partitions
Gitlab::Database::PostgresPartition.for_parent_table(table_name).map do |partition|
TimePartition.from_sql(table_name, partition.name, partition.condition)
end
end
# Check the currently existing partitions and determine which ones are missing
def missing_partitions
desired_partitions - current_partitions
end
def extra_partitions
partitions = current_partitions - desired_partitions
partitions.reject!(&:holds_data?) if retain_non_empty_partitions
partitions
end
def after_adding_partitions
# No-op, required by the partition manager
end
def validate_and_fix
# No-op, required by the partition manager
end
def partition_name(lower_bound)
suffix = lower_bound&.strftime('%Y%m') || '000000'
"#{table_name}_#{suffix}"
end
private
def desired_partitions
[].tap do |parts|
min_date, max_date = relevant_range
if pruning_old_partitions? && min_date <= oldest_active_date
min_date = oldest_active_date.beginning_of_month
else
parts << partition_for(upper_bound: min_date)
end
while min_date < max_date
next_date = min_date.next_month
parts << partition_for(lower_bound: min_date, upper_bound: next_date)
min_date = next_date
end
end
end
# This determines the relevant time range for which we expect to have data
# (and therefore need to create partitions for).
#
# Note: We typically expect the first partition to be half-unbounded, i.e.
# to start from MINVALUE to a specific date `x`. The range returned
# does not include the range of the first, half-unbounded partition.
def relevant_range
if (first_partition = current_partitions.min)
# Case 1: First partition starts with MINVALUE, i.e. from is nil -> start with first real partition
# Case 2: Rather unexpectedly, first partition does not start with MINVALUE, i.e. from is not nil
# In this case, use first partition beginning as a start
min_date = first_partition.from || first_partition.to
end
if pruning_old_partitions?
min_date ||= oldest_active_date
end
# In case we don't have a partition yet
min_date ||= Date.today
min_date = min_date.beginning_of_month
max_date = Date.today.end_of_month + HEADROOM
[min_date, max_date]
end
def partition_for(upper_bound:, lower_bound: nil)
TimePartition.new(table_name, lower_bound, upper_bound, partition_name: partition_name(lower_bound))
end
def pruning_old_partitions?
retain_for.present?
end
def oldest_active_date
(Date.today - retain_for).beginning_of_month
end
end
end
end
end

View File

@ -0,0 +1,89 @@
# frozen_string_literal: true
module Gitlab
module Database
module Partitioning
module Time
class MonthlyStrategy < BaseStrategy
HEADROOM = 6.months
PARTITION_SUFFIX = '%Y%m'
def current_partitions
Gitlab::Database::PostgresPartition.for_parent_table(table_name).map do |partition|
TimePartition.from_sql(table_name, partition.name, partition.condition)
end
end
# Check the currently existing partitions and determine which ones are missing
def missing_partitions
desired_partitions - current_partitions
end
def extra_partitions
partitions = current_partitions - desired_partitions
partitions.reject!(&:holds_data?) if retain_non_empty_partitions
partitions
end
def partition_name(lower_bound)
suffix = lower_bound&.strftime(PARTITION_SUFFIX) || '000000'
"#{table_name}_#{suffix}"
end
def desired_partitions
[].tap do |parts|
min_date, max_date = relevant_range
if pruning_old_partitions? && min_date <= oldest_active_date
min_date = oldest_active_date.beginning_of_month
else
parts << partition_for(upper_bound: min_date)
end
while min_date < max_date
next_date = min_date.next_month
parts << partition_for(lower_bound: min_date, upper_bound: next_date)
min_date = next_date
end
end
end
# This determines the relevant time range for which we expect to have data
# (and therefore need to create partitions for).
#
# Note: We typically expect the first partition to be half-unbounded, i.e.
# to start from MINVALUE to a specific date `x`. The range returned
# does not include the range of the first, half-unbounded partition.
def relevant_range
first_partition = current_partitions.min
if first_partition
# Case 1: First partition starts with MINVALUE, i.e. from is nil -> start with first real partition
# Case 2: Rather unexpectedly, first partition does not start with MINVALUE, i.e. from is not nil
# In this case, use first partition beginning as a start
min_date = first_partition.from || first_partition.to
end
min_date ||= oldest_active_date if pruning_old_partitions?
# In case we don't have a partition yet
min_date ||= Date.current
min_date = min_date.beginning_of_month
max_date = Date.current.end_of_month + HEADROOM
[min_date, max_date]
end
def oldest_active_date
retain_for.ago.beginning_of_month.to_date
end
end
end
end
end
end

View File

@ -10251,6 +10251,9 @@ msgstr ""
msgid "BulkImport|Import failed: Destination cannot be a subgroup of the source group. Change the destination and try again."
msgstr ""
msgid "BulkImport|Import from %{hostname} completed"
msgstr ""
msgid "BulkImport|Import groups by direct transfer"
msgstr ""
@ -28383,12 +28386,6 @@ msgstr ""
msgid "Import|GitHub import details"
msgstr ""
msgid "Import|Import completed"
msgstr ""
msgid "Import|Import from %{hostname} completed"
msgstr ""
msgid "Import|Import source user has an invalid status for this operation"
msgstr ""
@ -28422,18 +28419,9 @@ msgstr ""
msgid "Import|Partially completed"
msgstr ""
msgid "Import|Reassign contributions"
msgstr ""
msgid "Import|Show errors"
msgstr ""
msgid "Import|The import %{project_creator_name} started on %{start_date} from %{strong_open}%{hostname}%{strong_close} has completed."
msgstr ""
msgid "Import|The import you started on %{start_date} from %{strong_open}%{hostname}%{strong_close} has completed."
msgstr ""
msgid "Import|The repository could not be imported."
msgstr ""
@ -28449,21 +28437,9 @@ msgstr ""
msgid "Import|Timeout for decompressing archived files."
msgstr ""
msgid "Import|Users with the Owner role for the group can reassign contributions on the \"Members\" page."
msgstr ""
msgid "Import|View import results"
msgstr ""
msgid "Import|You can import a Subversion repository by using third-party tools. %{svn_link}."
msgstr ""
msgid "Import|You can now review your import results."
msgstr ""
msgid "Import|You can reassign contributions on the \"Members\" page of the group."
msgstr ""
msgid "Import|You do not have permission to view import source users for this namespace"
msgstr ""
@ -41226,6 +41202,9 @@ msgstr ""
msgid "Please enter a valid time interval"
msgstr ""
msgid "Please enter a value between 90 and 1827 days (5 years)."
msgstr ""
msgid "Please enter a value of 90 days or more"
msgstr ""

View File

@ -347,8 +347,6 @@ spec/frontend/vue_shared/components/file_finder/index_spec.js
spec/frontend/vue_shared/components/file_tree_spec.js
spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
spec/frontend/vue_shared/components/filtered_search_bar/tokens/date_token_spec.js
spec/frontend/vue_shared/components/list_selector/group_item_spec.js
spec/frontend/vue_shared/components/list_selector/user_item_spec.js
spec/frontend/vue_shared/components/markdown/header_spec.js
spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js

View File

@ -1,728 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe "Admin Runners", :freeze_time, feature_category: :fleet_visibility do
include Features::SortingHelpers
include Features::RunnersHelpers
include Spec::Support::Helpers::ModalHelpers
let_it_be(:admin) { create(:admin) }
before do
sign_in(admin)
enable_admin_mode!(admin)
end
describe "Admin Runners page", :js do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:namespace) { create(:namespace) }
let_it_be(:project) { create(:project, namespace: namespace, creator: user) }
describe "runners creation and registration" do
before do
visit admin_runners_path
end
it 'shows a create button' do
expect(page).to have_link s_('Runner|New instance runner'), href: new_admin_runner_path
end
it_behaves_like "shows and resets runner registration token" do
let(:dropdown_text) { s_('Runners|Register an instance runner') }
let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token }
end
end
context "when there are runners" do
context "with an instance runner" do
let_it_be(:instance_runner) { create(:ci_runner, :instance) }
before do
visit admin_runners_path
end
it_behaves_like 'shows runner summary and navigates to details' do
let(:runner) { instance_runner }
let(:runner_page_path) { admin_runner_path(instance_runner) }
end
it_behaves_like 'pauses, resumes and deletes a runner' do
let(:runner) { instance_runner }
end
it 'shows an instance badge' do
within_runner_row(instance_runner.id) do
expect(page).to have_selector '.badge', text: s_('Runners|Instance')
end
end
end
context "with multiple runners" do
before do
create(:ci_runner, :instance, :almost_offline)
create(:ci_runner, :instance, :offline)
create(:ci_runner, :instance, :stale)
visit admin_runners_path
end
it 'has all necessary texts' do
expect(page).to have_text "#{s_('Runners|All')} 3"
expect(page).to have_text "#{s_('Runners|Online')} 1"
expect(page).to have_text "#{s_('Runners|Offline')} 2"
expect(page).to have_text "#{s_('Runners|Stale')} 1"
end
it_behaves_like 'deletes runners in bulk' do
let(:runner_count) { '3' }
end
end
it 'shows a job count' do
runner = create(:ci_runner, :project, projects: [project])
create_list(:ci_build, 2, runner: runner)
visit admin_runners_path
within_runner_row(runner.id) do
expect(find_by_testid('job-count')).to have_content '2'
end
end
it 'shows an Active status badge that links to jobs tab' do
runner = create(:ci_runner, :project, projects: [project])
job = create(:ci_build, :running, runner: runner)
visit admin_runners_path
within_runner_row(runner.id) do
click_on(s_('Runners|Active'))
end
expect(current_url).to match(admin_runner_path(runner))
expect(find_by_testid('td-status')).to have_content "Running"
expect(find_by_testid('td-job')).to have_content "##{job.id}"
end
describe 'search' do
before_all do
create(:ci_runner, :instance, description: 'runner foo')
create(:ci_runner, :instance, description: 'runner bar')
end
before do
visit admin_runners_path
end
it 'runner types tabs have total counts and can be selected' do
expect(page).to have_link('All 2')
expect(page).to have_link('Instance 2')
expect(page).to have_link('Group 0')
expect(page).to have_link('Project 0')
end
it 'shows runners' do
expect(page).to have_content("runner foo")
expect(page).to have_content("runner bar")
end
it 'shows correct runner when description matches' do
input_filtered_search_keys('runner foo')
expect(page).to have_link('All 1')
expect(page).to have_link('Instance 1')
expect(page).to have_content("runner foo")
expect(page).not_to have_content("runner bar")
end
context 'when description does not match' do
before do
input_filtered_search_keys('runner baz')
end
it_behaves_like 'shows no runners found'
it 'shows no runner' do
expect(page).to have_link('All 0')
expect(page).to have_link('Instance 0')
end
end
end
describe 'filter by paused' do
before_all do
create(:ci_runner, :instance, description: 'runner-active')
create(:ci_runner, :instance, :paused, description: 'runner-paused')
end
before do
visit admin_runners_path
end
it 'shows all runners' do
expect(page).to have_link('All 2')
expect(page).to have_content 'runner-active'
expect(page).to have_content 'runner-paused'
end
it 'shows paused runners' do
input_filtered_search_filter_is_only(s_('Runners|Paused'), 'Yes')
expect(page).to have_link('All 1')
expect(page).not_to have_content 'runner-active'
expect(page).to have_content 'runner-paused'
end
it 'shows active runners' do
input_filtered_search_filter_is_only(s_('Runners|Paused'), 'No')
expect(page).to have_link('All 1')
expect(page).to have_content 'runner-active'
expect(page).not_to have_content 'runner-paused'
end
end
describe 'filter by version prefix' do
before_all do
runner_v15 = create(:ci_runner, :instance, description: 'runner-v15')
runner_v14 = create(:ci_runner, :instance, description: 'runner-v14')
create(:ci_runner_machine, runner: runner_v15, version: '15.0.0')
create(:ci_runner_machine, runner: runner_v14, version: '14.0.0')
end
before do
visit admin_runners_path
end
it 'shows all runners' do
expect(page).to have_link('All 2')
expect(page).to have_content 'runner-v15'
expect(page).to have_content 'runner-v14'
end
it 'shows filtered runner based on supplied prefix' do
input_filtered_search_filter_is_only(s_('Runners|Version starts with'), '15.0')
expect(page).to have_link('All 1')
expect(page).not_to have_content 'runner-v14'
expect(page).to have_content 'runner-v15'
end
end
describe 'filter by creator' do
before_all do
create(:ci_runner, :instance, description: 'runner-creator-admin', creator: admin)
create(:ci_runner, :instance, description: 'runner-creator-user', creator: user)
end
before do
visit admin_runners_path
end
it 'shows all runners' do
expect(page).to have_link('All 2')
expect(page).to have_content 'runner-creator-admin'
expect(page).to have_content 'runner-creator-user'
end
it 'shows filtered runner based on creator' do
input_filtered_search_filter_is_only(s_('Runners|Creator'), admin.username)
expect(page).to have_link('All 1')
expect(page).to have_content 'runner-creator-admin'
expect(page).not_to have_content 'runner-creator-user'
end
it 'shows filtered search suggestions' do
open_filtered_search_suggestions(s_('Runners|Creator'))
page.within(search_bar_selector) do
expect(page).to have_content admin.username
expect(page).to have_content user.username
end
end
end
describe 'filter by status' do
before_all do
freeze_time # Freeze time before `let_it_be` runs, so that runner statuses are frozen during execution
create(:ci_runner, :instance, :online, description: 'runner-1')
create(:ci_runner, :instance, :almost_offline, description: 'runner-2')
create(:ci_runner, :instance, :contacted_within_stale_deadline, description: 'runner-offline')
end
after :all do
unfreeze_time
end
let_it_be(:never_contacted) do
create(:ci_runner, :instance, :unregistered, description: 'runner-never-contacted')
end
before do
visit admin_runners_path
end
it 'shows all runners' do
expect(page).to have_link('All 4')
expect(page).to have_content 'runner-1'
expect(page).to have_content 'runner-2'
expect(page).to have_content 'runner-offline'
expect(page).to have_content 'runner-never-contacted'
end
it 'shows correct runner when status matches' do
input_filtered_search_filter_is_only('Status', s_('Runners|Online'))
expect(page).to have_link('All 2')
expect(page).to have_content 'runner-1'
expect(page).to have_content 'runner-2'
expect(page).not_to have_content 'runner-offline'
expect(page).not_to have_content 'runner-never-contacted'
end
it 'shows correct runner when status is selected and search term is entered' do
input_filtered_search_filter_is_only('Status', s_('Runners|Online'))
input_filtered_search_keys('runner-1')
expect(page).to have_link('All 1')
expect(page).to have_content 'runner-1'
expect(page).not_to have_content 'runner-2'
expect(page).not_to have_content 'runner-offline'
expect(page).not_to have_content 'runner-never-contacted'
end
it 'shows correct runner when status filter is entered' do
# use the string "Never" to avoid using space and trigger an early selection
input_filtered_search_filter_is_only('Status', 'Never')
expect(page).to have_link('All 1')
expect(page).not_to have_content 'runner-1'
expect(page).not_to have_content 'runner-2'
expect(page).not_to have_content 'runner-paused'
expect(page).to have_content 'runner-never-contacted'
within_runner_row(never_contacted.id) do
expect(page).to have_selector '.badge', text: s_('Runners|Never contacted')
end
end
context 'when status does not match' do
before do
input_filtered_search_filter_is_only('Status', 'Stale')
end
it_behaves_like 'shows no runners found'
it 'shows no runner' do
expect(page).to have_link('All 0')
end
end
end
describe 'filter by type' do
before_all do
create(:ci_runner, :project, description: 'runner-project', projects: [project])
create(:ci_runner, :group, description: 'runner-group', groups: [group])
end
it '"All" tab is selected by default' do
visit admin_runners_path
expect(page).to have_link('All 2')
expect(page).to have_link('Group 1')
expect(page).to have_link('Project 1')
within_testid('runner-type-tabs') do
expect(page).to have_link('All', class: 'active')
end
end
it 'shows correct runner when type matches' do
visit admin_runners_path
expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-group'
within_testid('runner-type-tabs') do
click_on('Project')
expect(page).to have_link('Project', class: 'active')
end
expect(page).to have_content 'runner-project'
expect(page).not_to have_content 'runner-group'
end
it 'show the same counts after selecting another tab' do
visit admin_runners_path
within_testid('runner-type-tabs') do
click_on('Project')
expect(page).to have_link('All 2')
expect(page).to have_link('Group 1')
expect(page).to have_link('Project 1')
end
end
it 'shows correct runner when type is selected and search term is entered' do
create(:ci_runner, :project, description: 'runner-2-project', projects: [project])
visit admin_runners_path
within_testid('runner-type-tabs') do
click_on 'Project'
end
expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-2-project'
expect(page).not_to have_content 'runner-group'
input_filtered_search_keys('runner-project')
expect(page).to have_content 'runner-project'
expect(page).not_to have_content 'runner-2-project'
expect(page).not_to have_content 'runner-group'
end
it 'maintains the same filter when switching between runner types' do
create(:ci_runner, :project, :paused, description: 'runner-paused-project', projects: [project])
visit admin_runners_path
input_filtered_search_filter_is_only(s_('Runners|Paused'), 'No')
expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-group'
expect(page).not_to have_content 'runner-paused-project'
within_testid('runner-type-tabs') do
click_on 'Project'
end
expect(page).to have_content 'runner-project'
expect(page).not_to have_content 'runner-group'
expect(page).not_to have_content 'runner-paused-project'
end
context 'when type does not match' do
before do
visit admin_runners_path
within_testid('runner-type-tabs') do
click_on 'Instance'
end
end
it_behaves_like 'shows no runners found'
it 'shows active tab' do
expect(page).to have_link('Instance', class: 'active')
end
end
end
describe 'filter by tag' do
let_it_be(:runner_1) { create(:ci_runner, :instance, description: 'runner-blue', tag_list: ['blue']) }
let_it_be(:runner_2) { create(:ci_runner, :instance, description: 'runner-2-blue', tag_list: ['blue']) }
let_it_be(:runner_3) { create(:ci_runner, :instance, description: 'runner-red', tag_list: ['red']) }
before do
visit admin_runners_path
end
it 'shows tags suggestions' do
open_filtered_search_suggestions('Tags')
page.within(search_bar_selector) do
expect(page).to have_content 'blue'
expect(page).to have_content 'red'
end
end
it_behaves_like 'filters by tag' do
let(:tag) { 'blue' }
let(:found_runner) { runner_1.description }
let(:missing_runner) { runner_3.description }
end
context 'when tag does not match' do
before do
input_filtered_search_filter_is_only('Tags', 'green')
end
it_behaves_like 'shows no runners found'
end
it 'shows correct runner when tag is selected and search term is entered' do
input_filtered_search_filter_is_only('Tags', 'blue')
expect(page).to have_content 'runner-blue'
expect(page).to have_content 'runner-2-blue'
expect(page).not_to have_content 'runner-red'
input_filtered_search_keys('runner-2-blue')
expect(page).to have_content 'runner-2-blue'
expect(page).not_to have_content 'runner-blue'
expect(page).not_to have_content 'runner-red'
end
end
it 'sorts by last contact date' do
create(:ci_runner, :instance, description: 'runner-1', contacted_at: '2018-07-12')
create(:ci_runner, :instance, description: 'runner-2', contacted_at: '2018-07-13')
visit admin_runners_path
within_testid('runner-list') do
within('tbody tr:nth-child(1)') do
expect(page).to have_content 'runner-2'
end
within('tbody tr:nth-child(2)') do
expect(page).to have_content 'runner-1'
end
end
pajamas_sort_by 'Last contact', from: 'Created date'
click_on 'Sort direction: Descending'
within_testid('runner-list') do
within('tbody tr:nth-child(1)') do
expect(page).to have_content 'runner-1'
end
within('tbody tr:nth-child(2)') do
expect(page).to have_content 'runner-2'
end
end
end
end
context "when there are no runners" do
before do
visit admin_runners_path
end
it_behaves_like 'shows no runners registered'
it 'shows tabs with total counts equal to 0' do
aggregate_failures do
expect(page).to have_link('All 0')
expect(page).to have_link('Instance 0')
expect(page).to have_link('Group 0')
expect(page).to have_link('Project 0')
end
end
end
context "when visiting outdated URLs" do
it 'updates ACTIVE runner status to paused=false' do
visit admin_runners_path('status[]': 'ACTIVE')
expect(page).to have_current_path(admin_runners_path('paused[]': 'false'))
end
it 'updates PAUSED runner status to paused=true' do
visit admin_runners_path('status[]': 'PAUSED')
expect(page).to have_current_path(admin_runners_path('paused[]': 'true'))
end
end
end
describe "Runner create page", :js do
before do
visit new_admin_runner_path
end
it_behaves_like 'creates runner and shows register page' do
let(:register_path_pattern) { register_admin_runner_path('.*') }
end
end
describe "Runner show page", :js do
let_it_be(:runner) { create(:ci_runner, description: 'runner-foo', tag_list: ['tag1']) }
let_it_be(:runner_job) { create(:ci_build, runner: runner) }
before do
visit admin_runner_path(runner)
end
describe 'runner show page breadcrumbs' do
it 'contains the current runner id and token' do
within_testid('breadcrumb-links') do
expect(find('li:last-of-type')).to have_link("##{runner.id} (#{runner.short_sha})")
end
end
end
it 'shows runner details' do
aggregate_failures do
expect(page).to have_content 'Description runner-foo'
expect(page).to have_content 'Last contact Never contacted'
expect(page).to have_content 'Configuration Runs untagged jobs'
expect(page).to have_content 'Maximum job timeout None'
expect(page).to have_content 'Tags tag1'
end
end
it_behaves_like 'shows runner jobs tab' do
let(:job_count) { '1' }
let(:job) { runner_job }
end
describe 'when a runner is deleted' do
before do
click_on 'Delete runner'
within_modal do
click_on 'Permanently delete runner'
end
end
it 'deletes runner and redirects to runner list' do
expect(find_by_testid('alert-success')).to have_content('deleted')
expect(current_url).to match(admin_runners_path)
end
end
end
describe "Runner edit page", :js do
let_it_be(:project1) { create(:project) }
let_it_be(:project2) { create(:project, organization: project1.organization) }
let_it_be(:project_runner) { create(:ci_runner, :project, :unregistered, projects: [create(:project)]) }
before do
visit edit_admin_runner_path(project_runner)
end
it_behaves_like 'submits edit runner form' do
let(:runner) { project_runner }
let(:runner_page_path) { admin_runner_path(project_runner) }
end
it_behaves_like 'shows locked field'
describe 'breadcrumbs' do
it 'contains the current runner id and token' do
within_testid('breadcrumb-links') do
expect(page).to have_link("##{project_runner.id} (#{project_runner.short_sha})")
expect(find('li:last-of-type')).to have_content("Edit")
end
end
end
describe 'runner header', :js do
it 'contains the runner status, type and id' do
expect(page).to have_content(
"##{project_runner.id} (#{project_runner.short_sha}) #{s_('Runners|Never contacted')} Project Created"
)
end
end
context 'when a runner is updated', :js do
before do
click_on _('Save changes')
end
it 'show success alert and redirects to runner page' do
expect(current_url).to match(admin_runner_path(project_runner))
expect(find_by_testid('alert-success')).to have_content('saved')
end
end
describe 'projects' do
it 'contains project names' do
expect(page).to have_content(project1.full_name)
expect(page).to have_content(project2.full_name)
end
end
describe 'search' do
before do
search_form = find('#runner-projects-search')
search_form.fill_in 'search', with: project1.name
search_form.click_button 'Search'
end
it 'contains name of correct project' do
expect(page).to have_content(project1.full_name)
expect(page).not_to have_content(project2.full_name)
end
end
describe 'enable/create' do
shared_examples 'assignable runner' do
it 'enables a runner for a project' do
within_testid('unassigned-projects') do
within('tr', text: project2.full_name) do
click_on 'Enable'
end
end
assigned_project = find_by_testid('assigned-projects')
expect(page).to have_content('Runner assigned to project.')
expect(assigned_project).to have_content(project2.name)
end
end
context 'with project runner' do
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project1]) }
before do
visit edit_admin_runner_path(project_runner)
end
it_behaves_like 'assignable runner'
end
context 'with locked runner' do
let_it_be(:locked_runner) { create(:ci_runner, :project, projects: [project1], locked: true) }
before do
visit edit_admin_runner_path(locked_runner)
end
it_behaves_like 'assignable runner'
end
end
describe 'disable/destroy' do
let_it_be(:runner) { create(:ci_runner, :project, projects: [project1]) }
before do
visit edit_admin_runner_path(runner)
end
it 'removed project runner from project' do
within_testid('assigned-projects') do
click_on 'Disable'
end
new_runner_project = find_by_testid('unassigned-projects')
expect(page).to have_content('Runner unassigned from project.')
expect(new_runner_project).to have_content(project1.name)
end
end
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe "Admin creates an instance runner", :js, feature_category: :fleet_visibility do
let_it_be(:admin) { create(:admin) }
before do
sign_in(admin)
enable_admin_mode!(admin)
end
describe "admin runners page" do
before do
visit admin_runners_path
end
it 'displays a create button' do
expect(page).to have_link s_('Runner|New instance runner'), href: new_admin_runner_path
end
it_behaves_like "shows and resets runner registration token" do
let(:dropdown_text) { s_('Runners|Register an instance runner') }
let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token }
end
end
describe "create runner" do
before do
visit new_admin_runner_path
end
it_behaves_like 'creates runner and shows register page' do
let(:register_path_pattern) { register_admin_runner_path('.*') }
end
end
end

View File

@ -0,0 +1,182 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe "Admin manages runner in admin section", :js, feature_category: :fleet_visibility do
include Spec::Support::Helpers::ModalHelpers
let_it_be(:admin) { create(:admin) }
before do
sign_in(admin)
enable_admin_mode!(admin)
end
describe 'shows runner' do
let_it_be(:runner) { create(:ci_runner, description: 'runner-foo', tag_list: ['tag1']) }
let_it_be(:runner_job) { create(:ci_build, runner: runner) }
before do
visit admin_runner_path(runner)
end
describe 'runner show page breadcrumbs' do
it 'contains the current runner id and token' do
within_testid('breadcrumb-links') do
expect(find('li:last-of-type')).to have_link("##{runner.id} (#{runner.short_sha})")
end
end
end
it 'shows runner details' do
aggregate_failures do
expect(page).to have_content 'Description runner-foo'
expect(page).to have_content 'Last contact Never contacted'
expect(page).to have_content 'Configuration Runs untagged jobs'
expect(page).to have_content 'Maximum job timeout None'
expect(page).to have_content 'Tags tag1'
end
end
it_behaves_like 'shows runner jobs tab' do
let(:job_count) { '1' }
let(:job) { runner_job }
end
describe 'when a runner is deleted' do
before do
click_on 'Delete runner'
within_modal do
click_on 'Permanently delete runner'
end
end
it 'deletes runner and redirects to runner list' do
expect(find_by_testid('alert-success')).to have_content('deleted')
expect(current_url).to match(admin_runners_path)
end
end
end
describe "edits runner" do
let_it_be(:project1) { create(:project) }
let_it_be(:project2) { create(:project, organization: project1.organization) }
let_it_be(:project_runner) { create(:ci_runner, :project, :unregistered, projects: [create(:project)]) }
before do
visit edit_admin_runner_path(project_runner)
end
it_behaves_like 'submits edit runner form' do
let(:runner) { project_runner }
let(:runner_page_path) { admin_runner_path(project_runner) }
end
it_behaves_like 'shows locked field'
describe 'breadcrumbs' do
it 'contains the current runner id and token' do
within_testid('breadcrumb-links') do
expect(page).to have_link("##{project_runner.id} (#{project_runner.short_sha})")
expect(find('li:last-of-type')).to have_content("Edit")
end
end
end
describe 'runner header' do
it 'contains the runner status, type and id' do
expect(page).to have_content(
"##{project_runner.id} (#{project_runner.short_sha}) #{s_('Runners|Never contacted')} Project Created"
)
end
end
context 'when a runner is updated' do
before do
click_on _('Save changes')
end
it 'show success alert and redirects to runner page' do
expect(current_url).to match(admin_runner_path(project_runner))
expect(find_by_testid('alert-success')).to have_content('saved')
end
end
describe 'projects' do
it 'contains project names' do
expect(page).to have_content(project1.full_name)
expect(page).to have_content(project2.full_name)
end
end
describe 'search' do
before do
search_form = find('#runner-projects-search')
search_form.fill_in 'search', with: project1.name
search_form.click_button 'Search'
end
it 'contains name of correct project' do
expect(page).to have_content(project1.full_name)
expect(page).not_to have_content(project2.full_name)
end
end
describe 'enable/create' do
shared_examples 'assignable runner' do
it 'enables a runner for a project' do
within_testid('unassigned-projects') do
within('tr', text: project2.full_name) do
click_on 'Enable'
end
end
assigned_project = find_by_testid('assigned-projects')
expect(page).to have_content('Runner assigned to project.')
expect(assigned_project).to have_content(project2.name)
end
end
context 'with project runner' do
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project1]) }
before do
visit edit_admin_runner_path(project_runner)
end
it_behaves_like 'assignable runner'
end
context 'with locked runner' do
let_it_be(:locked_runner) { create(:ci_runner, :project, projects: [project1], locked: true) }
before do
visit edit_admin_runner_path(locked_runner)
end
it_behaves_like 'assignable runner'
end
end
describe 'disable/destroy' do
let_it_be(:runner) { create(:ci_runner, :project, projects: [project1]) }
before do
visit edit_admin_runner_path(runner)
end
it 'removed project runner from project' do
within_testid('assigned-projects') do
click_on 'Disable'
end
new_runner_project = find_by_testid('unassigned-projects')
expect(page).to have_content('Runner unassigned from project.')
expect(new_runner_project).to have_content(project1.name)
end
end
end
end

View File

@ -0,0 +1,533 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe "Admin manages runners in admin runner list", :freeze_time, :js, feature_category: :fleet_visibility do
include Features::SortingHelpers
include Features::RunnersHelpers
include Spec::Support::Helpers::ModalHelpers
let_it_be(:admin) { create(:admin) }
before do
sign_in(admin)
enable_admin_mode!(admin)
end
context "with runners" do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:namespace) { create(:namespace) }
let_it_be(:project) { create(:project, namespace: namespace, creator: user) }
context "with an instance runner" do
let_it_be(:instance_runner) { create(:ci_runner, :instance) }
before do
visit admin_runners_path
end
it_behaves_like 'shows runner summary and navigates to details' do
let(:runner) { instance_runner }
let(:runner_page_path) { admin_runner_path(instance_runner) }
end
it_behaves_like 'pauses, resumes and deletes a runner' do
let(:runner) { instance_runner }
end
it 'shows an instance badge' do
within_runner_row(instance_runner.id) do
expect(page).to have_selector '.badge', text: 'Instance'
end
end
end
context "with multiple runners" do
before do
create(:ci_runner, :instance, :almost_offline)
create(:ci_runner, :instance, :offline)
create(:ci_runner, :instance, :stale)
visit admin_runners_path
end
it 'has all necessary texts' do
expect(page).to have_text "All 3"
expect(page).to have_text "Online 1"
expect(page).to have_text "Offline 2"
expect(page).to have_text "Stale 1"
end
it_behaves_like 'deletes runners in bulk' do
let(:runner_count) { '3' }
end
end
it 'shows a job count' do
runner = create(:ci_runner, :project, projects: [project])
create_list(:ci_build, 2, runner: runner)
visit admin_runners_path
within_runner_row(runner.id) do
expect(find_by_testid('job-count')).to have_content '2'
end
end
it 'shows an Active status badge that links to jobs tab' do
runner = create(:ci_runner, :project, projects: [project])
job = create(:ci_build, :running, runner: runner)
visit admin_runners_path
within_runner_row(runner.id) do
click_on('Active')
end
expect(current_url).to match(admin_runner_path(runner))
expect(find_by_testid('td-status')).to have_content "Running"
expect(find_by_testid('td-job')).to have_content "##{job.id}"
end
describe 'searches for a runner' do
before_all do
create(:ci_runner, :instance, description: 'runner foo')
create(:ci_runner, :instance, description: 'runner bar')
end
before do
visit admin_runners_path
end
it 'runner types tabs have total counts and can be selected' do
expect(page).to have_link('All 2')
expect(page).to have_link('Instance 2')
expect(page).to have_link('Group 0')
expect(page).to have_link('Project 0')
end
it 'shows runners' do
expect(page).to have_content("runner foo")
expect(page).to have_content("runner bar")
end
it 'shows correct runner when description matches' do
input_filtered_search_keys('runner foo')
expect(page).to have_link('All 1')
expect(page).to have_link('Instance 1')
expect(page).to have_content("runner foo")
expect(page).not_to have_content("runner bar")
end
context 'when description does not match' do
before do
input_filtered_search_keys('runner baz')
end
it_behaves_like 'shows no runners found'
it 'shows no runner' do
expect(page).to have_link('All 0')
expect(page).to have_link('Instance 0')
end
end
end
describe 'filter by paused' do
before_all do
create(:ci_runner, :instance, description: 'runner-active')
create(:ci_runner, :instance, :paused, description: 'runner-paused')
end
before do
visit admin_runners_path
end
it 'shows all runners' do
expect(page).to have_link('All 2')
expect(page).to have_content 'runner-active'
expect(page).to have_content 'runner-paused'
end
it 'shows paused runners' do
input_filtered_search_filter_is_only('Paused', 'Yes')
expect(page).to have_link('All 1')
expect(page).not_to have_content 'runner-active'
expect(page).to have_content 'runner-paused'
end
it 'shows active runners' do
input_filtered_search_filter_is_only('Paused', 'No')
expect(page).to have_link('All 1')
expect(page).to have_content 'runner-active'
expect(page).not_to have_content 'runner-paused'
end
end
describe 'filter by version prefix' do
before_all do
runner_v15 = create(:ci_runner, :instance, description: 'runner-v15')
runner_v14 = create(:ci_runner, :instance, description: 'runner-v14')
create(:ci_runner_machine, runner: runner_v15, version: '15.0.0')
create(:ci_runner_machine, runner: runner_v14, version: '14.0.0')
end
before do
visit admin_runners_path
end
it 'shows all runners' do
expect(page).to have_link('All 2')
expect(page).to have_content 'runner-v15'
expect(page).to have_content 'runner-v14'
end
it 'shows filtered runner based on supplied prefix' do
input_filtered_search_filter_is_only('Version starts with', '15.0')
expect(page).to have_link('All 1')
expect(page).not_to have_content 'runner-v14'
expect(page).to have_content 'runner-v15'
end
end
describe 'filter by creator' do
before_all do
create(:ci_runner, :instance, description: 'runner-creator-admin', creator: admin)
create(:ci_runner, :instance, description: 'runner-creator-user', creator: user)
end
before do
visit admin_runners_path
end
it 'shows all runners' do
expect(page).to have_link('All 2')
expect(page).to have_content 'runner-creator-admin'
expect(page).to have_content 'runner-creator-user'
end
it 'shows filtered runner based on creator' do
input_filtered_search_filter_is_only('Creator', admin.username)
expect(page).to have_link('All 1')
expect(page).to have_content 'runner-creator-admin'
expect(page).not_to have_content 'runner-creator-user'
end
it 'shows filtered search suggestions' do
open_filtered_search_suggestions('Creator')
page.within(search_bar_selector) do
expect(page).to have_content admin.username
expect(page).to have_content user.username
end
end
end
describe 'filter by status' do
before_all do
freeze_time # Freeze time before `let_it_be` runs, so that runner statuses are frozen during execution
create(:ci_runner, :instance, :online, description: 'runner-1')
create(:ci_runner, :instance, :almost_offline, description: 'runner-2')
create(:ci_runner, :instance, :contacted_within_stale_deadline, description: 'runner-offline')
end
after :all do
unfreeze_time
end
let_it_be(:never_contacted) do
create(:ci_runner, :instance, :unregistered, description: 'runner-never-contacted')
end
before do
visit admin_runners_path
end
it 'shows all runners' do
expect(page).to have_link('All 4')
expect(page).to have_content 'runner-1'
expect(page).to have_content 'runner-2'
expect(page).to have_content 'runner-offline'
expect(page).to have_content 'runner-never-contacted'
end
it 'shows correct runner when status matches' do
input_filtered_search_filter_is_only('Status', 'Online')
expect(page).to have_link('All 2')
expect(page).to have_content 'runner-1'
expect(page).to have_content 'runner-2'
expect(page).not_to have_content 'runner-offline'
expect(page).not_to have_content 'runner-never-contacted'
end
it 'shows correct runner when status is selected and search term is entered' do
input_filtered_search_filter_is_only('Status', 'Online')
input_filtered_search_keys('runner-1')
expect(page).to have_link('All 1')
expect(page).to have_content 'runner-1'
expect(page).not_to have_content 'runner-2'
expect(page).not_to have_content 'runner-offline'
expect(page).not_to have_content 'runner-never-contacted'
end
it 'shows correct runner when status filter is entered' do
# use the string "Never" to avoid using space and trigger an early selection
input_filtered_search_filter_is_only('Status', 'Never')
expect(page).to have_link('All 1')
expect(page).not_to have_content 'runner-1'
expect(page).not_to have_content 'runner-2'
expect(page).not_to have_content 'runner-paused'
expect(page).to have_content 'runner-never-contacted'
within_runner_row(never_contacted.id) do
expect(page).to have_selector '.badge', text: 'Never contacted'
end
end
context 'when status does not match' do
before do
input_filtered_search_filter_is_only('Status', 'Stale')
end
it_behaves_like 'shows no runners found'
it 'shows no runner' do
expect(page).to have_link('All 0')
end
end
end
describe 'filter by type' do
before_all do
create(:ci_runner, :project, description: 'runner-project', projects: [project])
create(:ci_runner, :group, description: 'runner-group', groups: [group])
end
it '"All" tab is selected by default' do
visit admin_runners_path
expect(page).to have_link('All 2')
expect(page).to have_link('Group 1')
expect(page).to have_link('Project 1')
within_testid('runner-type-tabs') do
expect(page).to have_link('All', class: 'active')
end
end
it 'shows correct runner when type matches' do
visit admin_runners_path
expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-group'
within_testid('runner-type-tabs') do
click_on('Project')
expect(page).to have_link('Project', class: 'active')
end
expect(page).to have_content 'runner-project'
expect(page).not_to have_content 'runner-group'
end
it 'show the same counts after selecting another tab' do
visit admin_runners_path
within_testid('runner-type-tabs') do
click_on('Project')
expect(page).to have_link('All 2')
expect(page).to have_link('Group 1')
expect(page).to have_link('Project 1')
end
end
it 'shows correct runner when type is selected and search term is entered' do
create(:ci_runner, :project, description: 'runner-2-project', projects: [project])
visit admin_runners_path
within_testid('runner-type-tabs') do
click_on 'Project'
end
expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-2-project'
expect(page).not_to have_content 'runner-group'
input_filtered_search_keys('runner-project')
expect(page).to have_content 'runner-project'
expect(page).not_to have_content 'runner-2-project'
expect(page).not_to have_content 'runner-group'
end
it 'maintains the same filter when switching between runner types' do
create(:ci_runner, :project, :paused, description: 'runner-paused-project', projects: [project])
visit admin_runners_path
input_filtered_search_filter_is_only('Paused', 'No')
expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-group'
expect(page).not_to have_content 'runner-paused-project'
within_testid('runner-type-tabs') do
click_on 'Project'
end
expect(page).to have_content 'runner-project'
expect(page).not_to have_content 'runner-group'
expect(page).not_to have_content 'runner-paused-project'
end
context 'when type does not match' do
before do
visit admin_runners_path
within_testid('runner-type-tabs') do
click_on 'Instance'
end
end
it_behaves_like 'shows no runners found'
it 'shows active tab' do
expect(page).to have_link('Instance', class: 'active')
end
end
end
describe 'filter by tag' do
let_it_be(:runner_1) { create(:ci_runner, :instance, description: 'runner-blue', tag_list: ['blue']) }
let_it_be(:runner_2) { create(:ci_runner, :instance, description: 'runner-2-blue', tag_list: ['blue']) }
let_it_be(:runner_3) { create(:ci_runner, :instance, description: 'runner-red', tag_list: ['red']) }
before do
visit admin_runners_path
end
it 'shows tags suggestions' do
open_filtered_search_suggestions('Tags')
page.within(search_bar_selector) do
expect(page).to have_content 'blue'
expect(page).to have_content 'red'
end
end
it_behaves_like 'filters by tag' do
let(:tag) { 'blue' }
let(:found_runner) { runner_1.description }
let(:missing_runner) { runner_3.description }
end
context 'when tag does not match' do
before do
input_filtered_search_filter_is_only('Tags', 'green')
end
it_behaves_like 'shows no runners found'
end
it 'shows correct runner when tag is selected and search term is entered' do
input_filtered_search_filter_is_only('Tags', 'blue')
expect(page).to have_content 'runner-blue'
expect(page).to have_content 'runner-2-blue'
expect(page).not_to have_content 'runner-red'
input_filtered_search_keys('runner-2-blue')
expect(page).to have_content 'runner-2-blue'
expect(page).not_to have_content 'runner-blue'
expect(page).not_to have_content 'runner-red'
end
end
it 'sorts by last contact date' do
create(:ci_runner, :instance, description: 'runner-1', contacted_at: '2018-07-12')
create(:ci_runner, :instance, description: 'runner-2', contacted_at: '2018-07-13')
visit admin_runners_path
within_testid('runner-list') do
within('tbody tr:nth-child(1)') do
expect(page).to have_content 'runner-2'
end
within('tbody tr:nth-child(2)') do
expect(page).to have_content 'runner-1'
end
end
pajamas_sort_by 'Last contact', from: 'Created date'
click_on 'Sort direction: Descending'
within_testid('runner-list') do
within('tbody tr:nth-child(1)') do
expect(page).to have_content 'runner-1'
end
within('tbody tr:nth-child(2)') do
expect(page).to have_content 'runner-2'
end
end
end
end
context "with no runners" do
before do
visit admin_runners_path
end
it_behaves_like 'shows no runners registered'
it 'shows tabs with total counts equal to 0' do
aggregate_failures do
expect(page).to have_link('All 0')
expect(page).to have_link('Instance 0')
expect(page).to have_link('Group 0')
expect(page).to have_link('Project 0')
end
end
end
context "when visiting outdated URLs" do
it 'updates ACTIVE runner status to paused=false' do
visit admin_runners_path('status[]': 'ACTIVE')
expect(page).to have_current_path(admin_runners_path('paused[]': 'false'))
end
it 'updates PAUSED runner status to paused=true' do
visit admin_runners_path('status[]': 'PAUSED')
expect(page).to have_current_path(admin_runners_path('paused[]': 'true'))
end
end
end

View File

@ -365,7 +365,7 @@ RSpec.describe 'Edit group settings', feature_category: :groups_and_projects do
end
expect(page).to have_selector(selector, visible: :visible)
expect(page).to have_content _('Please enter a value of 90 days or more')
expect(page).to have_content _('Please enter a value between 90 and 1827 days (5 years)')
end
it 'auto disables dormant members period field depending on parent checkbox', :js do

View File

@ -8,12 +8,13 @@ describe('GroupItem spec', () => {
const MOCK_GROUP = { id: 123, fullName: 'Group 1', name: 'group1', avatarUrl: 'some/avatar.jpg' };
const createComponent = (props) => {
const createComponent = (props, options) => {
wrapper = mountExtended(GroupItem, {
propsData: {
data: MOCK_GROUP,
...props,
},
...options,
});
};
@ -24,10 +25,8 @@ describe('GroupItem spec', () => {
it('renders an Avatar component', () => {
expect(findAvatar().props('size')).toBe(32);
expect(findAvatar().attributes()).toMatchObject({
src: MOCK_GROUP.avatarUrl,
alt: MOCK_GROUP.fullName,
});
expect(findAvatar().props('src')).toBe(MOCK_GROUP.avatarUrl);
expect(findAvatar().attributes('alt')).toBe(MOCK_GROUP.fullName);
});
it('renders a fullName and name', () => {
@ -40,7 +39,12 @@ describe('GroupItem spec', () => {
});
describe('hidden groups', () => {
beforeEach(() => createComponent({ data: { ...MOCK_GROUP, type: 'hidden_groups' } }));
beforeEach(() =>
createComponent(
{ data: { ...MOCK_GROUP, type: 'hidden_groups' } },
{ stubs: { HiddenGroupsItem } },
),
);
const findHiddenGroupsItem = () => wrapper.findComponent(HiddenGroupsItem);

View File

@ -23,10 +23,8 @@ describe('UserItem spec', () => {
it('renders an Avatar component', () => {
expect(findAvatar().props('size')).toBe(32);
expect(findAvatar().attributes()).toMatchObject({
src: MOCK_USER.avatarUrl,
alt: MOCK_USER.name,
});
expect(findAvatar().props('src')).toBe(MOCK_USER.avatarUrl);
expect(findAvatar().attributes('alt')).toBe(MOCK_USER.name);
});
it('renders a name and username', () => {

View File

@ -1,430 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy, feature_category: :database do
let(:connection) { ActiveRecord::Base.connection }
describe '#current_partitions' do
subject { described_class.new(model, partitioning_key).current_partitions }
let(:model) { double('model', table_name: table_name) }
let(:partitioning_key) { double }
let(:table_name) { :_test_partitioned_test }
before do
connection.execute(<<~SQL)
CREATE TABLE #{table_name}
(id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at))
PARTITION BY RANGE (created_at);
CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_000000
PARTITION OF #{table_name}
FOR VALUES FROM (MINVALUE) TO ('2020-05-01');
CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202005
PARTITION OF #{table_name}
FOR VALUES FROM ('2020-05-01') TO ('2020-06-01');
SQL
end
it 'detects both partitions' do
expect(subject).to eq(
[
Gitlab::Database::Partitioning::TimePartition.new(table_name, nil, '2020-05-01', partition_name: '_test_partitioned_test_000000'),
Gitlab::Database::Partitioning::TimePartition.new(table_name, '2020-05-01', '2020-06-01', partition_name: '_test_partitioned_test_202005')
])
end
end
describe '#missing_partitions' do
subject { described_class.new(model, partitioning_key).missing_partitions }
let(:model) do
Class.new(ActiveRecord::Base) do
self.table_name = '_test_partitioned_test'
self.primary_key = :id
end
end
let(:partitioning_key) { :created_at }
around do |example|
travel_to(Date.parse('2020-08-22')) { example.run }
end
context 'with existing partitions' do
before do
connection.execute(<<~SQL)
CREATE TABLE #{model.table_name}
(id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at))
PARTITION BY RANGE (created_at);
CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_000000
PARTITION OF #{model.table_name}
FOR VALUES FROM (MINVALUE) TO ('2020-05-01');
CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202006
PARTITION OF #{model.table_name}
FOR VALUES FROM ('2020-06-01') TO ('2020-07-01');
SQL
# Insert some data, it doesn't make a difference
model.create!(created_at: Date.parse('2020-04-20'))
model.create!(created_at: Date.parse('2020-06-15'))
end
context 'when pruning partitions before June 2020' do
subject { described_class.new(model, partitioning_key, retain_for: 1.month).missing_partitions }
it 'does not include the missing partition from May 2020 because it would be dropped' do
expect(subject).not_to include(Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-05-01', '2020-06-01', partition_name: "#{model.table_name}_202005"))
end
it 'detects the missing partition for 1 month ago (July 2020)' do
expect(subject).to include(Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-07-01', '2020-08-01', partition_name: "#{model.table_name}_202007"))
end
end
it 'detects the gap and the missing partition in May 2020' do
expect(subject).to include(Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-05-01', '2020-06-01', partition_name: "#{model.table_name}_202005"))
end
it 'detects the missing partitions at the end of the range and expects a partition for July 2020' do
expect(subject).to include(Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-07-01', '2020-08-01', partition_name: "#{model.table_name}_202007"))
end
it 'detects the missing partitions at the end of the range and expects a partition for August 2020' do
expect(subject).to include(Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-08-01', '2020-09-01', partition_name: "#{model.table_name}_202008"))
end
it 'creates partitions 6 months out from now (Sep 2020 through Feb 2021)' do
expect(subject).to include(
Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-09-01', '2020-10-01', partition_name: "#{model.table_name}_202009"),
Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-10-01', '2020-11-01', partition_name: "#{model.table_name}_202010"),
Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-11-01', '2020-12-01', partition_name: "#{model.table_name}_202011"),
Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-12-01', '2021-01-01', partition_name: "#{model.table_name}_202012"),
Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2021-01-01', '2021-02-01', partition_name: "#{model.table_name}_202101"),
Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2021-02-01', '2021-03-01', partition_name: "#{model.table_name}_202102")
)
end
it 'detects all missing partitions' do
expect(subject.size).to eq(9)
end
end
context 'without existing partitions' do
before do
connection.execute(<<~SQL)
CREATE TABLE #{model.table_name}
(id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at))
PARTITION BY RANGE (created_at);
SQL
end
context 'when pruning partitions before June 2020' do
subject { described_class.new(model, partitioning_key, retain_for: 1.month).missing_partitions }
it 'detects exactly the set of partitions from June 2020 to March 2021' do
months = %w[2020-07-01 2020-08-01 2020-09-01 2020-10-01 2020-11-01 2020-12-01 2021-01-01 2021-02-01 2021-03-01]
expected = months[..-2].zip(months.drop(1)).map do |(from, to)|
partition_name = "#{model.table_name}_#{Date.parse(from).strftime('%Y%m')}"
Gitlab::Database::Partitioning::TimePartition.new(model.table_name, from, to, partition_name: partition_name)
end
expect(subject).to match_array(expected)
end
end
it 'detects the missing catch-all partition at the beginning' do
expect(subject).to include(Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-08-01', partition_name: "#{model.table_name}_000000"))
end
it 'detects the missing partition for today and expects a partition for August 2020' do
expect(subject).to include(Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-08-01', '2020-09-01', partition_name: "#{model.table_name}_202008"))
end
it 'creates partitions 6 months out from now (Sep 2020 through Feb 2021' do
expect(subject).to include(
Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-09-01', '2020-10-01', partition_name: "#{model.table_name}_202009"),
Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-10-01', '2020-11-01', partition_name: "#{model.table_name}_202010"),
Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-11-01', '2020-12-01', partition_name: "#{model.table_name}_202011"),
Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-12-01', '2021-01-01', partition_name: "#{model.table_name}_202012"),
Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2021-01-01', '2021-02-01', partition_name: "#{model.table_name}_202101"),
Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2021-02-01', '2021-03-01', partition_name: "#{model.table_name}_202102")
)
end
it 'detects all missing partitions' do
expect(subject.size).to eq(8)
end
end
context 'with a regular partition but no catchall (MINVALUE, to) partition' do
before do
connection.execute(<<~SQL)
CREATE TABLE #{model.table_name}
(id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at))
PARTITION BY RANGE (created_at);
CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202006
PARTITION OF #{model.table_name}
FOR VALUES FROM ('2020-06-01') TO ('2020-07-01');
SQL
end
it 'detects a missing catch-all partition to add before the existing partition' do
expect(subject).to include(Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-06-01', partition_name: "#{model.table_name}_000000"))
end
end
end
describe '#extra_partitions' do
let(:model) do
Class.new(ActiveRecord::Base) do
self.table_name = '_test_partitioned_test'
self.primary_key = :id
end
end
let(:partitioning_key) { :created_at }
let(:table_name) { :_test_partitioned_test }
around do |example|
travel_to(Date.parse('2020-08-22')) { example.run }
end
describe 'with existing partitions' do
before do
ActiveRecord::Base.connection.execute(<<~SQL)
CREATE TABLE #{table_name}
(id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at))
PARTITION BY RANGE (created_at);
CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_000000
PARTITION OF #{table_name}
FOR VALUES FROM (MINVALUE) TO ('2020-05-01');
CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202005
PARTITION OF #{table_name}
FOR VALUES FROM ('2020-05-01') TO ('2020-06-01');
CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202006
PARTITION OF #{table_name}
FOR VALUES FROM ('2020-06-01') TO ('2020-07-01')
SQL
end
context 'without a time retention policy' do
subject { described_class.new(model, partitioning_key).extra_partitions }
it 'has no extra partitions to prune' do
expect(subject).to eq([])
end
end
context 'with a time retention policy that excludes no partitions' do
subject { described_class.new(model, partitioning_key, retain_for: 4.months).extra_partitions }
it 'has no extra partitions to prune' do
expect(subject).to eq([])
end
end
context 'with a time retention policy of 3 months' do
subject { described_class.new(model, partitioning_key, retain_for: 3.months).extra_partitions }
it 'prunes the unbounded partition ending 2020-05-01' do
min_value_to_may = Gitlab::Database::Partitioning::TimePartition.new(
model.table_name,
nil,
'2020-05-01',
partition_name: '_test_partitioned_test_000000'
)
expect(subject).to contain_exactly(min_value_to_may)
end
end
context 'with a time retention policy of 2 months' do
subject { described_class.new(model, partitioning_key, retain_for: 2.months).extra_partitions }
it 'prunes the unbounded partition and the partition for May-June' do
expect(subject).to contain_exactly(
Gitlab::Database::Partitioning::TimePartition.new(
model.table_name,
nil,
'2020-05-01',
partition_name: '_test_partitioned_test_000000'
),
Gitlab::Database::Partitioning::TimePartition.new(
model.table_name,
'2020-05-01',
'2020-06-01',
partition_name: '_test_partitioned_test_202005'
)
)
end
context 'when the retain_non_empty_partitions is true' do
subject { described_class.new(model, partitioning_key, retain_for: 2.months, retain_non_empty_partitions: true).extra_partitions }
it 'prunes empty partitions' do
expect(subject).to contain_exactly(
Gitlab::Database::Partitioning::TimePartition.new(
model.table_name,
nil,
'2020-05-01',
partition_name: '_test_partitioned_test_000000'
),
Gitlab::Database::Partitioning::TimePartition.new(
model.table_name,
'2020-05-01',
'2020-06-01',
partition_name: '_test_partitioned_test_202005'
)
)
end
it 'does not prune non-empty partitions' do
connection.execute("INSERT INTO #{table_name} (created_at) VALUES (('2020-05-15'))") # inserting one record into _test_partitioned_test_202005
expect(subject).to contain_exactly(
Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: '_test_partitioned_test_000000')
)
end
end
end
context 'with a time retention policy of 1 month' do
let(:retention_period) { 1.month }
subject(:partitions_to_remove) { described_class.new(model, partitioning_key, retain_for: retention_period).extra_partitions }
it 'prunes the unbounded partition and the partitions for May-June and June-July' do
expect(subject).to contain_exactly(
Gitlab::Database::Partitioning::TimePartition.new(
model.table_name,
nil,
'2020-05-01',
partition_name: '_test_partitioned_test_000000'
),
Gitlab::Database::Partitioning::TimePartition.new(
model.table_name,
'2020-05-01',
'2020-06-01',
partition_name: '_test_partitioned_test_202005'
),
Gitlab::Database::Partitioning::TimePartition.new(
model.table_name,
'2020-06-01',
'2020-07-01',
partition_name: '_test_partitioned_test_202006'
)
)
end
it 'contains partitions starting at least one month in the past' do
prune_to = partitions_to_remove.map(&:to).max
expect(1.month.ago).to be_after(prune_to)
strategy = described_class.new(model, partitioning_key, retain_for: retention_period)
desired_partitions = strategy.current_partitions - strategy.extra_partitions + strategy.missing_partitions
# Double check this is equivalent to the private method
expect(desired_partitions).to contain_exactly(*strategy.send(:desired_partitions))
end
context 'when the retain_non_empty_partitions is true' do
subject { described_class.new(model, partitioning_key, retain_for: retention_period, retain_non_empty_partitions: true).extra_partitions }
it 'prunes empty partitions' do
expect(subject).to contain_exactly(
Gitlab::Database::Partitioning::TimePartition.new(
model.table_name,
nil,
'2020-05-01',
partition_name: '_test_partitioned_test_000000'
),
Gitlab::Database::Partitioning::TimePartition.new(
model.table_name,
'2020-05-01',
'2020-06-01',
partition_name: '_test_partitioned_test_202005'
),
Gitlab::Database::Partitioning::TimePartition.new(
model.table_name,
'2020-06-01',
'2020-07-01',
partition_name: '_test_partitioned_test_202006'
)
)
end
it 'does not prune non-empty partitions' do
connection.execute("INSERT INTO #{table_name} (created_at) VALUES (('2020-05-15'))") # inserting one record into _test_partitioned_test_202005
expect(subject).to contain_exactly(
Gitlab::Database::Partitioning::TimePartition.new(model.table_name, nil, '2020-05-01', partition_name: '_test_partitioned_test_000000'),
Gitlab::Database::Partitioning::TimePartition.new(model.table_name, '2020-06-01', '2020-07-01', partition_name: '_test_partitioned_test_202006')
)
end
end
end
end
end
describe 'attributes' do
let(:partitioning_key) { :partition }
let(:retain_non_empty_partitions) { true }
let(:retain_for) { 12.months }
let(:analyze_interval) { 1.week }
let(:model) { class_double(ApplicationRecord, table_name: table_name, connection: connection) }
let(:table_name) { :_test_partitioned_test }
subject(:strategy) do
described_class.new(
model, partitioning_key,
retain_for: retain_for,
retain_non_empty_partitions: retain_non_empty_partitions,
analyze_interval: analyze_interval
)
end
specify do
expect(strategy).to have_attributes({
model: model,
partitioning_key: partitioning_key,
retain_for: retain_for,
retain_non_empty_partitions: retain_non_empty_partitions,
analyze_interval: analyze_interval
})
end
end
describe '#partition_name' do
let(:model) { double('model', table_name: table_name) }
let(:partitioning_key) { double }
let(:table_name) { '_test_partitioned_test' }
let(:from) { Date.parse('2020-04-01 00:00:00') }
let(:to) { Date.parse('2020-05-01 00:00:00') }
subject(:partition_name) { described_class.new(model, partitioning_key).partition_name(from) }
it 'uses table_name as prefix' do
expect(partition_name).to start_with(table_name)
end
it 'uses Year-Month (from) as suffix' do
expect(partition_name).to end_with("_202004")
end
context 'without from date' do
let(:from) { nil }
it 'uses 000000 as suffix for first partition' do
expect(partition_name).to end_with("_000000")
end
end
end
end

View File

@ -427,7 +427,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager, feature_categor
describe 'strategies that support analyze_interval' do
[
::Gitlab::Database::Partitioning::MonthlyStrategy,
::Gitlab::Database::Partitioning::Time::MonthlyStrategy,
::Gitlab::Database::Partitioning::SlidingListStrategy,
::Gitlab::Database::Partitioning::CiSlidingListStrategy
].each do |klass|
@ -463,7 +463,7 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager, feature_categor
it 'creates partitions for the future then drops the oldest one after a month' do
# 1 month for the current month, 1 month for the old month that we're retaining data for, headroom
expected_num_partitions = (Gitlab::Database::Partitioning::MonthlyStrategy::HEADROOM + 2.months) / 1.month
expected_num_partitions = (Gitlab::Database::Partitioning::Time::MonthlyStrategy::HEADROOM + 2.months) / 1.month
expect { described_class.new(my_model).sync_partitions }.to change { num_partitions(my_model) }.from(0).to(expected_num_partitions)
travel 1.month

View File

@ -0,0 +1,384 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::Partitioning::Time::MonthlyStrategy, feature_category: :database do
let(:connection) { ApplicationRecord.connection }
let(:monthly_strategy) do
described_class.new(model, partitioning_key, retain_for: retention_period, retain_non_empty_partitions: retain_data)
end
let(:retention_period) { nil }
let(:retain_data) { false }
let(:partitioning_key) { :created_at }
let(:table_name) { model.table_name }
let(:model) do
Class.new(ApplicationRecord) do
self.table_name = '_test_partitioned_test'
self.primary_key = :id
end
end
describe '#current_partitions' do
subject(:current_partitions) { monthly_strategy.current_partitions }
before do
connection.execute(<<~SQL)
CREATE TABLE #{table_name}
(id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at))
PARTITION BY RANGE (created_at);
CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_000000
PARTITION OF #{table_name}
FOR VALUES FROM (MINVALUE) TO ('2020-05-01');
CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202005
PARTITION OF #{table_name}
FOR VALUES FROM ('2020-05-01') TO ('2020-06-01');
SQL
end
it 'detects both partitions' do
expect(current_partitions).to eq(
[
time_partition(table_name, nil, '2020-05-01', "#{model.table_name}_000000"),
time_partition(table_name, '2020-05-01', '2020-06-01', "#{model.table_name}_202005")
]
)
end
end
describe '#missing_partitions', time_travel_to: '2020-08-22' do
subject(:missing_partitions) { monthly_strategy.missing_partitions }
context 'with existing partitions' do
before do
connection.execute(<<~SQL)
CREATE TABLE #{model.table_name}
(id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at))
PARTITION BY RANGE (created_at);
CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_000000
PARTITION OF #{model.table_name}
FOR VALUES FROM (MINVALUE) TO ('2020-05-01');
CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202006
PARTITION OF #{model.table_name}
FOR VALUES FROM ('2020-06-01') TO ('2020-07-01');
SQL
# Insert some data, it doesn't make a difference
model.create!(created_at: Date.parse('2020-04-20'))
model.create!(created_at: Date.parse('2020-06-15'))
end
context 'when pruning partitions before June 2020' do
let(:retention_period) { 1.month }
it 'does not include the missing partition from May 2020 because it would be dropped' do
expect(missing_partitions).not_to include(
time_partition(table_name, '2020-05-01', '2020-06-01', "#{model.table_name}_202005")
)
end
it 'detects the missing partition for 1 month ago (July 2020)' do
expect(missing_partitions).to include(
time_partition(table_name, '2020-07-01', '2020-08-01', "#{model.table_name}_202007")
)
end
end
it 'detects the gap and the missing partition in May 2020' do
expect(missing_partitions).to include(
time_partition(table_name, '2020-05-01', '2020-06-01', "#{model.table_name}_202005")
)
end
it 'detects the missing partitions at the end of the range and expects a partition for July 2020' do
expect(missing_partitions).to include(
time_partition(table_name, '2020-07-01', '2020-08-01', "#{model.table_name}_202007")
)
end
it 'detects the missing partitions at the end of the range and expects a partition for August 2020' do
expect(missing_partitions).to include(
time_partition(table_name, '2020-08-01', '2020-09-01', "#{model.table_name}_202008")
)
end
it 'creates partitions 6 months out from now (Sep 2020 through Feb 2021)' do
expect(missing_partitions).to include(
time_partition(table_name, '2020-09-01', '2020-10-01', "#{model.table_name}_202009"),
time_partition(table_name, '2020-10-01', '2020-11-01', "#{model.table_name}_202010"),
time_partition(table_name, '2020-11-01', '2020-12-01', "#{model.table_name}_202011"),
time_partition(table_name, '2020-12-01', '2021-01-01', "#{model.table_name}_202012"),
time_partition(table_name, '2021-01-01', '2021-02-01', "#{model.table_name}_202101"),
time_partition(table_name, '2021-02-01', '2021-03-01', "#{model.table_name}_202102")
)
end
it 'detects all missing partitions' do
expect(missing_partitions.size).to eq(9)
end
end
context 'without existing partitions' do
before do
connection.execute(<<~SQL)
CREATE TABLE #{model.table_name}
(id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at))
PARTITION BY RANGE (created_at);
SQL
end
context 'when pruning partitions before June 2020' do
let(:retention_period) { 1.month }
it 'detects exactly the set of partitions from June 2020 to March 2021' do
months = %w[2020-07-01 2020-08-01 2020-09-01 2020-10-01 2020-11-01 2020-12-01 2021-01-01 2021-02-01
2021-03-01]
expected = months[..-2].zip(months.drop(1)).map do |(from, to)|
partition_name = "#{model.table_name}_#{Date.parse(from).strftime('%Y%m')}"
time_partition(table_name, from, to, partition_name)
end
expect(missing_partitions).to match_array(expected)
end
end
it 'detects the missing catch-all partition at the beginning' do
expect(missing_partitions).to include(
time_partition(table_name, nil, '2020-08-01', "#{model.table_name}_000000")
)
end
it 'detects the missing partition for today and expects a partition for August 2020' do
expect(missing_partitions).to include(
time_partition(table_name, '2020-08-01', '2020-09-01', "#{model.table_name}_202008")
)
end
it 'creates partitions 6 months out from now (Sep 2020 through Feb 2021' do
expect(missing_partitions).to include(
time_partition(table_name, '2020-09-01', '2020-10-01', "#{model.table_name}_202009"),
time_partition(table_name, '2020-10-01', '2020-11-01', "#{model.table_name}_202010"),
time_partition(table_name, '2020-11-01', '2020-12-01', "#{model.table_name}_202011"),
time_partition(table_name, '2020-12-01', '2021-01-01', "#{model.table_name}_202012"),
time_partition(table_name, '2021-01-01', '2021-02-01', "#{model.table_name}_202101"),
time_partition(table_name, '2021-02-01', '2021-03-01', "#{model.table_name}_202102")
)
end
it 'detects all missing partitions' do
expect(missing_partitions.size).to eq(8)
end
end
context 'with a regular partition but no catchall (MINVALUE, to) partition' do
before do
connection.execute(<<~SQL)
CREATE TABLE #{model.table_name}
(id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at))
PARTITION BY RANGE (created_at);
CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202006
PARTITION OF #{model.table_name}
FOR VALUES FROM ('2020-06-01') TO ('2020-07-01');
SQL
end
it 'detects a missing catch-all partition to add before the existing partition' do
expect(missing_partitions).to include(
time_partition(table_name, nil, '2020-06-01', "#{model.table_name}_000000")
)
end
end
end
describe '#extra_partitions', time_travel_to: '2020-08-22' do
subject(:extra_partitions) { monthly_strategy.extra_partitions }
describe 'with existing partitions' do
before do
ActiveRecord::Base.connection.execute(<<~SQL)
CREATE TABLE #{table_name}
(id serial not null, created_at timestamptz not null, PRIMARY KEY (id, created_at))
PARTITION BY RANGE (created_at);
CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_000000
PARTITION OF #{table_name}
FOR VALUES FROM (MINVALUE) TO ('2020-05-01');
CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202005
PARTITION OF #{table_name}
FOR VALUES FROM ('2020-05-01') TO ('2020-06-01');
CREATE TABLE #{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_partitioned_test_202006
PARTITION OF #{table_name}
FOR VALUES FROM ('2020-06-01') TO ('2020-07-01')
SQL
end
context 'without a time retention policy' do
it 'has no extra partitions to prune' do
expect(extra_partitions).to be_empty
end
end
context 'with a time retention policy that excludes no partitions' do
let(:retention_period) { 4.months }
it 'has no extra partitions to prune' do
expect(extra_partitions).to be_empty
end
end
context 'with a time retention policy of 3 months' do
let(:retention_period) { 3.months }
it 'prunes the unbounded partition ending 2020-05-01' do
min_value_to_may = time_partition(table_name, nil, '2020-05-01', "#{model.table_name}_000000")
expect(extra_partitions).to contain_exactly(min_value_to_may)
end
end
context 'with a time retention policy of 2 months' do
let(:retention_period) { 2.months }
it 'prunes the unbounded partition and the partition for May-June' do
expect(extra_partitions).to contain_exactly(
time_partition(table_name, nil, '2020-05-01', "#{model.table_name}_000000"),
time_partition(table_name, '2020-05-01', '2020-06-01', "#{model.table_name}_202005")
)
end
context 'when the retain_non_empty_partitions is true' do
let(:retain_data) { true }
it 'prunes empty partitions' do
expect(extra_partitions).to contain_exactly(
time_partition(table_name, nil, '2020-05-01', "#{model.table_name}_000000"),
time_partition(table_name, '2020-05-01', '2020-06-01', "#{model.table_name}_202005")
)
end
it 'does not prune non-empty partitions' do
# inserting one record into _test_partitioned_test_202005
connection.execute("INSERT INTO #{table_name} (created_at) VALUES (('2020-05-15'))")
expect(extra_partitions).to contain_exactly(
time_partition(table_name, nil, '2020-05-01', "#{model.table_name}_000000")
)
end
end
end
context 'with a time retention policy of 1 month' do
let(:retention_period) { 1.month }
it 'prunes the unbounded partition and the partitions for May-June and June-July' do
expect(extra_partitions).to contain_exactly(
time_partition(table_name, nil, '2020-05-01', "#{model.table_name}_000000"),
time_partition(table_name, '2020-05-01', '2020-06-01', "#{model.table_name}_202005"),
time_partition(table_name, '2020-06-01', '2020-07-01', "#{model.table_name}_202006")
)
end
it 'contains partitions starting at least one month in the past' do
prune_to = extra_partitions.map(&:to).max
expect(1.month.ago).to be_after(prune_to)
strategy = described_class.new(model, partitioning_key, retain_for: retention_period)
desired_partitions = strategy.current_partitions - strategy.extra_partitions + strategy.missing_partitions
# Double check this is equivalent to the private method
expect(desired_partitions).to match_array(strategy.send(:desired_partitions))
end
context 'when the retain_non_empty_partitions is true' do
let(:retain_data) { true }
it 'prunes empty partitions' do
expect(extra_partitions).to contain_exactly(
time_partition(table_name, nil, '2020-05-01', "#{model.table_name}_000000"),
time_partition(table_name, '2020-05-01', '2020-06-01', "#{model.table_name}_202005"),
time_partition(table_name, '2020-06-01', '2020-07-01', "#{model.table_name}_202006")
)
end
it 'does not prune non-empty partitions' do
# inserting one record into _test_partitioned_test_202005
connection.execute("INSERT INTO #{table_name} (created_at) VALUES (('2020-05-15'))")
expect(extra_partitions).to contain_exactly(
time_partition(table_name, nil, '2020-05-01', "#{model.table_name}_000000"),
time_partition(table_name, '2020-06-01', '2020-07-01', "#{model.table_name}_202006")
)
end
end
end
end
end
describe 'attributes' do
let(:partitioning_key) { :partition }
let(:retain_non_empty_partitions) { true }
let(:retain_for) { 12.months }
let(:analyze_interval) { 1.week }
let(:model) { class_double(ApplicationRecord, table_name: table_name, connection: connection) }
let(:table_name) { :_test_partitioned_test }
subject(:strategy) do
described_class.new(
model, partitioning_key,
retain_for: retain_for,
retain_non_empty_partitions: retain_non_empty_partitions,
analyze_interval: analyze_interval
)
end
specify do
expect(strategy).to have_attributes({
model: model,
partitioning_key: partitioning_key,
retain_for: retain_for,
retain_non_empty_partitions: retain_non_empty_partitions,
analyze_interval: analyze_interval
})
end
end
describe '#partition_name' do
let(:from) { Date.parse('2020-04-01 00:00:00') }
let(:to) { Date.parse('2020-05-01 00:00:00') }
subject(:partition_name) { monthly_strategy.partition_name(from) }
it 'uses table_name as prefix' do
expect(partition_name).to start_with(table_name)
end
it 'uses Year-Month (from) as suffix' do
expect(partition_name).to end_with("_202004")
end
context 'without from date' do
let(:from) { nil }
it 'uses 000000 as suffix for first partition' do
expect(partition_name).to end_with("_000000")
end
end
end
private
def time_partition(table_name, lower_bound, upper_bound, partition_name)
Gitlab::Database::Partitioning::TimePartition.new(
table_name,
lower_bound,
upper_bound,
partition_name: partition_name
)
end
end

View File

@ -20,7 +20,7 @@ RSpec.describe PartitionedTable, feature_category: :database do
subject { my_class.partitioned_by(key, strategy: :monthly, retain_for: 3.months) }
it 'passes the keyword arguments to the strategy' do
expect(Gitlab::Database::Partitioning::MonthlyStrategy).to receive(:new).with(my_class, key, retain_for: 3.months).and_call_original
expect(Gitlab::Database::Partitioning::Time::MonthlyStrategy).to receive(:new).with(my_class, key, retain_for: 3.months).and_call_original
subject
end
@ -29,7 +29,7 @@ RSpec.describe PartitionedTable, feature_category: :database do
it 'assigns the MonthlyStrategy as the partitioning strategy' do
subject
expect(my_class.partitioning_strategy).to be_a(Gitlab::Database::Partitioning::MonthlyStrategy)
expect(my_class.partitioning_strategy).to be_a(Gitlab::Database::Partitioning::Time::MonthlyStrategy)
end
it 'passes the partitioning key to the strategy instance' do

View File

@ -10,20 +10,12 @@ RSpec.describe Namespaces::UserNamespace, type: :model do
it { is_expected.to validate_presence_of(:owner) }
end
describe 'owner methods' do
describe '#owners' do
let(:owner) { build(:user) }
let(:namespace) { build(:namespace, owner: owner) }
describe '#owners' do
specify do
expect(namespace.owners).to match_array([owner])
end
end
describe '#owner_ids' do
specify do
expect(namespace.owner_ids).to match_array([owner.id])
end
specify do
expect(namespace.owners).to match_array([owner])
end
end

View File

@ -170,15 +170,6 @@ RSpec.describe ProjectImportState, type: :model, feature_category: :importers do
expect { import_state.finish }.to change { import_state.last_error }.from(error_message).to(nil)
end
it 'sets the user mapping feature flag state from import data for other transitions' do
import_state = create(:import_state, :started)
import_state.project.build_or_assign_import_data(data: { user_contribution_mapping_enabled: true }).save!
import_state.finish
expect(import_state.user_mapping_enabled).to be(true)
end
it 'enqueues housekeeping when an import of a fresh project is completed' do
project = create(:project_empty_repo, :import_started, import_type: :github)
@ -239,15 +230,6 @@ RSpec.describe ProjectImportState, type: :model, feature_category: :importers do
end.to change { project.import_data }
.from(import_data).to(nil)
end
it 'sets the user mapping feature flag state from import data for other transitions' do
import_state = create(:import_state, :scheduled)
import_state.project.build_or_assign_import_data(data: { user_contribution_mapping_enabled: true }).save!
import_state.cancel
expect(import_state.user_mapping_enabled).to be(true)
end
end
context 'state transition: started: [:finished, :canceled, :failed]' do
@ -282,92 +264,6 @@ RSpec.describe ProjectImportState, type: :model, feature_category: :importers do
end
end
describe 'completion notification trigger', :aggregate_failures do
let_it_be_with_reload(:group) { create(:group) }
let_it_be_with_reload(:project) { create(:project, :import_user_mapping_enabled, namespace: group) }
let_it_be(:group_owner) { create(:user) }
let(:user_mapping_enabled) { true }
subject(:import_state) { create(:import_state, status: initial_status, project: project) }
before_all do
group.add_owner(group_owner)
group.add_owner(project.creator)
end
shared_examples 'only the project creator is notified' do |event|
it 'only sends completion email to the project creator' do
expect(Notify).to receive(:project_import_complete)
.with(project.id, project.creator_id, user_mapping_enabled)
.and_call_original
import_state.send(:"#{event}!")
end
end
shared_examples 'not a completing event' do |event, initial_status|
let(:initial_status) { initial_status }
it 'does not send any completion emails' do
expect(Notify).not_to receive(:project_import_complete)
import_state.send(:"#{event}!")
end
end
it_behaves_like 'not a completing event', :schedule, :none
it_behaves_like 'not a completing event', :force_start, :none
it_behaves_like 'not a completing event', :start, :scheduled
it_behaves_like 'not a completing event', :cancel, :started
context 'when the import state transitions to finished' do
let(:event) { :finish }
let(:initial_status) { :started }
it 'sends a completion email to the project creator and group owners' do
allow(Notify).to receive(:project_import_complete).and_call_original
import_state.send(:"#{event}!")
expect(Notify).to have_received(:project_import_complete).with(project.id, project.creator_id, true)
expect(Notify).to have_received(:project_import_complete).with(project.id, group_owner.id, true)
end
context 'when the project was imported to a personal namespace' do
let_it_be(:project) { create(:project, :import_user_mapping_enabled) }
it_behaves_like 'only the project creator is notified', :finish
end
context 'when user mapping is disabled' do
let(:user_mapping_enabled) { false }
before do
project.build_or_assign_import_data(data: { user_contribution_mapping_enabled: user_mapping_enabled }).save!
end
it_behaves_like 'only the project creator is notified', :finish
end
end
context 'when the import state transitions to failed' do
let(:initial_status) { :started }
it_behaves_like 'only the project creator is notified', :fail_op
context 'when user mapping is disabled' do
let(:user_mapping_enabled) { false }
before do
project.build_or_assign_import_data(data: { user_contribution_mapping_enabled: user_mapping_enabled }).save!
end
it_behaves_like 'only the project creator is notified', :fail_op
end
end
end
describe 'clearing `jid` after finish', :clean_gitlab_redis_cache do
context 'without an JID' do
it 'does nothing' do

View File

@ -142,6 +142,14 @@ RSpec.describe DraftNotes::PublishService, feature_category: :code_review_workfl
publish
end
it 'resolves todos for the MR' do
expect_any_instance_of(TodoService) do |todo_service|
expect(todo_service).to receive(:new_review).with(kind_of(Review), user)
end
publish
end
it 'tracks the publish event' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_publish_review_action)
@ -243,7 +251,7 @@ RSpec.describe DraftNotes::PublishService, feature_category: :code_review_workfl
recorder = ActiveRecord::QueryRecorder.new(skip_cached: false) { publish }
expect(recorder.count).not_to be > 112
expect(recorder.count).not_to be > 116
end
end

View File

@ -1285,6 +1285,32 @@ RSpec.describe TodoService, feature_category: :notifications do
should_not_create_todo(user: guest, target: mentioned_mr, action: Todo::MENTIONED)
end
end
describe '#new_review' do
it 'marks related pending todos to the target MR for the user as done' do
first_todo = create(:todo, :pending, :assigned, user: john_doe, project: project, target: mentioned_mr, author: author)
second_todo = create(:todo, :pending, :review_requested, user: john_doe, project: project, target: mentioned_mr, author: author)
third_todo = create(:todo, :pending, :mentioned, user: john_doe, project: project, target: mentioned_mr, author: author)
review = Review.new(merge_request: mentioned_mr)
service.new_review(review, john_doe)
expect(first_todo.reload).to be_done
expect(second_todo.reload).to be_done
expect(third_todo.reload).to be_done
end
it 'marks related pending todo to the target MR for the user as done when the multiple_todos feature is off' do
stub_feature_flags(multiple_todos: false)
only_todo = create(:todo, :pending, :assigned, user: john_doe, project: project, target: mentioned_mr, author: author)
review = Review.new(merge_request: mentioned_mr)
service.new_review(review, john_doe)
expect(only_todo.reload).to be_done
end
end
end
describe 'Designs' do

View File

@ -0,0 +1,96 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItems::DataSync::Widgets::TimeTracking, feature_category: :team_planning do
let_it_be(:current_user) { create(:user) }
let_it_be_with_reload(:work_item) { create(:work_item) }
let_it_be_with_reload(:target_work_item) { create(:work_item) }
let_it_be(:timelogs) { create_list(:timelog, 3, issue: work_item) }
let(:params) { { operation: :move } }
subject(:callback) do
described_class.new(
work_item: work_item, target_work_item: target_work_item, current_user: current_user, params: params
)
end
describe '#before_create' do
let_it_be(:work_item) { create(:work_item, time_estimate: 3600) }
context 'when target work item has time_tracking widget' do
before do
allow(target_work_item).to receive(:get_widget).with(:time_tracking).and_return(true)
end
it "updates the time_estimate attributes in target work item" do
expect { callback.before_create }.to change { target_work_item.time_estimate }.from(0).to(3600)
end
end
context 'when target work item does not have time_tracking widget' do
before do
allow(target_work_item).to receive(:get_widget).with(:time_tracking).and_return(false)
end
it "does not update the time_estimate attributes in target work item" do
expect { callback.before_create }.not_to change { target_work_item.time_estimate }
end
end
end
describe '#after_save_commit' do
context 'when target work item has time_tracking widget' do
before do
allow(target_work_item).to receive(:get_widget).with(:time_tracking).and_return(true)
end
it 'calls the copy timelogs worker' do
expect(WorkItems::CopyTimelogsWorker).to receive(:perform_async).with(work_item.id, target_work_item.id)
callback.after_save_commit
end
context 'when cloning work item' do
let(:params) { { operation: :clone } }
it 'does not call the copy timelogs worker' do
expect(WorkItems::CopyTimelogsWorker).not_to receive(:perform_async)
callback.after_save_commit
end
end
context "when work_item does not have timelogs" do
before do
work_item.timelogs.delete_all
end
it 'does not call the copy timelogs worker' do
expect(WorkItems::CopyTimelogsWorker).not_to receive(:perform_async)
callback.after_save_commit
end
end
end
context 'when target work item does not have time_tracking widget' do
before do
allow(target_work_item).to receive(:get_widget).with(:time_tracking).and_return(false)
end
it 'does not call the copy timelogs worker' do
expect(WorkItems::CopyTimelogsWorker).not_to receive(:perform_async)
callback.after_save_commit
end
end
end
describe '#post_move_cleanup' do
it "deletes all the original work item timelog records" do
expect { callback.post_move_cleanup }.to change { work_item.timelogs.count }.from(3).to(0)
end
end
end

View File

@ -88,6 +88,10 @@ RSpec.shared_examples 'cloneable and moveable widget data' do
work_item.reload.sent_notifications.pluck(:recipient_id)
end
def work_item_timelogs(work_item)
work_item.reload.timelogs.pluck(:user_id, :time_spent)
end
def work_item_crm_contacts(work_item)
work_item.reload.customer_relations_contacts
end
@ -169,6 +173,11 @@ RSpec.shared_examples 'cloneable and moveable widget data' do
milestone.title
end
let_it_be(:timelogs) do
timelogs = create_list(:timelog, 2, issue: original_work_item)
timelogs.pluck(:user_id, :time_spent)
end
where(:widget_name, :eval_value, :expected_data, :operations) do
:assignees | :work_item_assignees | ref(:assignees) | [ref(:move), ref(:clone)]
:award_emoji | :work_item_award_emoji | ref(:award_emojis) | [ref(:move)]
@ -176,6 +185,7 @@ RSpec.shared_examples 'cloneable and moveable widget data' do
:milestone | :work_item_milestone | ref(:milestone) | [ref(:move), ref(:clone)]
:subscriptions | :work_item_subscriptions | ref(:subscriptions) | [ref(:move)]
:sent_notifications | :work_item_sent_notifications | ref(:notifications) | [ref(:move)]
:timelogs | :work_item_timelogs | ref(:timelogs) | [ref(:move)]
:customer_relations_contacts | :work_item_crm_contacts | ref(:crm_contacts) | [ref(:move), ref(:clone)]
end
@ -183,6 +193,9 @@ RSpec.shared_examples 'cloneable and moveable widget data' do
context "with widget" do
before do
allow(original_work_item).to receive(:from_service_desk?).and_return(true)
allow(WorkItems::CopyTimelogsWorker).to receive(:perform_async) do |*args|
WorkItems::CopyTimelogsWorker.perform_inline(*args)
end
end
it 'clones and moves widget data', :aggregate_failures do