Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-08-02 06:09:24 +00:00
parent 1235828392
commit 8bf623d6ea
17 changed files with 597 additions and 46 deletions

View File

@ -1043,3 +1043,12 @@ Search/AvoidCheckingFinishedOnDeprecatedMigrations:
- 'ee/lib/elastic/**/*.rb'
- 'ee/lib/gitlab/elastic/**/*.rb'
- 'ee/spec/support/helpers/elasticsearch_helpers.rb'
# See https://gitlab.com/gitlab-org/gitlab/-/issues/407233
Cop/ExperimentsTestCoverage:
Enabled: true
Include:
- 'app/**/*'
- 'lib/**/*'
- 'ee/app/**/*'
- 'ee/lib/**/*'

View File

@ -0,0 +1,8 @@
---
Cop/ExperimentsTestCoverage:
Details: grace period
Exclude:
- 'app/controllers/groups/boards_controller.rb'
- 'app/controllers/projects/boards_controller.rb'
- 'app/experiments/build_ios_app_guide_email_experiment.rb'
- 'ee/app/views/shared/_tier_badge.html.haml.rb'

View File

@ -53,7 +53,7 @@ export default {
};
</script>
<template>
<li class="report-block-list-issue gl-p-3!" data-qa-selector="report_item_row">
<li class="report-block-list-issue gl-p-3!" data-testid="report-item-row">
<component
:is="iconComponent"
v-if="showReportSectionStatusIcon"

View File

@ -9,6 +9,8 @@ import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/cons
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
// This is added outside the component as each dropdown on the page triggers a query,
// so if multiple queries fail, we only show 1 error.
const reportNamespaceLoadError = debounce(
() =>
createAlert({

View File

@ -0,0 +1,119 @@
<script>
import { GlCollapsibleListbox } from '@gitlab/ui';
import { debounce } from 'lodash';
import { __, s__ } from '~/locale';
import { createAlert } from '~/alert';
import { truncate } from '~/lib/utils/text_utility';
import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
// This is added outside the component as each dropdown on the page triggers a query,
// so if multiple queries fail, we only show 1 error.
const reportNamespaceLoadError = debounce(
() =>
createAlert({
message: s__('ImportProjects|Requesting namespaces failed'),
}),
DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
);
export default {
components: {
GlCollapsibleListbox,
},
props: {
selected: {
type: String,
required: true,
},
userNamespace: {
type: String,
required: true,
},
},
MAX_IMPORT_TARGET_LENGTH: 24,
data() {
return {
searchTerm: '',
};
},
apollo: {
namespaces: {
query: searchNamespacesWhereUserCanImportProjectsQuery,
variables() {
return {
search: this.searchTerm,
};
},
skip() {
const hasNotEnoughSearchCharacters =
this.searchTerm.length > 0 && this.searchTerm.length < MINIMUM_SEARCH_LENGTH;
return hasNotEnoughSearchCharacters;
},
update(data) {
return data.currentUser.groups.nodes;
},
error: reportNamespaceLoadError,
debounce: DEBOUNCE_DELAY,
},
},
computed: {
filteredNamespaces() {
return (this.namespaces ?? []).filter((ns) =>
ns.fullPath.toLowerCase().includes(this.searchTerm.toLowerCase()),
);
},
toggleText() {
return truncate(this.selected, this.$options.MAX_IMPORT_TARGET_LENGTH);
},
items() {
return [
{
text: __('Users'),
options: [{ text: this.userNamespace, value: this.userNamespace }],
},
{
text: __('Groups'),
options: this.filteredNamespaces.map((namespace) => {
return { text: namespace.fullPath, value: namespace.fullPath };
}),
},
];
},
},
methods: {
onSelect(value) {
this.$emit('select', value);
},
onSearch(value) {
this.searchTerm = value.trim();
},
},
};
</script>
<template>
<gl-collapsible-listbox
:items="items"
:selected="selected"
:toggle-text="toggleText"
searchable
fluid-width
toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
data-qa-selector="target_namespace_selector_dropdown"
@select="onSelect"
@search="onSearch"
/>
</template>

View File

@ -5,9 +5,6 @@ import {
GlFormInput,
GlButton,
GlLink,
GlDropdownItem,
GlDropdownDivider,
GlDropdownSectionHeader,
GlTooltip,
GlSprintf,
GlTooltipDirective,
@ -15,7 +12,7 @@ import {
import { mapState, mapGetters, mapActions } from 'vuex';
import { __ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import ImportGroupDropdown from '../../components/group_dropdown.vue';
import ImportTargetDropdown from '../../components/import_target_dropdown.vue';
import ImportStatus from '../../components/import_status.vue';
import { STATUSES } from '../../constants';
import { isProjectImportable, isImporting, isIncompatible, getImportStatus } from '../utils';
@ -23,13 +20,10 @@ import { isProjectImportable, isImporting, isIncompatible, getImportStatus } fro
export default {
name: 'ProviderRepoTableRow',
components: {
ImportGroupDropdown,
ImportStatus,
ImportTargetDropdown,
GlFormInput,
GlButton,
GlDropdownItem,
GlDropdownDivider,
GlDropdownSectionHeader,
GlIcon,
GlBadge,
GlLink,
@ -151,6 +145,10 @@ export default {
});
}
},
onSelect(value) {
this.updateImportTarget({ targetNamespace: value });
},
},
helpUrl: helpPagePath('/user/project/import/github.md'),
@ -188,27 +186,13 @@ export default {
<template v-if="repo.importSource.target">{{ repo.importSource.target }}</template>
<template v-else-if="isImportNotStarted || isSelectedForReimport">
<div class="gl-display-flex gl-align-items-stretch gl-w-full">
<import-group-dropdown #default="{ namespaces }" :text="importTarget.targetNamespace">
<template v-if="namespaces.length">
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="ns in namespaces"
:key="ns.fullPath"
data-qa-selector="target_group_dropdown_item"
:data-qa-group-name="ns.fullPath"
@click="updateImportTarget({ targetNamespace: ns.fullPath })"
>
{{ ns.fullPath }}
</gl-dropdown-item>
<gl-dropdown-divider />
</template>
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
<gl-dropdown-item @click="updateImportTarget({ targetNamespace: userNamespace })">{{
userNamespace
}}</gl-dropdown-item>
</import-group-dropdown>
<import-target-dropdown
:selected="importTarget.targetNamespace"
:user-namespace="userNamespace"
@select="onSelect"
/>
<div
class="gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1"
class="gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1 gl-border-gray-400"
>
/
</div>

View File

@ -431,6 +431,7 @@ the coverage report itself and verify that:
to match the files in your repository.
- The pipeline has completed. If the pipeline is [blocked on a manual job](../jobs/job_control.md#types-of-manual-jobs),
the pipeline is not considered complete.
- The coverage report file does not exceed the [limits](#limits).
Report artifacts are not downloadable by default. If you want the report to be downloadable
from the job details page, add your coverage report to the artifact `paths`:

View File

@ -67,6 +67,12 @@ module QA
click_element :admin_overview_groups_link
end
def go_to_security_and_compliance
hover_element(:admin_settings_menu_link) do
click_element :admin_security_and_compliance_link
end
end
def go_to_applications
return click_element(:nav_item_link, submenu_item: 'Applications') if Runtime::Env.super_sidebar_enabled?

View File

@ -19,7 +19,7 @@ module QA
element :import_status_indicator
end
view "app/assets/javascripts/import_entities/components/group_dropdown.vue" do
view "app/assets/javascripts/import_entities/components/import_target_dropdown.vue" do
element :target_namespace_selector_dropdown
end
@ -47,7 +47,7 @@ module QA
def import!(gh_project_name, target_group_path, project_name)
within_element(:project_import_row, source_project: gh_project_name) do
click_element(:target_namespace_selector_dropdown)
click_element(:target_group_dropdown_item, group_name: target_group_path)
click_element(:"listbox-item-#{target_group_path}", wait: 10)
fill_element(:project_path_field, project_name)
retry_until do

View File

@ -0,0 +1,114 @@
# frozen_string_literal: true
module RuboCop
module Cop
# Check for test coverage for GitLab experiments.
class ExperimentsTestCoverage < RuboCop::Cop::Base
CLASS_OFFENSE = 'Make sure experiment class has test coverage for all the variants.'
BLOCK_OFFENSE = 'Make sure experiment block has test coverage for all the variants.'
# Validates classes inherited from ApplicationExperiment
# These classes are located under app/experiments or ee/app/experiments
def on_class(node)
return if node.parent_class&.const_name != 'ApplicationExperiment'
return if covered_with_tests?(node)
add_offense(node, message: CLASS_OFFENSE)
end
# Validates experiments block in *.rb and *.haml files:
# experiment(:experiment_name) do |e|
# e.candidate { 'candidate' }
# e.run
# end
def on_block(node)
return if node.method_name != :experiment
return if covered_with_tests?(node)
add_offense(node, message: BLOCK_OFFENSE)
end
private
def covered_with_tests?(node)
tests_code = test_files_code(node)
return false if tests_code.blank?
return false unless tests_code.match?(stub_experiments_matcher)
return false unless tests_code.include?(experiment_name(node))
experiment_variants(node).map { |variant| tests_code.include?(variant) }.all?(&:present?)
end
def test_files_code(node)
# haml-lint add .rb extension to *.haml files
# https://gitlab.com/gitlab-org/gitlab/-/issues/415330#caveats
test_file_path = filepath(node).gsub('app/', 'spec/').gsub('.rb', '_spec.rb')
"#{read_file(test_file_path)}\n#{additional_tests_code(test_file_path)}"
end
def additional_tests_code(test_file_path)
# rubocop:disable Gitlab/NoCodeCoverageComment
# :nocov: File paths stubed in tests
if test_file_path.include?('/controllers/')
read_file(test_file_path.gsub('/controllers/', '/requests/'))
elsif test_file_path.include?('/lib/api/')
read_file(test_file_path.gsub('/lib/', '/spec/requests/'))
end
# :nocov:
# rubocop:enable Gitlab/NoCodeCoverageComment
end
def read_file(file_path)
File.exist?(file_path) ? File.new(file_path).read : ''
end
def experiment_name(node)
if node.is_a?(RuboCop::AST::ClassNode)
File.basename(filepath(node), '_experiment.rb')
else
block_node_value(node)
end
end
def experiment_variants(node)
node.body.children.filter_map do |child|
next unless child.is_a?(RuboCop::AST::SendNode) || child.is_a?(RuboCop::AST::BlockNode)
extract_variant(child)
end
end
def extract_variant(node)
# control enabled by default for tests
case node.method_name
when :candidate then 'candidate'
when :variant then variant_name(node)
end
end
def variant_name(node)
return send_node_value(node) if node.is_a?(RuboCop::AST::SendNode)
block_node_value(node)
end
def block_node_value(node)
send_node_value(node.children[0])
end
def send_node_value(node)
node.children[2].value.to_s
end
def filepath(node)
node.location.expression.source_buffer.name
end
def stub_experiments_matcher
# validates test files contains uncommented stub_experiments(...
/^([^#]|\s*|\w*)stub_experiments\(/
end
end
end
end

View File

@ -0,0 +1,99 @@
import { GlCollapsibleListbox } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ImportTargetDropdown from '~/import_entities/components/import_target_dropdown.vue';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql';
import { mockAvailableNamespaces, mockNamespacesResponse, mockUserNamespace } from '../mock_data';
Vue.use(VueApollo);
describe('ImportTargetDropdown', () => {
let wrapper;
const defaultProps = {
selected: mockUserNamespace,
userNamespace: mockUserNamespace,
};
const createComponent = ({ props = {} } = {}) => {
const apolloProvider = createMockApollo([
[
searchNamespacesWhereUserCanImportProjectsQuery,
jest.fn().mockResolvedValue(mockNamespacesResponse),
],
]);
wrapper = shallowMount(ImportTargetDropdown, {
apolloProvider,
propsData: {
...defaultProps,
...props,
},
});
};
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findListboxUsersItems = () => findListbox().props('items')[0].options;
const findListboxGroupsItems = () => findListbox().props('items')[1].options;
const waitForQuery = async () => {
jest.advanceTimersByTime(DEBOUNCE_DELAY);
await waitForPromises();
};
it('renders listbox', () => {
createComponent();
expect(findListbox().exists()).toBe(true);
});
it('truncates "toggle-text" when "selected" is too long', () => {
const mockSelected = 'a-group-path-that-is-longer-than-24-characters';
createComponent({
props: { selected: mockSelected },
});
expect(findListbox().props('toggleText')).toBe('a-group-path-that-is-lo…');
});
it('passes userNamespace as "Users" group item', () => {
createComponent();
expect(findListboxUsersItems()).toEqual([
{ text: mockUserNamespace, value: mockUserNamespace },
]);
});
it('passes namespaces from GraphQL as "Groups" group item', async () => {
createComponent();
await waitForQuery();
expect(findListboxGroupsItems()).toEqual(
mockAvailableNamespaces.map((namespace) => ({
text: namespace.fullPath,
value: namespace.fullPath,
})),
);
});
it('filters namespaces based on user input', async () => {
createComponent();
findListbox().vm.$emit('search', 'match');
await waitForQuery();
expect(findListboxGroupsItems()).toEqual([
{ text: 'match1', value: 'match1' },
{ text: 'match2', value: 'match2' },
]);
});
});

View File

@ -1,9 +1,10 @@
import { GlBadge, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { STATUSES } from '~/import_entities//constants';
import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { STATUSES } from '~/import_entities/constants';
import ImportTargetDropdown from '~/import_entities/components/import_target_dropdown.vue';
import ImportStatus from '~/import_entities/components/import_status.vue';
import ProviderRepoTableRow from '~/import_entities/import_projects/components/provider_repo_table_row.vue';
@ -39,8 +40,9 @@ describe('ProviderRepoTableRow', () => {
const findImportButton = () => findButton('Import');
const findReimportButton = () => findButton('Re-import');
const findGroupDropdown = () => wrapper.findComponent(ImportGroupDropdown);
const findImportTargetDropdown = () => wrapper.findComponent(ImportTargetDropdown);
const findImportStatus = () => wrapper.findComponent(ImportStatus);
const findProviderLink = () => wrapper.findByTestId('providerLink');
const findCancelButton = () => {
const buttons = wrapper
@ -55,7 +57,7 @@ describe('ProviderRepoTableRow', () => {
const store = initStore();
wrapper = shallowMount(ProviderRepoTableRow, {
wrapper = shallowMountExtended(ProviderRepoTableRow, {
store,
propsData: { userNamespace, optionalStages: {}, ...props },
});
@ -75,7 +77,7 @@ describe('ProviderRepoTableRow', () => {
});
it('renders project information', () => {
const providerLink = wrapper.find('[data-testid=providerLink]');
const providerLink = findProviderLink();
expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
expect(providerLink.text()).toMatch(repo.importSource.fullName);
@ -86,7 +88,7 @@ describe('ProviderRepoTableRow', () => {
});
it('renders a group namespace select', () => {
expect(wrapper.findComponent(ImportGroupDropdown).exists()).toBe(true);
expect(findImportTargetDropdown().exists()).toBe(true);
});
it('renders import button', () => {
@ -106,7 +108,11 @@ describe('ProviderRepoTableRow', () => {
it('includes optionalStages to import', async () => {
const OPTIONAL_STAGES = { stage1: true, stage2: false };
await wrapper.setProps({ optionalStages: OPTIONAL_STAGES });
mountComponent({
repo,
optionalStages: OPTIONAL_STAGES,
});
findImportButton().vm.$emit('click');
@ -192,7 +198,7 @@ describe('ProviderRepoTableRow', () => {
});
it('renders project information', () => {
const providerLink = wrapper.find('[data-testid=providerLink]');
const providerLink = findProviderLink();
expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
expect(providerLink.text()).toMatch(repo.importSource.fullName);
@ -203,7 +209,7 @@ describe('ProviderRepoTableRow', () => {
});
it('does not render a namespace select', () => {
expect(findGroupDropdown().exists()).toBe(false);
expect(findImportTargetDropdown().exists()).toBe(false);
});
it('does not render import button', () => {
@ -219,7 +225,7 @@ describe('ProviderRepoTableRow', () => {
await nextTick();
expect(findGroupDropdown().exists()).toBe(true);
expect(findImportTargetDropdown().exists()).toBe(true);
});
it('imports repo when clicking re-import button', async () => {
@ -282,7 +288,7 @@ describe('ProviderRepoTableRow', () => {
});
it('renders project information', () => {
const providerLink = wrapper.find('[data-testid=providerLink]');
const providerLink = findProviderLink();
expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
expect(providerLink.text()).toMatch(repo.importSource.fullName);

View File

@ -0,0 +1,34 @@
const mockGroupFactory = (fullPath) => ({
id: `gid://gitlab/Group/${fullPath}`,
fullPath,
name: fullPath,
visibility: 'public',
webUrl: `http://gdk.test:3000/groups/${fullPath}`,
__typename: 'Group',
});
export const mockAvailableNamespaces = [
mockGroupFactory('match1'),
mockGroupFactory('unrelated'),
mockGroupFactory('match2'),
];
export const mockNamespacesResponse = {
data: {
currentUser: {
id: 'gid://gitlab/User/1',
groups: {
nodes: mockAvailableNamespaces,
__typename: 'GroupConnection',
},
namespace: {
id: 'gid://gitlab/Namespaces::UserNamespace/1',
fullPath: 'root',
__typename: 'Namespace',
},
__typename: 'UserCore',
},
},
};
export const mockUserNamespace = 'user1';

View File

@ -22,7 +22,7 @@ RSpec.describe Backup::Database, feature_category: :backup_restore do
end
end
before_all do
before(:all) do # rubocop:disable RSpec/BeforeAll
Rake::Task.define_task(:environment)
Rake.application.rake_require 'active_record/railties/databases'
Rake.application.rake_require 'tasks/gitlab/backup'

View File

@ -0,0 +1,169 @@
# frozen_string_literal: true
require 'rubocop_spec_helper'
require_relative '../../../rubocop/cop/experiments_test_coverage'
RSpec.describe RuboCop::Cop::ExperimentsTestCoverage, feature_category: :experimentation_conversion do
let(:class_offense) { described_class::CLASS_OFFENSE }
let(:block_offense) { described_class::BLOCK_OFFENSE }
before do
allow(File).to receive(:exist?).and_return(true)
allow(File).to receive(:new).and_return(instance_double(File, read: tests_code))
end
describe '#on_class' do
context 'when there are no tests' do
let(:tests_code) { '' }
it 'registers an offense' do
expect_offense(<<~RUBY)
class ExperimentName < ApplicationExperiment
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{class_offense}
end
RUBY
end
end
context 'when there is no stub_experiments' do
let(:tests_code) { "candidate third" }
it 'registers an offense' do
expect_offense(<<~RUBY)
class ExperimentName < ApplicationExperiment
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{class_offense}
candidate
variant(:third) { 'third option' }
end
RUBY
end
end
context 'when variant test is missing' do
let(:tests_code) { "\nstub_experiments(experiment_name: :candidate)" }
it 'registers an offense' do
expect_offense(<<~RUBY)
class ExperimentName < ApplicationExperiment
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{class_offense}
candidate
variant(:third) { 'third option' }
end
RUBY
end
end
context 'when stub_experiments is commented out' do
let(:tests_code) do
"\n# stub_experiments(experiment_name: :candidate, experiment_name: :third)"
end
it 'registers an offense' do
expect_offense(<<~RUBY)
class ExperimentName < ApplicationExperiment
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{class_offense}
candidate
variant(:third) { 'third option' }
end
RUBY
end
end
context 'when all tests are present' do
let(:tests_code) do
"#\nstub_experiments(experiment_name: :candidate, experiment_name: :third)"
end
before do
allow(cop).to receive(:filepath).and_return('app/experiments/experiment_name_experiment.rb')
end
it 'does not register an offense' do
expect_no_offenses(<<~RUBY)
class ExperimentName < ApplicationExperiment
candidate
variant(:third) { 'third option' }
end
RUBY
end
end
end
describe '#on_block' do
context 'when there are no tests' do
let(:tests_code) { '' }
it 'registers an offense' do
expect_offense(<<~RUBY)
experiment(:experiment_name) do |e|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{block_offense}
end
RUBY
end
end
context 'when there is no stub_experiments' do
let(:tests_code) { "candidate third" }
it 'registers an offense' do
expect_offense(<<~RUBY)
experiment(:experiment_name) do |e|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{block_offense}
e.candidate { 'candidate' }
e.variant(:third) { 'third option' }
e.run
end
RUBY
end
end
context 'when variant test is missing' do
let(:tests_code) { "\nstub_experiments(experiment_name: :candidate)" }
it 'registers an offense' do
expect_offense(<<~RUBY)
experiment(:experiment_name) do |e|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{block_offense}
e.candidate { 'candidate' }
e.variant(:third) { 'third option' }
e.run
end
RUBY
end
end
context 'when stub_experiments is commented out' do
let(:tests_code) do
"\n# stub_experiments(experiment_name: :candidate, experiment_name: :third)"
end
it 'registers an offense' do
expect_offense(<<~RUBY)
experiment(:experiment_name) do |e|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{block_offense}
e.candidate { 'candidate' }
e.variant(:third) { 'third option' }
e.run
end
RUBY
end
end
context 'when all tests are present' do
let(:tests_code) do
"#\nstub_experiments(experiment_name: :candidate, experiment_name: :third)"
end
it 'does not register an offense' do
expect_no_offenses(<<~RUBY)
experiment(:experiment_name) do |e|
e.candidate { 'candidate' }
e.variant(:third) { 'third option' }
e.run
end
RUBY
end
end
end
end

View File

@ -38,7 +38,7 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
%w[db repositories]
end
before_all do
before(:all) do # rubocop:disable RSpec/BeforeAll
Rake.application.rake_require 'active_record/railties/databases'
Rake.application.rake_require 'tasks/gitlab/backup'
Rake.application.rake_require 'tasks/gitlab/shell'

View File

@ -4,7 +4,7 @@ require 'spec_helper'
require 'rake'
RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_category: :database do
before_all do
before(:all) do # rubocop:disable RSpec/BeforeAll
Rake.application.rake_require 'active_record/railties/databases'
Rake.application.rake_require 'tasks/seed_fu'
Rake.application.rake_require 'tasks/gitlab/db'